Merge commit '9e8bf48a962ac24a0c7ca00a5f6cb05ebb8aaabd'

This commit is contained in:
Ali 2023-10-25 16:18:22 +04:00
commit 2bc324f131
64 changed files with 1587 additions and 860 deletions

View File

@ -329,9 +329,6 @@ alternate_icon_folders = [
"Premium", "Premium",
"PremiumBlack", "PremiumBlack",
"PremiumTurbo", "PremiumTurbo",
"PremiumCoffee",
"PremiumDuck",
"PremiumSteam",
] ]
[ [

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

View File

@ -10105,10 +10105,6 @@ Sorry for the inconvenience.";
"ChatContextMenu.TextSelectionTip2" = "Hold on a word, then move cursor to select more| text to copy or quote."; "ChatContextMenu.TextSelectionTip2" = "Hold on a word, then move cursor to select more| text to copy or quote.";
"Appearance.AppIconCoffee" = "Coffee";
"Appearance.AppIconDuck" = "Duck";
"Appearance.AppIconSteam" = "Steam";
"Notification.GiftLink" = "You received a gift"; "Notification.GiftLink" = "You received a gift";
"MESSAGE_GIFTCODE" = "%1$@ sent you a Gift Code for %2$@ months of Telegram Premium"; "MESSAGE_GIFTCODE" = "%1$@ sent you a Gift Code for %2$@ months of Telegram Premium";
@ -10123,6 +10119,7 @@ Sorry for the inconvenience.";
"Channel.AdminLog.MessageChangedNameColorSet" = "%1$@ set name color to %2$@"; "Channel.AdminLog.MessageChangedNameColorSet" = "%1$@ set name color to %2$@";
"Channel.AdminLog.MessageChangedBackgroundEmojiSet" = "%1$@ set background emoji to %2$@"; "Channel.AdminLog.MessageChangedBackgroundEmojiSet" = "%1$@ set background emoji to %2$@";
"Channel.AdminLog.MessageChangedBackgroundEmojiRemoved" = "%1$@ removed background emoji";
"Appearance.NameColor" = "Your Name Color"; "Appearance.NameColor" = "Your Name Color";
@ -10197,3 +10194,209 @@ Sorry for the inconvenience.";
"Conversation.MessageOptionsEnlargeVideo" = "Enlarge Video"; "Conversation.MessageOptionsEnlargeVideo" = "Enlarge Video";
"Conversation.LinkOptionsCancel" = "Do Not Preview"; "Conversation.LinkOptionsCancel" = "Do Not Preview";
"Conversation.MessageOptionsTabLink" = "Link"; "Conversation.MessageOptionsTabLink" = "Link";
"Stats.Boosts.ShowMoreBoosts_1" = "Show %@ More Boost";
"Stats.Boosts.ShowMoreBoosts_any" = "Show %@ More Boosts";
"ReassignBoost.Title" = "Reassign Boosts";
"ReassignBoost.Description" = "To boost **%1$@**, reassign a previous boost or gift **Telegram Premium** to a friend to get **%2$@** additional boosts.";
"ReassignBoost.ReassignBoosts" = "Reassign Boosts";
"ReassignBoost.AvailableIn" = "Available in %@";
"ReassignBoost.ExpiresOn" = "Boost expires on %@";
"ReassignBoost.WaitForCooldown" = "Wait until the boost is available or get **%1$@** more boosts by gifting a **Telegram Premium** subscription.";
"ReassignBoost.Success" = "%1$@ are reassigned from %2$@.";
"ReassignBoost.Boosts_1" = "%@ boost";
"ReassignBoost.Boosts_any" = "%@ boosts";
"ReassignBoost.OtherChannels_1" = "%@ other channel";
"ReassignBoost.OtherChannels_any" = "%@ other channels";
"ChannelBoost.MoreBoosts.Title" = "More Boosts Needed";
"ChannelBoost.MoreBoosts.Text" = "To boost **%1$@** again, gift **Telegram Premium** to a friend and get **%2$@** additional boosts.";
"BoostGift.Title" = "Boosts via Gifts";
"BoostGift.Description" = "Get more boosts for your channel by gifting\nPremium to your subscribers.";
"BoostGift.PrepaidGiveawayTitle" = "PREPAID GIVEAWAY";
"BoostGift.PrepaidGiveawayCount_1" = "%@ Telegram Premium";
"BoostGift.PrepaidGiveawayCount_any" = "%@ Telegram Premium";
"BoostGift.PrepaidGiveawayMonths" = "%@-month subscriptions";
"BoostGift.CreateGiveaway" = "Create Giveaway";
"BoostGift.CreateGiveawayInfo" = "winners are chosen randomly";
"BoostGift.AwardSpecificUsers" = "Award Specific Users";
"BoostGift.SelectRecipients" = "select recipients";
"BoostGift.QuantityTitle" = "QUANTITY OF PRIZES";
"BoostGift.QuantityBoosts_1" = "%@ BOOST";
"BoostGift.QuantityBoosts_any" = "%@ BOOSTS";
"BoostGift.QuantityInfo" = "Choose how many Premium subscriptions to give away and boosts to receive.";
"BoostGift.ChannelsTitle" = "CHANNELS INCLUDED IN THE GIVEAWAY";
"BoostGift.AddChannel" = "Add Channel";
"BoostGift.ChannelsBoosts_1" = "this channel will receive %@ boost";
"BoostGift.ChannelsBoosts_any" = "this channel will receive %@ boosts";
"BoostGift.ChannelsInfo" = "Choose the channels users need to be subscribed to take part in the giveaway";
"BoostGift.UsersTitle" = "USERS ELIGIBLE FOR THE GIVEAWAY";
"BoostGift.FromCountries_1" = "from %@ country";
"BoostGift.FromCountries_any" = "from %@ countries";
"BoostGift.FromTwoCountries" = "from %1$@ and %2$@";
"BoostGift.FromOneCountry" = "from %1$@";
"BoostGift.FromAllCountries" = "from all countries";
"BoostGift.AllSubscribers" = "All subscribers";
"BoostGift.OnlyNewSubscribers" = "Only new subscribers";
"BoostGift.LimitSubscribersInfo" = "Choose if you want to limit the giveaway only to those who joined the channel after the giveaway started.";
"BoostGift.DateTitle" = "DATE WHEN GIVEAWAY ENDS";
"BoostGift.DateEnds" = "Ends";
"BoostGift.DateInfo" = "Choose when %1$@ of your channel will be randomly selected to receive Telegram Premium.";
"BoostGift.DateInfoSubscribers_1" = "%@ subscriber";
"BoostGift.DateInfoSubscribers_any" = "%@ subscribers";
"BoostGift.DurationTitle" = "DURATION OF PREMIUM SUBSCRIPTIONS";
"BoostGift.PremiumInfo" = "You can review the list of features and terms of use for Telegram Premium [here]().";
"BoostGift.GiftPremium" = "Gift Premium";
"BoostGift.StartGiveaway" = "Start Giveaway";
"BoostGift.ReduceQuantity.Title" = "Reduce Quantity";
"BoostGift.ReduceQuantity.Text" = "You can't acquire %1$@ %2$@-month subscriptions in the app. Do you want to reduce quantity to %3$@?";
"BoostGift.ReduceQuantity.Reduce" = "Reduce";
"BoostGift.GiveawayCreated.Title" = "Giveaway Created";
"BoostGift.GiveawayCreated.Text" = "Check your channel's [Statistics]() to see how this giveaway boosted your channel.";
"BoostGift.PremiumGifted.Title" = "Premium Subscriptions Gifted";
"BoostGift.PremiumGifted.Text" = "Check your channel's [Statistics]() to see how gifts boosted your channel.";
"BoostGift.Subscribers.Title" = "Gift Premium";
"BoostGift.Subscribers.Subtitle" = "select up to %@ subscribers";
"BoostGift.Subscribers.SectionTitle" = "SUBSCRIBERS";
"BoostGift.Subscribers.Joined" = "joined %@";
"BoostGift.Subscribers.Search" = "Search Subscribers";
"BoostGift.Subscribers.MaximumReached" = "You can select maximum %@ subscribers.";
"BoostGift.Subscribers.Save" = "Save Recipients";
"BoostGift.Channels.Title" = "Add Channels";
"BoostGift.Channels.Subtitle" = "select up to %@ channels";
"BoostGift.Channels.SectionTitle" = "CHANNELS";
"BoostGift.Channels.Search" = "Search Channels";
"BoostGift.Channels.MaximumReached" = "You can select maximum %@ channels.";
"BoostGift.Channels.PrivateChannel.Title" = "Channel is Private";
"BoostGift.Channels.PrivateChannel.Text" = "Are you sure you want to add a private channel? Users won't be able to join it without an invite link.";
"BoostGift.Channels.PrivateChannel.Add" = "Add";
"BoostGift.Channels.Save" = "Save Channels";
"Stats.Boosts.PrepaidGiveawaysTitle" = "PREPAID GIVEAWAYS";
"Stats.Boosts.PrepaidGiveawayCount_1" = "%@ Telegram Premium";
"Stats.Boosts.PrepaidGiveawayCount_any" = "%@ Telegram Premiums";
"Stats.Boosts.PrepaidGiveawayMonths" = "%@-month subscriptions";
"Stats.Boosts.PrepaidGiveawaysInfo" = "Select a giveaway you already paid for to set it up.";
"Stats.Boosts.ShortMonth" = "%@m";
"Stats.Boosts.Giveaway" = "Giveaway";
"Stats.Boosts.Gift" = "Gift";
"Stats.Boosts.TabBoosts_1" = "%@ Boost";
"Stats.Boosts.TabBoosts_any" = "%@ Boosts";
"Stats.Boosts.TabGifts_1" = "%@ Boost";
"Stats.Boosts.TabGifts_any" = "%@ Boosts";
"Stats.Boosts.ToBeDistributed" = "To Be Distributed";
"Stats.Boosts.Unclaimed" = "Unclaimed";
"Stats.Boosts.GetBoosts" = "Get Boosts via Gifts";
"Stats.Boosts.GetBoostsInfo" = "Get more boosts for your channel by gifting Premium to your subscribers.";
"Notification.PremiumPrize.Title" = "Congratulations!";
"Notification.PremiumPrize.UnclaimedText" = "You have an unclaimed prize from a giveaway by **%1$@**.\n\nThis prize is a **Telegram Premium** subscription for %2$@.";
"Notification.PremiumPrize.GiveawayText" = "You won a prize in a giveaway organized by **%1$@**.\n\nYour prize is a **Telegram Premium** subscription for %2$@.";
"Notification.PremiumPrize.GiftText" = "You've received a gift from **%1$@**.\n\nYour gift is a **Telegram Premium** subscription for %2$@.";
"Notification.PremiumPrize.Months_1" = "**%@** month";
"Notification.PremiumPrize.Months_any" = "**%@** months";
"Notification.PremiumPrize.View" = "Open Gift Link";
"Notification.PremiumPrize.Unclaimed" = "Unclaimed Prize";
"Story.SlideToSeek" = "Slide left or right to seek";
"Story.Guide.Title" = "Watching Stories";
"Story.Guide.Description" = "You can use these gestures to control playback.";
"Story.Guide.ForwardTitle" = "Go forward";
"Story.Guide.ForwardDescription" = "Tap the screen";
"Story.Guide.PauseTitle" = "Pause and Seek";
"Story.Guide.PauseDescription" = "Hold and move sideways";
"Story.Guide.BackTitle" = "Go back";
"Story.Guide.BackDescription" = "Tap the left edge";
"Story.Guide.MoveTitle" = "Move between stories";
"Story.Guide.MoveDescription" = "Swipe left or right";
"Story.Guide.Proceed" = "Tap to keep watching";
"Chat.Giveaway.Info.Title" = "About This Giveaway";
"Chat.Giveaway.Info.EndedTitle" = "Giveaway Ended";
"Chat.Giveaway.Info.AlmostOver" = "The giveaway is almost over.";
"Chat.Giveaway.Info.OngoingIntro" = "The giveaway is sponsored by the admins of **%1$@**, who acquired %2$@ for %3$@ for its followers.";
"Chat.Giveaway.Info.OngoingNewMany" = "On **%1$@**, Telegram will automatically select %2$@ that joined **%3$@** and %4$@ after **%5$@**.";
"Chat.Giveaway.Info.OngoingNew" = "On **%1$@**, Telegram will automatically select %2$@ that joined **%3$@** after **%4$@**.";
"Chat.Giveaway.Info.OngoingMany" = "On **%1$@**, Telegram will automatically select %2$@ of **%3$@** and %4$@.";
"Chat.Giveaway.Info.Ongoing" = "On **%1$@**, Telegram will automatically select %2$@ of **%3$@**.";
"Chat.Giveaway.Info.EndedIntro" = "The giveaway was sponsored by the admins of **%1$@**, who acquired %2$@ for %3$@ for its followers.";
"Chat.Giveaway.Info.EndedNewMany" = "On **%1$@**, Telegram automatically selected %2$@ that joined **%3$@** and other listed channels after **%4$@**.";
"Chat.Giveaway.Info.EndedNew" = "On **%1$@**, Telegram automatically selected %2$@ that joined **%3$@** after **%4$@**.";
"Chat.Giveaway.Info.EndedMany" = "On **%1$@**, Telegram automatically selected %2$@ of **%3$@** and other listed channels.";
"Chat.Giveaway.Info.Ended" = "On **%1$@**, Telegram automatically selected %2$@ of **%3$@**.";
"Chat.Giveaway.Info.NotAllowedJoinedEarly" = "You are not eligible to participate in this giveaway, because you joined this channel on **%@**, which is before the contest started.";
"Chat.Giveaway.Info.NotAllowedAdmin" = "You are not eligible to participate in this giveaway, because you are an admin of participating channel (**%@**).";
"Chat.Giveaway.Info.NotAllowedCountry" = "You are not eligible to participate in this giveaway, because your country is not included in the terms of the giveaway.";
"Chat.Giveaway.Info.NotQualified" = "To take part in this giveaway please join the channel **%1$@** before **%2$@**.";
"Chat.Giveaway.Info.NotQualifiedMany" = "To take part in this giveaway please join the channel **%1$@** (and %2$@) before **%3$@**.";
"Chat.Giveaway.Info.Participating" = "You are participating in this giveaway, because you have joined the channel **%1$@**.";
"Chat.Giveaway.Info.ParticipatingMany" = "You are participating in this giveaway, because you have joined the channel **%1$@** (and %2$@).";
"Chat.Giveaway.Info.OtherChannels_1" = "**%@** other listed channel";
"Chat.Giveaway.Info.OtherChannels_any" = "**%@** other listed channels";
"Chat.Giveaway.Info.Subscriptions_1" = "**%@ Telegram Premium** subscription";
"Chat.Giveaway.Info.Subscriptions_any" = "**%@ Telegram Premium** subscriptions";
"Chat.Giveaway.Info.RandomUsers_1" = "**%@** random user";
"Chat.Giveaway.Info.RandomUsers_any" = "**%@** random user";
"Chat.Giveaway.Info.RandomSubscribers_1" = "**%@** random subscriber";
"Chat.Giveaway.Info.RandomSubscribers_any" = "**%@** random subscribers";
"Chat.Giveaway.Info.Months_1" = "**%@** month";
"Chat.Giveaway.Info.Months_any" = "**%@** months";
"Chat.Giveaway.Info.ActivatedLinks_1" = "%@ winner already used their gift link.";
"Chat.Giveaway.Info.ActivatedLinks_any" = "%@ of the winners already used their gift links.";
"Chat.Giveaway.Info.Refunded" = "The channel cancelled the prizes by reversing the payment for them.";
"Chat.Giveaway.Info.Won" = "You won a prize in this giveaway. %@";
"Chat.Giveaway.Info.DidntWin" = "You didn't win a prize in this giveaway.";
"Chat.Giveaway.Info.ViewPrize" = "View My Prize";
"Chat.Giveaway.Toast.NotAllowed" = "You can't participate in this giveaway.";
"Chat.Giveaway.Toast.Participating" = "You are participating in this giveaway.";
"Chat.Giveaway.Toast.NotQualified" = "You are not qualified for this giveaway yet.";
"Chat.Giveaway.Toast.AlmostOver" = "The giveaway is almost over.";
"Chat.Giveaway.Toast.Ended" = "The giveaway is ended.";
"Chat.Giveaway.Toast.LearnMore" = "Learn More";
"Chat.Giveaway.Message.PrizeTitle" = "Giveaway Prizes";
"Chat.Giveaway.Message.PrizeText" = "%1$@ for %2$@.";
"Chat.Giveaway.Message.Subscriptions_1" = "**%@** Telegram Premium Subscription";
"Chat.Giveaway.Message.Subscriptions_any" = "**%@** Telegram Premium Subscriptions";
"Chat.Giveaway.Message.Months_1" = "**%@** month";
"Chat.Giveaway.Message.Months_any" = "**%@** months";
"Chat.Giveaway.Message.ParticipantsTitle" = "Participants";
"Chat.Giveaway.Message.ParticipantsNewMany" = "All users who join the channels below after this date:";
"Chat.Giveaway.Message.ParticipantsNew" = "All users who join this channel after this date:";
"Chat.Giveaway.Message.ParticipantsMany" = "All subscribers of the channels below:";
"Chat.Giveaway.Message.Participants" = "All subscribers of this channel:";
"Chat.Giveaway.Message.CountriesFrom" = "from %@";
"Chat.Giveaway.Message.CountriesDelimiter" = ", ";
"Chat.Giveaway.Message.CountriesLastDelimiter" = " and ";
"Chat.Giveaway.Message.DateTitle" = "Winners Selection Date";
"Chat.Giveaway.Message.LearnMore" = "LEARN MORE";
"GiftLink.Title" = "Gift Link";
"GiftLink.UsedTitle" = "Used Gift Link";
"GiftLink.Description" = "This link allows you to activate a **Telegram Premium** subscription.";
"GiftLink.UsedDescription" = "This link was used to activate a **Telegram Premium** subscription.";
"GiftLink.PersonalDescription" = "This link allows **%@** to activate a **Telegram Premium** subscription.";
"GiftLink.PersonalUsedDescription" = "This link allowed **%@** to activate a **Telegram Premium** subscription.";
"GiftLink.UnclaimedDescription" = "This link allows to activate a **Telegram Premium** subscription.";
"GiftLink.Footer" = "You can also [send this link]() to a friend as a gift.";
"GiftLink.UsedFooter" = "This link was used on %@.";
"GiftLink.NotUsedFooter" = "This link hasn't been used yet.";
"GiftLink.UseLink" = "Use Link";
"GiftLink.Gift" = "Gift";
"GiftLink.From" = "From";
"GiftLink.To" = "To";
"GiftLink.Reason" = "Reason";
"GiftLink.Reason.Giveaway" = "Giveaway";
"GiftLink.Reason.Gift" = "You were selected by the channel";
"GiftLink.Reason.Unclaimed" = "Incomplete Giveaway";
"GiftLink.Date" = "Date";
"GiftLink.NoRecipient" = "No recipient";
"GiftLink.TelegramPremium_1" = "Telegram Premium for %@ month";
"GiftLink.TelegramPremium_any" = "Telegram Premium for %@ months";
"GiftLink.LinkHidden" = "Only the recipient can see the link.";

View File

@ -1044,6 +1044,7 @@ public protocol AccountContext: AnyObject {
var isPremium: Bool { get } var isPremium: Bool { get }
var userLimits: EngineConfiguration.UserLimits { get } var userLimits: EngineConfiguration.UserLimits { get }
var peerNameColors: PeerNameColors { get }
var imageCache: AnyObject? { get } var imageCache: AnyObject? { get }
@ -1062,19 +1063,21 @@ public protocol AccountContext: AnyObject {
public struct PremiumConfiguration { public struct PremiumConfiguration {
public static var defaultValue: PremiumConfiguration { public static var defaultValue: PremiumConfiguration {
return PremiumConfiguration(isPremiumDisabled: false, showPremiumGiftInAttachMenu: false, showPremiumGiftInTextField: false, giveawayGiftsPurchaseAvailable: false) return PremiumConfiguration(isPremiumDisabled: false, showPremiumGiftInAttachMenu: false, showPremiumGiftInTextField: false, giveawayGiftsPurchaseAvailable: false, boostsPerGiftCount: 3)
} }
public let isPremiumDisabled: Bool public let isPremiumDisabled: Bool
public let showPremiumGiftInAttachMenu: Bool public let showPremiumGiftInAttachMenu: Bool
public let showPremiumGiftInTextField: Bool public let showPremiumGiftInTextField: Bool
public let giveawayGiftsPurchaseAvailable: Bool public let giveawayGiftsPurchaseAvailable: Bool
public let boostsPerGiftCount: Int32
fileprivate init(isPremiumDisabled: Bool, showPremiumGiftInAttachMenu: Bool, showPremiumGiftInTextField: Bool, giveawayGiftsPurchaseAvailable: Bool) { fileprivate init(isPremiumDisabled: Bool, showPremiumGiftInAttachMenu: Bool, showPremiumGiftInTextField: Bool, giveawayGiftsPurchaseAvailable: Bool, boostsPerGiftCount: Int32) {
self.isPremiumDisabled = isPremiumDisabled self.isPremiumDisabled = isPremiumDisabled
self.showPremiumGiftInAttachMenu = showPremiumGiftInAttachMenu self.showPremiumGiftInAttachMenu = showPremiumGiftInAttachMenu
self.showPremiumGiftInTextField = showPremiumGiftInTextField self.showPremiumGiftInTextField = showPremiumGiftInTextField
self.giveawayGiftsPurchaseAvailable = giveawayGiftsPurchaseAvailable self.giveawayGiftsPurchaseAvailable = giveawayGiftsPurchaseAvailable
self.boostsPerGiftCount = boostsPerGiftCount
} }
public static func with(appConfiguration: AppConfiguration) -> PremiumConfiguration { public static func with(appConfiguration: AppConfiguration) -> PremiumConfiguration {
@ -1083,7 +1086,8 @@ public struct PremiumConfiguration {
isPremiumDisabled: data["premium_purchase_blocked"] as? Bool ?? false, isPremiumDisabled: data["premium_purchase_blocked"] as? Bool ?? false,
showPremiumGiftInAttachMenu: data["premium_gift_attach_menu_icon"] as? Bool ?? false, showPremiumGiftInAttachMenu: data["premium_gift_attach_menu_icon"] as? Bool ?? false,
showPremiumGiftInTextField: data["premium_gift_text_field_icon"] as? Bool ?? false, showPremiumGiftInTextField: data["premium_gift_text_field_icon"] as? Bool ?? false,
giveawayGiftsPurchaseAvailable: data["giveaway_gifts_purchase_available"] as? Bool ?? false giveawayGiftsPurchaseAvailable: data["giveaway_gifts_purchase_available"] as? Bool ?? false,
boostsPerGiftCount: Int32(data["boosts_per_sent_gift"] as? Double ?? 3)
) )
} else { } else {
return .defaultValue return .defaultValue
@ -1197,3 +1201,136 @@ public struct StickersSearchConfiguration {
} }
} }
} }
public class PeerNameColors: Equatable {
public struct Colors: Equatable {
public let main: UIColor
public let secondary: UIColor?
public let tertiary: UIColor?
init(main: UIColor, secondary: UIColor?, tertiary: UIColor?) {
self.main = main
self.secondary = secondary
self.tertiary = tertiary
}
init(main: UIColor) {
self.main = main
self.secondary = nil
self.tertiary = nil
}
init?(colors: [UIColor]) {
guard let first = colors.first else {
return nil
}
self.main = first
if colors.count == 3 {
self.secondary = colors[1]
self.tertiary = colors[2]
} else if colors.count == 2, let second = colors.last {
self.secondary = second
self.tertiary = nil
} else {
self.secondary = nil
self.tertiary = nil
}
}
}
public static var defaultSingleColors: [Int32: Colors] {
return [
0: Colors(main: UIColor(rgb: 0xcc5049)),
1: Colors(main: UIColor(rgb: 0xd67722)),
2: Colors(main: UIColor(rgb: 0x955cdb)),
3: Colors(main: UIColor(rgb: 0x40a920)),
4: Colors(main: UIColor(rgb: 0x309eba)),
5: Colors(main: UIColor(rgb: 0x368ad1)),
6: Colors(main: UIColor(rgb: 0xc7508b))
]
}
public static var defaultValue: PeerNameColors {
return PeerNameColors(
colors: defaultSingleColors,
darkColors: [:],
displayOrder: [5, 3, 1, 0, 2, 4, 6]
)
}
public let colors: [Int32: Colors]
public let darkColors: [Int32: Colors]
public let displayOrder: [Int32]
public func get(_ color: PeerNameColor) -> Colors {
if let colors = self.colors[color.rawValue] {
return colors
} else {
return PeerNameColors.defaultSingleColors[5]!
}
}
fileprivate init(colors: [Int32: Colors], darkColors: [Int32: Colors], displayOrder: [Int32]) {
self.colors = colors
self.darkColors = darkColors
self.displayOrder = displayOrder
}
public static func with(appConfiguration: AppConfiguration) -> PeerNameColors {
if let data = appConfiguration.data {
var colors = PeerNameColors.defaultSingleColors
var darkColors: [Int32: Colors] = [:]
if let peerColors = data["peer_colors"] as? [String: [String]] {
for (key, values) in peerColors {
if let index = Int32(key) {
let colorsArray = values.compactMap { UIColor(hexString: $0) }
if let colorValues = Colors(colors: colorsArray) {
colors[index] = colorValues
}
}
}
}
if let darkPeerColors = data["dark_peer_colors"] as? [String: [String]] {
for (key, values) in darkPeerColors {
if let index = Int32(key) {
let colorsArray = values.compactMap { UIColor(hexString: $0) }
if let colorValues = Colors(colors: colorsArray) {
darkColors[index] = colorValues
}
}
}
}
var displayOrder: [Int32] = []
if let order = data["peer_colors_available"] as? [Double] {
displayOrder = order.map { Int32($0) }
}
if displayOrder.isEmpty {
displayOrder = PeerNameColors.defaultValue.displayOrder
}
return PeerNameColors(
colors: colors,
darkColors: darkColors,
displayOrder: displayOrder
)
} else {
return .defaultValue
}
}
public static func == (lhs: PeerNameColors, rhs: PeerNameColors) -> Bool {
if lhs.colors != rhs.colors {
return false
}
if lhs.darkColors != rhs.darkColors {
return false
}
if lhs.displayOrder != rhs.displayOrder {
return false
}
return true
}
}

View File

@ -339,10 +339,15 @@ public final class ChatPresentationInterfaceState: Equatable {
} }
public struct AccountPeerColor: Equatable { public struct AccountPeerColor: Equatable {
public var isDashed: Bool public enum Style {
case solid
case doubleDashed
case tripleDashed
}
public var style: Style
public init(isDashed: Bool) { public init(style: Style) {
self.isDashed = isDashed self.style = style
} }
} }

View File

@ -278,7 +278,7 @@ final class ChatSendMessageActionSheetControllerNode: ViewControllerTracingNode,
quote: ChatInputTextView.Theme.Quote( quote: ChatInputTextView.Theme.Quote(
background: mainColor.withMultipliedAlpha(0.1), background: mainColor.withMultipliedAlpha(0.1),
foreground: mainColor, foreground: mainColor,
isDashed: textInputView.theme?.quote.isDashed == true lineStyle: textInputView.theme?.quote.lineStyle ?? .solid
) )
) )
} }

View File

@ -71,11 +71,13 @@ public final class TextNodeBlockQuoteData: NSObject {
public let title: NSAttributedString? public let title: NSAttributedString?
public let color: UIColor public let color: UIColor
public let secondaryColor: UIColor? public let secondaryColor: UIColor?
public let tertiaryColor: UIColor?
public init(title: NSAttributedString?, color: UIColor, secondaryColor: UIColor?) { public init(title: NSAttributedString?, color: UIColor, secondaryColor: UIColor?, tertiaryColor: UIColor?) {
self.title = title self.title = title
self.color = color self.color = color
self.secondaryColor = secondaryColor self.secondaryColor = secondaryColor
self.tertiaryColor = tertiaryColor
super.init() super.init()
} }
@ -102,6 +104,13 @@ public final class TextNodeBlockQuoteData: NSObject {
} else if (self.secondaryColor == nil) != (other.secondaryColor == nil) { } else if (self.secondaryColor == nil) != (other.secondaryColor == nil) {
return false return false
} }
if let lhsTertiaryColor = self.tertiaryColor, let rhsTertiaryColor = other.tertiaryColor {
if !lhsTertiaryColor.isEqual(rhsTertiaryColor) {
return false
}
} else if (self.tertiaryColor == nil) != (other.tertiaryColor == nil) {
return false
}
return true return true
} }
@ -141,11 +150,13 @@ private final class TextNodeBlockQuote {
let frame: CGRect let frame: CGRect
let tintColor: UIColor let tintColor: UIColor
let secondaryTintColor: UIColor? let secondaryTintColor: UIColor?
let tertiaryTintColor: UIColor?
init(frame: CGRect, tintColor: UIColor, secondaryTintColor: UIColor?) { init(frame: CGRect, tintColor: UIColor, secondaryTintColor: UIColor?, tertiaryTintColor: UIColor?) {
self.frame = frame self.frame = frame
self.tintColor = tintColor self.tintColor = tintColor
self.secondaryTintColor = secondaryTintColor self.secondaryTintColor = secondaryTintColor
self.tertiaryTintColor = tertiaryTintColor
} }
} }
@ -1212,6 +1223,7 @@ open class TextNode: ASDisplayNode {
let isBlockQuote: Bool let isBlockQuote: Bool
let tintColor: UIColor? let tintColor: UIColor?
let secondaryTintColor: UIColor? let secondaryTintColor: UIColor?
let tertiaryTintColor: UIColor?
} }
var stringSegments: [StringSegment] = [] var stringSegments: [StringSegment] = []
@ -1235,7 +1247,8 @@ open class TextNode: ASDisplayNode {
firstCharacterOffset: segmentCharacterOffset, firstCharacterOffset: segmentCharacterOffset,
isBlockQuote: false, isBlockQuote: false,
tintColor: nil, tintColor: nil,
secondaryTintColor: nil secondaryTintColor: nil,
tertiaryTintColor: nil
)) ))
} }
@ -1247,7 +1260,8 @@ open class TextNode: ASDisplayNode {
firstCharacterOffset: effectiveRange.location, firstCharacterOffset: effectiveRange.location,
isBlockQuote: true, isBlockQuote: true,
tintColor: value.color, tintColor: value.color,
secondaryTintColor: value.secondaryColor secondaryTintColor: value.secondaryColor,
tertiaryTintColor: value.tertiaryColor
)) ))
} }
segmentCharacterOffset = effectiveRange.location + effectiveRange.length segmentCharacterOffset = effectiveRange.location + effectiveRange.length
@ -1261,7 +1275,8 @@ open class TextNode: ASDisplayNode {
firstCharacterOffset: effectiveRange.location, firstCharacterOffset: effectiveRange.location,
isBlockQuote: false, isBlockQuote: false,
tintColor: nil, tintColor: nil,
secondaryTintColor: nil secondaryTintColor: nil,
tertiaryTintColor: nil
)) ))
segmentCharacterOffset = effectiveRange.location + effectiveRange.length segmentCharacterOffset = effectiveRange.location + effectiveRange.length
} }
@ -1277,7 +1292,8 @@ open class TextNode: ASDisplayNode {
firstCharacterOffset: segmentCharacterOffset, firstCharacterOffset: segmentCharacterOffset,
isBlockQuote: false, isBlockQuote: false,
tintColor: nil, tintColor: nil,
secondaryTintColor: nil secondaryTintColor: nil,
tertiaryTintColor: nil
)) ))
} }
@ -1290,6 +1306,7 @@ open class TextNode: ASDisplayNode {
var lines: [TextNodeLine] = [] var lines: [TextNodeLine] = []
var tintColor: UIColor? var tintColor: UIColor?
var secondaryTintColor: UIColor? var secondaryTintColor: UIColor?
var tertiaryTintColor: UIColor?
var isBlockQuote: Bool = false var isBlockQuote: Bool = false
var additionalWidth: CGFloat = 0.0 var additionalWidth: CGFloat = 0.0
} }
@ -1301,6 +1318,7 @@ open class TextNode: ASDisplayNode {
calculatedSegment.isBlockQuote = segment.isBlockQuote calculatedSegment.isBlockQuote = segment.isBlockQuote
calculatedSegment.tintColor = segment.tintColor calculatedSegment.tintColor = segment.tintColor
calculatedSegment.secondaryTintColor = segment.secondaryTintColor calculatedSegment.secondaryTintColor = segment.secondaryTintColor
calculatedSegment.tertiaryTintColor = segment.tertiaryTintColor
let rawSubstring = segment.substring.string as NSString let rawSubstring = segment.substring.string as NSString
let substringLength = rawSubstring.length let substringLength = rawSubstring.length
@ -1512,7 +1530,7 @@ open class TextNode: ASDisplayNode {
} }
if segment.isBlockQuote, let tintColor = segment.tintColor { if segment.isBlockQuote, let tintColor = segment.tintColor {
blockQuotes.append(TextNodeBlockQuote(frame: CGRect(origin: CGPoint(x: 0.0, y: blockMinY - 2.0), size: CGSize(width: blockWidth, height: blockMaxY - (blockMinY - 2.0) + 4.0)), tintColor: tintColor, secondaryTintColor: segment.secondaryTintColor)) blockQuotes.append(TextNodeBlockQuote(frame: CGRect(origin: CGPoint(x: 0.0, y: blockMinY - 2.0), size: CGSize(width: blockWidth, height: blockMaxY - (blockMinY - 2.0) + 4.0)), tintColor: tintColor, secondaryTintColor: segment.secondaryTintColor, tertiaryTintColor: segment.tertiaryTintColor))
} }
} }
@ -2216,9 +2234,19 @@ open class TextNode: ASDisplayNode {
if let secondaryTintColor = blockQuote.secondaryTintColor { if let secondaryTintColor = blockQuote.secondaryTintColor {
let isMonochrome = secondaryTintColor.alpha == 0.0 let isMonochrome = secondaryTintColor.alpha == 0.0
let tertiaryTintColor = blockQuote.tertiaryTintColor
let dashHeight: CGFloat = tertiaryTintColor != nil ? 6.0 : 9.0
do { do {
context.saveGState() context.saveGState()
let dashOffset: CGFloat
if let _ = tertiaryTintColor {
dashOffset = isMonochrome ? -2.0 : 0.0
} else {
dashOffset = isMonochrome ? -4.0 : 5.0
}
if isMonochrome { if isMonochrome {
context.setFillColor(blockQuote.tintColor.withMultipliedAlpha(0.2).cgColor) context.setFillColor(blockQuote.tintColor.withMultipliedAlpha(0.2).cgColor)
context.fill(lineFrame) context.fill(lineFrame)
@ -2229,23 +2257,37 @@ open class TextNode: ASDisplayNode {
context.setFillColor(secondaryTintColor.cgColor) context.setFillColor(secondaryTintColor.cgColor)
} }
let dashOffset: CGFloat = isMonochrome ? -4.0 : 5.0 func drawDashes() {
context.translateBy(x: blockFrame.minX, y: blockFrame.minY + dashOffset) context.translateBy(x: blockFrame.minX, y: blockFrame.minY + dashOffset)
var offset = 0.0 var offset = 0.0
while offset < blockFrame.height { while offset < blockFrame.height {
context.move(to: CGPoint(x: 0.0, y: 3.0)) context.move(to: CGPoint(x: 0.0, y: 3.0))
context.addLine(to: CGPoint(x: lineWidth, y: 0.0)) context.addLine(to: CGPoint(x: lineWidth, y: 0.0))
context.addLine(to: CGPoint(x: lineWidth, y: 9.0)) context.addLine(to: CGPoint(x: lineWidth, y: dashHeight))
context.addLine(to: CGPoint(x: 0.0, y: 9.0 + 3.0)) context.addLine(to: CGPoint(x: 0.0, y: dashHeight + 3.0))
context.closePath() context.closePath()
context.fillPath() context.fillPath()
context.translateBy(x: 0.0, y: 18.0) context.translateBy(x: 0.0, y: 18.0)
offset += 18.0 offset += 18.0
} }
}
drawDashes()
context.restoreGState() context.restoreGState()
if let tertiaryTintColor {
context.saveGState()
context.translateBy(x: 0.0, y: dashHeight)
if isMonochrome {
context.setFillColor(blockQuote.tintColor.withAlphaComponent(0.4).cgColor)
} else {
context.setFillColor(tertiaryTintColor.cgColor)
}
drawDashes()
context.restoreGState()
}
} }
} else { } else {
context.setFillColor(blockQuote.tintColor.cgColor) context.setFillColor(blockQuote.tintColor.cgColor)

View File

@ -108,6 +108,8 @@ swift_library(
"//submodules/TelegramUI/Components/Utils/RoundedRectWithTailPath", "//submodules/TelegramUI/Components/Utils/RoundedRectWithTailPath",
"//submodules/CountrySelectionUI", "//submodules/CountrySelectionUI",
"//submodules/TelegramUI/Components/Stories/PeerListItemComponent", "//submodules/TelegramUI/Components/Stories/PeerListItemComponent",
"//submodules/InvisibleInkDustNode",
"//submodules/AlertUI",
], ],
visibility = [ visibility = [
"//visibility:public", "//visibility:public",

View File

@ -38,7 +38,6 @@ final class AppIconsDemoComponent: Component {
private var component: AppIconsDemoComponent? private var component: AppIconsDemoComponent?
private var containerView: UIView private var containerView: UIView
private var axisView = UIView()
private var imageViews: [UIImageView] = [] private var imageViews: [UIImageView] = []
private var isVisible = false private var isVisible = false
@ -50,7 +49,6 @@ final class AppIconsDemoComponent: Component {
super.init(frame: frame) super.init(frame: frame)
self.addSubview(self.containerView) self.addSubview(self.containerView)
self.containerView.addSubview(self.axisView)
} }
required init?(coder: NSCoder) { required init?(coder: NSCoder) {
@ -64,11 +62,7 @@ final class AppIconsDemoComponent: Component {
self.containerView.frame = CGRect(origin: CGPoint(x: -availableSize.width / 2.0, y: 0.0), size: CGSize(width: availableSize.width * 2.0, height: availableSize.height)) self.containerView.frame = CGRect(origin: CGPoint(x: -availableSize.width / 2.0, y: 0.0), size: CGSize(width: availableSize.width * 2.0, height: availableSize.height))
self.axisView.bounds = CGRect(origin: .zero, size: availableSize)
self.axisView.center = CGPoint(x: availableSize.width, y: availableSize.height / 2.0)
if self.imageViews.isEmpty { if self.imageViews.isEmpty {
var i = 0
for icon in component.appIcons { for icon in component.appIcons {
let image: UIImage? let image: UIImage?
switch icon.imageName { switch icon.imageName {
@ -78,12 +72,6 @@ final class AppIconsDemoComponent: Component {
image = UIImage(bundleImageName: "Premium/Icons/Black") image = UIImage(bundleImageName: "Premium/Icons/Black")
case "PremiumTurbo": case "PremiumTurbo":
image = UIImage(bundleImageName: "Premium/Icons/Turbo") image = UIImage(bundleImageName: "Premium/Icons/Turbo")
case "PremiumDuck":
image = UIImage(bundleImageName: "Premium/Icons/Duck")
case "PremiumCoffee":
image = UIImage(bundleImageName: "Premium/Icons/Coffee")
case "PremiumSteam":
image = UIImage(bundleImageName: "Premium/Icons/Steam")
default: default:
image = nil image = nil
} }
@ -95,36 +83,30 @@ final class AppIconsDemoComponent: Component {
imageView.layer.cornerCurve = .continuous imageView.layer.cornerCurve = .continuous
} }
imageView.image = image imageView.image = image
if i == 0 {
self.containerView.addSubview(imageView) self.containerView.addSubview(imageView)
} else {
self.axisView.addSubview(imageView)
}
self.imageViews.append(imageView) self.imageViews.append(imageView)
i += 1
} }
} }
} }
let radius: CGFloat = availableSize.width * 0.33
let angleIncrement: CGFloat = 2 * .pi / CGFloat(self.imageViews.count - 1)
var i = 0 var i = 0
for view in self.imageViews { for view in self.imageViews {
let position: CGPoint let position: CGPoint
if i == 0 { switch i {
position = CGPoint(x: availableSize.width, y: availableSize.height / 2.0) case 0:
} else { position = CGPoint(x: availableSize.width * 0.5, y: availableSize.height * 0.333)
let angle = CGFloat(i - 1) * angleIncrement case 1:
let xPosition = radius * cos(angle) + availableSize.width / 2.0 position = CGPoint(x: availableSize.width * 0.333, y: availableSize.height * 0.667)
let yPosition = radius * sin(angle) + availableSize.height / 2.0 case 2:
position = CGPoint(x: availableSize.width * 0.667, y: availableSize.height * 0.667)
position = CGPoint(x: xPosition, y: yPosition) default:
position = CGPoint(x: availableSize.width * 0.5, y: availableSize.height * 0.5)
} }
view.center = position if !self.animating {
view.center = position.offsetBy(dx: availableSize.width / 2.0, dy: 0.0)
}
i += 1 i += 1
} }
@ -143,48 +125,6 @@ final class AppIconsDemoComponent: Component {
} }
self.isVisible = isDisplaying self.isVisible = isDisplaying
let rotationDuration: Double = 12.0
if isDisplaying {
if self.axisView.layer.animation(forKey: "rotationAnimation") == nil {
let rotationAnimation = CABasicAnimation(keyPath: "transform.rotation")
rotationAnimation.fromValue = 0.0
rotationAnimation.toValue = 2.0 * CGFloat.pi
rotationAnimation.duration = rotationDuration
rotationAnimation.repeatCount = Float.infinity
self.axisView.layer.add(rotationAnimation, forKey: "rotationAnimation")
var i = 0
for view in self.imageViews {
if i == 0 {
let animation = CABasicAnimation(keyPath: "transform.scale")
animation.duration = 2.0
animation.fromValue = 1.0
animation.toValue = 1.15
animation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
animation.autoreverses = true
animation.repeatCount = .infinity
view.layer.add(animation, forKey: "scale")
} else {
view.transform = CGAffineTransformMakeScale(0.8, 0.8)
let rotationAnimation = CABasicAnimation(keyPath: "transform.rotation")
rotationAnimation.fromValue = 0.0
rotationAnimation.toValue = -2.0 * CGFloat.pi
rotationAnimation.duration = rotationDuration
rotationAnimation.repeatCount = Float.infinity
view.layer.add(rotationAnimation, forKey: "rotationAnimation")
}
i += 1
}
}
} else {
self.axisView.layer.removeAllAnimations()
for view in self.imageViews {
view.layer.removeAllAnimations()
}
}
return availableSize return availableSize
} }
@ -192,37 +132,38 @@ final class AppIconsDemoComponent: Component {
func animateIn(availableSize: CGSize) { func animateIn(availableSize: CGSize) {
self.animating = true self.animating = true
let radius: CGFloat = availableSize.width * 2.5
let angleIncrement: CGFloat = 2 * .pi / CGFloat(self.imageViews.count - 1)
var i = 0 var i = 0
for view in self.imageViews { for view in self.imageViews {
if i > 0 { let from: CGPoint
let delay: Double = 0.033 * Double(i - 1) let delay: Double
switch i {
case 0:
from = CGPoint(x: -availableSize.width * 0.333, y: -availableSize.height * 0.8)
delay = 0.1
case 1:
from = CGPoint(x: -availableSize.width * 0.55, y: availableSize.height * 0.75)
delay = 0.15
case 2:
from = CGPoint(x: availableSize.width * 0.9, y: availableSize.height * 0.75)
delay = 0.0
default:
from = CGPoint(x: availableSize.width * 0.5, y: availableSize.height * 0.5)
delay = 0.0
}
let angle = CGFloat(i - 1) * angleIncrement
let xPosition = radius * cos(angle)
let yPosition = radius * sin(angle)
let from = CGPoint(x: xPosition, y: yPosition)
let initialPosition = view.layer.position let initialPosition = view.layer.position
view.layer.position = initialPosition.offsetBy(dx: xPosition, dy: yPosition) view.layer.position = initialPosition.offsetBy(dx: from.x, dy: from.y)
view.alpha = 0.0
Queue.mainQueue().after(delay) { Queue.mainQueue().after(delay) {
view.alpha = 1.0
view.layer.position = initialPosition view.layer.position = initialPosition
view.layer.animateScale(from: 3.0, to: 0.8, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring) view.layer.animateScale(from: 3.0, to: 1.0, duration: 0.5, delay: 0.0, timingFunction: kCAMediaTimingFunctionSpring)
view.layer.animatePosition(from: from, to: CGPoint(), duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, additive: true) view.layer.animatePosition(from: from, to: CGPoint(), duration: 0.5, delay: 0.0, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15)
if i == self.imageViews.count - 1 { if i == 2 {
self.animating = false self.animating = false
} }
} }
} else {
}
i += 1 i += 1
} }
} }

View File

@ -361,7 +361,7 @@ private enum CreateGiveawayEntry: ItemListNodeEntry {
default: default:
color = .blue color = .blue
} }
return GiftOptionItem(presentationData: presentationData, context: arguments.context, icon: .image(color: color, name: "Premium/Giveaway"), title: title, titleFont: .bold, subtitle: subtitle, label: .boosts(prepaidGiveaway.quantity), sectionId: self.section, action: nil) return GiftOptionItem(presentationData: presentationData, context: arguments.context, icon: .image(color: color, name: "Premium/Giveaway"), title: title, titleFont: .bold, titleBadge: "\(prepaidGiveaway.quantity * 4)", subtitle: subtitle, sectionId: self.section, action: nil)
case let .subscriptionsHeader(_, text, additionalText): case let .subscriptionsHeader(_, text, additionalText):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, accessoryText: ItemListSectionHeaderAccessoryText(value: additionalText, color: .generic), sectionId: self.section) return ItemListSectionHeaderItem(presentationData: presentationData, text: text, accessoryText: ItemListSectionHeaderAccessoryText(value: additionalText, color: .generic), sectionId: self.section)
case let .subscriptions(_, value): case let .subscriptions(_, value):
@ -377,8 +377,7 @@ private enum CreateGiveawayEntry: ItemListNodeEntry {
case let .channelsHeader(_, text): case let .channelsHeader(_, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .channel(_, _, peer, boosts, isRevealed): case let .channel(_, _, peer, boosts, isRevealed):
return ItemListPeerItem(presentationData: presentationData, dateTimeFormat: PresentationDateTimeFormat(), nameDisplayOrder: presentationData.nameDisplayOrder, context: arguments.context, peer: peer, presence: nil, text: boosts.flatMap { .text("this channel will receive \($0) boosts", .secondary) } ?? .none, label: .none, editing: ItemListPeerItemEditing(editable: boosts == nil, editing: false, revealed: isRevealed), switchValue: nil, enabled: true, selectable: peer.id != arguments.context.account.peerId, sectionId: self.section, action: { return ItemListPeerItem(presentationData: presentationData, dateTimeFormat: PresentationDateTimeFormat(), nameDisplayOrder: presentationData.nameDisplayOrder, context: arguments.context, peer: peer, presence: nil, text: boosts.flatMap { .text(presentationData.strings.BoostGift_ChannelsBoosts($0), .secondary) } ?? .none, label: .none, editing: ItemListPeerItemEditing(editable: boosts == nil, editing: false, revealed: isRevealed), switchValue: nil, enabled: true, selectable: peer.id != arguments.context.account.peerId, sectionId: self.section, action: {
// arguments.openPeer(peer)
}, setPeerIdWithRevealedOptions: { lhs, rhs in }, setPeerIdWithRevealedOptions: { lhs, rhs in
arguments.setItemIdWithRevealedOptions(lhs, rhs) arguments.setItemIdWithRevealedOptions(lhs, rhs)
}, removePeer: { id in }, removePeer: { id in
@ -433,7 +432,7 @@ private enum CreateGiveawayEntry: ItemListNodeEntry {
} else { } else {
text = presentationData.strings.InviteLink_Create_TimeLimitExpiryDateNever text = presentationData.strings.InviteLink_Create_TimeLimitExpiryDateNever
} }
return ItemListDisclosureItem(presentationData: presentationData, title: "Ends", label: text, labelStyle: active ? .coloredText(theme.list.itemAccentColor) : .text, sectionId: self.section, style: .blocks, disclosureStyle: .none, action: { return ItemListDisclosureItem(presentationData: presentationData, title: presentationData.strings.BoostGift_DateEnds, label: text, labelStyle: active ? .coloredText(theme.list.itemAccentColor) : .text, sectionId: self.section, style: .blocks, disclosureStyle: .none, action: {
arguments.dismissInput() arguments.dismissInput()
var focus = false var focus = false
arguments.updateState { state in arguments.updateState { state in
@ -515,7 +514,7 @@ private func createGiveawayControllerEntries(
switch subject { switch subject {
case .generic: case .generic:
entries.append(.createGiveaway(presentationData.theme, "Create Giveaway", "winners are chosen randomly", state.mode == .giveaway)) entries.append(.createGiveaway(presentationData.theme, presentationData.strings.BoostGift_CreateGiveaway, presentationData.strings.BoostGift_CreateGiveawayInfo, state.mode == .giveaway))
let recipientsText: String let recipientsText: String
if !state.peers.isEmpty { if !state.peers.isEmpty {
@ -533,59 +532,68 @@ private func createGiveawayControllerEntries(
recipientsText = presentationData.strings.PremiumGift_LabelRecipients(Int32(peersCount)) recipientsText = presentationData.strings.PremiumGift_LabelRecipients(Int32(peersCount))
} }
} else { } else {
recipientsText = "select recipients" recipientsText = presentationData.strings.BoostGift_SelectRecipients
} }
entries.append(.awardUsers(presentationData.theme, "Award Specific Users", recipientsText, state.mode == .gift)) entries.append(.awardUsers(presentationData.theme, presentationData.strings.BoostGift_AwardSpecificUsers, recipientsText, state.mode == .gift))
case let .prepaid(prepaidGiveaway): case let .prepaid(prepaidGiveaway):
entries.append(.prepaidHeader(presentationData.theme, "PREPAID GIVEAWAY")) entries.append(.prepaidHeader(presentationData.theme, presentationData.strings.BoostGift_PrepaidGiveawayTitle))
entries.append(.prepaid(presentationData.theme, "\(prepaidGiveaway.quantity) Telegram Premium", "\(prepaidGiveaway.months)-month subscriptions", prepaidGiveaway)) entries.append(.prepaid(presentationData.theme, presentationData.strings.BoostGift_PrepaidGiveawayCount(prepaidGiveaway.quantity), presentationData.strings.BoostGift_PrepaidGiveawayMonths("\(prepaidGiveaway.months)").string, prepaidGiveaway))
} }
if case .giveaway = state.mode { if case .giveaway = state.mode {
if case .generic = subject { if case .generic = subject {
entries.append(.subscriptionsHeader(presentationData.theme, "QUANTITY OF PRIZES".uppercased(), "\(state.subscriptions) BOOSTS")) entries.append(.subscriptionsHeader(presentationData.theme, presentationData.strings.BoostGift_QuantityTitle.uppercased(), presentationData.strings.BoostGift_QuantityBoosts(state.subscriptions * 4)))
entries.append(.subscriptions(presentationData.theme, state.subscriptions)) entries.append(.subscriptions(presentationData.theme, state.subscriptions))
entries.append(.subscriptionsInfo(presentationData.theme, "Choose how many Premium subscriptions to give away and boosts to receive.")) entries.append(.subscriptionsInfo(presentationData.theme, presentationData.strings.BoostGift_QuantityInfo))
} }
entries.append(.channelsHeader(presentationData.theme, "CHANNELS INCLUDED IN THE GIVEAWAY".uppercased())) entries.append(.channelsHeader(presentationData.theme, presentationData.strings.BoostGift_ChannelsTitle.uppercased()))
var index: Int32 = 0 var index: Int32 = 0
let channels = [peerId] + state.channels let channels = [peerId] + state.channels
for channelId in channels { for channelId in channels {
if let channel = peers[channelId] { if let channel = peers[channelId] {
entries.append(.channel(index, presentationData.theme, channel, channel.id == peerId ? state.subscriptions : nil, false)) entries.append(.channel(index, presentationData.theme, channel, channel.id == peerId ? state.subscriptions * 4 : nil, false))
} }
index += 1 index += 1
} }
entries.append(.channelAdd(presentationData.theme, "Add Channel")) entries.append(.channelAdd(presentationData.theme, presentationData.strings.BoostGift_AddChannel))
entries.append(.channelsInfo(presentationData.theme, "Choose the channels users need to be subscribed to take part in the giveaway")) entries.append(.channelsInfo(presentationData.theme, presentationData.strings.BoostGift_ChannelsInfo))
entries.append(.usersHeader(presentationData.theme, "USERS ELIGIBLE FOR THE GIVEAWAY".uppercased())) entries.append(.usersHeader(presentationData.theme, presentationData.strings.BoostGift_UsersTitle.uppercased()))
let countriesText: String let countriesText: String
if state.countries.count > 2 { if state.countries.count > 2 {
countriesText = "from \(state.countries.count) countries" countriesText = presentationData.strings.BoostGift_FromCountries(Int32(state.countries.count))
} else if !state.countries.isEmpty { } else if !state.countries.isEmpty {
let allCountries = state.countries.map { locale.localizedString(forRegionCode: $0) ?? $0 }.joined(separator: " and ") if state.countries.count == 2 {
countriesText = "from \(allCountries)" let firstCountryCode = state.countries.first ?? ""
let secondCountryCode = state.countries.last ?? ""
let firstCountryName = locale.localizedString(forRegionCode: firstCountryCode) ?? firstCountryCode
let secondCountryName = locale.localizedString(forRegionCode: secondCountryCode) ?? secondCountryCode
countriesText = presentationData.strings.BoostGift_FromTwoCountries(firstCountryName, secondCountryName).string
} else { } else {
countriesText = "from all countries" let countryCode = state.countries.first ?? ""
let countryName = locale.localizedString(forRegionCode: countryCode) ?? countryCode
countriesText = presentationData.strings.BoostGift_FromOneCountry(countryName).string
}
} else {
countriesText = presentationData.strings.BoostGift_FromAllCountries
} }
entries.append(.usersAll(presentationData.theme, "All subscribers", countriesText, !state.onlyNewEligible)) entries.append(.usersAll(presentationData.theme, presentationData.strings.BoostGift_AllSubscribers, countriesText, !state.onlyNewEligible))
entries.append(.usersNew(presentationData.theme, "Only new subscribers", countriesText, state.onlyNewEligible)) entries.append(.usersNew(presentationData.theme, presentationData.strings.BoostGift_OnlyNewSubscribers, countriesText, state.onlyNewEligible))
entries.append(.usersInfo(presentationData.theme, "Choose if you want to limit the giveaway only to those who joined the channel after the giveaway started.")) entries.append(.usersInfo(presentationData.theme, presentationData.strings.BoostGift_LimitSubscribersInfo))
entries.append(.timeHeader(presentationData.theme, "DATE WHEN GIVEAWAY ENDS".uppercased())) entries.append(.timeHeader(presentationData.theme, presentationData.strings.BoostGift_DateTitle.uppercased()))
entries.append(.timeExpiryDate(presentationData.theme, presentationData.dateTimeFormat, state.time, state.pickingTimeLimit)) entries.append(.timeExpiryDate(presentationData.theme, presentationData.dateTimeFormat, state.time, state.pickingTimeLimit))
if state.pickingTimeLimit { if state.pickingTimeLimit {
entries.append(.timeCustomPicker(presentationData.theme, presentationData.dateTimeFormat, state.time, minDate, maxDate)) entries.append(.timeCustomPicker(presentationData.theme, presentationData.dateTimeFormat, state.time, minDate, maxDate))
} }
entries.append(.timeInfo(presentationData.theme, "Choose when \(state.subscriptions) subscribers of your channel will be randomly selected to receive Telegram Premium.")) entries.append(.timeInfo(presentationData.theme, presentationData.strings.BoostGift_DateInfo(presentationData.strings.BoostGift_DateInfoSubscribers(Int32(state.subscriptions))).string))
} }
if case .generic = subject { if case .generic = subject {
entries.append(.durationHeader(presentationData.theme, "DURATION OF PREMIUM SUBSCRIPTIONS".uppercased())) entries.append(.durationHeader(presentationData.theme, presentationData.strings.BoostGift_DurationTitle.uppercased()))
let recipientCount: Int let recipientCount: Int
switch state.mode { switch state.mode {
@ -628,7 +636,7 @@ private func createGiveawayControllerEntries(
i += 1 i += 1
} }
entries.append(.durationInfo(presentationData.theme, "You can review the list of features and terms of use for Telegram Premium [here]().")) entries.append(.durationInfo(presentationData.theme, presentationData.strings.BoostGift_PremiumInfo))
} }
return entries return entries
@ -776,7 +784,7 @@ public func createGiveawayController(context: AccountContext, updatedPresentatio
let (state, peersMap) = stateAndPeersMap let (state, peersMap) = stateAndPeersMap
let headerItem = CreateGiveawayHeaderItem(theme: presentationData.theme, title: "Boosts via Gifts", text: "Get more boosts for your channel by gifting\nPremium to your subscribers.", cancel: { let headerItem = CreateGiveawayHeaderItem(theme: presentationData.theme, strings: presentationData.strings, title: presentationData.strings.BoostGift_Title, text: presentationData.strings.BoostGift_Description, cancel: {
dismissImpl?() dismissImpl?()
}) })
@ -787,7 +795,7 @@ public func createGiveawayController(context: AccountContext, updatedPresentatio
case .gift: case .gift:
badgeCount = Int32(state.peers.count) badgeCount = Int32(state.peers.count)
} }
let footerItem = CreateGiveawayFooterItem(theme: presentationData.theme, title: state.mode == .gift ? "Gift Premium" : "Start Giveaway", badgeCount: badgeCount, isLoading: state.updating, action: { let footerItem = CreateGiveawayFooterItem(theme: presentationData.theme, title: state.mode == .gift ? presentationData.strings.BoostGift_GiftPremium : presentationData.strings.BoostGift_StartGiveaway, badgeCount: badgeCount, isLoading: state.updating, action: {
buyActionImpl?() buyActionImpl?()
}) })
let leftNavigationButton = ItemListNavigationButton(content: .none, style: .regular, enabled: false, action: {}) let leftNavigationButton = ItemListNavigationButton(content: .none, style: .regular, enabled: false, action: {})
@ -857,7 +865,7 @@ public func createGiveawayController(context: AccountContext, updatedPresentatio
} }
guard let selectedProduct else { guard let selectedProduct else {
let alertController = textAlertController(context: context, title: "Reduce Quantity", text: "You can't acquire \(state.subscriptions) \(selectedMonths)-month subscriptions in the app. Do you want to reduce quantity to 25?", actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: "Reduce", action: { let alertController = textAlertController(context: context, title: presentationData.strings.BoostGift_ReduceQuantity_Title, text: presentationData.strings.BoostGift_ReduceQuantity_Text("\(state.subscriptions)", "\(selectedMonths)", "\(25)").string, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: presentationData.strings.BoostGift_ReduceQuantity_Reduce, action: {
updateState { state in updateState { state in
var updatedState = state var updatedState = state
updatedState.subscriptions = 25 updatedState.subscriptions = 25
@ -915,11 +923,11 @@ public func createGiveawayController(context: AccountContext, updatedPresentatio
let text: String let text: String
switch state.mode { switch state.mode {
case .giveaway: case .giveaway:
title = "Giveaway Created" title = presentationData.strings.BoostGift_GiveawayCreated_Title
text = "Check your channel's [Statistics]() to see how this giveaway boosted your channel." text = presentationData.strings.BoostGift_GiveawayCreated_Text
case .gift: case .gift:
title = "Premium Subscriptions Gifted" title = presentationData.strings.BoostGift_PremiumGifted_Title
text = "Check your channel's [Statistics]() to see how gifts boosted your channel." text = presentationData.strings.BoostGift_PremiumGifted_Text
} }
let tooltipController = UndoOverlayController(presentationData: presentationData, content: .premiumPaywall(title: title, text: text, customUndoText: nil, timeout: nil, linkAction: { [weak navigationController] _ in let tooltipController = UndoOverlayController(presentationData: presentationData, content: .premiumPaywall(title: title, text: text, customUndoText: nil, timeout: nil, linkAction: { [weak navigationController] _ in
@ -992,8 +1000,8 @@ public func createGiveawayController(context: AccountContext, updatedPresentatio
controllers.removeLast(count) controllers.removeLast(count)
navigationController.setViewControllers(controllers, animated: true) navigationController.setViewControllers(controllers, animated: true)
let title = "Giveaway Created" let title = presentationData.strings.BoostGift_GiveawayCreated_Title
let text = "Check your channel's [Statistics]() to see how this giveaway boosted your channel." let text = presentationData.strings.BoostGift_GiveawayCreated_Text
let tooltipController = UndoOverlayController(presentationData: presentationData, content: .premiumPaywall(title: title, text: text, customUndoText: nil, timeout: nil, linkAction: { [weak navigationController] _ in let tooltipController = UndoOverlayController(presentationData: presentationData, content: .premiumPaywall(title: title, text: text, customUndoText: nil, timeout: nil, linkAction: { [weak navigationController] _ in
let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.StatsDatacenterId(id: peerId)) let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.StatsDatacenterId(id: peerId))

View File

@ -11,12 +11,14 @@ import ComponentFlow
final class CreateGiveawayHeaderItem: ItemListControllerHeaderItem { final class CreateGiveawayHeaderItem: ItemListControllerHeaderItem {
let theme: PresentationTheme let theme: PresentationTheme
let strings: PresentationStrings
let title: String let title: String
let text: String let text: String
let cancel: () -> Void let cancel: () -> Void
init(theme: PresentationTheme, title: String, text: String, cancel: @escaping () -> Void) { init(theme: PresentationTheme, strings: PresentationStrings, title: String, text: String, cancel: @escaping () -> Void) {
self.theme = theme self.theme = theme
self.strings = strings
self.title = title self.title = title
self.text = text self.text = text
self.cancel = cancel self.cancel = cancel
@ -132,7 +134,7 @@ class CreateGiveawayHeaderItemNode: ItemListControllerHeaderItemNode {
self.titleNode.attributedText = attributedTitle self.titleNode.attributedText = attributedTitle
self.textNode.attributedText = attributedText self.textNode.attributedText = attributedText
self.cancelNode.setAttributedTitle(NSAttributedString(string: "Cancel", font: Font.regular(17.0), textColor: self.item.theme.rootController.navigationBar.accentTextColor), for: .normal) self.cancelNode.setAttributedTitle(NSAttributedString(string: self.item.strings.Common_Cancel, font: Font.regular(17.0), textColor: self.item.theme.rootController.navigationBar.accentTextColor), for: .normal)
} }
override func updateContentOffset(_ contentOffset: CGFloat, transition: ContainedViewLayoutTransition) { override func updateContentOffset(_ contentOffset: CGFloat, transition: ContainedViewLayoutTransition) {

View File

@ -4,12 +4,164 @@ import AsyncDisplayKit
import Display import Display
import SwiftSignalKit import SwiftSignalKit
import TelegramCore import TelegramCore
import TelegramPresentationData
import TextFormat
import AccountContext import AccountContext
import AlertUI import TelegramStringFormatting
import PresentationDataUtils import TelegramPresentationData
import Markdown import Markdown
import AlertUI
public func giveawayInfoController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, message: EngineMessage, giveawayInfo: PremiumGiveawayInfo) -> ViewController? {
guard let giveaway = message.media.first(where: { $0 is TelegramMediaGiveaway }) as? TelegramMediaGiveaway else {
return nil
}
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
var peerName = ""
if let peerId = giveaway.channelPeerIds.first, let peer = message.peers[peerId] {
peerName = EnginePeer(peer).compactDisplayTitle
}
let untilDate = stringForDate(timestamp: giveaway.untilDate, strings: presentationData.strings)
let title: String
let text: String
var warning: String?
var dismissImpl: (() -> Void)?
var actions: [TextAlertAction] = [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {
dismissImpl?()
})]
switch giveawayInfo {
case let .ongoing(start, status):
let startDate = stringForDate(timestamp: start, strings: presentationData.strings)
title = presentationData.strings.Chat_Giveaway_Info_Title
let intro: String
if case .almostOver = status {
intro = presentationData.strings.Chat_Giveaway_Info_EndedIntro(peerName, presentationData.strings.Chat_Giveaway_Info_Subscriptions(giveaway.quantity), presentationData.strings.Chat_Giveaway_Info_Months(giveaway.months)).string
} else {
intro = presentationData.strings.Chat_Giveaway_Info_OngoingIntro(peerName, presentationData.strings.Chat_Giveaway_Info_Subscriptions(giveaway.quantity), presentationData.strings.Chat_Giveaway_Info_Months(giveaway.months)).string
}
let ending: String
if giveaway.flags.contains(.onlyNewSubscribers) {
let randomUsers = presentationData.strings.Chat_Giveaway_Info_RandomUsers(giveaway.quantity)
if giveaway.channelPeerIds.count > 1 {
ending = presentationData.strings.Chat_Giveaway_Info_OngoingNewMany(untilDate, randomUsers, peerName, presentationData.strings.Chat_Giveaway_Info_OtherChannels(Int32(giveaway.channelPeerIds.count - 1)), startDate).string
} else {
ending = presentationData.strings.Chat_Giveaway_Info_OngoingNew(untilDate, randomUsers, peerName, startDate).string
}
} else {
let randomSubscribers = presentationData.strings.Chat_Giveaway_Info_RandomSubscribers(giveaway.quantity)
if giveaway.channelPeerIds.count > 1 {
ending = presentationData.strings.Chat_Giveaway_Info_OngoingMany(untilDate, randomSubscribers, peerName, presentationData.strings.Chat_Giveaway_Info_OtherChannels(Int32(giveaway.channelPeerIds.count - 1))).string
} else {
ending = presentationData.strings.Chat_Giveaway_Info_Ongoing(untilDate, randomSubscribers, peerName).string
}
}
var participation: String
switch status {
case .notQualified:
if giveaway.channelPeerIds.count > 1 {
participation = presentationData.strings.Chat_Giveaway_Info_NotQualifiedMany(peerName, presentationData.strings.Chat_Giveaway_Info_OtherChannels(Int32(giveaway.channelPeerIds.count - 1)), untilDate).string
} else {
participation = presentationData.strings.Chat_Giveaway_Info_NotQualified(peerName, untilDate).string
}
case let .notAllowed(reason):
switch reason {
case let .joinedTooEarly(joinedOn):
let joinDate = stringForDate(timestamp: joinedOn, strings: presentationData.strings)
participation = presentationData.strings.Chat_Giveaway_Info_NotAllowedJoinedEarly(joinDate).string
case let .channelAdmin(adminId):
let _ = adminId
participation = presentationData.strings.Chat_Giveaway_Info_NotAllowedAdmin(peerName).string
case .disallowedCountry:
participation = presentationData.strings.Chat_Giveaway_Info_NotAllowedCountry
}
case .participating:
if giveaway.channelPeerIds.count > 1 {
participation = presentationData.strings.Chat_Giveaway_Info_ParticipatingMany(peerName, presentationData.strings.Chat_Giveaway_Info_OtherChannels(Int32(giveaway.channelPeerIds.count - 1))).string
} else {
participation = presentationData.strings.Chat_Giveaway_Info_Participating(peerName).string
}
case .almostOver:
participation = presentationData.strings.Chat_Giveaway_Info_AlmostOver
}
if !participation.isEmpty {
participation = "\n\n\(participation)"
}
text = "\(intro)\n\n\(ending)\(participation)"
case let .finished(status, start, finish, _, activatedCount):
let startDate = stringForDate(timestamp: start, strings: presentationData.strings)
let finishDate = stringForDate(timestamp: finish, strings: presentationData.strings)
title = presentationData.strings.Chat_Giveaway_Info_EndedTitle
let intro = presentationData.strings.Chat_Giveaway_Info_EndedIntro(peerName, presentationData.strings.Chat_Giveaway_Info_Subscriptions(giveaway.quantity), presentationData.strings.Chat_Giveaway_Info_Months(giveaway.months)).string
var ending: String
if giveaway.flags.contains(.onlyNewSubscribers) {
let randomUsers = presentationData.strings.Chat_Giveaway_Info_RandomUsers(giveaway.quantity)
if giveaway.channelPeerIds.count > 1 {
ending = presentationData.strings.Chat_Giveaway_Info_EndedNewMany(finishDate, randomUsers, peerName, startDate).string
} else {
ending = presentationData.strings.Chat_Giveaway_Info_EndedNew(finishDate, randomUsers, peerName, startDate).string
}
} else {
let randomSubscribers = presentationData.strings.Chat_Giveaway_Info_RandomSubscribers(giveaway.quantity)
if giveaway.channelPeerIds.count > 1 {
ending = presentationData.strings.Chat_Giveaway_Info_EndedMany(finishDate, randomSubscribers, peerName).string
} else {
ending = presentationData.strings.Chat_Giveaway_Info_Ended(finishDate, randomSubscribers, peerName).string
}
}
if activatedCount > 0 {
ending += " " + presentationData.strings.Chat_Giveaway_Info_ActivatedLinks(activatedCount)
}
var result: String
switch status {
case .refunded:
result = ""
warning = presentationData.strings.Chat_Giveaway_Info_Refunded
actions = [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Close, action: {
dismissImpl?()
})]
case .notWon:
result = "\n\n" + presentationData.strings.Chat_Giveaway_Info_DidntWin
case let .won(slug):
result = "\n\n" + presentationData.strings.Chat_Giveaway_Info_Won("🏆").string
let _ = slug
actions = [TextAlertAction(type: .defaultAction, title: presentationData.strings.Chat_Giveaway_Info_ViewPrize, action: {
dismissImpl?()
}), TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {
dismissImpl?()
})]
}
text = "\(intro)\n\n\(ending)\(result)"
}
let alertController = giveawayInfoAlertController(
context: context,
updatedPresentationData: updatedPresentationData,
title: title,
text: text,
warning: warning,
actions: actions
)
dismissImpl = { [weak alertController] in
alertController?.dismissAnimated()
}
return alertController
}
private final class GiveawayInfoAlertContentNode: AlertContentNode { private final class GiveawayInfoAlertContentNode: AlertContentNode {
private let title: String private let title: String
@ -229,7 +381,7 @@ private final class GiveawayInfoAlertContentNode: AlertContentNode {
} }
} }
func giveawayInfoAlertController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, title: String, text: String, warning: String?, actions: [TextAlertAction]) -> AlertController { private func giveawayInfoAlertController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, title: String, text: String, warning: String?, actions: [TextAlertAction]) -> AlertController {
let presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 } let presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 }
let contentNode = GiveawayInfoAlertContentNode(theme: AlertControllerTheme(presentationData: presentationData), ptheme: presentationData.theme, title: title, text: text, warning: warning, actions: actions) let contentNode = GiveawayInfoAlertContentNode(theme: AlertControllerTheme(presentationData: presentationData), ptheme: presentationData.theme, title: title, text: text, warning: warning, actions: actions)

View File

@ -8,8 +8,6 @@ import TelegramPresentationData
import UndoUI import UndoUI
import PresentationDataUtils import PresentationDataUtils
//TODO:localize
private struct BoostState { private struct BoostState {
let level: Int32 let level: Int32
let currentLevelBoosts: Int32 let currentLevelBoosts: Int32
@ -106,7 +104,7 @@ public func PremiumBoostScreen(
Queue.mainQueue().after(0.3) { Queue.mainQueue().after(0.3) {
let presentationData = context.sharedContext.currentPresentationData.with { $0 } let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let undoController = UndoOverlayController(presentationData: presentationData, content: .image(image: generateTintedImage(image: UIImage(bundleImageName: "Premium/BoostReplaceIcon"), color: .white)!, title: nil, text: "\(replacedBoosts) boosts are reassigned from \(inChannels) other channel.", round: false, undoText: nil), elevatedLayout: false, position: .bottom, action: { _ in return true }) let undoController = UndoOverlayController(presentationData: presentationData, content: .image(image: generateTintedImage(image: UIImage(bundleImageName: "Premium/BoostReplaceIcon"), color: .white)!, title: nil, text: presentationData.strings.ReassignBoost_Success(presentationData.strings.ReassignBoost_Boosts(replacedBoosts), presentationData.strings.ReassignBoost_OtherChannels(inChannels)).string, round: false, undoText: nil), elevatedLayout: false, position: .bottom, action: { _ in return true })
controller.present(undoController, in: .current) controller.present(undoController, in: .current)
} }
} }
@ -182,11 +180,12 @@ public func PremiumBoostScreen(
} }
} else { } else {
if isPremium { if isPremium {
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with({ $0 }))
let controller = textAlertController( let controller = textAlertController(
sharedContext: context.sharedContext, sharedContext: context.sharedContext,
updatedPresentationData: nil, updatedPresentationData: nil,
title: "More Boosts Needed", title: presentationData.strings.ChannelBoost_MoreBoosts_Title,
text: "To boost **\(peer.compactDisplayTitle)**, get more boosts by gifting **Telegram Premium** to a friend.", text: presentationData.strings.ChannelBoost_MoreBoosts_Text(peer.compactDisplayTitle, "\(premiumConfiguration.boostsPerGiftCount)").string,
actions: [ actions: [
TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {}) TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})
], ],

View File

@ -20,44 +20,48 @@ import AvatarNode
import TextFormat import TextFormat
import TelegramStringFormatting import TelegramStringFormatting
import UndoUI import UndoUI
import InvisibleInkDustNode
private final class PremiumGiftCodeSheetContent: CombinedComponent { private final class PremiumGiftCodeSheetContent: CombinedComponent {
typealias EnvironmentType = ViewControllerComponentContainer.Environment typealias EnvironmentType = ViewControllerComponentContainer.Environment
let context: AccountContext let context: AccountContext
let giftCode: PremiumGiftCodeInfo let subject: PremiumGiftCodeScreen.Subject
let action: () -> Void let action: () -> Void
let cancel: (Bool) -> Void let cancel: (Bool) -> Void
let openPeer: (EnginePeer) -> Void let openPeer: (EnginePeer) -> Void
let openMessage: (EngineMessage.Id) -> Void let openMessage: (EngineMessage.Id) -> Void
let copyLink: (String) -> Void let copyLink: (String) -> Void
let shareLink: (String) -> Void let shareLink: (String) -> Void
let displayHiddenTooltip: () -> Void
init( init(
context: AccountContext, context: AccountContext,
giftCode: PremiumGiftCodeInfo, subject: PremiumGiftCodeScreen.Subject,
action: @escaping () -> Void, action: @escaping () -> Void,
cancel: @escaping (Bool) -> Void, cancel: @escaping (Bool) -> Void,
openPeer: @escaping (EnginePeer) -> Void, openPeer: @escaping (EnginePeer) -> Void,
openMessage: @escaping (EngineMessage.Id) -> Void, openMessage: @escaping (EngineMessage.Id) -> Void,
copyLink: @escaping (String) -> Void, copyLink: @escaping (String) -> Void,
shareLink: @escaping (String) -> Void shareLink: @escaping (String) -> Void,
displayHiddenTooltip: @escaping () -> Void
) { ) {
self.context = context self.context = context
self.giftCode = giftCode self.subject = subject
self.action = action self.action = action
self.cancel = cancel self.cancel = cancel
self.openPeer = openPeer self.openPeer = openPeer
self.openMessage = openMessage self.openMessage = openMessage
self.copyLink = copyLink self.copyLink = copyLink
self.shareLink = shareLink self.shareLink = shareLink
self.displayHiddenTooltip = displayHiddenTooltip
} }
static func ==(lhs: PremiumGiftCodeSheetContent, rhs: PremiumGiftCodeSheetContent) -> Bool { static func ==(lhs: PremiumGiftCodeSheetContent, rhs: PremiumGiftCodeSheetContent) -> Bool {
if lhs.context !== rhs.context { if lhs.context !== rhs.context {
return false return false
} }
if lhs.giftCode != rhs.giftCode { if lhs.subject != rhs.subject {
return false return false
} }
return true return true
@ -72,16 +76,26 @@ private final class PremiumGiftCodeSheetContent: CombinedComponent {
var cachedCloseImage: (UIImage, PresentationTheme)? var cachedCloseImage: (UIImage, PresentationTheme)?
init(context: AccountContext, giftCode: PremiumGiftCodeInfo) { var inProgress = false
init(context: AccountContext, subject: PremiumGiftCodeScreen.Subject) {
self.context = context self.context = context
super.init() super.init()
var peerIds: [EnginePeer.Id] = [] var peerIds: [EnginePeer.Id] = []
switch subject {
case let .giftCode(giftCode):
peerIds.append(giftCode.fromPeerId) peerIds.append(giftCode.fromPeerId)
if let toPeerId = giftCode.toPeerId { if let toPeerId = giftCode.toPeerId {
peerIds.append(toPeerId) peerIds.append(toPeerId)
} }
case let .boost(channelId, boost):
peerIds.append(channelId)
if let peerId = boost.peer?.id {
peerIds.append(peerId)
}
}
self.disposable = (context.engine.data.get( self.disposable = (context.engine.data.get(
EngineDataMap( EngineDataMap(
@ -111,7 +125,7 @@ private final class PremiumGiftCodeSheetContent: CombinedComponent {
} }
func makeState() -> State { func makeState() -> State {
return State(context: self.context, giftCode: self.giftCode) return State(context: self.context, subject: self.subject)
} }
static var body: Body { static var body: Body {
@ -133,7 +147,7 @@ private final class PremiumGiftCodeSheetContent: CombinedComponent {
let accountContext = context.component.context let accountContext = context.component.context
let state = context.state let state = context.state
let giftCode = component.giftCode let subject = component.subject
let sideInset: CGFloat = 16.0 + environment.safeInsets.left let sideInset: CGFloat = 16.0 + environment.safeInsets.left
let textSideInset: CGFloat = 32.0 + environment.safeInsets.left let textSideInset: CGFloat = 32.0 + environment.safeInsets.left
@ -161,17 +175,64 @@ private final class PremiumGiftCodeSheetContent: CombinedComponent {
let descriptionText: String let descriptionText: String
let additionalText: String let additionalText: String
let buttonText: String let buttonText: String
let link: String?
let date: Int32
let fromPeer: EnginePeer?
var toPeerId: EnginePeer.Id?
let toPeer: EnginePeer?
let months: Int32
var gloss = false
switch subject {
case let .giftCode(giftCode):
gloss = !giftCode.isUsed
if let usedDate = giftCode.usedDate { if let usedDate = giftCode.usedDate {
let dateString = stringForMediumDate(timestamp: usedDate, strings: strings, dateTimeFormat: dateTimeFormat) let dateString = stringForMediumDate(timestamp: usedDate, strings: strings, dateTimeFormat: dateTimeFormat)
titleText = "Used Gift Link" titleText = strings.GiftLink_UsedTitle
descriptionText = "This link was used to activate a **Telegram Premium** subscription." descriptionText = strings.GiftLink_UsedDescription
additionalText = "This link was used on \(dateString)." additionalText = strings.GiftLink_UsedFooter(dateString).string
buttonText = strings.Common_OK buttonText = strings.Common_OK
} else { } else {
titleText = "Gift Link" titleText = strings.GiftLink_Title
descriptionText = "This link allows you to activate a **Telegram Premium** subscription." descriptionText = strings.GiftLink_Description
additionalText = "You can also [send this link]() to a friend as a gift." additionalText = strings.GiftLink_Footer
buttonText = "Use Link" buttonText = strings.GiftLink_UseLink
}
link = "https://t.me/giftcode/\(giftCode.slug)"
date = giftCode.date
fromPeer = state.peerMap[giftCode.fromPeerId]
toPeerId = giftCode.toPeerId
if let toPeerId = giftCode.toPeerId {
toPeer = state.peerMap[toPeerId]
} else {
toPeer = nil
}
months = giftCode.months
case let .boost(channelId, boost):
titleText = strings.GiftLink_Title
if let peer = boost.peer, !boost.flags.contains(.isUnclaimed) {
toPeer = boost.peer
if boost.slug == nil {
descriptionText = strings.GiftLink_PersonalDescription(peer.compactDisplayTitle).string
} else {
descriptionText = strings.GiftLink_PersonalUsedDescription(peer.compactDisplayTitle).string
}
} else {
toPeer = nil
descriptionText = strings.GiftLink_UnclaimedDescription
}
if boost.slug == nil {
additionalText = strings.GiftLink_NotUsedFooter
} else {
additionalText = ""
}
buttonText = strings.Common_OK
link = nil
date = boost.date
toPeerId = boost.peer?.id
fromPeer = state.peerMap[channelId]
months = Int32(round(Float(boost.expires - boost.date) / (86400.0 * 30.0)))
} }
let title = title.update( let title = title.update(
@ -213,14 +274,17 @@ private final class PremiumGiftCodeSheetContent: CombinedComponent {
transition: .immediate transition: .immediate
) )
let link = "https://t.me/giftcode/\(giftCode.slug)"
let linkButton = linkButton.update( let linkButton = linkButton.update(
component: Button( component: Button(
content: AnyComponent( content: AnyComponent(
LinkButtonContentComponent(theme: environment.theme, text: link) LinkButtonContentComponent(theme: environment.theme, text: link)
), ),
action: { action: {
if let link {
component.copyLink(link) component.copyLink(link)
} else {
component.displayHiddenTooltip()
}
} }
), ),
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: 50.0), availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: 50.0),
@ -232,10 +296,9 @@ private final class PremiumGiftCodeSheetContent: CombinedComponent {
let tableLinkColor = theme.list.itemAccentColor let tableLinkColor = theme.list.itemAccentColor
var tableItems: [TableComponent.Item] = [] var tableItems: [TableComponent.Item] = []
let fromPeer = state.peerMap[giftCode.fromPeerId]
tableItems.append(.init( tableItems.append(.init(
id: "from", id: "from",
title: "From", title: strings.GiftLink_From,
component: AnyComponent( component: AnyComponent(
Button( Button(
content: AnyComponent(PeerCellComponent(context: context.component.context, textColor: tableLinkColor, peer: fromPeer)), content: AnyComponent(PeerCellComponent(context: context.component.context, textColor: tableLinkColor, peer: fromPeer)),
@ -250,17 +313,16 @@ private final class PremiumGiftCodeSheetContent: CombinedComponent {
) )
) )
)) ))
if let toPeerId = giftCode.toPeerId { if let toPeer {
let toPeer = state.peerMap[toPeerId]
tableItems.append(.init( tableItems.append(.init(
id: "to", id: "to",
title: "To", title: strings.GiftLink_To,
component: AnyComponent( component: AnyComponent(
Button( Button(
content: AnyComponent(PeerCellComponent(context: context.component.context, textColor: tableLinkColor, peer: toPeer)), content: AnyComponent(PeerCellComponent(context: context.component.context, textColor: tableLinkColor, peer: toPeer)),
action: { action: {
if let peer = toPeer, peer.id != accountContext.account.peerId { if toPeer.id != accountContext.account.peerId {
component.openPeer(peer) component.openPeer(toPeer)
Queue.mainQueue().after(1.0, { Queue.mainQueue().after(1.0, {
component.cancel(false) component.cancel(false)
}) })
@ -269,38 +331,34 @@ private final class PremiumGiftCodeSheetContent: CombinedComponent {
) )
) )
)) ))
} else if giftCode.isGiveaway { } else if toPeerId == nil {
tableItems.append(.init( tableItems.append(.init(
id: "to", id: "to",
title: "To", title: strings.GiftLink_To,
component: AnyComponent( component: AnyComponent(
MultilineTextComponent(text: .plain(NSAttributedString(string: "No recipient", font: tableFont, textColor: tableTextColor))) MultilineTextComponent(text: .plain(NSAttributedString(string: strings.GiftLink_NoRecipient, font: tableFont, textColor: tableTextColor)))
) )
)) ))
} }
let giftTitle: String let giftTitle = strings.GiftLink_TelegramPremium(months)
if giftCode.months == 12 {
giftTitle = "Telegram Premium for 1 year"
} else {
giftTitle = "Telegram Premium for \(giftCode.months) months"
}
tableItems.append(.init( tableItems.append(.init(
id: "gift", id: "gift",
title: "Gift", title: strings.GiftLink_Gift,
component: AnyComponent( component: AnyComponent(
MultilineTextComponent(text: .plain(NSAttributedString(string: giftTitle, font: tableFont, textColor: tableTextColor))) MultilineTextComponent(text: .plain(NSAttributedString(string: giftTitle, font: tableFont, textColor: tableTextColor)))
) )
)) ))
if case let .giftCode(giftCode) = component.subject {
let giftReason: String let giftReason: String
if giftCode.toPeerId == nil { if giftCode.toPeerId == nil {
giftReason = "Incomplete Giveaway" giftReason = strings.GiftLink_Reason_Unclaimed
} else { } else {
giftReason = giftCode.isGiveaway ? "Giveaway" : "You were selected by the channel" giftReason = giftCode.isGiveaway ? strings.GiftLink_Reason_Giveaway : strings.GiftLink_Reason_Gift
} }
tableItems.append(.init( tableItems.append(.init(
id: "reason", id: "reason",
title: "Reason", title: strings.GiftLink_Reason,
component: AnyComponent( component: AnyComponent(
Button( Button(
content: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: giftReason, font: tableFont, textColor: giftCode.messageId != nil ? tableLinkColor : tableTextColor)))), content: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: giftReason, font: tableFont, textColor: giftCode.messageId != nil ? tableLinkColor : tableTextColor)))),
@ -316,11 +374,12 @@ private final class PremiumGiftCodeSheetContent: CombinedComponent {
) )
) )
)) ))
}
tableItems.append(.init( tableItems.append(.init(
id: "date", id: "date",
title: "Date", title: strings.GiftLink_Date,
component: AnyComponent( component: AnyComponent(
MultilineTextComponent(text: .plain(NSAttributedString(string: stringForMediumDate(timestamp: giftCode.date, strings: strings, dateTimeFormat: dateTimeFormat), font: tableFont, textColor: tableTextColor))) MultilineTextComponent(text: .plain(NSAttributedString(string: stringForMediumDate(timestamp: date, strings: strings, dateTimeFormat: dateTimeFormat), font: tableFont, textColor: tableTextColor)))
) )
)) ))
@ -348,7 +407,9 @@ private final class PremiumGiftCodeSheetContent: CombinedComponent {
} }
}, },
tapAction: { attributes, _ in tapAction: { attributes, _ in
component.shareLink("https://t.me/giftcode/\(giftCode.slug)") if let link {
component.shareLink(link)
}
} }
), ),
availableSize: CGSize(width: context.availableSize.width - textSideInset * 2.0, height: context.availableSize.height), availableSize: CGSize(width: context.availableSize.width - textSideInset * 2.0, height: context.availableSize.height),
@ -363,15 +424,20 @@ private final class PremiumGiftCodeSheetContent: CombinedComponent {
fontSize: 17.0, fontSize: 17.0,
height: 50.0, height: 50.0,
cornerRadius: 10.0, cornerRadius: 10.0,
gloss: !giftCode.isUsed, gloss: gloss,
iconName: nil, iconName: nil,
animationName: nil, animationName: nil,
iconPosition: .left, iconPosition: .left,
action: { isLoading: state.inProgress,
if giftCode.isUsed { action: { [weak state] in
component.cancel(true) if gloss {
} else {
component.action() component.action()
if let state {
state.inProgress = true
state.updated()
}
} else {
component.cancel(true)
} }
} }
), ),
@ -430,36 +496,39 @@ private final class PremiumGiftCodeSheetComponent: CombinedComponent {
typealias EnvironmentType = ViewControllerComponentContainer.Environment typealias EnvironmentType = ViewControllerComponentContainer.Environment
let context: AccountContext let context: AccountContext
let giftCode: PremiumGiftCodeInfo let subject: PremiumGiftCodeScreen.Subject
let action: () -> Void let action: () -> Void
let openPeer: (EnginePeer) -> Void let openPeer: (EnginePeer) -> Void
let openMessage: (EngineMessage.Id) -> Void let openMessage: (EngineMessage.Id) -> Void
let copyLink: (String) -> Void let copyLink: (String) -> Void
let shareLink: (String) -> Void let shareLink: (String) -> Void
let displayHiddenTooltip: () -> Void
init( init(
context: AccountContext, context: AccountContext,
giftCode: PremiumGiftCodeInfo, subject: PremiumGiftCodeScreen.Subject,
action: @escaping () -> Void, action: @escaping () -> Void,
openPeer: @escaping (EnginePeer) -> Void, openPeer: @escaping (EnginePeer) -> Void,
openMessage: @escaping (EngineMessage.Id) -> Void, openMessage: @escaping (EngineMessage.Id) -> Void,
copyLink: @escaping (String) -> Void, copyLink: @escaping (String) -> Void,
shareLink: @escaping (String) -> Void shareLink: @escaping (String) -> Void,
displayHiddenTooltip: @escaping () -> Void
) { ) {
self.context = context self.context = context
self.giftCode = giftCode self.subject = subject
self.action = action self.action = action
self.openPeer = openPeer self.openPeer = openPeer
self.openMessage = openMessage self.openMessage = openMessage
self.copyLink = copyLink self.copyLink = copyLink
self.shareLink = shareLink self.shareLink = shareLink
self.displayHiddenTooltip = displayHiddenTooltip
} }
static func ==(lhs: PremiumGiftCodeSheetComponent, rhs: PremiumGiftCodeSheetComponent) -> Bool { static func ==(lhs: PremiumGiftCodeSheetComponent, rhs: PremiumGiftCodeSheetComponent) -> Bool {
if lhs.context !== rhs.context { if lhs.context !== rhs.context {
return false return false
} }
if lhs.giftCode != rhs.giftCode { if lhs.subject != rhs.subject {
return false return false
} }
return true return true
@ -477,7 +546,7 @@ private final class PremiumGiftCodeSheetComponent: CombinedComponent {
component: SheetComponent<EnvironmentType>( component: SheetComponent<EnvironmentType>(
content: AnyComponent<EnvironmentType>(PremiumGiftCodeSheetContent( content: AnyComponent<EnvironmentType>(PremiumGiftCodeSheetContent(
context: context.component.context, context: context.component.context,
giftCode: context.component.giftCode, subject: context.component.subject,
action: context.component.action, action: context.component.action,
cancel: { animate in cancel: { animate in
if animate { if animate {
@ -493,9 +562,11 @@ private final class PremiumGiftCodeSheetComponent: CombinedComponent {
openPeer: context.component.openPeer, openPeer: context.component.openPeer,
openMessage: context.component.openMessage, openMessage: context.component.openMessage,
copyLink: context.component.copyLink, copyLink: context.component.copyLink,
shareLink: context.component.shareLink shareLink: context.component.shareLink,
displayHiddenTooltip: context.component.displayHiddenTooltip
)), )),
backgroundColor: .color(environment.theme.actionSheet.opaqueItemBackgroundColor), backgroundColor: .color(environment.theme.actionSheet.opaqueItemBackgroundColor),
followContentSizeChanges: true,
animateOut: animateOut animateOut: animateOut
), ),
environment: { environment: {
@ -534,6 +605,11 @@ private final class PremiumGiftCodeSheetComponent: CombinedComponent {
} }
public class PremiumGiftCodeScreen: ViewControllerComponentContainer { public class PremiumGiftCodeScreen: ViewControllerComponentContainer {
public enum Subject: Equatable {
case giftCode(PremiumGiftCodeInfo)
case boost(EnginePeer.Id, ChannelBoostersContext.State.Boost)
}
private let context: AccountContext private let context: AccountContext
public var disposed: () -> Void = {} public var disposed: () -> Void = {}
@ -541,7 +617,7 @@ public class PremiumGiftCodeScreen: ViewControllerComponentContainer {
public init( public init(
context: AccountContext, context: AccountContext,
giftCode: PremiumGiftCodeInfo, subject: PremiumGiftCodeScreen.Subject,
forceDark: Bool = false, forceDark: Bool = false,
action: @escaping () -> Void, action: @escaping () -> Void,
openPeer: @escaping (EnginePeer) -> Void = { _ in }, openPeer: @escaping (EnginePeer) -> Void = { _ in },
@ -551,9 +627,27 @@ public class PremiumGiftCodeScreen: ViewControllerComponentContainer {
self.context = context self.context = context
var copyLinkImpl: ((String) -> Void)? var copyLinkImpl: ((String) -> Void)?
super.init(context: context, component: PremiumGiftCodeSheetComponent(context: context, giftCode: giftCode, action: action, openPeer: openPeer, openMessage: openMessage, copyLink: { link in var displayHiddenTooltipImpl: (() -> Void)?
super.init(
context: context,
component: PremiumGiftCodeSheetComponent(
context: context,
subject: subject,
action: action,
openPeer: openPeer,
openMessage: openMessage,
copyLink: { link in
copyLinkImpl?(link) copyLinkImpl?(link)
}, shareLink: shareLink), navigationBarAppearance: .none, statusBarStyle: .ignore, theme: forceDark ? .dark : .default) },
shareLink: shareLink,
displayHiddenTooltip: {
displayHiddenTooltipImpl?()
}
),
navigationBarAppearance: .none,
statusBarStyle: .ignore,
theme: forceDark ? .dark : .default
)
self.navigationPresentation = .flatModal self.navigationPresentation = .flatModal
@ -563,9 +657,21 @@ public class PremiumGiftCodeScreen: ViewControllerComponentContainer {
guard let self else { guard let self else {
return return
} }
self.dismissAllTooltips()
let presentationData = context.sharedContext.currentPresentationData.with { $0 } let presentationData = context.sharedContext.currentPresentationData.with { $0 }
self.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, position: .top, action: { _ in return true }), in: .window(.root)) self.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, position: .top, action: { _ in return true }), in: .window(.root))
} }
displayHiddenTooltipImpl = { [weak self] in
guard let self else {
return
}
self.dismissAllTooltips()
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
self.present(UndoOverlayController(presentationData: presentationData, content: .info(title: nil, text: presentationData.strings.GiftLink_LinkHidden, timeout: nil, customUndoText: nil), elevatedLayout: false, position: .top, action: { _ in return true }), in: .window(.root))
}
} }
required public init(coder aDecoder: NSCoder) { required public init(coder aDecoder: NSCoder) {
@ -581,15 +687,40 @@ public class PremiumGiftCodeScreen: ViewControllerComponentContainer {
self.view.disablesInteractiveModalDismiss = true self.view.disablesInteractiveModalDismiss = true
} }
public override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
self.dismissAllTooltips()
}
public func dismissAnimated() {
if let view = self.node.hostView.findTaggedView(tag: SheetComponent<ViewControllerComponentContainer.Environment>.View.Tag()) as? SheetComponent<ViewControllerComponentContainer.Environment>.View {
view.dismissAnimated()
}
}
fileprivate func dismissAllTooltips() {
self.window?.forEachController({ controller in
if let controller = controller as? UndoOverlayController {
controller.dismiss()
}
})
self.forEachController({ controller in
if let controller = controller as? UndoOverlayController {
controller.dismiss()
}
return true
})
}
} }
private final class LinkButtonContentComponent: CombinedComponent { private final class LinkButtonContentComponent: CombinedComponent {
let theme: PresentationTheme let theme: PresentationTheme
let text: String let text: String?
public init( public init(
theme: PresentationTheme, theme: PresentationTheme,
text: String text: String?
) { ) {
self.theme = theme self.theme = theme
self.text = text self.text = text
@ -609,6 +740,7 @@ private final class LinkButtonContentComponent: CombinedComponent {
let background = Child(RoundedRectangle.self) let background = Child(RoundedRectangle.self)
let text = Child(MultilineTextComponent.self) let text = Child(MultilineTextComponent.self)
let icon = Child(BundleIconComponent.self) let icon = Child(BundleIconComponent.self)
let dust = Child(DustComponent.self)
return { context in return { context in
let component = context.component let component = context.component
@ -621,10 +753,15 @@ private final class LinkButtonContentComponent: CombinedComponent {
transition: context.transition transition: context.transition
) )
context.add(background
.position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0))
)
if let _ = component.text {
let text = text.update( let text = text.update(
component: MultilineTextComponent( component: MultilineTextComponent(
text: .plain(NSAttributedString( text: .plain(NSAttributedString(
string: component.text.replacingOccurrences(of: "https://", with: ""), string: (component.text ?? "").replacingOccurrences(of: "https://", with: ""),
font: Font.regular(17.0), font: Font.regular(17.0),
textColor: component.theme.list.itemPrimaryTextColor, textColor: component.theme.list.itemPrimaryTextColor,
paragraphAlignment: .natural paragraphAlignment: .natural
@ -641,16 +778,23 @@ private final class LinkButtonContentComponent: CombinedComponent {
availableSize: context.availableSize, availableSize: context.availableSize,
transition: context.transition transition: context.transition
) )
context.add(icon
context.add(background .position(CGPoint(x: context.availableSize.width - icon.size.width / 2.0 - 14.0, y: context.availableSize.height / 2.0))
.position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0))
) )
context.add(text context.add(text
.position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0)) .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0))
) )
context.add(icon } else {
.position(CGPoint(x: context.availableSize.width - icon.size.width / 2.0 - 14.0, y: context.availableSize.height / 2.0)) let dust = dust.update(
component: DustComponent(color: component.theme.list.itemSecondaryTextColor),
availableSize: CGSize(width: context.availableSize.width * 0.8, height: context.availableSize.height * 0.54),
transition: context.transition
) )
context.add(dust
.position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0))
)
}
return context.availableSize return context.availableSize
} }
} }
@ -955,3 +1099,53 @@ private final class PeerCellComponent: Component {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
} }
} }
private final class DustComponent: Component {
let color: UIColor
init(color: UIColor) {
self.color = color
}
static func ==(lhs: DustComponent, rhs: DustComponent) -> Bool {
if lhs.color != rhs.color {
return false
}
return true
}
final class View: UIView {
private let dustView = InvisibleInkDustView(textNode: nil, enableAnimations: true)
private var component: DustComponent?
private weak var state: EmptyComponentState?
override init(frame: CGRect) {
super.init(frame: frame)
self.addSubview(self.dustView)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(component: DustComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
self.component = component
self.state = state
let rects: [CGRect] = [CGRect(origin: .zero, size: availableSize).insetBy(dx: 5.0, dy: 5.0)]
self.dustView.update(size: availableSize, color: component.color, textColor: component.color, rects: rects, wordRects: rects)
return availableSize
}
}
func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}

View File

@ -1522,10 +1522,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
guard let self else { guard let self else {
return return
} }
var newPerks: [String] = [] let newPerks: [String] = []
if !dismissedPremiumAppIconsBadge {
newPerks.append(PremiumPerk.appIcons.identifier)
}
self.newPerks = newPerks self.newPerks = newPerks
self.updated() self.updated()
}) })
@ -1856,7 +1853,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
demoSubject = .animatedUserpics demoSubject = .animatedUserpics
case .appIcons: case .appIcons:
demoSubject = .appIcons demoSubject = .appIcons
let _ = ApplicationSpecificNotice.setDismissedPremiumAppIconsBadge(accountManager: accountContext.sharedContext.accountManager).startStandalone() // let _ = ApplicationSpecificNotice.setDismissedPremiumAppIconsBadge(accountManager: accountContext.sharedContext.accountManager).startStandalone()
case .animatedEmoji: case .animatedEmoji:
demoSubject = .animatedEmoji demoSubject = .animatedEmoji
case .emojiStatus: case .emojiStatus:
@ -2918,8 +2915,8 @@ public final class PremiumIntroScreen: ViewControllerComponentContainer {
} }
completionImpl = { [weak self] in completionImpl = { [weak self] in
if let strongSelf = self { if let self {
strongSelf.view.addSubview(ConfettiView(frame: strongSelf.view.bounds)) self.animateSuccess()
} }
} }
} }
@ -2933,6 +2930,10 @@ public final class PremiumIntroScreen: ViewControllerComponentContainer {
self.wasDismissed?() self.wasDismissed?()
} }
public func animateSuccess() {
self.view.addSubview(ConfettiView(frame: self.view.bounds))
}
public override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { public override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition) super.containerLayoutUpdated(layout, transition: transition)

View File

@ -19,8 +19,6 @@ import PeerListItemComponent
import TelegramStringFormatting import TelegramStringFormatting
import AvatarNode import AvatarNode
//TODO:localize
private final class ReplaceBoostScreenComponent: CombinedComponent { private final class ReplaceBoostScreenComponent: CombinedComponent {
typealias EnvironmentType = ViewControllerComponentContainer.Environment typealias EnvironmentType = ViewControllerComponentContainer.Environment
@ -104,7 +102,6 @@ private final class ReplaceBoostScreenComponent: CombinedComponent {
} }
static var body: Body { static var body: Body {
// let closeButton = Child(Button.self)
let header = Child(ReplaceBoostHeaderComponent.self) let header = Child(ReplaceBoostHeaderComponent.self)
let description = Child(MultilineTextComponent.self) let description = Child(MultilineTextComponent.self)
let boostsBackground = Child(RoundedRectangle.self) let boostsBackground = Child(RoundedRectangle.self)
@ -117,7 +114,6 @@ private final class ReplaceBoostScreenComponent: CombinedComponent {
let theme = environment.theme let theme = environment.theme
let strings = environment.strings let strings = environment.strings
// let topInset: CGFloat = environment.navigationHeight + 22.0
let textSideInset: CGFloat = 32.0 let textSideInset: CGFloat = 32.0
let sideInset: CGFloat = 16.0 + environment.safeInsets.left let sideInset: CGFloat = 16.0 + environment.safeInsets.left
@ -168,8 +164,13 @@ private final class ReplaceBoostScreenComponent: CombinedComponent {
return (TelegramTextAttributes.URL, contents) return (TelegramTextAttributes.URL, contents)
}) })
let channelName = state.peer?.compactDisplayTitle ?? "" let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.component.context.currentAppConfiguration.with({ $0 }))
let descriptionString = "To boost **\(channelName)**, reassign a previous boost or gift **Telegram Premium** to a friend to get **3** additional boosts."
var channelName = state.peer?.compactDisplayTitle ?? ""
if channelName.count > 48 {
channelName = "\(channelName.prefix(48))..."
}
let descriptionString = strings.ReassignBoost_Description(channelName, "\(premiumConfiguration.boostsPerGiftCount)").string
let description = description.update( let description = description.update(
component: MultilineTextComponent( component: MultilineTextComponent(
@ -201,11 +202,11 @@ private final class ReplaceBoostScreenComponent: CombinedComponent {
if let cooldownUntil = boost.cooldownUntil, cooldownUntil > state.currentTime { if let cooldownUntil = boost.cooldownUntil, cooldownUntil > state.currentTime {
let duration = cooldownUntil - state.currentTime let duration = cooldownUntil - state.currentTime
let durationValue = stringForDuration(duration, position: nil) let durationValue = stringForDuration(duration, position: nil)
subtitle = "Available in \(durationValue)" subtitle = strings.ReassignBoost_AvailableIn(durationValue).string
isEnabled = false isEnabled = false
} else { } else {
let expiresValue = stringForDate(timestamp: boost.expires, strings: strings) let expiresValue = stringForDate(timestamp: boost.expires, strings: strings)
subtitle = "Boost expires on \(expiresValue)" subtitle = strings.ReassignBoost_ExpiresOn(expiresValue).string
} }
let accountContext = context.component.context let accountContext = context.component.context
@ -242,7 +243,8 @@ private final class ReplaceBoostScreenComponent: CombinedComponent {
selectedSlotsUpdated(state.selectedSlots) selectedSlotsUpdated(state.selectedSlots)
} else { } else {
let presentationData = accountContext.sharedContext.currentPresentationData.with { $0 } let presentationData = accountContext.sharedContext.currentPresentationData.with { $0 }
let undoController = UndoOverlayController(presentationData: presentationData, content: .info(title: nil, text: "Wait until the boost is available or get **3** more boosts by gifting a **Telegram Premium** subscription.", timeout: nil, customUndoText: nil), elevatedLayout: false, position: .top, action: { _ in return true })
let undoController = UndoOverlayController(presentationData: presentationData, content: .info(title: nil, text: strings.ReassignBoost_WaitForCooldown("\(premiumConfiguration.boostsPerGiftCount)").string, timeout: nil, customUndoText: nil), elevatedLayout: false, position: .top, action: { _ in return true })
presentController(undoController) presentController(undoController)
} }
}) })
@ -254,7 +256,7 @@ private final class ReplaceBoostScreenComponent: CombinedComponent {
let boosts = boosts.update( let boosts = boosts.update(
component: List(boostItems), component: List(boostItems),
environment: {}, environment: {},
availableSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 1000.0), availableSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100000.0),
transition: context.transition transition: context.transition
) )
@ -272,9 +274,7 @@ private final class ReplaceBoostScreenComponent: CombinedComponent {
.position(CGPoint(x: availableSize.width / 2.0, y: 226 + boosts.size.height / 2.0)) .position(CGPoint(x: availableSize.width / 2.0, y: 226 + boosts.size.height / 2.0))
) )
let contentSize = CGSize(width: availableSize.width, height: 226.0 + boosts.size.height) return CGSize(width: availableSize.width, height: 226.0 + boosts.size.height + environment.safeInsets.bottom + 91.0)
return contentSize
} }
} }
} }
@ -293,6 +293,8 @@ public class ReplaceBoostScreen: ViewController {
let hostView: ComponentHostView<ViewControllerComponentContainer.Environment> let hostView: ComponentHostView<ViewControllerComponentContainer.Environment>
private let footerView: FooterView private let footerView: FooterView
private var footerHeight: CGFloat = 0.0
private var bottomOffset: CGFloat = 1000.0
private(set) var isExpanded = false private(set) var isExpanded = false
private var panGestureRecognizer: UIPanGestureRecognizer? private var panGestureRecognizer: UIPanGestureRecognizer?
@ -350,6 +352,7 @@ public class ReplaceBoostScreen: ViewController {
} }
self.controller?.replaceBoosts?(self.selectedSlots) self.controller?.replaceBoosts?(self.selectedSlots)
} }
self.footerView.updateBackgroundAlpha(1.0, transition: .immediate)
} }
override func didLoad() { override func didLoad() {
@ -382,9 +385,22 @@ public class ReplaceBoostScreen: ViewController {
return true return true
} }
private func updateFooterAlpha() {
guard let (layout, _) = self.currentLayout else {
return
}
let contentFrame = self.scrollView.convert(self.hostView.frame, to: self.view)
let bottomOffset = contentFrame.maxY - layout.size.height
let backgroundAlpha: CGFloat = min(30.0, max(0.0, bottomOffset)) / 30.0
self.footerView.updateBackgroundAlpha(backgroundAlpha, transition: .immediate)
}
func scrollViewDidScroll(_ scrollView: UIScrollView) { func scrollViewDidScroll(_ scrollView: UIScrollView) {
let contentOffset = self.scrollView.contentOffset.y let contentOffset = self.scrollView.contentOffset.y
self.controller?.navigationBar?.updateBackgroundAlpha(min(30.0, contentOffset) / 30.0, transition: .immediate) self.controller?.navigationBar?.updateBackgroundAlpha(min(30.0, contentOffset) / 30.0, transition: .immediate)
self.updateFooterAlpha()
} }
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
@ -429,6 +445,7 @@ public class ReplaceBoostScreen: ViewController {
} }
func containerLayoutUpdated(layout: ContainerViewLayout, navigationHeight: CGFloat, transition: Transition) { func containerLayoutUpdated(layout: ContainerViewLayout, navigationHeight: CGFloat, transition: Transition) {
let hadLayout = self.currentLayout != nil
self.currentLayout = (layout, navigationHeight) self.currentLayout = (layout, navigationHeight)
if let controller = self.controller, let navigationBar = controller.navigationBar, navigationBar.view.superview !== self.wrappingView { if let controller = self.controller, let navigationBar = controller.navigationBar, navigationBar.view.superview !== self.wrappingView {
@ -541,9 +558,11 @@ public class ReplaceBoostScreen: ViewController {
let footerInsets = UIEdgeInsets(top: 0.0, left: layout.safeInsets.left, bottom: layout.intrinsicInsets.bottom, right: layout.safeInsets.right) let footerInsets = UIEdgeInsets(top: 0.0, left: layout.safeInsets.left, bottom: layout.intrinsicInsets.bottom, right: layout.safeInsets.right)
transition.setFrame(view: self.footerView, frame: CGRect(origin: CGPoint(x: 0.0, y: -topInset), size: layout.size)) transition.setFrame(view: self.footerView, frame: CGRect(origin: CGPoint(x: 0.0, y: -topInset), size: layout.size))
let _ = self.footerView.update(size: layout.size, insets: footerInsets, theme: self.presentationData.theme, count: Int32(self.selectedSlots.count)) self.footerHeight = self.footerView.update(size: layout.size, insets: footerInsets, theme: self.presentationData.theme, strings: self.presentationData.strings, count: Int32(self.selectedSlots.count))
self.footerView.updateBackgroundAlpha(0.0, transition: .immediate) if !hadLayout {
self.updateFooterAlpha()
}
} }
private var didPlayAppearAnimation = false private var didPlayAppearAnimation = false
@ -574,7 +593,7 @@ public class ReplaceBoostScreen: ViewController {
factor = 0.15 factor = 0.15
} }
if self.scrollView.contentSize.height > 0.0 && self.scrollView.contentSize.height < layout.size.height / 2.0 { if self.scrollView.contentSize.height > 0.0 && self.scrollView.contentSize.height < layout.size.height / 2.0 {
return layout.size.height - self.scrollView.contentSize.height - layout.intrinsicInsets.bottom - 154.0 return layout.size.height - self.scrollView.contentSize.height - layout.intrinsicInsets.bottom - 30.0
} else { } else {
return floor(max(layout.size.width, layout.size.height) * factor) return floor(max(layout.size.width, layout.size.height) * factor)
} }
@ -673,6 +692,8 @@ public class ReplaceBoostScreen: ViewController {
self.bounds = bounds self.bounds = bounds
self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: .immediate) self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: .immediate)
self.updateFooterAlpha()
case .ended: case .ended:
guard let (currentTopInset, panOffset, scrollView, listNode) = self.panGestureArguments else { guard let (currentTopInset, panOffset, scrollView, listNode) = self.panGestureArguments else {
return return
@ -760,10 +781,14 @@ public class ReplaceBoostScreen: ViewController {
self.bounds = bounds self.bounds = bounds
self.layer.animateBounds(from: previousBounds, to: self.bounds, duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue) self.layer.animateBounds(from: previousBounds, to: self.bounds, duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue)
} }
self.updateFooterAlpha()
case .cancelled: case .cancelled:
self.panGestureArguments = nil self.panGestureArguments = nil
self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: Transition(.animated(duration: 0.3, curve: .easeInOut))) self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: Transition(.animated(duration: 0.3, curve: .easeInOut)))
self.updateFooterAlpha()
default: default:
break break
} }
@ -812,9 +837,9 @@ public class ReplaceBoostScreen: ViewController {
presentControllerImpl?(c) presentControllerImpl?(c)
})) }))
self.title = "Reassign Boosts" self.title = presentationData.strings.ReassignBoost_Title
self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: presentationData.strings.Common_Close, style: .plain, target: self, action: #selector(self.cancelPressed)) self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(self.cancelPressed))
self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait) self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait)
@ -890,6 +915,11 @@ public class ReplaceBoostScreen: ViewController {
self.node.updateIsVisible(isVisible: true) self.node.updateIsVisible(isVisible: true)
} }
override open func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
self.dismissAllTooltips()
}
override open func viewDidDisappear(_ animated: Bool) { override open func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated) super.viewDidDisappear(animated)
@ -953,10 +983,10 @@ private final class FooterView: UIView {
fileprivate var inProgress = false fileprivate var inProgress = false
private var currentLayout: (CGSize, UIEdgeInsets, PresentationTheme, Int32)? private var currentLayout: (CGSize, UIEdgeInsets, PresentationTheme, PresentationStrings, Int32)?
func update(size: CGSize, insets: UIEdgeInsets, theme: PresentationTheme, count: Int32) -> CGFloat { func update(size: CGSize, insets: UIEdgeInsets, theme: PresentationTheme, strings: PresentationStrings, count: Int32) -> CGFloat {
let hadLayout = self.currentLayout != nil let hadLayout = self.currentLayout != nil
self.currentLayout = (size, insets, theme, count) self.currentLayout = (size, insets, theme, strings, count)
self.backgroundNode.updateColor(color: theme.rootController.tabBar.backgroundColor, transition: .immediate) self.backgroundNode.updateColor(color: theme.rootController.tabBar.backgroundColor, transition: .immediate)
self.separatorView.backgroundColor = theme.rootController.tabBar.separatorColor self.separatorView.backgroundColor = theme.rootController.tabBar.separatorColor
@ -988,7 +1018,7 @@ private final class FooterView: UIView {
content: AnyComponentWithIdentity( content: AnyComponentWithIdentity(
id: AnyHashable(0), id: AnyHashable(0),
component: AnyComponent(ButtonTextContentComponent( component: AnyComponent(ButtonTextContentComponent(
text: "Reassign Boosts", text: strings.ReassignBoost_ReassignBoosts,
badge: Int(count), badge: Int(count),
textColor: theme.list.itemCheckColors.foregroundColor, textColor: theme.list.itemCheckColors.foregroundColor,
badgeBackground: theme.list.itemCheckColors.foregroundColor, badgeBackground: theme.list.itemCheckColors.foregroundColor,
@ -1005,8 +1035,8 @@ private final class FooterView: UIView {
return return
} }
self.inProgress = true self.inProgress = true
if let (size, insets, theme, count) = self.currentLayout { if let (size, insets, theme, strings, count) = self.currentLayout {
let _ = self.update(size: size, insets: insets, theme: theme, count: count) let _ = self.update(size: size, insets: insets, theme: theme, strings: strings, count: count)
} }
self.action() self.action()
} }

View File

@ -365,12 +365,6 @@ class ThemeSettingsAppIconItemNode: ListViewItemNode, ItemListItemNode {
name = item.strings.Appearance_AppIconBlack name = item.strings.Appearance_AppIconBlack
case "PremiumTurbo": case "PremiumTurbo":
name = item.strings.Appearance_AppIconTurbo name = item.strings.Appearance_AppIconTurbo
case "PremiumDuck":
name = item.strings.Appearance_AppIconDuck
case "PremiumCoffee":
name = item.strings.Appearance_AppIconCoffee
case "PremiumSteam":
name = item.strings.Appearance_AppIconSteam
default: default:
name = icon.name name = icon.name
} }

View File

@ -369,7 +369,7 @@ private enum ThemeSettingsControllerEntry: ItemListNodeEntry {
} }
} }
private func themeSettingsControllerEntries(presentationData: PresentationData, presentationThemeSettings: PresentationThemeSettings, mediaSettings: MediaDisplaySettings, themeReference: PresentationThemeReference, availableThemes: [PresentationThemeReference], availableAppIcons: [PresentationAppIcon], currentAppIconName: String?, isPremium: Bool, chatThemes: [PresentationThemeReference], animatedEmojiStickers: [String: [StickerPackItem]], accountPeer: EnginePeer?) -> [ThemeSettingsControllerEntry] { private func themeSettingsControllerEntries(presentationData: PresentationData, presentationThemeSettings: PresentationThemeSettings, mediaSettings: MediaDisplaySettings, themeReference: PresentationThemeReference, availableThemes: [PresentationThemeReference], availableAppIcons: [PresentationAppIcon], currentAppIconName: String?, isPremium: Bool, chatThemes: [PresentationThemeReference], animatedEmojiStickers: [String: [StickerPackItem]], accountPeer: EnginePeer?, nameColors: PeerNameColors) -> [ThemeSettingsControllerEntry] {
var entries: [ThemeSettingsControllerEntry] = [] var entries: [ThemeSettingsControllerEntry] = []
let strings = presentationData.strings let strings = presentationData.strings
@ -382,7 +382,8 @@ private func themeSettingsControllerEntries(presentationData: PresentationData,
entries.append(.wallpaper(presentationData.theme, strings.Settings_ChatBackground)) entries.append(.wallpaper(presentationData.theme, strings.Settings_ChatBackground))
if let accountPeer { if let accountPeer {
entries.append(.nameColor(presentationData.theme, strings.Appearance_NameColor, accountPeer.compactDisplayTitle, (accountPeer.nameColor ?? .blue).color)) let colors = nameColors.get(accountPeer.nameColor ?? .blue)
entries.append(.nameColor(presentationData.theme, strings.Appearance_NameColor, accountPeer.compactDisplayTitle, colors.main))
} }
entries.append(.autoNight(presentationData.theme, strings.Appearance_NightTheme, presentationThemeSettings.automaticThemeSwitchSetting.force, !presentationData.autoNightModeTriggered || presentationThemeSettings.automaticThemeSwitchSetting.force)) entries.append(.autoNight(presentationData.theme, strings.Appearance_NightTheme, presentationThemeSettings.automaticThemeSwitchSetting.force, !presentationData.autoNightModeTriggered || presentationThemeSettings.automaticThemeSwitchSetting.force))
@ -1064,7 +1065,7 @@ public func themeSettingsController(context: AccountContext, focusOnItemTag: The
chatThemes.insert(.builtin(.dayClassic), at: 0) chatThemes.insert(.builtin(.dayClassic), at: 0)
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.Appearance_Title), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back)) let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.Appearance_Title), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back))
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: themeSettingsControllerEntries(presentationData: presentationData, presentationThemeSettings: settings, mediaSettings: mediaSettings, themeReference: themeReference, availableThemes: availableThemes, availableAppIcons: availableAppIcons, currentAppIconName: currentAppIconName, isPremium: isPremium, chatThemes: chatThemes, animatedEmojiStickers: animatedEmojiStickers, accountPeer: accountPeer), style: .blocks, ensureVisibleItemTag: focusOnItemTag, animateChanges: false) let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: themeSettingsControllerEntries(presentationData: presentationData, presentationThemeSettings: settings, mediaSettings: mediaSettings, themeReference: themeReference, availableThemes: availableThemes, availableAppIcons: availableAppIcons, currentAppIconName: currentAppIconName, isPremium: isPremium, chatThemes: chatThemes, animatedEmojiStickers: animatedEmojiStickers, accountPeer: accountPeer, nameColors: context.peerNameColors), style: .blocks, ensureVisibleItemTag: focusOnItemTag, animateChanges: false)
return (controllerState, (listState, arguments)) return (controllerState, (listState, arguments))
} }

View File

@ -22,7 +22,7 @@ import ShareController
import ItemListPeerActionItem import ItemListPeerActionItem
import PremiumUI import PremiumUI
private let maxUsersDisplayedLimit: Int32 = 50 private let maxUsersDisplayedLimit: Int32 = 5
private final class ChannelStatsControllerArguments { private final class ChannelStatsControllerArguments {
let context: AccountContext let context: AccountContext
@ -31,20 +31,20 @@ private final class ChannelStatsControllerArguments {
let contextAction: (MessageId, ASDisplayNode, ContextGesture?) -> Void let contextAction: (MessageId, ASDisplayNode, ContextGesture?) -> Void
let copyBoostLink: (String) -> Void let copyBoostLink: (String) -> Void
let shareBoostLink: (String) -> Void let shareBoostLink: (String) -> Void
let openPeer: (EnginePeer) -> Void let openBoost: (ChannelBoostersContext.State.Boost) -> Void
let expandBoosters: () -> Void let expandBoosters: () -> Void
let openGifts: () -> Void let openGifts: () -> Void
let createPrepaidGiveaway: (PrepaidGiveaway) -> Void let createPrepaidGiveaway: (PrepaidGiveaway) -> Void
let updateGiftsSelected: (Bool) -> Void let updateGiftsSelected: (Bool) -> Void
init(context: AccountContext, loadDetailedGraph: @escaping (StatsGraph, Int64) -> Signal<StatsGraph?, NoError>, openMessage: @escaping (MessageId) -> Void, contextAction: @escaping (MessageId, ASDisplayNode, ContextGesture?) -> Void, copyBoostLink: @escaping (String) -> Void, shareBoostLink: @escaping (String) -> Void, openPeer: @escaping (EnginePeer) -> Void, expandBoosters: @escaping () -> Void, openGifts: @escaping () -> Void, createPrepaidGiveaway: @escaping (PrepaidGiveaway) -> Void, updateGiftsSelected: @escaping (Bool) -> Void) { init(context: AccountContext, loadDetailedGraph: @escaping (StatsGraph, Int64) -> Signal<StatsGraph?, NoError>, openMessage: @escaping (MessageId) -> Void, contextAction: @escaping (MessageId, ASDisplayNode, ContextGesture?) -> Void, copyBoostLink: @escaping (String) -> Void, shareBoostLink: @escaping (String) -> Void, openBoost: @escaping (ChannelBoostersContext.State.Boost) -> Void, expandBoosters: @escaping () -> Void, openGifts: @escaping () -> Void, createPrepaidGiveaway: @escaping (PrepaidGiveaway) -> Void, updateGiftsSelected: @escaping (Bool) -> Void) {
self.context = context self.context = context
self.loadDetailedGraph = loadDetailedGraph self.loadDetailedGraph = loadDetailedGraph
self.openMessageStats = openMessage self.openMessageStats = openMessage
self.contextAction = contextAction self.contextAction = contextAction
self.copyBoostLink = copyBoostLink self.copyBoostLink = copyBoostLink
self.shareBoostLink = shareBoostLink self.shareBoostLink = shareBoostLink
self.openPeer = openPeer self.openBoost = openBoost
self.expandBoosters = expandBoosters self.expandBoosters = expandBoosters
self.openGifts = openGifts self.openGifts = openGifts
self.createPrepaidGiveaway = createPrepaidGiveaway self.createPrepaidGiveaway = createPrepaidGiveaway
@ -118,7 +118,7 @@ private enum StatsEntry: ItemListNodeEntry {
case boostersTitle(PresentationTheme, String) case boostersTitle(PresentationTheme, String)
case boostersPlaceholder(PresentationTheme, String) case boostersPlaceholder(PresentationTheme, String)
case boosterTabs(PresentationTheme, String, String, Bool) case boosterTabs(PresentationTheme, String, String, Bool)
case booster(Int32, PresentationTheme, PresentationDateTimeFormat, EnginePeer?, Int32, ChannelBoostersContext.State.Boost.Flags, Int32, Int32) case booster(Int32, PresentationTheme, PresentationDateTimeFormat, ChannelBoostersContext.State.Boost)
case boostersExpand(PresentationTheme, String) case boostersExpand(PresentationTheme, String)
case boostersInfo(PresentationTheme, String) case boostersInfo(PresentationTheme, String)
@ -232,7 +232,7 @@ private enum StatsEntry: ItemListNodeEntry {
return 2102 return 2102
case .boosterTabs: case .boosterTabs:
return 2103 return 2103
case let .booster(index, _, _, _, _, _, _, _): case let .booster(index, _, _, _):
return 2104 + index return 2104 + index
case .boostersExpand: case .boostersExpand:
return 10000 return 10000
@ -439,8 +439,8 @@ private enum StatsEntry: ItemListNodeEntry {
} else { } else {
return false return false
} }
case let .booster(lhsIndex, lhsTheme, lhsDateTimeFormat, lhsPeer, lhsCount, lhsFlags, lhsDate, lhsExpires): case let .booster(lhsIndex, lhsTheme, lhsDateTimeFormat, lhsBoost):
if case let .booster(rhsIndex, rhsTheme, rhsDateTimeFormat, rhsPeer, rhsCount, rhsFlags, rhsDate, rhsExpires) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsDateTimeFormat == rhsDateTimeFormat, lhsPeer == rhsPeer, lhsCount == rhsCount, lhsFlags == rhsFlags, lhsDate == rhsDate, lhsExpires == rhsExpires { if case let .booster(rhsIndex, rhsTheme, rhsDateTimeFormat, rhsBoost) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsDateTimeFormat == rhsDateTimeFormat, lhsBoost == rhsBoost {
return true return true
} else { } else {
return false return false
@ -548,26 +548,23 @@ private enum StatsEntry: ItemListNodeEntry {
return BoostsTabsItem(theme: presentationData.theme, boostsText: boostText, giftsText: giftText, selectedTab: giftSelected ? .gifts : .boosts, sectionId: self.section, selectionUpdated: { tab in return BoostsTabsItem(theme: presentationData.theme, boostsText: boostText, giftsText: giftText, selectedTab: giftSelected ? .gifts : .boosts, sectionId: self.section, selectionUpdated: { tab in
arguments.updateGiftsSelected(tab == .gifts) arguments.updateGiftsSelected(tab == .gifts)
}) })
case let .booster(_, _, _, peer, count, flags, date, expires): case let .booster(_, _, _, boost):
let expiresValue = stringForDate(timestamp: expires, strings: presentationData.strings) let count = boost.multiplier
let expiresValue = stringForDate(timestamp: boost.expires, strings: presentationData.strings)
let expiresString: String let expiresString: String
let durationMonths = Int32(round(Float(expires - date) / (86400.0 * 30.0))) let durationMonths = Int32(round(Float(boost.expires - boost.date) / (86400.0 * 30.0)))
let durationString = "\(durationMonths)m" let durationString = presentationData.strings.Stats_Boosts_ShortMonth("\(durationMonths)").string
let title: String let title: String
let icon: GiftOptionItem.Icon let icon: GiftOptionItem.Icon
var label: String? var label: String?
if flags.contains(.isGiveaway) { if boost.flags.contains(.isGiveaway) {
label = "🏆 Giveaway" label = "🏆 \(presentationData.strings.Stats_Boosts_Giveaway)"
} else if flags.contains(.isGift) { } else if boost.flags.contains(.isGift) {
label = "🎁 Gift" label = "🎁 \(presentationData.strings.Stats_Boosts_Gift)"
} }
if let peer {
title = peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)
icon = .peer(peer)
expiresString = presentationData.strings.Stats_Boosts_ExpiresOn(expiresValue).string
} else {
let color: GiftOptionItem.Icon.Color let color: GiftOptionItem.Icon.Color
if durationMonths > 11 { if durationMonths > 11 {
color = .red color = .red
@ -576,11 +573,25 @@ private enum StatsEntry: ItemListNodeEntry {
} else { } else {
color = .green color = .green
} }
if flags.contains(.isUnclaimed) {
title = "Unclaimed" if boost.flags.contains(.isUnclaimed) {
title = presentationData.strings.Stats_Boosts_Unclaimed
icon = .image(color: color, name: "Premium/Unclaimed") icon = .image(color: color, name: "Premium/Unclaimed")
} else if flags.contains(.isGiveaway) { expiresString = "\(durationString)\(expiresValue)"
title = "To be distributed" } else if let peer = boost.peer {
title = peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)
icon = .peer(peer)
if let _ = label {
expiresString = expiresValue
} else {
expiresString = presentationData.strings.Stats_Boosts_ExpiresOn(expiresValue).string
}
} else {
if boost.flags.contains(.isUnclaimed) {
title = presentationData.strings.Stats_Boosts_Unclaimed
icon = .image(color: color, name: "Premium/Unclaimed")
} else if boost.flags.contains(.isGiveaway) {
title = presentationData.strings.Stats_Boosts_ToBeDistributed
icon = .image(color: color, name: "Premium/ToBeDistributed") icon = .image(color: color, name: "Premium/ToBeDistributed")
} else { } else {
title = "Unknown" title = "Unknown"
@ -588,9 +599,9 @@ private enum StatsEntry: ItemListNodeEntry {
} }
expiresString = "\(durationString)\(expiresValue)" expiresString = "\(durationString)\(expiresValue)"
} }
return GiftOptionItem(presentationData: presentationData, context: arguments.context, icon: icon, title: title, titleFont: .bold, titleBadge: count > 1 ? "\(count)" : nil, subtitle: expiresString, label: label.flatMap { .semitransparent($0) }, sectionId: self.section, action: peer != nil && peer?.id != arguments.context.account.peerId ? { return GiftOptionItem(presentationData: presentationData, context: arguments.context, icon: icon, title: title, titleFont: .bold, titleBadge: count > 1 ? "\(count)" : nil, subtitle: expiresString, label: label.flatMap { .semitransparent($0) }, sectionId: self.section, action: {
arguments.openPeer(peer!) arguments.openBoost(boost)
} : nil) })
case let .boostersExpand(theme, title): case let .boostersExpand(theme, title):
return ItemListPeerActionItem(presentationData: presentationData, icon: PresentationResourcesItemList.downArrowImage(theme), title: title, sectionId: self.section, editing: false, action: { return ItemListPeerActionItem(presentationData: presentationData, icon: PresentationResourcesItemList.downArrowImage(theme), title: title, sectionId: self.section, editing: false, action: {
arguments.expandBoosters() arguments.expandBoosters()
@ -626,7 +637,7 @@ private enum StatsEntry: ItemListNodeEntry {
default: default:
color = .blue color = .blue
} }
return GiftOptionItem(presentationData: presentationData, context: arguments.context, icon: .image(color: color, name: "Premium/Giveaway"), title: title, titleFont: .bold, titleBadge: "\(prepaidGiveaway.quantity)", subtitle: subtitle, label: nil, sectionId: self.section, action: { return GiftOptionItem(presentationData: presentationData, context: arguments.context, icon: .image(color: color, name: "Premium/Giveaway"), title: title, titleFont: .bold, titleBadge: "\(prepaidGiveaway.quantity * 4)", subtitle: subtitle, label: nil, sectionId: self.section, action: {
arguments.createPrepaidGiveaway(prepaidGiveaway) arguments.createPrepaidGiveaway(prepaidGiveaway)
}) })
} }
@ -763,15 +774,14 @@ private func channelStatsControllerEntries(state: ChannelStatsControllerState, p
entries.append(.boostOverviewTitle(presentationData.theme, presentationData.strings.Stats_Boosts_OverviewHeader)) entries.append(.boostOverviewTitle(presentationData.theme, presentationData.strings.Stats_Boosts_OverviewHeader))
entries.append(.boostOverview(presentationData.theme, boostData)) entries.append(.boostOverview(presentationData.theme, boostData))
//TODO:localize
if !boostData.prepaidGiveaways.isEmpty { if !boostData.prepaidGiveaways.isEmpty {
entries.append(.boostPrepaidTitle(presentationData.theme, "PREPAID GIVEAWAYS")) entries.append(.boostPrepaidTitle(presentationData.theme, presentationData.strings.Stats_Boosts_PrepaidGiveawaysTitle))
var i: Int32 = 0 var i: Int32 = 0
for giveaway in boostData.prepaidGiveaways { for giveaway in boostData.prepaidGiveaways {
entries.append(.boostPrepaid(i, presentationData.theme, "\(giveaway.quantity) Telegram Premium", "\(giveaway.months)-month subscriptions", giveaway)) entries.append(.boostPrepaid(i, presentationData.theme, presentationData.strings.Stats_Boosts_PrepaidGiveawayCount(giveaway.quantity), presentationData.strings.Stats_Boosts_PrepaidGiveawayMonths("\(giveaway.months)").string, giveaway))
i += 1 i += 1
} }
entries.append(.boostPrepaidInfo(presentationData.theme, "Select a giveaway you already paid for to set it up.")) entries.append(.boostPrepaidInfo(presentationData.theme, presentationData.strings.Stats_Boosts_PrepaidGiveawaysInfo))
} }
let boostersTitle: String let boostersTitle: String
@ -802,7 +812,7 @@ private func channelStatsControllerEntries(state: ChannelStatsControllerState, p
} }
if boostsCount > 0 && giftsCount > 0 && boostsCount != giftsCount { if boostsCount > 0 && giftsCount > 0 && boostsCount != giftsCount {
entries.append(.boosterTabs(presentationData.theme, "\(boostsCount) Boosts", "\(giftsCount) Gifts", state.giftsSelected)) entries.append(.boosterTabs(presentationData.theme, presentationData.strings.Stats_Boosts_TabBoosts(boostsCount), presentationData.strings.Stats_Boosts_TabGifts(giftsCount), state.giftsSelected))
} }
let selectedState: ChannelBoostersContext.State? let selectedState: ChannelBoostersContext.State?
@ -824,12 +834,12 @@ private func channelStatsControllerEntries(state: ChannelStatsControllerState, p
} }
for booster in boosters { for booster in boosters {
entries.append(.booster(boosterIndex, presentationData.theme, presentationData.dateTimeFormat, booster.peer, booster.multiplier, booster.flags, booster.date, booster.expires)) entries.append(.booster(boosterIndex, presentationData.theme, presentationData.dateTimeFormat, booster))
boosterIndex += 1 boosterIndex += 1
} }
if !effectiveExpanded { if !effectiveExpanded {
entries.append(.boostersExpand(presentationData.theme, presentationData.strings.PeopleNearby_ShowMorePeople(Int32(selectedState.count) - maxUsersDisplayedLimit))) entries.append(.boostersExpand(presentationData.theme, presentationData.strings.Stats_Boosts_ShowMoreBoosts(Int32(selectedState.count) - maxUsersDisplayedLimit)))
} }
} }
@ -842,8 +852,8 @@ private func channelStatsControllerEntries(state: ChannelStatsControllerState, p
entries.append(.boostLinkInfo(presentationData.theme, presentationData.strings.Stats_Boosts_LinkInfo)) entries.append(.boostLinkInfo(presentationData.theme, presentationData.strings.Stats_Boosts_LinkInfo))
if giveawayAvailable { if giveawayAvailable {
entries.append(.gifts(presentationData.theme, "Get Boosts via Gifts")) entries.append(.gifts(presentationData.theme, presentationData.strings.Stats_Boosts_GetBoosts))
entries.append(.giftsInfo(presentationData.theme, "Get more boosts for your channel by gifting Premium to your subscribers.")) entries.append(.giftsInfo(presentationData.theme, presentationData.strings.Stats_Boosts_GetBoostsInfo))
} }
} }
} }
@ -899,7 +909,6 @@ public func channelStatsController(context: AccountContext, updatedPresentationD
var presentImpl: ((ViewController) -> Void)? var presentImpl: ((ViewController) -> Void)?
var pushImpl: ((ViewController) -> Void)? var pushImpl: ((ViewController) -> Void)?
var navigateToProfileImpl: ((EnginePeer) -> Void)?
let arguments = ChannelStatsControllerArguments(context: context, loadDetailedGraph: { graph, x -> Signal<StatsGraph?, NoError> in let arguments = ChannelStatsControllerArguments(context: context, loadDetailedGraph: { graph, x -> Signal<StatsGraph?, NoError> in
return statsContext.loadDetailedGraph(graph, x: x) return statsContext.loadDetailedGraph(graph, x: x)
@ -954,8 +963,9 @@ public func channelStatsController(context: AccountContext, updatedPresentationD
} }
presentImpl?(shareController) presentImpl?(shareController)
}, },
openPeer: { peer in openBoost: { boost in
navigateToProfileImpl?(peer) let controller = PremiumGiftCodeScreen(context: context, subject: .boost(peerId, boost), action: {})
pushImpl?(controller)
}, },
expandBoosters: { expandBoosters: {
updateState { $0.withUpdatedBoostersExpanded(true) } updateState { $0.withUpdatedBoostersExpanded(true) }
@ -1087,11 +1097,6 @@ public func channelStatsController(context: AccountContext, updatedPresentationD
pushImpl = { [weak controller] c in pushImpl = { [weak controller] c in
controller?.push(c) controller?.push(c)
} }
navigateToProfileImpl = { [weak controller] peer in
if let navigationController = controller?.navigationController as? NavigationController, let controller = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: peer.largeProfileImage != nil, fromChat: false, requestsContext: nil) {
navigationController.pushViewController(controller)
}
}
return controller return controller
} }

View File

@ -10,6 +10,14 @@ public struct MyBoostStatus: Equatable {
public let date: Int32 public let date: Int32
public let expires: Int32 public let expires: Int32
public let cooldownUntil: Int32? public let cooldownUntil: Int32?
public init(slot: Int32, peer: EnginePeer?, date: Int32, expires: Int32, cooldownUntil: Int32?) {
self.slot = slot
self.peer = peer
self.date = date
self.expires = expires
self.cooldownUntil = cooldownUntil
}
} }
public let boosts: [Boost] public let boosts: [Boost]
@ -175,7 +183,7 @@ private final class ChannelBoostersContextImpl {
var result: [ChannelBoostersContext.State.Boost] = [] var result: [ChannelBoostersContext.State.Boost] = []
for boost in cachedResult.boosts { for boost in cachedResult.boosts {
let peer = boost.peerId.flatMap { transaction.getPeer($0) } let peer = boost.peerId.flatMap { transaction.getPeer($0) }
result.append(ChannelBoostersContext.State.Boost(flags: ChannelBoostersContext.State.Boost.Flags(rawValue: boost.flags), id: boost.id, peer: peer.flatMap { EnginePeer($0) }, date: boost.date, expires: boost.expires, multiplier: boost.multiplier)) result.append(ChannelBoostersContext.State.Boost(flags: ChannelBoostersContext.State.Boost.Flags(rawValue: boost.flags), id: boost.id, peer: peer.flatMap { EnginePeer($0) }, date: boost.date, expires: boost.expires, multiplier: boost.multiplier, slug: boost.slug))
} }
return (result, cachedResult.count, true) return (result, cachedResult.count, true)
} else { } else {
@ -211,7 +219,7 @@ private final class ChannelBoostersContextImpl {
} }
func loadMore() { func loadMore() {
if self.isLoadingMore { if self.isLoadingMore || !self.canLoadMore {
return return
} }
self.isLoadingMore = true self.isLoadingMore = true
@ -256,7 +264,6 @@ private final class ChannelBoostersContextImpl {
switch boost { switch boost {
case let .boost(flags, id, userId, giveawayMessageId, date, expires, usedGiftSlug, multiplier): case let .boost(flags, id, userId, giveawayMessageId, date, expires, usedGiftSlug, multiplier):
let _ = giveawayMessageId let _ = giveawayMessageId
let _ = usedGiftSlug
var boostFlags: ChannelBoostersContext.State.Boost.Flags = [] var boostFlags: ChannelBoostersContext.State.Boost.Flags = []
var boostPeer: EnginePeer? var boostPeer: EnginePeer?
if let userId = userId { if let userId = userId {
@ -274,7 +281,7 @@ private final class ChannelBoostersContextImpl {
if (flags & (1 << 3)) != 0 { if (flags & (1 << 3)) != 0 {
boostFlags.insert(.isUnclaimed) boostFlags.insert(.isUnclaimed)
} }
resultBoosts.append(ChannelBoostersContext.State.Boost(flags: boostFlags, id: id, peer: boostPeer, date: date, expires: expires, multiplier: multiplier ?? 1)) resultBoosts.append(ChannelBoostersContext.State.Boost(flags: boostFlags, id: id, peer: boostPeer, date: date, expires: expires, multiplier: multiplier ?? 1, slug: usedGiftSlug))
} }
} }
if populateCache { if populateCache {
@ -305,11 +312,19 @@ private final class ChannelBoostersContextImpl {
} }
strongSelf.isLoadingMore = false strongSelf.isLoadingMore = false
strongSelf.hasLoadedOnce = true strongSelf.hasLoadedOnce = true
strongSelf.canLoadMore = !boosters.isEmpty strongSelf.canLoadMore = !boosters.isEmpty && nextOffset != nil
if strongSelf.canLoadMore { if strongSelf.canLoadMore {
strongSelf.count = max(updatedCount, Int32(strongSelf.results.count)) var resultsCount: Int32 = 0
for result in strongSelf.results {
resultsCount += result.multiplier
}
strongSelf.count = max(updatedCount, resultsCount)
} else { } else {
strongSelf.count = Int32(strongSelf.results.count) var resultsCount: Int32 = 0
for result in strongSelf.results {
resultsCount += result.multiplier
}
strongSelf.count = resultsCount
} }
strongSelf.updateState() strongSelf.updateState()
})) }))
@ -357,6 +372,7 @@ public final class ChannelBoostersContext {
public var date: Int32 public var date: Int32
public var expires: Int32 public var expires: Int32
public var multiplier: Int32 public var multiplier: Int32
public var slug: String?
} }
public var boosts: [Boost] public var boosts: [Boost]
public var isLoadingMore: Bool public var isLoadingMore: Bool
@ -418,6 +434,7 @@ private final class CachedChannelBoosters: Codable {
case date case date
case expires case expires
case multiplier case multiplier
case slug
} }
var flags: Int32 var flags: Int32
@ -426,14 +443,16 @@ private final class CachedChannelBoosters: Codable {
var date: Int32 var date: Int32
var expires: Int32 var expires: Int32
var multiplier: Int32 var multiplier: Int32
var slug: String?
init(flags: Int32, id: String, peerId: EnginePeer.Id?, date: Int32, expires: Int32, multiplier: Int32) { init(flags: Int32, id: String, peerId: EnginePeer.Id?, date: Int32, expires: Int32, multiplier: Int32, slug: String?) {
self.flags = flags self.flags = flags
self.id = id self.id = id
self.peerId = peerId self.peerId = peerId
self.date = date self.date = date
self.expires = expires self.expires = expires
self.multiplier = multiplier self.multiplier = multiplier
self.slug = slug
} }
init(from decoder: Decoder) throws { init(from decoder: Decoder) throws {
@ -445,6 +464,7 @@ private final class CachedChannelBoosters: Codable {
self.date = try container.decode(Int32.self, forKey: .date) self.date = try container.decode(Int32.self, forKey: .date)
self.expires = try container.decode(Int32.self, forKey: .expires) self.expires = try container.decode(Int32.self, forKey: .expires)
self.multiplier = try container.decode(Int32.self, forKey: .multiplier) self.multiplier = try container.decode(Int32.self, forKey: .multiplier)
self.slug = try container.decodeIfPresent(String.self, forKey: .slug)
} }
func encode(to encoder: Encoder) throws { func encode(to encoder: Encoder) throws {
@ -456,6 +476,7 @@ private final class CachedChannelBoosters: Codable {
try container.encode(self.date, forKey: .date) try container.encode(self.date, forKey: .date)
try container.encode(self.expires, forKey: .expires) try container.encode(self.expires, forKey: .expires)
try container.encode(self.multiplier, forKey: .multiplier) try container.encode(self.multiplier, forKey: .multiplier)
try container.encodeIfPresent(self.slug, forKey: .slug)
} }
} }
@ -469,7 +490,7 @@ private final class CachedChannelBoosters: Codable {
} }
init(boosts: [ChannelBoostersContext.State.Boost], count: Int32) { init(boosts: [ChannelBoostersContext.State.Boost], count: Int32) {
self.boosts = boosts.map { CachedBoost(flags: $0.flags.rawValue, id: $0.id, peerId: $0.peer?.id, date: $0.date, expires: $0.expires, multiplier: $0.multiplier) } self.boosts = boosts.map { CachedBoost(flags: $0.flags.rawValue, id: $0.id, peerId: $0.peer?.id, date: $0.date, expires: $0.expires, multiplier: $0.multiplier, slug: $0.slug) }
self.count = count self.count = count
} }

View File

@ -114,7 +114,7 @@ public struct CachedPremiumGiftOption: Equatable, PostboxCoding {
} }
} }
public enum PeerNameColor: Int32, CaseIterable { public enum PeerNameColor: Equatable {
case red case red
case orange case orange
case violet case violet
@ -122,13 +122,49 @@ public enum PeerNameColor: Int32, CaseIterable {
case cyan case cyan
case blue case blue
case pink case pink
case redDash case other(Int32)
case orangeDash
case violetDash public init(rawValue: Int32) {
case greenDash switch rawValue {
case cyanDash case 0:
case blueDash self = .red
case pinkDash case 1:
self = .orange
case 2:
self = .violet
case 3:
self = .green
case 4:
self = .cyan
case 5:
self = .blue
case 6:
self = .pink
default:
self = .other(rawValue)
}
}
public var rawValue: Int32 {
switch self {
case .red:
return 0
case .orange:
return 1
case .violet:
return 2
case .green:
return 3
case .cyan:
return 4
case .blue:
return 5
case .pink:
return 6
case let .other(value):
return value
}
}
} }
public struct PeerEmojiStatus: Equatable, Codable { public struct PeerEmojiStatus: Equatable, Codable {

View File

@ -348,12 +348,10 @@ func channelAdminLogEvents(accountPeerId: PeerId, postbox: Postbox, network: Net
action = .toggleForum(isForum: newValue == .boolTrue) action = .toggleForum(isForum: newValue == .boolTrue)
case let .channelAdminLogEventActionToggleAntiSpam(newValue): case let .channelAdminLogEventActionToggleAntiSpam(newValue):
action = .toggleAntiSpam(isEnabled: newValue == .boolTrue) action = .toggleAntiSpam(isEnabled: newValue == .boolTrue)
default: case let .channelAdminLogEventActionChangeColor(prevValue, newValue):
action = .toggleInvites(false) action = .changeNameColor(prev: PeerNameColor(rawValue: prevValue), new: PeerNameColor(rawValue: newValue))
// case let .channelAdminLogEventActionChangeColor(prevValue, newValue): case let .channelAdminLogEventActionChangeBackgroundEmoji(prevValue, newValue):
// action = .changeNameColor(prev: PeerNameColor(rawValue: prevValue), new: PeerNameColor(rawValue: newValue)) action = .changeBackgroundEmojiId(prev: prevValue, new: newValue)
// case let .channelAdminLogEventActionChangeBackgroundEmoji(prevValue, newValue):
// action = .changeBackgroundEmojiId(prev: prevValue, new: newValue)
} }
let peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId)) let peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId))
if let action = action { if let action = action {

View File

@ -223,27 +223,19 @@ public extension Peer {
if let nameColor = user.nameColor { if let nameColor = user.nameColor {
return nameColor return nameColor
} else { } else {
return PeerNameColor(rawValue: Int32(self.id.id._internalGetInt64Value() % 7)) ?? .blue return PeerNameColor(rawValue: Int32(self.id.id._internalGetInt64Value() % 7))
} }
case let channel as TelegramChannel: case let channel as TelegramChannel:
if let nameColor = channel.nameColor { if let nameColor = channel.nameColor {
return nameColor return nameColor
} else { } else {
return PeerNameColor(rawValue: Int32(self.id.id._internalGetInt64Value() % 7)) ?? .blue return PeerNameColor(rawValue: Int32(self.id.id._internalGetInt64Value() % 7))
} }
default: default:
return nil return nil
} }
} }
var hasCustomNameColor: Bool {
let defaultNameColor = PeerNameColor(rawValue: Int32(self.id.id._internalGetInt64Value() % 7)) ?? .blue
if self.nameColor != defaultNameColor {
return true
}
return false
}
var backgroundEmojiId: Int64? { var backgroundEmojiId: Int64? {
switch self { switch self {
case let user as TelegramUser: case let user as TelegramUser:

View File

@ -1,42 +0,0 @@
import Foundation
import UIKit
import TelegramCore
public extension PeerNameColor {
var color: UIColor {
return self.dashColors.0
}
var dashColors: (UIColor, UIColor?) {
switch self {
case .red:
return (UIColor(rgb: 0xCC5049), nil)
case .orange:
return (UIColor(rgb: 0xD67722), nil)
case .violet:
return (UIColor(rgb: 0x955CDB), nil)
case .green:
return (UIColor(rgb: 0x40A920), nil)
case .cyan:
return (UIColor(rgb: 0x309EBA), nil)
case .blue:
return (UIColor(rgb: 0x368AD1), nil)
case .pink:
return (UIColor(rgb: 0xC7508B), nil)
case .redDash:
return (UIColor(rgb: 0xE15052), UIColor(rgb: 0xF9AE63))
case .orangeDash:
return (UIColor(rgb: 0xE0802B), UIColor(rgb: 0xFAC534))
case .violetDash:
return (UIColor(rgb: 0xA05FF3), UIColor(rgb: 0xF48FFF))
case .greenDash:
return (UIColor(rgb: 0x27A910), UIColor(rgb: 0xA7DC57))
case .cyanDash:
return (UIColor(rgb: 0x27ACCE), UIColor(rgb: 0x82E8D6))
case .blueDash:
return (UIColor(rgb: 0x3391D4), UIColor(rgb: 0x7DD3F0))
case .pinkDash:
return (UIColor(rgb: 0xdd4371), UIColor(rgb: 0xffbe9f))
}
}
}

View File

@ -259,18 +259,23 @@ private final class ChatInputTextContainer: NSTextContainer {
public final class ChatInputTextView: ChatInputTextViewImpl, NSLayoutManagerDelegate, NSTextStorageDelegate { public final class ChatInputTextView: ChatInputTextViewImpl, NSLayoutManagerDelegate, NSTextStorageDelegate {
public final class Theme: Equatable { public final class Theme: Equatable {
public final class Quote: Equatable { public final class Quote: Equatable {
public enum LineStyle {
case solid
case doubleDashed
case tripleDashed
}
public let background: UIColor public let background: UIColor
public let foreground: UIColor public let foreground: UIColor
public let isDashed: Bool public let lineStyle: LineStyle
public init( public init(
background: UIColor, background: UIColor,
foreground: UIColor, foreground: UIColor,
isDashed: Bool lineStyle: LineStyle
) { ) {
self.background = background self.background = background
self.foreground = foreground self.foreground = foreground
self.isDashed = isDashed self.lineStyle = lineStyle
} }
public static func ==(lhs: Quote, rhs: Quote) -> Bool { public static func ==(lhs: Quote, rhs: Quote) -> Bool {
@ -280,7 +285,7 @@ public final class ChatInputTextView: ChatInputTextViewImpl, NSLayoutManagerDele
if !lhs.foreground.isEqual(rhs.foreground) { if !lhs.foreground.isEqual(rhs.foreground) {
return false return false
} }
if lhs.isDashed != rhs.isDashed { if lhs.lineStyle != rhs.lineStyle {
return false return false
} }
return true return true
@ -763,7 +768,8 @@ private final class QuoteBackgroundView: UIView {
self.backgroundView.update( self.backgroundView.update(
size: size, size: size,
primaryColor: theme.foreground, primaryColor: theme.foreground,
secondaryColor: theme.isDashed ? .clear : nil, secondaryColor: theme.lineStyle != .solid ? .clear : nil,
thirdColor: theme.lineStyle == .tripleDashed ? .clear : nil,
pattern: nil, pattern: nil,
animation: .None animation: .None
) )

View File

@ -1213,7 +1213,7 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
} }
} }
let availableWidth = max(60.0, availableContentWidth + 6.0) let availableWidth = max(60.0, availableContentWidth + 6.0)
forwardInfoSizeApply = makeForwardInfoLayout(item.presentationData, item.presentationData.strings, .standalone, forwardSource, forwardAuthorSignature, forwardPsaType, nil, CGSize(width: availableWidth, height: CGFloat.greatestFiniteMagnitude)) forwardInfoSizeApply = makeForwardInfoLayout(item.context, item.presentationData, item.presentationData.strings, .standalone, forwardSource, forwardAuthorSignature, forwardPsaType, nil, CGSize(width: availableWidth, height: CGFloat.greatestFiniteMagnitude))
} }
if replyInfoApply != nil || viaBotApply != nil || forwardInfoSizeApply != nil { if replyInfoApply != nil || viaBotApply != nil || forwardInfoSizeApply != nil {

View File

@ -177,17 +177,24 @@ public final class ChatMessageAttachedContentNode: ASDisplayNode {
let messageTheme = incoming ? presentationData.theme.theme.chat.message.incoming : presentationData.theme.theme.chat.message.outgoing let messageTheme = incoming ? presentationData.theme.theme.chat.message.incoming : presentationData.theme.theme.chat.message.outgoing
let author = message.author let author = message.author
let nameColors = author?.nameColor.flatMap { context.peerNameColors.get($0) }
let mainColor: UIColor let mainColor: UIColor
var secondaryColor: UIColor? var secondaryColor: UIColor?
var tertiaryColor: UIColor?
if !incoming { if !incoming {
mainColor = messageTheme.accentTextColor mainColor = messageTheme.accentTextColor
if let _ = author?.nameColor?.dashColors.1 { if let _ = nameColors?.secondary {
secondaryColor = .clear secondaryColor = .clear
} }
if let _ = nameColors?.tertiary {
tertiaryColor = .clear
}
} else { } else {
var authorNameColor: UIColor? var authorNameColor: UIColor?
authorNameColor = author?.nameColor?.color authorNameColor = nameColors?.main
secondaryColor = author?.nameColor?.dashColors.1 secondaryColor = nameColors?.secondary
tertiaryColor = nameColors?.tertiary
if let authorNameColor { if let authorNameColor {
mainColor = authorNameColor mainColor = authorNameColor
@ -853,13 +860,13 @@ public final class ChatMessageAttachedContentNode: ASDisplayNode {
if let current = self.backgroundView { if let current = self.backgroundView {
backgroundView = current backgroundView = current
animation.animator.updateFrame(layer: backgroundView.layer, frame: backgroundFrame, completion: nil) animation.animator.updateFrame(layer: backgroundView.layer, frame: backgroundFrame, completion: nil)
backgroundView.update(size: backgroundFrame.size, primaryColor: mainColor, secondaryColor: secondaryColor, pattern: nil, animation: animation) backgroundView.update(size: backgroundFrame.size, primaryColor: mainColor, secondaryColor: secondaryColor, thirdColor: tertiaryColor, pattern: nil, animation: animation)
} else { } else {
backgroundView = MessageInlineBlockBackgroundView() backgroundView = MessageInlineBlockBackgroundView()
self.backgroundView = backgroundView self.backgroundView = backgroundView
backgroundView.frame = backgroundFrame backgroundView.frame = backgroundFrame
self.transformContainer.view.insertSubview(backgroundView, at: 0) self.transformContainer.view.insertSubview(backgroundView, at: 0)
backgroundView.update(size: backgroundFrame.size, primaryColor: mainColor, secondaryColor: secondaryColor, pattern: nil, animation: .None) backgroundView.update(size: backgroundFrame.size, primaryColor: mainColor, secondaryColor: secondaryColor, thirdColor: tertiaryColor, pattern: nil, animation: .None)
} }
} else { } else {
if let backgroundView = self.backgroundView { if let backgroundView = self.backgroundView {

View File

@ -1270,7 +1270,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
authorNameLayout: (TextNodeLayoutArguments) -> (TextNodeLayout, () -> TextNode), authorNameLayout: (TextNodeLayoutArguments) -> (TextNodeLayout, () -> TextNode),
adminBadgeLayout: (TextNodeLayoutArguments) -> (TextNodeLayout, () -> TextNode), adminBadgeLayout: (TextNodeLayoutArguments) -> (TextNodeLayout, () -> TextNode),
threadInfoLayout: (ChatMessageThreadInfoNode.Arguments) -> (CGSize, (Bool) -> ChatMessageThreadInfoNode), threadInfoLayout: (ChatMessageThreadInfoNode.Arguments) -> (CGSize, (Bool) -> ChatMessageThreadInfoNode),
forwardInfoLayout: (ChatPresentationData, PresentationStrings, ChatMessageForwardInfoType, Peer?, String?, String?, ChatMessageForwardInfoNode.StoryData?, CGSize) -> (CGSize, (CGFloat) -> ChatMessageForwardInfoNode), forwardInfoLayout: (AccountContext, ChatPresentationData, PresentationStrings, ChatMessageForwardInfoType, Peer?, String?, String?, ChatMessageForwardInfoNode.StoryData?, CGSize) -> (CGSize, (CGFloat) -> ChatMessageForwardInfoNode),
replyInfoLayout: (ChatMessageReplyInfoNode.Arguments) -> (CGSize, (CGSize, Bool, ListViewItemUpdateAnimation) -> ChatMessageReplyInfoNode), replyInfoLayout: (ChatMessageReplyInfoNode.Arguments) -> (CGSize, (CGSize, Bool, ListViewItemUpdateAnimation) -> ChatMessageReplyInfoNode),
actionButtonsLayout: (AccountContext, ChatPresentationThemeData, PresentationChatBubbleCorners, PresentationStrings, WallpaperBackgroundNode?, ReplyMarkupMessageAttribute, Message, CGFloat) -> (minWidth: CGFloat, layout: (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> ChatMessageActionButtonsNode)), actionButtonsLayout: (AccountContext, ChatPresentationThemeData, PresentationChatBubbleCorners, PresentationStrings, WallpaperBackgroundNode?, ReplyMarkupMessageAttribute, Message, CGFloat) -> (minWidth: CGFloat, layout: (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> ChatMessageActionButtonsNode)),
reactionButtonsLayout: (ChatMessageReactionButtonsNode.Arguments) -> (minWidth: CGFloat, layout: (CGFloat) -> (size: CGSize, apply: (ListViewItemUpdateAnimation) -> ChatMessageReactionButtonsNode)), reactionButtonsLayout: (ChatMessageReactionButtonsNode.Arguments) -> (minWidth: CGFloat, layout: (CGFloat) -> (size: CGSize, apply: (ListViewItemUpdateAnimation) -> ChatMessageReactionButtonsNode)),
@ -1881,31 +1881,40 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
} }
if let peer = firstMessage.peers[firstMessage.id.peerId] as? TelegramChannel, case .broadcast = peer.info, item.content.firstMessage.adAttribute == nil { if let peer = firstMessage.peers[firstMessage.id.peerId] as? TelegramChannel, case .broadcast = peer.info, item.content.firstMessage.adAttribute == nil {
authorNameColor = (peer as Peer).nameColor?.color let peer = (peer as Peer)
let nameColors = peer.nameColor.flatMap { item.context.peerNameColors.get($0) }
authorNameColor = nameColors?.main
} else if let effectiveAuthor = effectiveAuthor { } else if let effectiveAuthor = effectiveAuthor {
let nameColor: UIColor let nameColor = effectiveAuthor.nameColor ?? .blue
let nameColors = item.context.peerNameColors.get(nameColor)
let color: UIColor
if incoming { if incoming {
nameColor = (effectiveAuthor.nameColor ?? .blue).color color = nameColors.main
} else { } else {
nameColor = item.presentationData.theme.theme.chat.message.outgoing.accentTextColor color = item.presentationData.theme.theme.chat.message.outgoing.accentTextColor
} }
authorNameColor = nameColor authorNameColor = color
} }
if initialDisplayHeader && displayAuthorInfo { if initialDisplayHeader && displayAuthorInfo {
if let peer = firstMessage.peers[firstMessage.id.peerId] as? TelegramChannel, case .broadcast = peer.info, item.content.firstMessage.adAttribute == nil { if let peer = firstMessage.peers[firstMessage.id.peerId] as? TelegramChannel, case .broadcast = peer.info, item.content.firstMessage.adAttribute == nil {
authorNameString = EnginePeer(peer).displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder) authorNameString = EnginePeer(peer).displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder)
authorNameColor = (peer as Peer).nameColor?.color
let peer = (peer as Peer)
let nameColors = peer.nameColor.flatMap { item.context.peerNameColors.get($0) }
authorNameColor = nameColors?.main
} else if let effectiveAuthor = effectiveAuthor { } else if let effectiveAuthor = effectiveAuthor {
authorNameString = EnginePeer(effectiveAuthor).displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder) authorNameString = EnginePeer(effectiveAuthor).displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder)
let nameColor: UIColor let nameColor = effectiveAuthor.nameColor ?? .blue
let nameColors = item.context.peerNameColors.get(nameColor)
let color: UIColor
if incoming { if incoming {
nameColor = (effectiveAuthor.nameColor ?? .blue).color color = nameColors.main
} else { } else {
nameColor = item.presentationData.theme.theme.chat.message.outgoing.accentTextColor color = item.presentationData.theme.theme.chat.message.outgoing.accentTextColor
} }
authorNameColor = nameColor authorNameColor = color
if case let .peer(peerId) = item.chatLocation, let authorPeerId = item.message.author?.id, authorPeerId == peerId { if case let .peer(peerId) = item.chatLocation, let authorPeerId = item.message.author?.id, authorPeerId == peerId {
} else if effectiveAuthor.isScam { } else if effectiveAuthor.isScam {
@ -1913,11 +1922,11 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
} else if effectiveAuthor.isFake { } else if effectiveAuthor.isFake {
currentCredibilityIcon = .text(color: incoming ? item.presentationData.theme.theme.chat.message.incoming.scamColor : item.presentationData.theme.theme.chat.message.outgoing.scamColor, string: item.presentationData.strings.Message_FakeAccount.uppercased()) currentCredibilityIcon = .text(color: incoming ? item.presentationData.theme.theme.chat.message.incoming.scamColor : item.presentationData.theme.theme.chat.message.outgoing.scamColor, string: item.presentationData.strings.Message_FakeAccount.uppercased())
} else if let user = effectiveAuthor as? TelegramUser, let emojiStatus = user.emojiStatus { } else if let user = effectiveAuthor as? TelegramUser, let emojiStatus = user.emojiStatus {
currentCredibilityIcon = .animation(content: .customEmoji(fileId: emojiStatus.fileId), size: CGSize(width: 20.0, height: 20.0), placeholderColor: incoming ? item.presentationData.theme.theme.chat.message.incoming.mediaPlaceholderColor : item.presentationData.theme.theme.chat.message.outgoing.mediaPlaceholderColor, themeColor: nameColor.withMultipliedAlpha(0.4), loopMode: .count(2)) currentCredibilityIcon = .animation(content: .customEmoji(fileId: emojiStatus.fileId), size: CGSize(width: 20.0, height: 20.0), placeholderColor: incoming ? item.presentationData.theme.theme.chat.message.incoming.mediaPlaceholderColor : item.presentationData.theme.theme.chat.message.outgoing.mediaPlaceholderColor, themeColor: color.withMultipliedAlpha(0.4), loopMode: .count(2))
} else if effectiveAuthor.isVerified { } else if effectiveAuthor.isVerified {
currentCredibilityIcon = .verified(fillColor: item.presentationData.theme.theme.list.itemCheckColors.fillColor, foregroundColor: item.presentationData.theme.theme.list.itemCheckColors.foregroundColor, sizeType: .compact) currentCredibilityIcon = .verified(fillColor: item.presentationData.theme.theme.list.itemCheckColors.fillColor, foregroundColor: item.presentationData.theme.theme.list.itemCheckColors.foregroundColor, sizeType: .compact)
} else if effectiveAuthor.isPremium { } else if effectiveAuthor.isPremium {
currentCredibilityIcon = .premium(color: nameColor.withMultipliedAlpha(0.4)) currentCredibilityIcon = .premium(color: color.withMultipliedAlpha(0.4))
} }
} }
if let rawAuthorNameColor = authorNameColor { if let rawAuthorNameColor = authorNameColor {
@ -2178,7 +2187,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
forwardAuthorSignature = forwardInfo.authorSignature forwardAuthorSignature = forwardInfo.authorSignature
} }
} }
let sizeAndApply = forwardInfoLayout(item.presentationData, item.presentationData.strings, .bubble(incoming: incoming), forwardSource, forwardAuthorSignature, forwardPsaType, nil, CGSize(width: maximumNodeWidth - layoutConstants.text.bubbleInsets.left - layoutConstants.text.bubbleInsets.right, height: CGFloat.greatestFiniteMagnitude)) let sizeAndApply = forwardInfoLayout(item.context, item.presentationData, item.presentationData.strings, .bubble(incoming: incoming), forwardSource, forwardAuthorSignature, forwardPsaType, nil, CGSize(width: maximumNodeWidth - layoutConstants.text.bubbleInsets.left - layoutConstants.text.bubbleInsets.right, height: CGFloat.greatestFiniteMagnitude))
forwardInfoSizeApply = (sizeAndApply.0, { width in sizeAndApply.1(width) }) forwardInfoSizeApply = (sizeAndApply.0, { width in sizeAndApply.1(width) })
forwardInfoOriginY = headerSize.height forwardInfoOriginY = headerSize.height
@ -2205,7 +2214,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
} }
} }
let sizeAndApply = forwardInfoLayout(item.presentationData, item.presentationData.strings, .bubble(incoming: incoming), forwardSource, nil, nil, ChatMessageForwardInfoNode.StoryData(storyType: storyType), CGSize(width: maximumNodeWidth - layoutConstants.text.bubbleInsets.left - layoutConstants.text.bubbleInsets.right, height: CGFloat.greatestFiniteMagnitude)) let sizeAndApply = forwardInfoLayout(item.context, item.presentationData, item.presentationData.strings, .bubble(incoming: incoming), forwardSource, nil, nil, ChatMessageForwardInfoNode.StoryData(storyType: storyType), CGSize(width: maximumNodeWidth - layoutConstants.text.bubbleInsets.left - layoutConstants.text.bubbleInsets.right, height: CGFloat.greatestFiniteMagnitude))
forwardInfoSizeApply = (sizeAndApply.0, { width in sizeAndApply.1(width) }) forwardInfoSizeApply = (sizeAndApply.0, { width in sizeAndApply.1(width) })
if storyType != .regular { if storyType != .regular {

View File

@ -6,6 +6,7 @@ import Postbox
import TelegramCore import TelegramCore
import TelegramPresentationData import TelegramPresentationData
import LocalizedPeerData import LocalizedPeerData
import AccountContext
public enum ChatMessageForwardInfoType: Equatable { public enum ChatMessageForwardInfoType: Equatable {
case bubble(incoming: Bool) case bubble(incoming: Bool)
@ -106,10 +107,10 @@ public class ChatMessageForwardInfoNode: ASDisplayNode {
} }
} }
public static func asyncLayout(_ maybeNode: ChatMessageForwardInfoNode?) -> (_ presentationData: ChatPresentationData, _ strings: PresentationStrings, _ type: ChatMessageForwardInfoType, _ peer: Peer?, _ authorName: String?, _ psaType: String?, _ storyData: StoryData?, _ constrainedSize: CGSize) -> (CGSize, (CGFloat) -> ChatMessageForwardInfoNode) { public static func asyncLayout(_ maybeNode: ChatMessageForwardInfoNode?) -> (_ context: AccountContext, _ presentationData: ChatPresentationData, _ strings: PresentationStrings, _ type: ChatMessageForwardInfoType, _ peer: Peer?, _ authorName: String?, _ psaType: String?, _ storyData: StoryData?, _ constrainedSize: CGSize) -> (CGSize, (CGFloat) -> ChatMessageForwardInfoNode) {
let textNodeLayout = TextNode.asyncLayout(maybeNode?.textNode) let textNodeLayout = TextNode.asyncLayout(maybeNode?.textNode)
return { presentationData, strings, type, peer, authorName, psaType, storyData, constrainedSize in return { context, presentationData, strings, type, peer, authorName, psaType, storyData, constrainedSize in
let fontSize = floor(presentationData.fontSize.baseDisplaySize * 13.0 / 17.0) let fontSize = floor(presentationData.fontSize.baseDisplaySize * 13.0 / 17.0)
let prefixFont = Font.regular(fontSize) let prefixFont = Font.regular(fontSize)
let peerFont = Font.medium(fontSize) let peerFont = Font.medium(fontSize)
@ -163,8 +164,8 @@ public class ChatMessageForwardInfoNode: ASDisplayNode {
} }
} else { } else {
if incoming { if incoming {
if let color = peer?.nameColor?.color { if let nameColor = peer?.nameColor {
titleColor = color titleColor = context.peerNameColors.get(nameColor).main
} else { } else {
titleColor = presentationData.theme.theme.chat.message.incoming.accentTextColor titleColor = presentationData.theme.theme.chat.message.incoming.accentTextColor
} }

View File

@ -189,7 +189,6 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
} }
} }
//TODO:localize
override public func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize, _ avatarInset: CGFloat) -> (ChatMessageBubbleContentProperties, unboundSize: CGSize?, maxWidth: CGFloat, layout: (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))) { override public func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize, _ avatarInset: CGFloat) -> (ChatMessageBubbleContentProperties, unboundSize: CGSize?, maxWidth: CGFloat, layout: (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))) {
let makeLabelLayout = TextNode.asyncLayout(self.labelNode) let makeLabelLayout = TextNode.asyncLayout(self.labelNode)
let makeTitleLayout = TextNode.asyncLayout(self.titleNode) let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
@ -226,24 +225,24 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
textSpacing += 13.0 textSpacing += 13.0
if unclaimed { if unclaimed {
title = "Unclaimed Prize" title = item.presentationData.strings.Notification_PremiumPrize_Unclaimed
} else { } else {
title = "Congratulations!" title = item.presentationData.strings.Notification_PremiumPrize_Title
} }
var peerName = "" var peerName = ""
if let channelId, let channel = item.message.peers[channelId] { if let channelId, let channel = item.message.peers[channelId] {
peerName = EnginePeer(channel).compactDisplayTitle peerName = EnginePeer(channel).compactDisplayTitle
} }
if unclaimed { if unclaimed {
text = "You have an unclaimed prize from a giveaway by **\(peerName)**.\n\nThis prize is a **Telegram Premium** subscription for **\(monthsValue)** months." text = item.presentationData.strings.Notification_PremiumPrize_UnclaimedText(peerName, item.presentationData.strings.Notification_PremiumPrize_Months(monthsValue)).string
} else if fromGiveaway { } else if fromGiveaway {
text = "You won a prize in a giveaway organized by **\(peerName)**.\n\nYour prize is a **Telegram Premium** subscription for **\(monthsValue)** months." text = item.presentationData.strings.Notification_PremiumPrize_GiveawayText(peerName, item.presentationData.strings.Notification_PremiumPrize_Months(monthsValue)).string
} else { } else {
text = "You've received a gift from **\(peerName)**.\n\nYour gift is a **Telegram Premium** subscription for **\(monthsValue)** months." text = item.presentationData.strings.Notification_PremiumPrize_GiftText(peerName, item.presentationData.strings.Notification_PremiumPrize_Months(monthsValue)).string
} }
months = monthsValue months = monthsValue
buttonTitle = "Open Gift Link" buttonTitle = item.presentationData.strings.Notification_PremiumPrize_View
hasServiceMessage = false hasServiceMessage = false
default: default:
break break

View File

@ -222,11 +222,13 @@ public class ChatMessageGiveawayBubbleContentNode: ChatMessageBubbleContentNode
let badgeString = NSAttributedString(string: "X\(giveaway?.quantity ?? 1)", font: Font.with(size: 10.0, design: .round , weight: .bold, traits: .monospacedNumbers), textColor: .white) let badgeString = NSAttributedString(string: "X\(giveaway?.quantity ?? 1)", font: Font.with(size: 10.0, design: .round , weight: .bold, traits: .monospacedNumbers), textColor: .white)
//TODO:localize let prizeTitleString = NSAttributedString(string: item.presentationData.strings.Chat_Giveaway_Message_PrizeTitle, font: titleFont, textColor: textColor)
let prizeTitleString = NSAttributedString(string: "Giveaway Prizes", font: titleFont, textColor: textColor)
var prizeTextString: NSAttributedString? var prizeTextString: NSAttributedString?
if let giveaway { if let giveaway {
prizeTextString = parseMarkdownIntoAttributedString("**\(giveaway.quantity)** Telegram Premium Subscriptions for **\(giveaway.months)** months.", attributes: MarkdownAttributes( prizeTextString = parseMarkdownIntoAttributedString(item.presentationData.strings.Chat_Giveaway_Message_PrizeText(
item.presentationData.strings.Chat_Giveaway_Message_Subscriptions(giveaway.quantity),
item.presentationData.strings.Chat_Giveaway_Message_Months(giveaway.months)
).string, attributes: MarkdownAttributes(
body: MarkdownAttributeSet(font: textFont, textColor: textColor), body: MarkdownAttributeSet(font: textFont, textColor: textColor),
bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor), bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor),
link: MarkdownAttributeSet(font: textFont, textColor: textColor), link: MarkdownAttributeSet(font: textFont, textColor: textColor),
@ -236,22 +238,22 @@ public class ChatMessageGiveawayBubbleContentNode: ChatMessageBubbleContentNode
), textAlignment: .center) ), textAlignment: .center)
} }
let participantsTitleString = NSAttributedString(string: "Participants", font: titleFont, textColor: textColor) let participantsTitleString = NSAttributedString(string: item.presentationData.strings.Chat_Giveaway_Message_ParticipantsTitle, font: titleFont, textColor: textColor)
let participantsText: String let participantsText: String
let countriesText: String let countriesText: String
if let giveaway { if let giveaway {
if giveaway.flags.contains(.onlyNewSubscribers) { if giveaway.flags.contains(.onlyNewSubscribers) {
if giveaway.channelPeerIds.count > 1 { if giveaway.channelPeerIds.count > 1 {
participantsText = "All users who join the channels below after this date:" participantsText = item.presentationData.strings.Chat_Giveaway_Message_ParticipantsNewMany
} else { } else {
participantsText = "All users who join this channel after this date:" participantsText = item.presentationData.strings.Chat_Giveaway_Message_ParticipantsNew
} }
} else { } else {
if giveaway.channelPeerIds.count > 1 { if giveaway.channelPeerIds.count > 1 {
participantsText = "All subscribers of the channels below:" participantsText = item.presentationData.strings.Chat_Giveaway_Message_ParticipantsMany
} else { } else {
participantsText = "All subscribers of this channel:" participantsText = item.presentationData.strings.Chat_Giveaway_Message_Participants
} }
} }
if !giveaway.countries.isEmpty { if !giveaway.countries.isEmpty {
@ -270,13 +272,13 @@ public class ChatMessageGiveawayBubbleContentNode: ChatMessageBubbleContentNode
for i in 0 ..< countryNames.count { for i in 0 ..< countryNames.count {
countries.append(countryNames[i]) countries.append(countryNames[i])
if i == countryNames.count - 2 { if i == countryNames.count - 2 {
countries.append(" and ") countries.append(item.presentationData.strings.Chat_Giveaway_Message_CountriesLastDelimiter)
} else if i < countryNames.count - 2 { } else if i < countryNames.count - 2 {
countries.append(", ") countries.append(item.presentationData.strings.Chat_Giveaway_Message_CountriesDelimiter)
} }
} }
} }
countriesText = "from \(countries)" countriesText = item.presentationData.strings.Chat_Giveaway_Message_CountriesFrom(countries).string
} else { } else {
countriesText = "" countriesText = ""
} }
@ -289,7 +291,7 @@ public class ChatMessageGiveawayBubbleContentNode: ChatMessageBubbleContentNode
let countriesTextString = NSAttributedString(string: countriesText, font: textFont, textColor: textColor) let countriesTextString = NSAttributedString(string: countriesText, font: textFont, textColor: textColor)
let dateTitleString = NSAttributedString(string: "Winners Selection Date", font: titleFont, textColor: textColor) let dateTitleString = NSAttributedString(string: item.presentationData.strings.Chat_Giveaway_Message_DateTitle, font: titleFont, textColor: textColor)
var dateTextString: NSAttributedString? var dateTextString: NSAttributedString?
if let giveaway { if let giveaway {
dateTextString = NSAttributedString(string: stringForFullDate(timestamp: giveaway.untilDate, strings: item.presentationData.strings, dateTimeFormat: item.presentationData.dateTimeFormat), font: textFont, textColor: textColor) dateTextString = NSAttributedString(string: stringForFullDate(timestamp: giveaway.untilDate, strings: item.presentationData.strings, dateTimeFormat: item.presentationData.dateTimeFormat), font: textFont, textColor: textColor)
@ -392,7 +394,7 @@ public class ChatMessageGiveawayBubbleContentNode: ChatMessageBubbleContentNode
titleColor = item.presentationData.theme.theme.chat.message.outgoing.accentTextColor titleColor = item.presentationData.theme.theme.chat.message.outgoing.accentTextColor
} }
let (buttonWidth, continueLayout) = makeButtonLayout(constrainedSize.width, nil, false, "LEARN MORE", titleColor, false, true) let (buttonWidth, continueLayout) = makeButtonLayout(constrainedSize.width, nil, false, item.presentationData.strings.Chat_Giveaway_Message_LearnMore.uppercased(), titleColor, false, true)
let months = giveaway?.months ?? 0 let months = giveaway?.months ?? 0
let animationName: String let animationName: String

View File

@ -567,7 +567,7 @@ public class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureReco
} }
} }
let availableWidth = max(60.0, availableContentWidth - normalDisplaySize.width + 6.0) let availableWidth = max(60.0, availableContentWidth - normalDisplaySize.width + 6.0)
forwardInfoSizeApply = makeForwardInfoLayout(item.presentationData, item.presentationData.strings, .standalone, forwardSource, forwardAuthorSignature, forwardPsaType, nil, CGSize(width: availableWidth, height: CGFloat.greatestFiniteMagnitude)) forwardInfoSizeApply = makeForwardInfoLayout(item.context, item.presentationData, item.presentationData.strings, .standalone, forwardSource, forwardAuthorSignature, forwardPsaType, nil, CGSize(width: availableWidth, height: CGFloat.greatestFiniteMagnitude))
} }
if replyInfoApply != nil || viaBotApply != nil || forwardInfoSizeApply != nil { if replyInfoApply != nil || viaBotApply != nil || forwardInfoSizeApply != nil {

View File

@ -432,7 +432,7 @@ public class ChatMessageInteractiveInstantVideoNode: ASDisplayNode {
} }
} }
let availableWidth: CGFloat = max(60.0, availableContentWidth - 210.0 + 6.0) let availableWidth: CGFloat = max(60.0, availableContentWidth - 210.0 + 6.0)
forwardInfoSizeApply = makeForwardInfoLayout(item.presentationData, item.presentationData.strings, .standalone, forwardSource, forwardAuthorSignature, forwardPsaType, nil, CGSize(width: availableWidth, height: CGFloat.greatestFiniteMagnitude)) forwardInfoSizeApply = makeForwardInfoLayout(item.context, item.presentationData, item.presentationData.strings, .standalone, forwardSource, forwardAuthorSignature, forwardPsaType, nil, CGSize(width: availableWidth, height: CGFloat.greatestFiniteMagnitude))
} }
var notConsumed = false var notConsumed = false

View File

@ -193,14 +193,18 @@ public class ChatMessageReplyInfoNode: ASDisplayNode {
let mainColor: UIColor let mainColor: UIColor
let dustColor: UIColor let dustColor: UIColor
var secondaryColor: UIColor? var secondaryColor: UIColor?
var tertiaryColor: UIColor?
var authorNameColor: UIColor? var authorNameColor: UIColor?
var dashSecondaryColor: UIColor? var dashSecondaryColor: UIColor?
var dashTertiaryColor: UIColor?
let author = arguments.message?.effectiveAuthor let author = arguments.message?.effectiveAuthor
authorNameColor = author?.nameColor?.color let colors = author?.nameColor.flatMap { arguments.context.peerNameColors.get($0) }
dashSecondaryColor = author?.nameColor?.dashColors.1 authorNameColor = colors?.main
dashSecondaryColor = colors?.secondary
dashTertiaryColor = colors?.tertiary
switch arguments.type { switch arguments.type {
case let .bubble(incoming): case let .bubble(incoming):
@ -209,6 +213,7 @@ public class ChatMessageReplyInfoNode: ASDisplayNode {
if let authorNameColor { if let authorNameColor {
mainColor = authorNameColor mainColor = authorNameColor
secondaryColor = dashSecondaryColor secondaryColor = dashSecondaryColor
tertiaryColor = dashTertiaryColor
} else { } else {
mainColor = arguments.presentationData.theme.theme.chat.message.incoming.accentTextColor mainColor = arguments.presentationData.theme.theme.chat.message.incoming.accentTextColor
} }
@ -217,6 +222,9 @@ public class ChatMessageReplyInfoNode: ASDisplayNode {
if dashSecondaryColor != nil { if dashSecondaryColor != nil {
secondaryColor = .clear secondaryColor = .clear
} }
if dashTertiaryColor != nil {
tertiaryColor = .clear
}
} }
dustColor = incoming ? arguments.presentationData.theme.theme.chat.message.incoming.secondaryTextColor : arguments.presentationData.theme.theme.chat.message.outgoing.secondaryTextColor dustColor = incoming ? arguments.presentationData.theme.theme.chat.message.incoming.secondaryTextColor : arguments.presentationData.theme.theme.chat.message.outgoing.secondaryTextColor
case .standalone: case .standalone:
@ -225,6 +233,9 @@ public class ChatMessageReplyInfoNode: ASDisplayNode {
if dashSecondaryColor != nil { if dashSecondaryColor != nil {
secondaryColor = .clear secondaryColor = .clear
} }
if dashTertiaryColor != nil {
tertiaryColor = .clear
}
mainColor = serviceMessageColorComponents(chatTheme: arguments.presentationData.theme.theme.chat, wallpaper: arguments.presentationData.theme.wallpaper).primaryText mainColor = serviceMessageColorComponents(chatTheme: arguments.presentationData.theme.theme.chat, wallpaper: arguments.presentationData.theme.wallpaper).primaryText
dustColor = titleColor dustColor = titleColor
@ -754,6 +765,7 @@ public class ChatMessageReplyInfoNode: ASDisplayNode {
size: backgroundFrame.size, size: backgroundFrame.size,
primaryColor: mainColor, primaryColor: mainColor,
secondaryColor: secondaryColor, secondaryColor: secondaryColor,
thirdColor: tertiaryColor,
pattern: pattern, pattern: pattern,
animation: animation animation: animation
) )

View File

@ -780,7 +780,7 @@ public class ChatMessageStickerItemNode: ChatMessageItemView {
} }
} }
let availableForwardWidth = max(60.0, availableWidth + 6.0) let availableForwardWidth = max(60.0, availableWidth + 6.0)
forwardInfoSizeApply = makeForwardInfoLayout(item.presentationData, item.presentationData.strings, .standalone, forwardSource, forwardAuthorSignature, forwardPsaType, nil, CGSize(width: availableForwardWidth, height: CGFloat.greatestFiniteMagnitude)) forwardInfoSizeApply = makeForwardInfoLayout(item.context, item.presentationData, item.presentationData.strings, .standalone, forwardSource, forwardAuthorSignature, forwardPsaType, nil, CGSize(width: availableForwardWidth, height: CGFloat.greatestFiniteMagnitude))
} }
var needsReplyBackground = false var needsReplyBackground = false

View File

@ -382,16 +382,22 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
let author = item.message.author let author = item.message.author
let mainColor: UIColor let mainColor: UIColor
var secondaryColor: UIColor? var secondaryColor: UIColor? = nil
var tertiaryColor: UIColor? = nil
let nameColors = author?.nameColor.flatMap { item.context.peerNameColors.get($0) }
if !incoming { if !incoming {
mainColor = messageTheme.accentTextColor mainColor = messageTheme.accentTextColor
if let _ = author?.nameColor?.dashColors.1 { if let _ = nameColors?.secondary {
secondaryColor = .clear secondaryColor = .clear
} }
if let _ = nameColors?.tertiary {
tertiaryColor = .clear
}
} else { } else {
var authorNameColor: UIColor? let authorNameColor = nameColors?.main
authorNameColor = author?.nameColor?.color secondaryColor = nameColors?.secondary
secondaryColor = author?.nameColor?.dashColors.1 tertiaryColor = nameColors?.tertiary
if let authorNameColor { if let authorNameColor {
mainColor = authorNameColor mainColor = authorNameColor
@ -400,7 +406,7 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
} }
} }
attributedText = stringWithAppliedEntities(rawText, entities: entities, baseColor: messageTheme.primaryTextColor, linkColor: messageTheme.linkTextColor, baseQuoteTintColor: mainColor, baseQuoteSecondaryTintColor: secondaryColor, baseFont: textFont, linkFont: textFont, boldFont: item.presentationData.messageBoldFont, italicFont: item.presentationData.messageItalicFont, boldItalicFont: item.presentationData.messageBoldItalicFont, fixedFont: item.presentationData.messageFixedFont, blockQuoteFont: item.presentationData.messageBlockQuoteFont, underlineLinks: underlineLinks, message: item.message, adjustQuoteFontSize: true) attributedText = stringWithAppliedEntities(rawText, entities: entities, baseColor: messageTheme.primaryTextColor, linkColor: messageTheme.linkTextColor, baseQuoteTintColor: mainColor, baseQuoteSecondaryTintColor: secondaryColor, baseQuoteTertiaryTintColor: tertiaryColor, baseFont: textFont, linkFont: textFont, boldFont: item.presentationData.messageBoldFont, italicFont: item.presentationData.messageItalicFont, boldItalicFont: item.presentationData.messageBoldItalicFont, fixedFont: item.presentationData.messageFixedFont, blockQuoteFont: item.presentationData.messageBlockQuoteFont, underlineLinks: underlineLinks, message: item.message, adjustQuoteFontSize: true)
} else if !rawText.isEmpty { } else if !rawText.isEmpty {
attributedText = NSAttributedString(string: rawText, font: textFont, textColor: messageTheme.primaryTextColor) attributedText = NSAttributedString(string: rawText, font: textFont, textColor: messageTheme.primaryTextColor)
} else { } else {

View File

@ -1873,7 +1873,9 @@ struct ChatRecentActionsEntry: Comparable, Identifiable {
var text: String = "" var text: String = ""
var entities: [MessageTextEntity] = [] var entities: [MessageTextEntity] = []
let rawText = self.presentationData.strings.Channel_AdminLog_MessageChangedNameColorSet(author.flatMap(EnginePeer.init)?.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder) ?? "", "\(updatedValue)") let _ = updatedValue
let rawText = self.presentationData.strings.Channel_AdminLog_MessageChangedNameColorSet(author.flatMap(EnginePeer.init)?.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder) ?? "", "")
appendAttributedText(text: rawText, generateEntities: { index in appendAttributedText(text: rawText, generateEntities: { index in
if index == 0, let author = author { if index == 0, let author = author {
@ -1899,16 +1901,25 @@ struct ChatRecentActionsEntry: Comparable, Identifiable {
var text: String = "" var text: String = ""
var entities: [MessageTextEntity] = [] var entities: [MessageTextEntity] = []
let rawText = self.presentationData.strings.Channel_AdminLog_MessageChangedBackgroundEmojiSet(author.flatMap(EnginePeer.init)?.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder) ?? "", "\(updatedValue ?? 0)") if let updatedValue, updatedValue != 0 {
appendAttributedText(text: self.presentationData.strings.Channel_AdminLog_MessageChangedBackgroundEmojiSet(author.flatMap(EnginePeer.init)?.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder) ?? "", "."), generateEntities: { index in
if index == 0, let author = author {
return [.TextMention(peerId: author.id)]
} else if index == 1 {
return [.CustomEmoji(stickerPack: nil, fileId: updatedValue)]
}
return []
}, to: &text, entities: &entities)
} else {
let rawText = self.presentationData.strings.Channel_AdminLog_MessageChangedBackgroundEmojiRemoved(author.flatMap(EnginePeer.init)?.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder) ?? "")
appendAttributedText(text: rawText, generateEntities: { index in appendAttributedText(text: rawText, generateEntities: { index in
if index == 0, let author = author { if index == 0, let author = author {
return [.TextMention(peerId: author.id)] return [.TextMention(peerId: author.id)]
} else if index == 1 {
return [.Bold]
} }
return [] return []
}, to: &text, entities: &entities) }, to: &text, entities: &entities)
}
let action = TelegramMediaActionType.customText(text: text, entities: entities) let action = TelegramMediaActionType.customText(text: text, entities: entities)

View File

@ -92,24 +92,31 @@ private let dashBackgroundTemplateImage: UIImage = {
return generateDashBackgroundTemplateImage() return generateDashBackgroundTemplateImage()
}() }()
private func generateDashTemplateImage(isMonochrome: Bool) -> UIImage { private func generateDashTemplateImage(isMonochrome: Bool, isTriple: Bool) -> UIImage {
return generateImage(CGSize(width: radius * 2.0, height: 18.0), rotatedContext: { size, context in return generateImage(CGSize(width: radius * 2.0, height: 18.0), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size)) context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(UIColor.white.cgColor) context.setFillColor(UIColor.white.cgColor)
let dashOffset: CGFloat = isMonochrome ? -4.0 : 5.0 let dashOffset: CGFloat
if isTriple {
dashOffset = isMonochrome ? -2.0 : 0.0
} else {
dashOffset = isMonochrome ? -4.0 : 5.0
}
let dashHeight: CGFloat = isTriple ? 6.0 : 9.0
context.translateBy(x: 0.0, y: dashOffset) context.translateBy(x: 0.0, y: dashOffset)
for _ in 0 ..< 2 { for _ in 0 ..< 2 {
context.move(to: CGPoint(x: 0.0, y: 3.0)) context.move(to: CGPoint(x: 0.0, y: 3.0))
context.addLine(to: CGPoint(x: lineWidth, y: 0.0)) context.addLine(to: CGPoint(x: lineWidth, y: 0.0))
context.addLine(to: CGPoint(x: lineWidth, y: 9.0)) context.addLine(to: CGPoint(x: lineWidth, y: dashHeight))
context.addLine(to: CGPoint(x: 0.0, y: 9.0 + 3.0)) context.addLine(to: CGPoint(x: 0.0, y: dashHeight + 3.0))
context.closePath() context.closePath()
context.fillPath() context.fillPath()
context.translateBy(x: 0.0, y: 18.0) context.translateBy(x: 0.0, y: size.height)
} }
context.clear(CGRect(origin: CGPoint(x: lineWidth, y: 0.0), size: CGSize(width: size.width - lineWidth, height: size.height))) context.clear(CGRect(origin: CGPoint(x: lineWidth, y: 0.0), size: CGSize(width: size.width - lineWidth, height: size.height)))
@ -117,11 +124,19 @@ private func generateDashTemplateImage(isMonochrome: Bool) -> UIImage {
} }
private let dashOpaqueTemplateImage: UIImage = { private let dashOpaqueTemplateImage: UIImage = {
return generateDashTemplateImage(isMonochrome: false) return generateDashTemplateImage(isMonochrome: false, isTriple: false)
}()
private let dashOpaqueTripleTemplateImage: UIImage = {
return generateDashTemplateImage(isMonochrome: false, isTriple: true)
}() }()
private let dashMonochromeTemplateImage: UIImage = { private let dashMonochromeTemplateImage: UIImage = {
return generateDashTemplateImage(isMonochrome: true) return generateDashTemplateImage(isMonochrome: true, isTriple: false)
}()
private let dashMonochromeTripleTemplateImage: UIImage = {
return generateDashTemplateImage(isMonochrome: true, isTriple: true)
}() }()
private func generateGradient(gradientWidth: CGFloat, baseAlpha: CGFloat) -> UIImage { private func generateGradient(gradientWidth: CGFloat, baseAlpha: CGFloat) -> UIImage {
@ -175,6 +190,7 @@ private final class PatternContentsTarget: MultiAnimationRenderTarget {
private final class LineView: UIView { private final class LineView: UIView {
private let backgroundView: UIImageView private let backgroundView: UIImageView
private var dashBackgroundView: UIImageView? private var dashBackgroundView: UIImageView?
private var dashThirdBackgroundView: UIImageView?
private var params: Params? private var params: Params?
private var isAnimating: Bool = false private var isAnimating: Bool = false
@ -183,12 +199,14 @@ private final class LineView: UIView {
var size: CGSize var size: CGSize
var primaryColor: UIColor var primaryColor: UIColor
var secondaryColor: UIColor? var secondaryColor: UIColor?
var thirdColor: UIColor?
var displayProgress: Bool var displayProgress: Bool
init(size: CGSize, primaryColor: UIColor, secondaryColor: UIColor?, displayProgress: Bool) { init(size: CGSize, primaryColor: UIColor, secondaryColor: UIColor?, thirdColor: UIColor?, displayProgress: Bool) {
self.size = size self.size = size
self.primaryColor = primaryColor self.primaryColor = primaryColor
self.secondaryColor = secondaryColor self.secondaryColor = secondaryColor
self.thirdColor = thirdColor
self.displayProgress = displayProgress self.displayProgress = displayProgress
} }
} }
@ -231,6 +249,21 @@ private final class LineView: UIView {
} }
dashBackgroundView.layer.add(animation, forKey: "progress") dashBackgroundView.layer.add(animation, forKey: "progress")
} }
if let dashThirdBackgroundView = self.dashThirdBackgroundView {
if dashThirdBackgroundView.layer.animation(forKey: "progress") == nil {
let animation = dashThirdBackgroundView.layer.makeAnimation(from: 18.0 as NSNumber, to: 0.0 as NSNumber, keyPath: "position.y", timingFunction: CAMediaTimingFunctionName.linear.rawValue, duration: 0.2, delay: 0.0, mediaTimingFunction: nil, removeOnCompletion: true, additive: true)
animation.repeatCount = 1.0
self.isAnimating = true
animation.completion = { [weak self] _ in
guard let self else {
return
}
self.isAnimating = false
self.updateAnimations()
}
dashThirdBackgroundView.layer.add(animation, forKey: "progress")
}
}
} else { } else {
let phaseDuration: Double = 1.0 let phaseDuration: Double = 1.0
if self.backgroundView.layer.animation(forKey: "progress") == nil { if self.backgroundView.layer.animation(forKey: "progress") == nil {
@ -271,11 +304,12 @@ private final class LineView: UIView {
self.layer.masksToBounds = params.secondaryColor != nil || self.isAnimating self.layer.masksToBounds = params.secondaryColor != nil || self.isAnimating
} }
func update(size: CGSize, primaryColor: UIColor, secondaryColor: UIColor?, displayProgress: Bool, animation: ListViewItemUpdateAnimation) { func update(size: CGSize, primaryColor: UIColor, secondaryColor: UIColor?, thirdColor: UIColor?, displayProgress: Bool, animation: ListViewItemUpdateAnimation) {
let params = Params( let params = Params(
size: size, size: size,
primaryColor: primaryColor, primaryColor: primaryColor,
secondaryColor: secondaryColor, secondaryColor: secondaryColor,
thirdColor: thirdColor,
displayProgress: displayProgress displayProgress: displayProgress
) )
if self.params == params { if self.params == params {
@ -305,13 +339,52 @@ private final class LineView: UIView {
dashBackgroundView.frame = dashBackgroundFrame dashBackgroundView.frame = dashBackgroundFrame
} }
let templateImage: UIImage
let monochromeTemplateImage: UIImage
if let thirdColor {
let thirdDashBackgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: -12.0), size: CGSize(width: radius * 2.0, height: size.height + 18.0))
templateImage = dashOpaqueTripleTemplateImage
monochromeTemplateImage = dashMonochromeTripleTemplateImage
let dashThirdBackgroundView: UIImageView
if let current = self.dashThirdBackgroundView {
dashThirdBackgroundView = current
animation.animator.updateFrame(layer: dashThirdBackgroundView.layer, frame: thirdDashBackgroundFrame, completion: nil)
} else {
dashThirdBackgroundView = UIImageView()
self.dashThirdBackgroundView = dashThirdBackgroundView
self.addSubview(dashThirdBackgroundView)
dashThirdBackgroundView.frame = thirdDashBackgroundFrame
}
if thirdColor.alpha == 0.0 {
dashThirdBackgroundView.alpha = 0.4
dashThirdBackgroundView.image = monochromeTemplateImage
dashThirdBackgroundView.tintColor = primaryColor
} else {
dashThirdBackgroundView.alpha = 1.0
dashThirdBackgroundView.image = templateImage
dashThirdBackgroundView.tintColor = thirdColor
}
} else {
templateImage = dashOpaqueTemplateImage
monochromeTemplateImage = dashMonochromeTemplateImage
if let dashThirdBackgroundView = self.dashThirdBackgroundView {
self.dashThirdBackgroundView = nil
dashThirdBackgroundView.removeFromSuperview()
}
}
if secondaryColor.alpha == 0.0 { if secondaryColor.alpha == 0.0 {
self.backgroundView.alpha = 0.2 self.backgroundView.alpha = 0.2
dashBackgroundView.image = dashMonochromeTemplateImage dashBackgroundView.image = monochromeTemplateImage
dashBackgroundView.tintColor = primaryColor dashBackgroundView.tintColor = primaryColor
} else { } else {
self.backgroundView.alpha = 1.0 self.backgroundView.alpha = 1.0
dashBackgroundView.image = dashOpaqueTemplateImage dashBackgroundView.image = templateImage
dashBackgroundView.tintColor = secondaryColor dashBackgroundView.tintColor = secondaryColor
} }
} else { } else {
@ -365,6 +438,7 @@ public final class MessageInlineBlockBackgroundView: UIView {
var size: CGSize var size: CGSize
var primaryColor: UIColor var primaryColor: UIColor
var secondaryColor: UIColor? var secondaryColor: UIColor?
var thirdColor: UIColor?
var pattern: Pattern? var pattern: Pattern?
var displayProgress: Bool var displayProgress: Bool
@ -372,12 +446,14 @@ public final class MessageInlineBlockBackgroundView: UIView {
size: CGSize, size: CGSize,
primaryColor: UIColor, primaryColor: UIColor,
secondaryColor: UIColor?, secondaryColor: UIColor?,
thirdColor: UIColor?,
pattern: Pattern?, pattern: Pattern?,
displayProgress: Bool displayProgress: Bool
) { ) {
self.size = size self.size = size
self.primaryColor = primaryColor self.primaryColor = primaryColor
self.secondaryColor = secondaryColor self.secondaryColor = secondaryColor
self.thirdColor = thirdColor
self.pattern = pattern self.pattern = pattern
self.displayProgress = displayProgress self.displayProgress = displayProgress
} }
@ -393,6 +469,7 @@ public final class MessageInlineBlockBackgroundView: UIView {
size: params.size, size: params.size,
primaryColor: params.primaryColor, primaryColor: params.primaryColor,
secondaryColor: params.secondaryColor, secondaryColor: params.secondaryColor,
thirdColor: params.thirdColor,
pattern: params.pattern, pattern: params.pattern,
animation: .None animation: .None
) )
@ -502,6 +579,7 @@ public final class MessageInlineBlockBackgroundView: UIView {
size: CGSize, size: CGSize,
primaryColor: UIColor, primaryColor: UIColor,
secondaryColor: UIColor?, secondaryColor: UIColor?,
thirdColor: UIColor?,
pattern: Pattern?, pattern: Pattern?,
animation: ListViewItemUpdateAnimation animation: ListViewItemUpdateAnimation
) { ) {
@ -509,6 +587,7 @@ public final class MessageInlineBlockBackgroundView: UIView {
size: size, size: size,
primaryColor: primaryColor, primaryColor: primaryColor,
secondaryColor: secondaryColor, secondaryColor: secondaryColor,
thirdColor: thirdColor,
pattern: pattern, pattern: pattern,
displayProgress: self.displayProgress displayProgress: self.displayProgress
) )
@ -588,6 +667,7 @@ public final class MessageInlineBlockBackgroundView: UIView {
size: lineFrame.size, size: lineFrame.size,
primaryColor: params.primaryColor, primaryColor: params.primaryColor,
secondaryColor: params.secondaryColor, secondaryColor: params.secondaryColor,
thirdColor: params.thirdColor,
displayProgress: params.displayProgress, displayProgress: params.displayProgress,
animation: animation animation: animation
) )

View File

@ -9,25 +9,26 @@ import TelegramUIPreferences
import MergeLists import MergeLists
import ItemListUI import ItemListUI
import PresentationDataUtils import PresentationDataUtils
import AccountContext
private enum PeerNameColorEntryId: Hashable { private enum PeerNameColorEntryId: Hashable {
case color(Int32) case color(Int32)
} }
private enum PeerNameColorEntry: Comparable, Identifiable { private enum PeerNameColorEntry: Comparable, Identifiable {
case color(Int, PeerNameColor, Bool) case color(Int, PeerNameColor, PeerNameColors.Colors, Bool)
var stableId: PeerNameColorEntryId { var stableId: PeerNameColorEntryId {
switch self { switch self {
case let .color(_, color, _): case let .color(_, color, _, _):
return .color(color.rawValue) return .color(color.rawValue)
} }
} }
static func ==(lhs: PeerNameColorEntry, rhs: PeerNameColorEntry) -> Bool { static func ==(lhs: PeerNameColorEntry, rhs: PeerNameColorEntry) -> Bool {
switch lhs { switch lhs {
case let .color(lhsIndex, lhsAccentColor, lhsSelected): case let .color(lhsIndex, lhsColor, lhsAccentColor, lhsSelected):
if case let .color(rhsIndex, rhsAccentColor, rhsSelected) = rhs, lhsIndex == rhsIndex, lhsAccentColor == rhsAccentColor, lhsSelected == rhsSelected { if case let .color(rhsIndex, rhsColor, rhsAccentColor, rhsSelected) = rhs, lhsIndex == rhsIndex, lhsColor == rhsColor, lhsAccentColor == rhsAccentColor, lhsSelected == rhsSelected {
return true return true
} else { } else {
return false return false
@ -37,9 +38,9 @@ private enum PeerNameColorEntry: Comparable, Identifiable {
static func <(lhs: PeerNameColorEntry, rhs: PeerNameColorEntry) -> Bool { static func <(lhs: PeerNameColorEntry, rhs: PeerNameColorEntry) -> Bool {
switch lhs { switch lhs {
case let .color(lhsIndex, _, _): case let .color(lhsIndex, _, _, _):
switch rhs { switch rhs {
case let .color(rhsIndex, _, _): case let .color(rhsIndex, _, _, _):
return lhsIndex < rhsIndex return lhsIndex < rhsIndex
} }
} }
@ -47,20 +48,22 @@ private enum PeerNameColorEntry: Comparable, Identifiable {
func item(action: @escaping (PeerNameColor) -> Void) -> ListViewItem { func item(action: @escaping (PeerNameColor) -> Void) -> ListViewItem {
switch self { switch self {
case let .color(_, color, selected): case let .color(_, index, colors, selected):
return PeerNameColorIconItem(color: color, selected: selected, action: action) return PeerNameColorIconItem(index: index, colors: colors, selected: selected, action: action)
} }
} }
} }
private class PeerNameColorIconItem: ListViewItem { private class PeerNameColorIconItem: ListViewItem {
let color: PeerNameColor let index: PeerNameColor
let colors: PeerNameColors.Colors
let selected: Bool let selected: Bool
let action: (PeerNameColor) -> Void let action: (PeerNameColor) -> Void
public init(color: PeerNameColor, selected: Bool, action: @escaping (PeerNameColor) -> Void) { public init(index: PeerNameColor, colors: PeerNameColors.Colors, selected: Bool, action: @escaping (PeerNameColor) -> Void) {
self.color = color self.index = index
self.colors = colors
self.selected = selected self.selected = selected
self.action = action self.action = action
} }
@ -107,22 +110,22 @@ private class PeerNameColorIconItem: ListViewItem {
public var selectable = true public var selectable = true
public func selected(listView: ListView) { public func selected(listView: ListView) {
self.action(self.color) self.action(self.index)
} }
} }
private func generateRingImage(nameColor: PeerNameColor) -> UIImage? { private func generateRingImage(nameColor: PeerNameColors.Colors) -> UIImage? {
return generateImage(CGSize(width: 40.0, height: 40.0), rotatedContext: { size, context in return generateImage(CGSize(width: 40.0, height: 40.0), rotatedContext: { size, context in
let bounds = CGRect(origin: CGPoint(), size: size) let bounds = CGRect(origin: CGPoint(), size: size)
context.clear(bounds) context.clear(bounds)
context.setStrokeColor(nameColor.color.cgColor) context.setStrokeColor(nameColor.main.cgColor)
context.setLineWidth(2.0) context.setLineWidth(2.0)
context.strokeEllipse(in: bounds.insetBy(dx: 1.0, dy: 1.0)) context.strokeEllipse(in: bounds.insetBy(dx: 1.0, dy: 1.0))
}) })
} }
private func generateFillImage(nameColor: PeerNameColor) -> UIImage? { private func generateFillImage(nameColor: PeerNameColors.Colors) -> UIImage? {
return generateImage(CGSize(width: 40.0, height: 40.0), rotatedContext: { size, context in return generateImage(CGSize(width: 40.0, height: 40.0), rotatedContext: { size, context in
let bounds = CGRect(origin: CGPoint(), size: size) let bounds = CGRect(origin: CGPoint(), size: size)
context.clear(bounds) context.clear(bounds)
@ -131,8 +134,7 @@ private func generateFillImage(nameColor: PeerNameColor) -> UIImage? {
context.addEllipse(in: circleBounds) context.addEllipse(in: circleBounds)
context.clip() context.clip()
let (firstColor, secondColor) = nameColor.dashColors if let secondColor = nameColor.secondary {
if let secondColor {
context.setFillColor(secondColor.cgColor) context.setFillColor(secondColor.cgColor)
context.fill(circleBounds) context.fill(circleBounds)
@ -140,10 +142,20 @@ private func generateFillImage(nameColor: PeerNameColor) -> UIImage? {
context.addLine(to: CGPoint(x: size.width, y: 0.0)) context.addLine(to: CGPoint(x: size.width, y: 0.0))
context.addLine(to: CGPoint(x: 0.0, y: size.height)) context.addLine(to: CGPoint(x: 0.0, y: size.height))
context.closePath() context.closePath()
context.setFillColor(firstColor.cgColor) context.setFillColor(nameColor.main.cgColor)
context.fillPath() context.fillPath()
if let thirdColor = nameColor.tertiary {
context.setFillColor(thirdColor.cgColor)
context.translateBy(x: size.width / 2.0, y: size.height / 2.0)
context.rotate(by: .pi / 4.0)
let path = UIBezierPath(roundedRect: CGRect(origin: CGPoint(x: -9.0, y: -9.0), size: CGSize(width: 18.0, height: 18.0)), cornerRadius: 4.0)
context.addPath(path.cgPath)
context.fillPath()
}
} else { } else {
context.setFillColor(firstColor.cgColor) context.setFillColor(nameColor.main.cgColor)
context.fill(circleBounds) context.fill(circleBounds)
} }
}) })
@ -198,7 +210,7 @@ private final class PeerNameColorIconItemNode : ListViewItemNode {
var updatedAccentColor = false var updatedAccentColor = false
var updatedSelected = false var updatedSelected = false
if currentItem == nil || currentItem?.color != item.color { if currentItem == nil || currentItem?.colors != item.colors {
updatedAccentColor = true updatedAccentColor = true
} }
if currentItem?.selected != item.selected { if currentItem?.selected != item.selected {
@ -211,8 +223,8 @@ private final class PeerNameColorIconItemNode : ListViewItemNode {
strongSelf.item = item strongSelf.item = item
if updatedAccentColor { if updatedAccentColor {
strongSelf.fillNode.image = generateFillImage(nameColor: item.color) strongSelf.fillNode.image = generateFillImage(nameColor: item.colors)
strongSelf.ringNode.image = generateRingImage(nameColor: item.color) strongSelf.ringNode.image = generateRingImage(nameColor: item.colors)
} }
let center = CGPoint(x: 30.0, y: 28.0) let center = CGPoint(x: 30.0, y: 28.0)
@ -256,12 +268,12 @@ final class PeerNameColorItem: ListViewItem, ItemListItem {
var sectionId: ItemListSectionId var sectionId: ItemListSectionId
let theme: PresentationTheme let theme: PresentationTheme
let colors: [PeerNameColor] let colors: PeerNameColors
let currentColor: PeerNameColor let currentColor: PeerNameColor
let updated: (PeerNameColor) -> Void let updated: (PeerNameColor) -> Void
let tag: ItemListItemTag? let tag: ItemListItemTag?
init(theme: PresentationTheme, colors: [PeerNameColor], currentColor: PeerNameColor, updated: @escaping (PeerNameColor) -> Void, tag: ItemListItemTag? = nil, sectionId: ItemListSectionId) { init(theme: PresentationTheme, colors: PeerNameColors, currentColor: PeerNameColor, updated: @escaping (PeerNameColor) -> Void, tag: ItemListItemTag? = nil, sectionId: ItemListSectionId) {
self.theme = theme self.theme = theme
self.colors = colors self.colors = colors
self.currentColor = currentColor self.currentColor = currentColor
@ -325,7 +337,7 @@ private func ensureColorVisible(listNode: ListView, color: PeerNameColor, animat
var resultNode: PeerNameColorIconItemNode? var resultNode: PeerNameColorIconItemNode?
listNode.forEachItemNode { node in listNode.forEachItemNode { node in
if resultNode == nil, let node = node as? PeerNameColorIconItemNode { if resultNode == nil, let node = node as? PeerNameColorIconItemNode {
if node.item?.color == color { if node.item?.index == color {
resultNode = node resultNode = node
} }
} }
@ -406,7 +418,7 @@ final class PeerNameColorItemNode: ListViewItemNode, ItemListItemNode {
let options = ListViewDeleteAndInsertOptions() let options = ListViewDeleteAndInsertOptions()
var scrollToItem: ListViewScrollToItem? var scrollToItem: ListViewScrollToItem?
if !self.initialized || transition.updatePosition || !self.tapping { if !self.initialized || transition.updatePosition || !self.tapping {
if let index = item.colors.firstIndex(where: { $0 == item.currentColor }) { if let index = item.colors.displayOrder.firstIndex(where: { $0 == item.currentColor.rawValue }) {
scrollToItem = ListViewScrollToItem(index: index, position: .bottom(-70.0), animated: false, curve: .Default(duration: 0.0), directionHint: .Down) scrollToItem = ListViewScrollToItem(index: index, position: .bottom(-70.0), animated: false, curve: .Default(duration: 0.0), directionHint: .Down)
self.initialized = true self.initialized = true
} }
@ -501,10 +513,13 @@ final class PeerNameColorItemNode: ListViewItemNode, ItemListItemNode {
var entries: [PeerNameColorEntry] = [] var entries: [PeerNameColorEntry] = []
var index: Int = 0 var i: Int = 0
for color in item.colors { for index in item.colors.displayOrder {
entries.append(.color(index, color, color == item.currentColor)) let color = PeerNameColor(rawValue: index)
index += 1 if let colors = item.colors.colors[index] {
entries.append(.color(i, color, colors, color == item.currentColor))
}
i += 1
} }
let action: (PeerNameColor) -> Void = { [weak self] color in let action: (PeerNameColor) -> Void = { [weak self] color in

View File

@ -47,7 +47,7 @@ private enum PeerNameColorScreenEntry: ItemListNodeEntry {
case colorHeader(String) case colorHeader(String)
case colorMessage(wallpaper: TelegramWallpaper, fontSize: PresentationFontSize, bubbleCorners: PresentationChatBubbleCorners, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, items: [PeerNameColorChatPreviewItem.MessageItem]) case colorMessage(wallpaper: TelegramWallpaper, fontSize: PresentationFontSize, bubbleCorners: PresentationChatBubbleCorners, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, items: [PeerNameColorChatPreviewItem.MessageItem])
case colorPicker(colors: [PeerNameColor], currentColor: PeerNameColor) case colorPicker(colors: PeerNameColors, currentColor: PeerNameColor)
case colorDescription(String) case colorDescription(String)
case backgroundEmojiHeader(String) case backgroundEmojiHeader(String)
case backgroundEmoji(EmojiPagerContentComponent, UIColor) case backgroundEmoji(EmojiPagerContentComponent, UIColor)
@ -185,6 +185,7 @@ private struct PeerNameColorScreenState: Equatable {
} }
private func peerNameColorScreenEntries( private func peerNameColorScreenEntries(
nameColors: PeerNameColors,
presentationData: PresentationData, presentationData: PresentationData,
state: PeerNameColorScreenState, state: PeerNameColorScreenState,
peer: EnginePeer?, peer: EnginePeer?,
@ -194,11 +195,6 @@ private func peerNameColorScreenEntries(
var entries: [PeerNameColorScreenEntry] = [] var entries: [PeerNameColorScreenEntry] = []
if let peer { if let peer {
var allColors: [PeerNameColor] = [
.blue
]
allColors.append(contentsOf: PeerNameColor.allCases.filter { $0 != .blue})
let nameColor: PeerNameColor let nameColor: PeerNameColor
if let updatedNameColor = state.updatedNameColor { if let updatedNameColor = state.updatedNameColor {
nameColor = updatedNameColor nameColor = updatedNameColor
@ -208,6 +204,8 @@ private func peerNameColorScreenEntries(
nameColor = .blue nameColor = .blue
} }
let colors = nameColors.get(nameColor)
let backgroundEmojiId: Int64? let backgroundEmojiId: Int64?
if let updatedBackgroundEmojiId = state.updatedBackgroundEmojiId { if let updatedBackgroundEmojiId = state.updatedBackgroundEmojiId {
if updatedBackgroundEmojiId == 0 { if updatedBackgroundEmojiId == 0 {
@ -250,14 +248,14 @@ private func peerNameColorScreenEntries(
items: [messageItem] items: [messageItem]
)) ))
entries.append(.colorPicker( entries.append(.colorPicker(
colors: allColors, colors: nameColors,
currentColor: nameColor currentColor: nameColor
)) ))
entries.append(.colorDescription(presentationData.strings.NameColor_ChatPreview_Description_Account)) entries.append(.colorDescription(presentationData.strings.NameColor_ChatPreview_Description_Account))
if let emojiContent { if let emojiContent {
entries.append(.backgroundEmojiHeader(presentationData.strings.NameColor_BackgroundEmoji_Title)) entries.append(.backgroundEmojiHeader(presentationData.strings.NameColor_BackgroundEmoji_Title))
entries.append(.backgroundEmoji(emojiContent, nameColor.color)) entries.append(.backgroundEmoji(emojiContent, colors.main))
} }
} }
@ -314,6 +312,7 @@ public func PeerNameColorScreen(
peerId = channelId peerId = channelId
} }
let emojiContent = combineLatest( let emojiContent = combineLatest(
statePromise.get(), statePromise.get(),
context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))
@ -325,12 +324,13 @@ public func PeerNameColorScreen(
} else { } else {
selectedEmojiId = peer?.backgroundEmojiId selectedEmojiId = peer?.backgroundEmojiId
} }
let nameColor: UIColor let nameColor: PeerNameColor
if let updatedNameColor = state.updatedNameColor { if let updatedNameColor = state.updatedNameColor {
nameColor = updatedNameColor.color nameColor = updatedNameColor
} else { } else {
nameColor = (peer?.nameColor ?? .blue).color nameColor = (peer?.nameColor ?? .blue)
} }
let color = context.peerNameColors.get(nameColor)
let selectedItems: [EngineMedia.Id] let selectedItems: [EngineMedia.Id]
if let selectedEmojiId, selectedEmojiId != 0 { if let selectedEmojiId, selectedEmojiId != 0 {
@ -351,7 +351,7 @@ public func PeerNameColorScreen(
areCustomEmojiEnabled: true, areCustomEmojiEnabled: true,
chatPeerId: context.account.peerId, chatPeerId: context.account.peerId,
selectedItems: Set(selectedItems), selectedItems: Set(selectedItems),
backgroundIconColor: nameColor backgroundIconColor: color.main
) )
} }
@ -547,6 +547,7 @@ public func PeerNameColorScreen(
) )
let entries = peerNameColorScreenEntries( let entries = peerNameColorScreenEntries(
nameColors: context.peerNameColors,
presentationData: presentationData, presentationData: presentationData,
state: state, state: state,
peer: peer, peer: peer,

View File

@ -25,7 +25,6 @@ import TelegramUIPreferences
import UndoUI import UndoUI
import TelegramStringFormatting import TelegramStringFormatting
//TODO:localize
final class ShareWithPeersScreenComponent: Component { final class ShareWithPeersScreenComponent: Component {
typealias EnvironmentType = ViewControllerComponentContainer.Environment typealias EnvironmentType = ViewControllerComponentContainer.Environment
@ -941,9 +940,9 @@ final class ShareWithPeersScreenComponent: Component {
sectionTitle = environment.strings.Story_Privacy_WhoCanViewHeader sectionTitle = environment.strings.Story_Privacy_WhoCanViewHeader
} else if section.id == 1 { } else if section.id == 1 {
if case .members = component.stateContext.subject { if case .members = component.stateContext.subject {
sectionTitle = "SUBSCRIBERS" sectionTitle = environment.strings.BoostGift_Subscribers_SectionTitle
} else if case .channels = component.stateContext.subject { } else if case .channels = component.stateContext.subject {
sectionTitle = "CHANNELS" sectionTitle = environment.strings.BoostGift_Channels_SectionTitle
} else { } else {
sectionTitle = environment.strings.Story_Privacy_ContactsHeader sectionTitle = environment.strings.Story_Privacy_ContactsHeader
} }
@ -1390,7 +1389,7 @@ final class ShareWithPeersScreenComponent: Component {
} else { } else {
if case .members = component.stateContext.subject { if case .members = component.stateContext.subject {
if let invitedAt = stateValue.invitedAt[peer.id] { if let invitedAt = stateValue.invitedAt[peer.id] {
subtitle = "joined \(stringForMediumDate(timestamp: invitedAt, strings: environment.strings, dateTimeFormat: environment.dateTimeFormat))" subtitle = environment.strings.BoostGift_Subscribers_Joined(stringForMediumDate(timestamp: invitedAt, strings: environment.strings, dateTimeFormat: environment.dateTimeFormat)).string
} else { } else {
subtitle = nil subtitle = nil
} }
@ -1445,7 +1444,7 @@ final class ShareWithPeersScreenComponent: Component {
self.hapticFeedback.error() self.hapticFeedback.error()
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
controller.present(UndoOverlayController(presentationData: presentationData, content: .info(title: nil, text: "You can select maximum \(component.context.userLimits.maxGiveawayChannelsCount) channels.", timeout: nil, customUndoText: nil), elevatedLayout: false, position: .bottom, animateInAsReplacement: false, action: { _ in return false }), in: .current) controller.present(UndoOverlayController(presentationData: presentationData, content: .info(title: nil, text: environment.strings.BoostGift_Channels_MaximumReached("\(component.context.userLimits.maxGiveawayChannelsCount)").string, timeout: nil, customUndoText: nil), elevatedLayout: false, position: .bottom, animateInAsReplacement: false, action: { _ in return false }), in: .current)
return return
} }
if case .channels = component.stateContext.subject { if case .channels = component.stateContext.subject {
@ -1453,11 +1452,11 @@ final class ShareWithPeersScreenComponent: Component {
let alertController = textAlertController( let alertController = textAlertController(
context: component.context, context: component.context,
forceTheme: environment.theme, forceTheme: environment.theme,
title: "Channel is Private", title: environment.strings.BoostGift_Channels_PrivateChannel_Title,
text: "Are you sure you want to add a private channel? Users won't be able to join it without an invite link.", text: environment.strings.BoostGift_Channels_PrivateChannel_Text,
actions: [ actions: [
TextAlertAction(type: .genericAction, title: environment.strings.Common_Cancel, action: {}), TextAlertAction(type: .genericAction, title: environment.strings.Common_Cancel, action: {}),
TextAlertAction(type: .defaultAction, title: "Add", action: { TextAlertAction(type: .defaultAction, title: environment.strings.BoostGift_Channels_PrivateChannel_Add, action: {
togglePeer() togglePeer()
}) })
] ]
@ -1475,7 +1474,7 @@ final class ShareWithPeersScreenComponent: Component {
self.hapticFeedback.error() self.hapticFeedback.error()
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
controller.present(UndoOverlayController(presentationData: presentationData, content: .info(title: nil, text: "You can select maximum 10 subscribers.", timeout: nil, customUndoText: nil), elevatedLayout: false, position: .bottom, animateInAsReplacement: false, action: { _ in return false }), in: .current) controller.present(UndoOverlayController(presentationData: presentationData, content: .info(title: nil, text: environment.strings.BoostGift_Subscribers_MaximumReached("\(10)").string, timeout: nil, customUndoText: nil), elevatedLayout: false, position: .bottom, animateInAsReplacement: false, action: { _ in return false }), in: .current)
return return
} }
togglePeer() togglePeer()
@ -2018,9 +2017,9 @@ final class ShareWithPeersScreenComponent: Component {
let placeholder: String let placeholder: String
switch component.stateContext.subject { switch component.stateContext.subject {
case .members: case .members:
placeholder = "Search Subscribers" placeholder = environment.strings.BoostGift_Subscribers_Search
case .channels: case .channels:
placeholder = "Search Channels" placeholder = environment.strings.BoostGift_Channels_Search
case .chats: case .chats:
placeholder = environment.strings.Story_Privacy_SearchChats placeholder = environment.strings.Story_Privacy_SearchChats
default: default:
@ -2367,13 +2366,13 @@ final class ShareWithPeersScreenComponent: Component {
case .contactsSearch: case .contactsSearch:
title = "" title = ""
case .members: case .members:
title = "Gift Premium" title = environment.strings.BoostGift_Subscribers_Title
actionButtonTitle = "Save Recipients" subtitle = environment.strings.BoostGift_Subscribers_Subtitle("\(10)").string
subtitle = "select up to 10 subscribers" actionButtonTitle = environment.strings.BoostGift_Subscribers_Save
case .channels: case .channels:
title = "Add Channels" title = environment.strings.BoostGift_Channels_Title
actionButtonTitle = "Save Channels" subtitle = environment.strings.BoostGift_Channels_Subtitle("\(component.context.userLimits.maxGiveawayChannelsCount)").string
subtitle = "select up to \(component.context.userLimits.maxGiveawayChannelsCount) channels" actionButtonTitle = environment.strings.BoostGift_Channels_Save
} }
let titleComponent: AnyComponent<Empty> let titleComponent: AnyComponent<Empty>

View File

@ -180,8 +180,6 @@ final class MediaNavigationStripComponent: Component {
let itemHeight: CGFloat = 2.0 let itemHeight: CGFloat = 2.0
let minItemWidth: CGFloat = 2.0 let minItemWidth: CGFloat = 2.0
var size = CGSize(width: availableSize.width, height: itemHeight)
var didSetCompletion = false var didSetCompletion = false
var validIndices: [Int] = [] var validIndices: [Int] = []
@ -223,7 +221,6 @@ final class MediaNavigationStripComponent: Component {
var itemFrame = CGRect(origin: CGPoint(x: -globalOffset + CGFloat(i) * (itemWidth + spacing), y: 0.0), size: CGSize(width: itemWidth, height: itemHeight)) var itemFrame = CGRect(origin: CGPoint(x: -globalOffset + CGFloat(i) * (itemWidth + spacing), y: 0.0), size: CGSize(width: itemWidth, height: itemHeight))
if component.isSeeking { if component.isSeeking {
itemFrame = CGRect(origin: .zero, size: CGSize(width: availableSize.width, height: 6.0)) itemFrame = CGRect(origin: .zero, size: CGSize(width: availableSize.width, height: 6.0))
size.height = itemFrame.height
} }
if itemFrame.maxX < 0.0 || itemFrame.minX >= availableSize.width { if itemFrame.maxX < 0.0 || itemFrame.minX >= availableSize.width {
continue continue

View File

@ -546,6 +546,14 @@ private final class StoryContainerScreenComponent: Component {
} }
self.initialSeekTimestamp = nil self.initialSeekTimestamp = nil
self.previousSeekTime = nil self.previousSeekTime = nil
guard let stateValue = self.stateValue, let slice = stateValue.slice, let itemSetView = self.visibleItemSetViews[slice.peer.id], let itemSetComponentView = itemSetView.view.view as? StoryItemSetContainerComponent.View else {
return
}
guard let visibleItemView = itemSetComponentView.visibleItems[slice.item.storyItem.id]?.view.view as? StoryItemContentComponent.View else {
return
}
visibleItemView.seekEnded()
} }
longPressRecognizer.shouldBegin = { [weak self] touch in longPressRecognizer.shouldBegin = { [weak self] touch in
guard let self else { guard let self else {

View File

@ -95,12 +95,13 @@ final class StoryInteractionGuideComponent: Component {
self.component = component self.component = component
self.state = state self.state = state
let strings = component.strings
let sideInset: CGFloat = 48.0 let sideInset: CGFloat = 48.0
//TODO:localize
let titleSize = self.titleLabel.update( let titleSize = self.titleLabel.update(
transition: .immediate, transition: .immediate,
component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: "Watching Stories", font: Font.semibold(20.0), textColor: .white, paragraphAlignment: .center)))), component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: strings.Story_Guide_Title, font: Font.semibold(20.0), textColor: .white, paragraphAlignment: .center)))),
environment: {}, environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: availableSize.height) containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: availableSize.height)
) )
@ -114,7 +115,7 @@ final class StoryInteractionGuideComponent: Component {
let textSize = self.descriptionLabel.update( let textSize = self.descriptionLabel.update(
transition: .immediate, transition: .immediate,
component: AnyComponent(BalancedTextComponent(text: .plain(NSAttributedString(string: "You can use these gestures to control playback.", font: Font.regular(15.0), textColor: UIColor(rgb: 0xffffff, alpha: 0.6), paragraphAlignment: .center)), maximumNumberOfLines: 0, lineSpacing: 0.2)), component: AnyComponent(BalancedTextComponent(text: .plain(NSAttributedString(string: strings.Story_Guide_Description, font: Font.regular(15.0), textColor: UIColor(rgb: 0xffffff, alpha: 0.6), paragraphAlignment: .center)), maximumNumberOfLines: 0, lineSpacing: 0.2)),
environment: {}, environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: availableSize.height) containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: availableSize.height)
) )
@ -132,8 +133,8 @@ final class StoryInteractionGuideComponent: Component {
component: AnyComponent( component: AnyComponent(
GuideItemComponent( GuideItemComponent(
context: component.context, context: component.context,
title: "Go forward", title: strings.Story_Guide_ForwardTitle,
text: "Tap the screen", text: strings.Story_Guide_ForwardDescription,
animationName: "story_forward", animationName: "story_forward",
isPlaying: self.currentIndex == 0, isPlaying: self.currentIndex == 0,
playbackCompleted: { [weak self] in playbackCompleted: { [weak self] in
@ -151,8 +152,8 @@ final class StoryInteractionGuideComponent: Component {
component: AnyComponent( component: AnyComponent(
GuideItemComponent( GuideItemComponent(
context: component.context, context: component.context,
title: "Pause and Seek", title: strings.Story_Guide_PauseTitle,
text: "Hold and move sideways", text: strings.Story_Guide_PauseDescription,
animationName: "story_pause", animationName: "story_pause",
isPlaying: self.currentIndex == 1, isPlaying: self.currentIndex == 1,
playbackCompleted: { [weak self] in playbackCompleted: { [weak self] in
@ -170,8 +171,8 @@ final class StoryInteractionGuideComponent: Component {
component: AnyComponent( component: AnyComponent(
GuideItemComponent( GuideItemComponent(
context: component.context, context: component.context,
title: "Go back", title: strings.Story_Guide_BackTitle,
text: "Tap the left edge", text: strings.Story_Guide_BackDescription,
animationName: "story_back", animationName: "story_back",
isPlaying: self.currentIndex == 2, isPlaying: self.currentIndex == 2,
playbackCompleted: { [weak self] in playbackCompleted: { [weak self] in
@ -189,8 +190,8 @@ final class StoryInteractionGuideComponent: Component {
component: AnyComponent( component: AnyComponent(
GuideItemComponent( GuideItemComponent(
context: component.context, context: component.context,
title: "Move between stories", title: strings.Story_Guide_MoveTitle,
text: "Swipe left or right", text: strings.Story_Guide_MoveDescription,
animationName: "story_move", animationName: "story_move",
isPlaying: self.currentIndex == 3, isPlaying: self.currentIndex == 3,
playbackCompleted: { [weak self] in playbackCompleted: { [weak self] in
@ -222,7 +223,7 @@ final class StoryInteractionGuideComponent: Component {
let buttonSize = self.proceedButton.update( let buttonSize = self.proceedButton.update(
transition: .immediate, transition: .immediate,
component: AnyComponent(Button( component: AnyComponent(Button(
content: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: "Tap to keep watching", font: Font.semibold(17.0), textColor: .white, paragraphAlignment: .center)))), content: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: strings.Story_Guide_Proceed, font: Font.semibold(17.0), textColor: .white, paragraphAlignment: .center)))),
action: { [weak self] in action: { [weak self] in
self?.handleTap() self?.handleTap()
} }

View File

@ -277,7 +277,9 @@ final class StoryItemContentComponent: Component {
} }
self.videoPlaybackStatus = status self.videoPlaybackStatus = status
if !self.isSeeking {
self.updateVideoPlaybackProgress() self.updateVideoPlaybackProgress()
}
}) })
} }
} }
@ -360,7 +362,9 @@ final class StoryItemContentComponent: Component {
} }
if case .file = self.currentMessageMedia { if case .file = self.currentMessageMedia {
if !self.isSeeking {
self.updateVideoPlaybackProgress() self.updateVideoPlaybackProgress()
}
} else { } else {
if !self.markedAsSeen { if !self.markedAsSeen {
self.markedAsSeen = true self.markedAsSeen = true
@ -536,6 +540,7 @@ final class StoryItemContentComponent: Component {
) )
} }
private var isSeeking = false
func seekTo(_ timestamp: Double, apply: Bool) { func seekTo(_ timestamp: Double, apply: Bool) {
guard let videoNode = self.videoNode else { guard let videoNode = self.videoNode else {
return return
@ -543,9 +548,14 @@ final class StoryItemContentComponent: Component {
if apply { if apply {
videoNode.seek(timestamp) videoNode.seek(timestamp)
} }
self.isSeeking = true
self.updateVideoPlaybackProgress(timestamp) self.updateVideoPlaybackProgress(timestamp)
} }
func seekEnded() {
self.isSeeking = false
}
func update(component: StoryItemContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<StoryContentItem.Environment>, transition: Transition) -> CGSize { func update(component: StoryItemContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<StoryContentItem.Environment>, transition: Transition) -> CGSize {
let previousItem = self.component?.item let previousItem = self.component?.item

View File

@ -4708,7 +4708,7 @@ public final class StoryItemSetContainerComponent: Component {
let seekLabelSize = self.seekLabel.update( let seekLabelSize = self.seekLabel.update(
transition: .immediate, transition: .immediate,
component: AnyComponent(Text(text: "Slide left or right to seek", font: Font.semibold(14.0), color: .white)), component: AnyComponent(Text(text: component.strings.Story_SlideToSeek, font: Font.semibold(14.0), color: .white)),
environment: {}, environment: {},
containerSize: availableSize containerSize: availableSize
) )

Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 KiB

View File

@ -1,21 +0,0 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "Coffee.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -1,21 +0,0 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "Duck.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

View File

@ -1,21 +0,0 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "Steam.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

View File

@ -244,6 +244,9 @@ public final class AccountContextImpl: AccountContext {
private var userLimitsConfigurationDisposable: Disposable? private var userLimitsConfigurationDisposable: Disposable?
public private(set) var userLimits: EngineConfiguration.UserLimits public private(set) var userLimits: EngineConfiguration.UserLimits
private var peerNameColorsConfigurationDisposable: Disposable?
public private(set) var peerNameColors: PeerNameColors
public private(set) var isPremium: Bool public private(set) var isPremium: Bool
public let imageCache: AnyObject? public let imageCache: AnyObject?
@ -257,6 +260,7 @@ public final class AccountContextImpl: AccountContext {
self.imageCache = DirectMediaImageCache(account: account) self.imageCache = DirectMediaImageCache(account: account)
self.userLimits = EngineConfiguration.UserLimits(UserLimitsConfiguration.defaultValue) self.userLimits = EngineConfiguration.UserLimits(UserLimitsConfiguration.defaultValue)
self.peerNameColors = PeerNameColors.defaultValue
self.isPremium = false self.isPremium = false
self.downloadedMediaStoreManager = DownloadedMediaStoreManagerImpl(postbox: account.postbox, accountManager: sharedContext.accountManager) self.downloadedMediaStoreManager = DownloadedMediaStoreManagerImpl(postbox: account.postbox, accountManager: sharedContext.accountManager)
@ -395,12 +399,20 @@ public final class AccountContextImpl: AccountContext {
return (isPremium, userLimits) return (isPremium, userLimits)
} }
} }
|> deliverOnMainQueue).start(next: { [weak self] isPremium, userLimits in |> deliverOnMainQueue).startStrict(next: { [weak self] isPremium, userLimits in
guard let strongSelf = self else { guard let self = self else {
return return
} }
strongSelf.isPremium = isPremium self.isPremium = isPremium
strongSelf.userLimits = userLimits self.userLimits = userLimits
})
self.peerNameColorsConfigurationDisposable = (self._appConfiguration.get()
|> deliverOnMainQueue).startStrict(next: { [weak self] appConfiguration in
guard let self = self else {
return
}
self.peerNameColors = PeerNameColors.with(appConfiguration: appConfiguration)
}) })
} }
@ -413,6 +425,7 @@ public final class AccountContextImpl: AccountContext {
self.experimentalUISettingsDisposable?.dispose() self.experimentalUISettingsDisposable?.dispose()
self.animatedEmojiStickersDisposable?.dispose() self.animatedEmojiStickersDisposable?.dispose()
self.userLimitsConfigurationDisposable?.dispose() self.userLimitsConfigurationDisposable?.dispose()
self.peerNameColorsConfigurationDisposable?.dispose()
} }
public func storeSecureIdPassword(password: String) { public func storeSecureIdPassword(password: String) {

View File

@ -803,10 +803,6 @@ private func extractAccountManagerState(records: AccountRecordsView<TelegramAcco
icons.append(PresentationAppIcon(name: "PremiumBlack", imageName: "PremiumBlack", isPremium: true)) icons.append(PresentationAppIcon(name: "PremiumBlack", imageName: "PremiumBlack", isPremium: true))
icons.append(PresentationAppIcon(name: "PremiumTurbo", imageName: "PremiumTurbo", isPremium: true)) icons.append(PresentationAppIcon(name: "PremiumTurbo", imageName: "PremiumTurbo", isPremium: true))
icons.append(PresentationAppIcon(name: "PremiumDuck", imageName: "PremiumDuck", isPremium: true))
icons.append(PresentationAppIcon(name: "PremiumCoffee", imageName: "PremiumCoffee", isPremium: true))
icons.append(PresentationAppIcon(name: "PremiumSteam", imageName: "PremiumSteam", isPremium: true))
return icons return icons
} else { } else {
return [] return []

View File

@ -4807,17 +4807,16 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
case let .ongoing(_, status): case let .ongoing(_, status):
switch status { switch status {
case .notAllowed: case .notAllowed:
content = .info(title: nil, text: "You can't participate in this giveaway.", timeout: nil, customUndoText: "Learn More") content = .info(title: nil, text: self.presentationData.strings.Chat_Giveaway_Toast_NotAllowed, timeout: nil, customUndoText: self.presentationData.strings.Chat_Giveaway_Toast_LearnMore)
case .participating: case .participating:
content = .succeed(text: "You are participating in this giveaway.", timeout: nil, customUndoText: "Learn More") content = .succeed(text: self.presentationData.strings.Chat_Giveaway_Toast_Participating, timeout: nil, customUndoText: self.presentationData.strings.Chat_Giveaway_Toast_LearnMore)
case .notQualified: case .notQualified:
content = .info(title: nil, text: "You are not qualified for this giveaway yet.", timeout: nil, customUndoText: "Learn More") content = .info(title: nil, text: self.presentationData.strings.Chat_Giveaway_Toast_NotQualified, timeout: nil, customUndoText: self.presentationData.strings.Chat_Giveaway_Toast_LearnMore)
case .almostOver: case .almostOver:
content = .info(title: nil, text: "The giveaway is almost over.", timeout: nil, customUndoText: "Learn More") content = .info(title: nil, text: self.presentationData.strings.Chat_Giveaway_Toast_AlmostOver, timeout: nil, customUndoText: self.presentationData.strings.Chat_Giveaway_Toast_LearnMore)
} }
case let .finished(status, _, _, _, _): case .finished:
let _ = status content = .info(title: nil, text: self.presentationData.strings.Chat_Giveaway_Toast_Ended, timeout: nil, customUndoText: self.presentationData.strings.Chat_Giveaway_Toast_LearnMore)
content = .info(title: nil, text: "The giveaway is ended.", timeout: nil, customUndoText: "Learn More")
} }
let controller = UndoOverlayController(presentationData: self.presentationData, content: content, elevatedLayout: false, position: .bottom, animateInAsReplacement: false, action: { [weak self] action in let controller = UndoOverlayController(presentationData: self.presentationData, content: content, elevatedLayout: false, position: .bottom, animateInAsReplacement: false, action: { [weak self] action in
if case .undo = action, let self { if case .undo = action, let self {
@ -5110,8 +5109,15 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
if let strongSelf = self { if let strongSelf = self {
let isPremium = peerView.peers[peerView.peerId]?.isPremium ?? false let isPremium = peerView.peers[peerView.peerId]?.isPremium ?? false
var accountPeerColor: ChatPresentationInterfaceState.AccountPeerColor? var accountPeerColor: ChatPresentationInterfaceState.AccountPeerColor?
if peerView.peers[peerView.peerId]?.nameColor?.dashColors.1 != nil { if let nameColor = peerView.peers[peerView.peerId]?.nameColor {
accountPeerColor = ChatPresentationInterfaceState.AccountPeerColor(isDashed: true) let colors = strongSelf.context.peerNameColors.get(nameColor)
var style: ChatPresentationInterfaceState.AccountPeerColor.Style = .solid
if colors.tertiary != nil {
style = .tripleDashed
} else if colors.secondary != nil {
style = .doubleDashed
}
accountPeerColor = ChatPresentationInterfaceState.AccountPeerColor(style: style)
} }
strongSelf.updateChatPresentationInterfaceState(animated: false, interactive: false, { state in strongSelf.updateChatPresentationInterfaceState(animated: false, interactive: false, { state in
return state.updatedIsPremium(isPremium).updatedAccountPeerColor(accountPeerColor) return state.updatedIsPremium(isPremium).updatedAccountPeerColor(accountPeerColor)
@ -19577,150 +19583,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
func displayGiveawayStatusInfo(messageId: EngineMessage.Id, giveawayInfo: PremiumGiveawayInfo) { func displayGiveawayStatusInfo(messageId: EngineMessage.Id, giveawayInfo: PremiumGiveawayInfo) {
let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Messages.Message(id: messageId)) let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Messages.Message(id: messageId))
|> deliverOnMainQueue).startStandalone(next: { [weak self] message in |> deliverOnMainQueue).startStandalone(next: { [weak self] message in
guard let self, let message, let giveaway = message.media.first(where: { $0 is TelegramMediaGiveaway }) as? TelegramMediaGiveaway else { guard let self, let message else {
return return
} }
var peerName = "" if let controller = giveawayInfoController(context: self.context, updatedPresentationData: self.updatedPresentationData, message: message, giveawayInfo: giveawayInfo) {
if let peerId = giveaway.channelPeerIds.first, let peer = message.peers[peerId] { self.present(controller, in: .window(.root))
peerName = EnginePeer(peer).compactDisplayTitle
}
let untilDate = stringForDate(timestamp: giveaway.untilDate, strings: self.presentationData.strings)
let title: String
let text: String
var warning: String?
var dismissImpl: (() -> Void)?
var actions: [TextAlertAction] = [TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: {
dismissImpl?()
})]
switch giveawayInfo {
case let .ongoing(start, status):
let startDate = stringForDate(timestamp: start, strings: self.presentationData.strings)
title = "About This Giveaway"
let intro: String
if case .almostOver = status {
intro = "The giveaway was sponsored by the admins of **\(peerName)**, who acquired **\(giveaway.quantity) Telegram Premium** subscriptions for **\(giveaway.months)** months for its followers."
} else {
intro = "The giveaway is sponsored by the admins of **\(peerName)**, who acquired **\(giveaway.quantity) Telegram Premium** subscriptions for **\(giveaway.months)** months for its followers."
}
let ending: String
if giveaway.flags.contains(.onlyNewSubscribers) {
if giveaway.channelPeerIds.count > 1 {
ending = "On **\(untilDate)**, Telegram will automatically select **\(giveaway.quantity)** random users that joined **\(peerName)** and **\(giveaway.channelPeerIds.count - 1)** other listed channels after **\(startDate)**."
} else {
ending = "On **\(untilDate)**, Telegram will automatically select **\(giveaway.quantity)** random users that joined **\(peerName)** after **\(startDate)**."
}
} else {
if giveaway.channelPeerIds.count > 1 {
ending = "On **\(untilDate)**, Telegram will automatically select **\(giveaway.quantity)** random subscribers of **\(peerName)** and **\(giveaway.channelPeerIds.count - 1)** other listed channels."
} else {
ending = "On **\(untilDate)**, Telegram will automatically select **\(giveaway.quantity)** random subscribers of **\(peerName)**."
}
}
var participation: String
switch status {
case .notQualified:
if giveaway.channelPeerIds.count > 1 {
participation = "To take part in this giveaway please join the channel **\(peerName)** (**\(giveaway.channelPeerIds.count - 1)** other listed channels) before **\(untilDate)**."
} else {
participation = "To take part in this giveaway please join the channel **\(peerName)** before **\(untilDate)**."
}
case let .notAllowed(reason):
switch reason {
case let .joinedTooEarly(joinedOn):
let joinDate = stringForDate(timestamp: joinedOn, strings: self.presentationData.strings)
participation = "You are not eligible to participate in this giveaway, because you joined this channel on **\(joinDate)**, which is before the contest started."
case let .channelAdmin(adminId):
let _ = adminId
participation = "You are not eligible to participate in this giveaway, because you are an admin of participating channel (**\(peerName)**)."
case let .disallowedCountry(countryCode):
let _ = countryCode
participation = "You are not eligible to participate in this giveaway, because your country is not included in the terms of the giveaway."
}
case .participating:
if giveaway.channelPeerIds.count > 1 {
participation = "You are participating in this giveaway, because you have joined the channel **\(peerName)** (**\(giveaway.channelPeerIds.count - 1)** other listed channels)."
} else {
participation = "You are participating in this giveaway, because you have joined the channel **\(peerName)**."
}
case .almostOver:
participation = "The giveaway is over, preparing results."
}
if !participation.isEmpty {
participation = "\n\n\(participation)"
}
text = "\(intro)\n\n\(ending)\(participation)"
case let .finished(status, start, finish, _, activatedCount):
let startDate = stringForDate(timestamp: start, strings: self.presentationData.strings)
let finishDate = stringForDate(timestamp: finish, strings: self.presentationData.strings)
title = "Giveaway Ended"
let intro = "The giveaway was sponsored by the admins of **\(peerName)**, who acquired **\(giveaway.quantity) Telegram Premium** subscriptions for **\(giveaway.months)** months for its followers."
var ending: String
if giveaway.flags.contains(.onlyNewSubscribers) {
if giveaway.channelPeerIds.count > 1 {
ending = "On **\(finishDate)**, Telegram automatically selected **\(giveaway.quantity)** random users that joined **\(peerName)** and other listed channels after **\(startDate)**."
} else {
ending = "On **\(finishDate)**, Telegram automatically selected **\(giveaway.quantity)** random users that joined **\(peerName)** after **\(startDate)**."
}
} else {
if giveaway.channelPeerIds.count > 1 {
ending = "On **\(finishDate)**, Telegram automatically selected **\(giveaway.quantity)** random subscribers of **\(peerName)** and other listed channels."
} else {
ending = "On **\(finishDate)**, Telegram automatically selected **\(giveaway.quantity)** random subscribers of **\(peerName)**."
}
}
if activatedCount > 0 {
ending += " \(activatedCount) of the winners already used their gift links."
}
var result: String
switch status {
case .refunded:
result = ""
warning = "The channel cancelled the prizes by reversing the payment for them."
actions = [TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_Close, action: {
dismissImpl?()
})]
case .notWon:
result = "\n\nYou didn't win a prize in this giveaway."
case let .won(slug):
result = "\n\nYou won a prize in this giveaway. 🏆"
let _ = slug
actions = [TextAlertAction(type: .defaultAction, title: "View My Prize", action: {
dismissImpl?()
}), TextAlertAction(type: .genericAction, title: self.presentationData.strings.Common_Cancel, action: {
dismissImpl?()
})]
}
text = "\(intro)\n\n\(ending)\(result)"
}
let alertController = giveawayInfoAlertController(
context: self.context,
updatedPresentationData: self.updatedPresentationData,
title: title,
text: text,
warning: warning,
actions: actions
)
self.present(alertController, in: .window(.root))
dismissImpl = { [weak alertController] in
alertController?.dismissAnimated()
} }
}) })
} }

View File

@ -1045,11 +1045,22 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch
var baseFontSize: CGFloat = 17.0 var baseFontSize: CGFloat = 17.0
var keyboardAppearance: UIKeyboardAppearance = UIKeyboardAppearance.default var keyboardAppearance: UIKeyboardAppearance = UIKeyboardAppearance.default
if let presentationInterfaceState = self.presentationInterfaceState { if let presentationInterfaceState = self.presentationInterfaceState {
var lineStyle: ChatInputTextView.Theme.Quote.LineStyle = .solid
if let accountPeerColor = presentationInterfaceState.accountPeerColor {
switch accountPeerColor.style {
case .solid:
lineStyle = .solid
case .doubleDashed:
lineStyle = .doubleDashed
case .tripleDashed:
lineStyle = .tripleDashed
}
}
textInputNode.textView.theme = ChatInputTextView.Theme( textInputNode.textView.theme = ChatInputTextView.Theme(
quote: ChatInputTextView.Theme.Quote( quote: ChatInputTextView.Theme.Quote(
background: presentationInterfaceState.theme.list.itemAccentColor.withMultipliedAlpha(presentationInterfaceState.theme.overallDarkAppearance ? 0.2 : 0.1), background: presentationInterfaceState.theme.list.itemAccentColor.withMultipliedAlpha(presentationInterfaceState.theme.overallDarkAppearance ? 0.2 : 0.1),
foreground: presentationInterfaceState.theme.list.itemAccentColor, foreground: presentationInterfaceState.theme.list.itemAccentColor,
isDashed: presentationInterfaceState.accountPeerColor?.isDashed == true lineStyle: lineStyle
) )
) )
@ -1628,11 +1639,22 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch
} }
if let textInputNode = self.textInputNode { if let textInputNode = self.textInputNode {
var lineStyle: ChatInputTextView.Theme.Quote.LineStyle = .solid
if let accountPeerColor = interfaceState.accountPeerColor {
switch accountPeerColor.style {
case .solid:
lineStyle = .solid
case .doubleDashed:
lineStyle = .doubleDashed
case .tripleDashed:
lineStyle = .tripleDashed
}
}
textInputNode.textView.theme = ChatInputTextView.Theme( textInputNode.textView.theme = ChatInputTextView.Theme(
quote: ChatInputTextView.Theme.Quote( quote: ChatInputTextView.Theme.Quote(
background: interfaceState.theme.list.itemAccentColor.withMultipliedAlpha(interfaceState.theme.overallDarkAppearance ? 0.2 : 0.1), background: interfaceState.theme.list.itemAccentColor.withMultipliedAlpha(interfaceState.theme.overallDarkAppearance ? 0.2 : 0.1),
foreground: interfaceState.theme.list.itemAccentColor, foreground: interfaceState.theme.list.itemAccentColor,
isDashed: interfaceState.accountPeerColor?.isDashed == true lineStyle: lineStyle
) )
) )
} }

View File

@ -891,17 +891,26 @@ func openResolvedUrlImpl(_ resolvedUrl: ResolvedUrl, context: AccountContext, ur
forceDark = true forceDark = true
} }
let _ = (context.engine.payments.checkPremiumGiftCode(slug: slug) let _ = (context.engine.payments.checkPremiumGiftCode(slug: slug)
|> deliverOnMainQueue).startStandalone(next: { giftCode in |> deliverOnMainQueue).startStandalone(next: { [weak navigationController] giftCode in
if let giftCode { if let giftCode {
var dismissImpl: (() -> Void)? var dismissImpl: (() -> Void)?
let controller = PremiumGiftCodeScreen( let controller = PremiumGiftCodeScreen(
context: context, context: context,
giftCode: giftCode, subject: .giftCode(giftCode),
forceDark: forceDark, forceDark: forceDark,
action: { action: { [weak navigationController] in
let _ = (context.engine.payments.applyPremiumGiftCode(slug: slug)
|> deliverOnMainQueue).startStandalone(completed: {
dismissImpl?() dismissImpl?()
let _ = context.engine.payments.applyPremiumGiftCode(slug: slug).startStandalone() let controller = context.sharedContext.makePremiumIntroController(context: context, source: .settings, forceDark: forceDark, dismissed: nil)
navigationController?.pushViewController(controller)
if let controller = controller as? PremiumIntroScreen {
Queue.mainQueue().after(0.3, {
controller.animateSuccess()
})
}
})
}, },
openPeer: { peer in openPeer: { peer in
if peer.id != context.account.peerId { if peer.id != context.account.peerId {
@ -941,7 +950,7 @@ func openResolvedUrlImpl(_ resolvedUrl: ResolvedUrl, context: AccountContext, ur
} }
) )
dismissImpl = { [weak controller] in dismissImpl = { [weak controller] in
controller?.dismiss() controller?.dismissAnimated()
} }
navigationController?.pushViewController(controller) navigationController?.pushViewController(controller)
} else { } else {

View File

@ -1621,7 +1621,8 @@ private func editingItems(data: PeerInfoScreenData?, state: PeerInfoState, chatL
} }
if isCreator || (channel.adminRights?.rights.contains(.canChangeInfo) == true) { if isCreator || (channel.adminRights?.rights.contains(.canChangeInfo) == true) {
items[.peerSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemNameColor, label: .semitransparentBadge(EnginePeer(channel).compactDisplayTitle, (data.peer?.nameColor ?? .blue).color), text: "Channel Color", icon: UIImage(bundleImageName: "Chat/Info/NameColorIcon"), action: { let colors = context.peerNameColors.get(data.peer?.nameColor ?? .blue)
items[.peerSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemNameColor, label: .semitransparentBadge(EnginePeer(channel).compactDisplayTitle, colors.main), text: "Channel Color", icon: UIImage(bundleImageName: "Chat/Info/NameColorIcon"), action: {
interaction.editingOpenNameColorSetup() interaction.editingOpenNameColorSetup()
})) }))
} }

View File

@ -55,7 +55,7 @@ public func chatInputStateStringWithAppliedEntities(_ text: String, entities: [M
return string return string
} }
public func stringWithAppliedEntities(_ text: String, entities: [MessageTextEntity], baseColor: UIColor, linkColor: UIColor, baseQuoteTintColor: UIColor? = nil, baseQuoteSecondaryTintColor: UIColor? = nil, baseFont: UIFont, linkFont: UIFont, boldFont: UIFont, italicFont: UIFont, boldItalicFont: UIFont, fixedFont: UIFont, blockQuoteFont: UIFont, underlineLinks: Bool = true, external: Bool = false, message: Message?, entityFiles: [MediaId: TelegramMediaFile] = [:], adjustQuoteFontSize: Bool = false) -> NSAttributedString { public func stringWithAppliedEntities(_ text: String, entities: [MessageTextEntity], baseColor: UIColor, linkColor: UIColor, baseQuoteTintColor: UIColor? = nil, baseQuoteSecondaryTintColor: UIColor? = nil, baseQuoteTertiaryTintColor: UIColor? = nil, baseFont: UIFont, linkFont: UIFont, boldFont: UIFont, italicFont: UIFont, boldItalicFont: UIFont, fixedFont: UIFont, blockQuoteFont: UIFont, underlineLinks: Bool = true, external: Bool = false, message: Message?, entityFiles: [MediaId: TelegramMediaFile] = [:], adjustQuoteFontSize: Bool = false) -> NSAttributedString {
let baseQuoteTintColor = baseQuoteTintColor ?? baseColor let baseQuoteTintColor = baseQuoteTintColor ?? baseColor
var nsString: NSString? var nsString: NSString?
@ -214,7 +214,7 @@ public func stringWithAppliedEntities(_ text: String, entities: [MessageTextEnti
case .BlockQuote: case .BlockQuote:
addFontAttributes(range, .blockQuote) addFontAttributes(range, .blockQuote)
string.addAttribute(NSAttributedString.Key(rawValue: "Attribute__Blockquote"), value: TextNodeBlockQuoteData(title: nil, color: baseQuoteTintColor, secondaryColor: baseQuoteSecondaryTintColor), range: range) string.addAttribute(NSAttributedString.Key(rawValue: "Attribute__Blockquote"), value: TextNodeBlockQuoteData(title: nil, color: baseQuoteTintColor, secondaryColor: baseQuoteSecondaryTintColor, tertiaryColor: baseQuoteTertiaryTintColor), range: range)
case .BankCard: case .BankCard:
string.addAttribute(NSAttributedString.Key.foregroundColor, value: linkColor, range: range) string.addAttribute(NSAttributedString.Key.foregroundColor, value: linkColor, range: range)
if underlineLinks && underlineAllLinks { if underlineLinks && underlineAllLinks {