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",
"PremiumBlack",
"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.";
"Appearance.AppIconCoffee" = "Coffee";
"Appearance.AppIconDuck" = "Duck";
"Appearance.AppIconSteam" = "Steam";
"Notification.GiftLink" = "You received a gift";
"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.MessageChangedBackgroundEmojiSet" = "%1$@ set background emoji to %2$@";
"Channel.AdminLog.MessageChangedBackgroundEmojiRemoved" = "%1$@ removed background emoji";
"Appearance.NameColor" = "Your Name Color";
@ -10197,3 +10194,209 @@ Sorry for the inconvenience.";
"Conversation.MessageOptionsEnlargeVideo" = "Enlarge Video";
"Conversation.LinkOptionsCancel" = "Do Not Preview";
"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 userLimits: EngineConfiguration.UserLimits { get }
var peerNameColors: PeerNameColors { get }
var imageCache: AnyObject? { get }
@ -1062,19 +1063,21 @@ public protocol AccountContext: AnyObject {
public struct 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 showPremiumGiftInAttachMenu: Bool
public let showPremiumGiftInTextField: 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.showPremiumGiftInAttachMenu = showPremiumGiftInAttachMenu
self.showPremiumGiftInTextField = showPremiumGiftInTextField
self.giveawayGiftsPurchaseAvailable = giveawayGiftsPurchaseAvailable
self.boostsPerGiftCount = boostsPerGiftCount
}
public static func with(appConfiguration: AppConfiguration) -> PremiumConfiguration {
@ -1083,7 +1086,8 @@ public struct PremiumConfiguration {
isPremiumDisabled: data["premium_purchase_blocked"] as? Bool ?? false,
showPremiumGiftInAttachMenu: data["premium_gift_attach_menu_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 {
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 var isDashed: Bool
public enum Style {
case solid
case doubleDashed
case tripleDashed
}
public var style: Style
public init(isDashed: Bool) {
self.isDashed = isDashed
public init(style: Style) {
self.style = style
}
}

View File

@ -278,7 +278,7 @@ final class ChatSendMessageActionSheetControllerNode: ViewControllerTracingNode,
quote: ChatInputTextView.Theme.Quote(
background: mainColor.withMultipliedAlpha(0.1),
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 color: 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.color = color
self.secondaryColor = secondaryColor
self.tertiaryColor = tertiaryColor
super.init()
}
@ -102,6 +104,13 @@ public final class TextNodeBlockQuoteData: NSObject {
} else if (self.secondaryColor == nil) != (other.secondaryColor == nil) {
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
}
@ -141,11 +150,13 @@ private final class TextNodeBlockQuote {
let frame: CGRect
let tintColor: 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.tintColor = tintColor
self.secondaryTintColor = secondaryTintColor
self.tertiaryTintColor = tertiaryTintColor
}
}
@ -1212,6 +1223,7 @@ open class TextNode: ASDisplayNode {
let isBlockQuote: Bool
let tintColor: UIColor?
let secondaryTintColor: UIColor?
let tertiaryTintColor: UIColor?
}
var stringSegments: [StringSegment] = []
@ -1235,7 +1247,8 @@ open class TextNode: ASDisplayNode {
firstCharacterOffset: segmentCharacterOffset,
isBlockQuote: false,
tintColor: nil,
secondaryTintColor: nil
secondaryTintColor: nil,
tertiaryTintColor: nil
))
}
@ -1247,7 +1260,8 @@ open class TextNode: ASDisplayNode {
firstCharacterOffset: effectiveRange.location,
isBlockQuote: true,
tintColor: value.color,
secondaryTintColor: value.secondaryColor
secondaryTintColor: value.secondaryColor,
tertiaryTintColor: value.tertiaryColor
))
}
segmentCharacterOffset = effectiveRange.location + effectiveRange.length
@ -1261,7 +1275,8 @@ open class TextNode: ASDisplayNode {
firstCharacterOffset: effectiveRange.location,
isBlockQuote: false,
tintColor: nil,
secondaryTintColor: nil
secondaryTintColor: nil,
tertiaryTintColor: nil
))
segmentCharacterOffset = effectiveRange.location + effectiveRange.length
}
@ -1277,7 +1292,8 @@ open class TextNode: ASDisplayNode {
firstCharacterOffset: segmentCharacterOffset,
isBlockQuote: false,
tintColor: nil,
secondaryTintColor: nil
secondaryTintColor: nil,
tertiaryTintColor: nil
))
}
@ -1290,6 +1306,7 @@ open class TextNode: ASDisplayNode {
var lines: [TextNodeLine] = []
var tintColor: UIColor?
var secondaryTintColor: UIColor?
var tertiaryTintColor: UIColor?
var isBlockQuote: Bool = false
var additionalWidth: CGFloat = 0.0
}
@ -1301,6 +1318,7 @@ open class TextNode: ASDisplayNode {
calculatedSegment.isBlockQuote = segment.isBlockQuote
calculatedSegment.tintColor = segment.tintColor
calculatedSegment.secondaryTintColor = segment.secondaryTintColor
calculatedSegment.tertiaryTintColor = segment.tertiaryTintColor
let rawSubstring = segment.substring.string as NSString
let substringLength = rawSubstring.length
@ -1512,7 +1530,7 @@ open class TextNode: ASDisplayNode {
}
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 {
let isMonochrome = secondaryTintColor.alpha == 0.0
let tertiaryTintColor = blockQuote.tertiaryTintColor
let dashHeight: CGFloat = tertiaryTintColor != nil ? 6.0 : 9.0
do {
context.saveGState()
let dashOffset: CGFloat
if let _ = tertiaryTintColor {
dashOffset = isMonochrome ? -2.0 : 0.0
} else {
dashOffset = isMonochrome ? -4.0 : 5.0
}
if isMonochrome {
context.setFillColor(blockQuote.tintColor.withMultipliedAlpha(0.2).cgColor)
context.fill(lineFrame)
@ -2229,23 +2257,37 @@ open class TextNode: ASDisplayNode {
context.setFillColor(secondaryTintColor.cgColor)
}
let dashOffset: CGFloat = isMonochrome ? -4.0 : 5.0
context.translateBy(x: blockFrame.minX, y: blockFrame.minY + dashOffset)
var offset = 0.0
while offset < blockFrame.height {
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: 9.0))
context.addLine(to: CGPoint(x: 0.0, y: 9.0 + 3.0))
context.closePath()
context.fillPath()
func drawDashes() {
context.translateBy(x: blockFrame.minX, y: blockFrame.minY + dashOffset)
context.translateBy(x: 0.0, y: 18.0)
offset += 18.0
var offset = 0.0
while offset < blockFrame.height {
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: dashHeight))
context.addLine(to: CGPoint(x: 0.0, y: dashHeight + 3.0))
context.closePath()
context.fillPath()
context.translateBy(x: 0.0, y: 18.0)
offset += 18.0
}
}
drawDashes()
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 {
context.setFillColor(blockQuote.tintColor.cgColor)

View File

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

View File

@ -38,7 +38,6 @@ final class AppIconsDemoComponent: Component {
private var component: AppIconsDemoComponent?
private var containerView: UIView
private var axisView = UIView()
private var imageViews: [UIImageView] = []
private var isVisible = false
@ -50,7 +49,6 @@ final class AppIconsDemoComponent: Component {
super.init(frame: frame)
self.addSubview(self.containerView)
self.containerView.addSubview(self.axisView)
}
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.axisView.bounds = CGRect(origin: .zero, size: availableSize)
self.axisView.center = CGPoint(x: availableSize.width, y: availableSize.height / 2.0)
if self.imageViews.isEmpty {
var i = 0
for icon in component.appIcons {
let image: UIImage?
switch icon.imageName {
@ -78,12 +72,6 @@ final class AppIconsDemoComponent: Component {
image = UIImage(bundleImageName: "Premium/Icons/Black")
case "PremiumTurbo":
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:
image = nil
}
@ -95,37 +83,31 @@ final class AppIconsDemoComponent: Component {
imageView.layer.cornerCurve = .continuous
}
imageView.image = image
if i == 0 {
self.containerView.addSubview(imageView)
} else {
self.axisView.addSubview(imageView)
}
self.containerView.addSubview(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
for view in self.imageViews {
let position: CGPoint
if i == 0 {
position = CGPoint(x: availableSize.width, y: availableSize.height / 2.0)
} else {
let angle = CGFloat(i - 1) * angleIncrement
let xPosition = radius * cos(angle) + availableSize.width / 2.0
let yPosition = radius * sin(angle) + availableSize.height / 2.0
position = CGPoint(x: xPosition, y: yPosition)
switch i {
case 0:
position = CGPoint(x: availableSize.width * 0.5, y: availableSize.height * 0.333)
case 1:
position = CGPoint(x: availableSize.width * 0.333, y: availableSize.height * 0.667)
case 2:
position = CGPoint(x: availableSize.width * 0.667, y: availableSize.height * 0.667)
default:
position = CGPoint(x: availableSize.width * 0.5, y: availableSize.height * 0.5)
}
if !self.animating {
view.center = position.offsetBy(dx: availableSize.width / 2.0, dy: 0.0)
}
view.center = position
i += 1
}
@ -143,48 +125,6 @@ final class AppIconsDemoComponent: Component {
}
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
}
@ -192,37 +132,38 @@ final class AppIconsDemoComponent: Component {
func animateIn(availableSize: CGSize) {
self.animating = true
let radius: CGFloat = availableSize.width * 2.5
let angleIncrement: CGFloat = 2 * .pi / CGFloat(self.imageViews.count - 1)
var i = 0
for view in self.imageViews {
if i > 0 {
let delay: Double = 0.033 * Double(i - 1)
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
view.layer.position = initialPosition.offsetBy(dx: xPosition, dy: yPosition)
view.alpha = 0.0
Queue.mainQueue().after(delay) {
view.alpha = 1.0
view.layer.position = initialPosition
view.layer.animateScale(from: 3.0, to: 0.8, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring)
view.layer.animatePosition(from: from, to: CGPoint(), duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15)
if i == self.imageViews.count - 1 {
self.animating = false
}
}
} else {
let from: CGPoint
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 initialPosition = view.layer.position
view.layer.position = initialPosition.offsetBy(dx: from.x, dy: from.y)
Queue.mainQueue().after(delay) {
view.layer.position = initialPosition
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.5, delay: 0.0, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
if i == 2 {
self.animating = false
}
}
i += 1
}
}

View File

@ -361,7 +361,7 @@ private enum CreateGiveawayEntry: ItemListNodeEntry {
default:
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):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, accessoryText: ItemListSectionHeaderAccessoryText(value: additionalText, color: .generic), sectionId: self.section)
case let .subscriptions(_, value):
@ -377,8 +377,7 @@ private enum CreateGiveawayEntry: ItemListNodeEntry {
case let .channelsHeader(_, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
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: {
// arguments.openPeer(peer)
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: {
}, setPeerIdWithRevealedOptions: { lhs, rhs in
arguments.setItemIdWithRevealedOptions(lhs, rhs)
}, removePeer: { id in
@ -433,7 +432,7 @@ private enum CreateGiveawayEntry: ItemListNodeEntry {
} else {
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()
var focus = false
arguments.updateState { state in
@ -515,7 +514,7 @@ private func createGiveawayControllerEntries(
switch subject {
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
if !state.peers.isEmpty {
@ -533,59 +532,68 @@ private func createGiveawayControllerEntries(
recipientsText = presentationData.strings.PremiumGift_LabelRecipients(Int32(peersCount))
}
} 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):
entries.append(.prepaidHeader(presentationData.theme, "PREPAID GIVEAWAY"))
entries.append(.prepaid(presentationData.theme, "\(prepaidGiveaway.quantity) Telegram Premium", "\(prepaidGiveaway.months)-month subscriptions", prepaidGiveaway))
entries.append(.prepaidHeader(presentationData.theme, presentationData.strings.BoostGift_PrepaidGiveawayTitle))
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 .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(.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
let channels = [peerId] + state.channels
for channelId in channels {
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
}
entries.append(.channelAdd(presentationData.theme, "Add Channel"))
entries.append(.channelsInfo(presentationData.theme, "Choose the channels users need to be subscribed to take part in the giveaway"))
entries.append(.channelAdd(presentationData.theme, presentationData.strings.BoostGift_AddChannel))
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
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 {
let allCountries = state.countries.map { locale.localizedString(forRegionCode: $0) ?? $0 }.joined(separator: " and ")
countriesText = "from \(allCountries)"
if state.countries.count == 2 {
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 {
let countryCode = state.countries.first ?? ""
let countryName = locale.localizedString(forRegionCode: countryCode) ?? countryCode
countriesText = presentationData.strings.BoostGift_FromOneCountry(countryName).string
}
} else {
countriesText = "from all countries"
countriesText = presentationData.strings.BoostGift_FromAllCountries
}
entries.append(.usersAll(presentationData.theme, "All subscribers", countriesText, !state.onlyNewEligible))
entries.append(.usersNew(presentationData.theme, "Only new subscribers", 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(.usersAll(presentationData.theme, presentationData.strings.BoostGift_AllSubscribers, countriesText, !state.onlyNewEligible))
entries.append(.usersNew(presentationData.theme, presentationData.strings.BoostGift_OnlyNewSubscribers, countriesText, state.onlyNewEligible))
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))
if state.pickingTimeLimit {
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 {
entries.append(.durationHeader(presentationData.theme, "DURATION OF PREMIUM SUBSCRIPTIONS".uppercased()))
entries.append(.durationHeader(presentationData.theme, presentationData.strings.BoostGift_DurationTitle.uppercased()))
let recipientCount: Int
switch state.mode {
@ -628,7 +636,7 @@ private func createGiveawayControllerEntries(
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
@ -776,7 +784,7 @@ public func createGiveawayController(context: AccountContext, updatedPresentatio
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?()
})
@ -787,7 +795,7 @@ public func createGiveawayController(context: AccountContext, updatedPresentatio
case .gift:
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?()
})
let leftNavigationButton = ItemListNavigationButton(content: .none, style: .regular, enabled: false, action: {})
@ -857,7 +865,7 @@ public func createGiveawayController(context: AccountContext, updatedPresentatio
}
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
var updatedState = state
updatedState.subscriptions = 25
@ -915,11 +923,11 @@ public func createGiveawayController(context: AccountContext, updatedPresentatio
let text: String
switch state.mode {
case .giveaway:
title = "Giveaway Created"
text = "Check your channel's [Statistics]() to see how this giveaway boosted your channel."
title = presentationData.strings.BoostGift_GiveawayCreated_Title
text = presentationData.strings.BoostGift_GiveawayCreated_Text
case .gift:
title = "Premium Subscriptions Gifted"
text = "Check your channel's [Statistics]() to see how gifts boosted your channel."
title = presentationData.strings.BoostGift_PremiumGifted_Title
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
@ -992,8 +1000,8 @@ public func createGiveawayController(context: AccountContext, updatedPresentatio
controllers.removeLast(count)
navigationController.setViewControllers(controllers, animated: true)
let title = "Giveaway Created"
let text = "Check your channel's [Statistics]() to see how this giveaway boosted your channel."
let title = presentationData.strings.BoostGift_GiveawayCreated_Title
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 _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.StatsDatacenterId(id: peerId))

View File

@ -11,12 +11,14 @@ import ComponentFlow
final class CreateGiveawayHeaderItem: ItemListControllerHeaderItem {
let theme: PresentationTheme
let strings: PresentationStrings
let title: String
let text: String
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.strings = strings
self.title = title
self.text = text
self.cancel = cancel
@ -132,7 +134,7 @@ class CreateGiveawayHeaderItemNode: ItemListControllerHeaderItemNode {
self.titleNode.attributedText = attributedTitle
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) {

View File

@ -4,12 +4,164 @@ import AsyncDisplayKit
import Display
import SwiftSignalKit
import TelegramCore
import TelegramPresentationData
import TextFormat
import AccountContext
import AlertUI
import PresentationDataUtils
import TelegramStringFormatting
import TelegramPresentationData
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 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 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 PresentationDataUtils
//TODO:localize
private struct BoostState {
let level: Int32
let currentLevelBoosts: Int32
@ -106,7 +104,7 @@ public func PremiumBoostScreen(
Queue.mainQueue().after(0.3) {
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)
}
}
@ -182,11 +180,12 @@ public func PremiumBoostScreen(
}
} else {
if isPremium {
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with({ $0 }))
let controller = textAlertController(
sharedContext: context.sharedContext,
updatedPresentationData: nil,
title: "More Boosts Needed",
text: "To boost **\(peer.compactDisplayTitle)**, get more boosts by gifting **Telegram Premium** to a friend.",
title: presentationData.strings.ChannelBoost_MoreBoosts_Title,
text: presentationData.strings.ChannelBoost_MoreBoosts_Text(peer.compactDisplayTitle, "\(premiumConfiguration.boostsPerGiftCount)").string,
actions: [
TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})
],

View File

@ -20,44 +20,48 @@ import AvatarNode
import TextFormat
import TelegramStringFormatting
import UndoUI
import InvisibleInkDustNode
private final class PremiumGiftCodeSheetContent: CombinedComponent {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let context: AccountContext
let giftCode: PremiumGiftCodeInfo
let subject: PremiumGiftCodeScreen.Subject
let action: () -> Void
let cancel: (Bool) -> Void
let openPeer: (EnginePeer) -> Void
let openMessage: (EngineMessage.Id) -> Void
let copyLink: (String) -> Void
let shareLink: (String) -> Void
let displayHiddenTooltip: () -> Void
init(
context: AccountContext,
giftCode: PremiumGiftCodeInfo,
subject: PremiumGiftCodeScreen.Subject,
action: @escaping () -> Void,
cancel: @escaping (Bool) -> Void,
openPeer: @escaping (EnginePeer) -> Void,
openMessage: @escaping (EngineMessage.Id) -> Void,
copyLink: @escaping (String) -> Void,
shareLink: @escaping (String) -> Void
shareLink: @escaping (String) -> Void,
displayHiddenTooltip: @escaping () -> Void
) {
self.context = context
self.giftCode = giftCode
self.subject = subject
self.action = action
self.cancel = cancel
self.openPeer = openPeer
self.openMessage = openMessage
self.copyLink = copyLink
self.shareLink = shareLink
self.displayHiddenTooltip = displayHiddenTooltip
}
static func ==(lhs: PremiumGiftCodeSheetContent, rhs: PremiumGiftCodeSheetContent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.giftCode != rhs.giftCode {
if lhs.subject != rhs.subject {
return false
}
return true
@ -72,15 +76,25 @@ private final class PremiumGiftCodeSheetContent: CombinedComponent {
var cachedCloseImage: (UIImage, PresentationTheme)?
init(context: AccountContext, giftCode: PremiumGiftCodeInfo) {
var inProgress = false
init(context: AccountContext, subject: PremiumGiftCodeScreen.Subject) {
self.context = context
super.init()
var peerIds: [EnginePeer.Id] = []
peerIds.append(giftCode.fromPeerId)
if let toPeerId = giftCode.toPeerId {
peerIds.append(toPeerId)
switch subject {
case let .giftCode(giftCode):
peerIds.append(giftCode.fromPeerId)
if let toPeerId = giftCode.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(
@ -111,7 +125,7 @@ private final class PremiumGiftCodeSheetContent: CombinedComponent {
}
func makeState() -> State {
return State(context: self.context, giftCode: self.giftCode)
return State(context: self.context, subject: self.subject)
}
static var body: Body {
@ -133,7 +147,7 @@ private final class PremiumGiftCodeSheetContent: CombinedComponent {
let accountContext = context.component.context
let state = context.state
let giftCode = component.giftCode
let subject = component.subject
let sideInset: CGFloat = 16.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 additionalText: String
let buttonText: String
if let usedDate = giftCode.usedDate {
let dateString = stringForMediumDate(timestamp: usedDate, strings: strings, dateTimeFormat: dateTimeFormat)
titleText = "Used Gift Link"
descriptionText = "This link was used to activate a **Telegram Premium** subscription."
additionalText = "This link was used on \(dateString)."
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 {
let dateString = stringForMediumDate(timestamp: usedDate, strings: strings, dateTimeFormat: dateTimeFormat)
titleText = strings.GiftLink_UsedTitle
descriptionText = strings.GiftLink_UsedDescription
additionalText = strings.GiftLink_UsedFooter(dateString).string
buttonText = strings.Common_OK
} else {
titleText = strings.GiftLink_Title
descriptionText = strings.GiftLink_Description
additionalText = strings.GiftLink_Footer
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
} else {
titleText = "Gift Link"
descriptionText = "This link allows you to activate a **Telegram Premium** subscription."
additionalText = "You can also [send this link]() to a friend as a gift."
buttonText = "Use Link"
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(
@ -213,14 +274,17 @@ private final class PremiumGiftCodeSheetContent: CombinedComponent {
transition: .immediate
)
let link = "https://t.me/giftcode/\(giftCode.slug)"
let linkButton = linkButton.update(
component: Button(
content: AnyComponent(
LinkButtonContentComponent(theme: environment.theme, text: link)
),
action: {
component.copyLink(link)
if let link {
component.copyLink(link)
} else {
component.displayHiddenTooltip()
}
}
),
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: 50.0),
@ -231,11 +295,10 @@ private final class PremiumGiftCodeSheetContent: CombinedComponent {
let tableTextColor = theme.list.itemPrimaryTextColor
let tableLinkColor = theme.list.itemAccentColor
var tableItems: [TableComponent.Item] = []
let fromPeer = state.peerMap[giftCode.fromPeerId]
tableItems.append(.init(
id: "from",
title: "From",
title: strings.GiftLink_From,
component: AnyComponent(
Button(
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 {
let toPeer = state.peerMap[toPeerId]
if let toPeer {
tableItems.append(.init(
id: "to",
title: "To",
title: strings.GiftLink_To,
component: AnyComponent(
Button(
content: AnyComponent(PeerCellComponent(context: context.component.context, textColor: tableLinkColor, peer: toPeer)),
action: {
if let peer = toPeer, peer.id != accountContext.account.peerId {
component.openPeer(peer)
if toPeer.id != accountContext.account.peerId {
component.openPeer(toPeer)
Queue.mainQueue().after(1.0, {
component.cancel(false)
})
@ -269,58 +331,55 @@ private final class PremiumGiftCodeSheetContent: CombinedComponent {
)
)
))
} else if giftCode.isGiveaway {
} else if toPeerId == nil {
tableItems.append(.init(
id: "to",
title: "To",
title: strings.GiftLink_To,
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
if giftCode.months == 12 {
giftTitle = "Telegram Premium for 1 year"
} else {
giftTitle = "Telegram Premium for \(giftCode.months) months"
}
let giftTitle = strings.GiftLink_TelegramPremium(months)
tableItems.append(.init(
id: "gift",
title: "Gift",
title: strings.GiftLink_Gift,
component: AnyComponent(
MultilineTextComponent(text: .plain(NSAttributedString(string: giftTitle, font: tableFont, textColor: tableTextColor)))
)
))
let giftReason: String
if giftCode.toPeerId == nil {
giftReason = "Incomplete Giveaway"
} else {
giftReason = giftCode.isGiveaway ? "Giveaway" : "You were selected by the channel"
if case let .giftCode(giftCode) = component.subject {
let giftReason: String
if giftCode.toPeerId == nil {
giftReason = strings.GiftLink_Reason_Unclaimed
} else {
giftReason = giftCode.isGiveaway ? strings.GiftLink_Reason_Giveaway : strings.GiftLink_Reason_Gift
}
tableItems.append(.init(
id: "reason",
title: strings.GiftLink_Reason,
component: AnyComponent(
Button(
content: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: giftReason, font: tableFont, textColor: giftCode.messageId != nil ? tableLinkColor : tableTextColor)))),
isEnabled: true,
action: {
if let messageId = giftCode.messageId {
component.openMessage(messageId)
}
Queue.mainQueue().after(1.0) {
component.cancel(false)
}
}
)
)
))
}
tableItems.append(.init(
id: "reason",
title: "Reason",
component: AnyComponent(
Button(
content: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: giftReason, font: tableFont, textColor: giftCode.messageId != nil ? tableLinkColor : tableTextColor)))),
isEnabled: true,
action: {
if let messageId = giftCode.messageId {
component.openMessage(messageId)
}
Queue.mainQueue().after(1.0) {
component.cancel(false)
}
}
)
)
))
tableItems.append(.init(
id: "date",
title: "Date",
title: strings.GiftLink_Date,
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
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),
@ -363,15 +424,20 @@ private final class PremiumGiftCodeSheetContent: CombinedComponent {
fontSize: 17.0,
height: 50.0,
cornerRadius: 10.0,
gloss: !giftCode.isUsed,
gloss: gloss,
iconName: nil,
animationName: nil,
iconPosition: .left,
action: {
if giftCode.isUsed {
component.cancel(true)
} else {
isLoading: state.inProgress,
action: { [weak state] in
if gloss {
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
let context: AccountContext
let giftCode: PremiumGiftCodeInfo
let subject: PremiumGiftCodeScreen.Subject
let action: () -> Void
let openPeer: (EnginePeer) -> Void
let openMessage: (EngineMessage.Id) -> Void
let copyLink: (String) -> Void
let shareLink: (String) -> Void
let displayHiddenTooltip: () -> Void
init(
context: AccountContext,
giftCode: PremiumGiftCodeInfo,
subject: PremiumGiftCodeScreen.Subject,
action: @escaping () -> Void,
openPeer: @escaping (EnginePeer) -> Void,
openMessage: @escaping (EngineMessage.Id) -> Void,
copyLink: @escaping (String) -> Void,
shareLink: @escaping (String) -> Void
shareLink: @escaping (String) -> Void,
displayHiddenTooltip: @escaping () -> Void
) {
self.context = context
self.giftCode = giftCode
self.subject = subject
self.action = action
self.openPeer = openPeer
self.openMessage = openMessage
self.copyLink = copyLink
self.shareLink = shareLink
self.displayHiddenTooltip = displayHiddenTooltip
}
static func ==(lhs: PremiumGiftCodeSheetComponent, rhs: PremiumGiftCodeSheetComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.giftCode != rhs.giftCode {
if lhs.subject != rhs.subject {
return false
}
return true
@ -477,7 +546,7 @@ private final class PremiumGiftCodeSheetComponent: CombinedComponent {
component: SheetComponent<EnvironmentType>(
content: AnyComponent<EnvironmentType>(PremiumGiftCodeSheetContent(
context: context.component.context,
giftCode: context.component.giftCode,
subject: context.component.subject,
action: context.component.action,
cancel: { animate in
if animate {
@ -493,9 +562,11 @@ private final class PremiumGiftCodeSheetComponent: CombinedComponent {
openPeer: context.component.openPeer,
openMessage: context.component.openMessage,
copyLink: context.component.copyLink,
shareLink: context.component.shareLink
shareLink: context.component.shareLink,
displayHiddenTooltip: context.component.displayHiddenTooltip
)),
backgroundColor: .color(environment.theme.actionSheet.opaqueItemBackgroundColor),
followContentSizeChanges: true,
animateOut: animateOut
),
environment: {
@ -534,6 +605,11 @@ private final class PremiumGiftCodeSheetComponent: CombinedComponent {
}
public class PremiumGiftCodeScreen: ViewControllerComponentContainer {
public enum Subject: Equatable {
case giftCode(PremiumGiftCodeInfo)
case boost(EnginePeer.Id, ChannelBoostersContext.State.Boost)
}
private let context: AccountContext
public var disposed: () -> Void = {}
@ -541,7 +617,7 @@ public class PremiumGiftCodeScreen: ViewControllerComponentContainer {
public init(
context: AccountContext,
giftCode: PremiumGiftCodeInfo,
subject: PremiumGiftCodeScreen.Subject,
forceDark: Bool = false,
action: @escaping () -> Void,
openPeer: @escaping (EnginePeer) -> Void = { _ in },
@ -551,9 +627,27 @@ public class PremiumGiftCodeScreen: ViewControllerComponentContainer {
self.context = context
var copyLinkImpl: ((String) -> Void)?
super.init(context: context, component: PremiumGiftCodeSheetComponent(context: context, giftCode: giftCode, action: action, openPeer: openPeer, openMessage: openMessage, copyLink: { link in
copyLinkImpl?(link)
}, shareLink: shareLink), navigationBarAppearance: .none, statusBarStyle: .ignore, theme: forceDark ? .dark : .default)
var displayHiddenTooltipImpl: (() -> Void)?
super.init(
context: context,
component: PremiumGiftCodeSheetComponent(
context: context,
subject: subject,
action: action,
openPeer: openPeer,
openMessage: openMessage,
copyLink: { link in
copyLinkImpl?(link)
},
shareLink: shareLink,
displayHiddenTooltip: {
displayHiddenTooltipImpl?()
}
),
navigationBarAppearance: .none,
statusBarStyle: .ignore,
theme: forceDark ? .dark : .default
)
self.navigationPresentation = .flatModal
@ -563,9 +657,21 @@ public class PremiumGiftCodeScreen: ViewControllerComponentContainer {
guard let self else {
return
}
self.dismissAllTooltips()
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))
}
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) {
@ -581,15 +687,40 @@ public class PremiumGiftCodeScreen: ViewControllerComponentContainer {
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 {
let theme: PresentationTheme
let text: String
let text: String?
public init(
theme: PresentationTheme,
text: String
text: String?
) {
self.theme = theme
self.text = text
@ -609,6 +740,7 @@ private final class LinkButtonContentComponent: CombinedComponent {
let background = Child(RoundedRectangle.self)
let text = Child(MultilineTextComponent.self)
let icon = Child(BundleIconComponent.self)
let dust = Child(DustComponent.self)
return { context in
let component = context.component
@ -621,36 +753,48 @@ private final class LinkButtonContentComponent: CombinedComponent {
transition: context.transition
)
let text = text.update(
component: MultilineTextComponent(
text: .plain(NSAttributedString(
string: component.text.replacingOccurrences(of: "https://", with: ""),
font: Font.regular(17.0),
textColor: component.theme.list.itemPrimaryTextColor,
paragraphAlignment: .natural
)),
horizontalAlignment: .center,
maximumNumberOfLines: 1
),
availableSize: CGSize(width: context.availableSize.width - sideInset - sideInset, height: CGFloat.greatestFiniteMagnitude),
transition: .immediate
)
let icon = icon.update(
component: BundleIconComponent(name: "Chat/Context Menu/Copy", tintColor: component.theme.list.itemAccentColor),
availableSize: context.availableSize,
transition: context.transition
)
context.add(background
.position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0))
)
context.add(text
.position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0))
)
context.add(icon
.position(CGPoint(x: context.availableSize.width - icon.size.width / 2.0 - 14.0, y: context.availableSize.height / 2.0))
)
if let _ = component.text {
let text = text.update(
component: MultilineTextComponent(
text: .plain(NSAttributedString(
string: (component.text ?? "").replacingOccurrences(of: "https://", with: ""),
font: Font.regular(17.0),
textColor: component.theme.list.itemPrimaryTextColor,
paragraphAlignment: .natural
)),
horizontalAlignment: .center,
maximumNumberOfLines: 1
),
availableSize: CGSize(width: context.availableSize.width - sideInset - sideInset, height: CGFloat.greatestFiniteMagnitude),
transition: .immediate
)
let icon = icon.update(
component: BundleIconComponent(name: "Chat/Context Menu/Copy", tintColor: component.theme.list.itemAccentColor),
availableSize: context.availableSize,
transition: context.transition
)
context.add(icon
.position(CGPoint(x: context.availableSize.width - icon.size.width / 2.0 - 14.0, y: context.availableSize.height / 2.0))
)
context.add(text
.position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0))
)
} else {
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
}
}
@ -955,3 +1099,53 @@ private final class PeerCellComponent: Component {
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 {
return
}
var newPerks: [String] = []
if !dismissedPremiumAppIconsBadge {
newPerks.append(PremiumPerk.appIcons.identifier)
}
let newPerks: [String] = []
self.newPerks = newPerks
self.updated()
})
@ -1856,7 +1853,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
demoSubject = .animatedUserpics
case .appIcons:
demoSubject = .appIcons
let _ = ApplicationSpecificNotice.setDismissedPremiumAppIconsBadge(accountManager: accountContext.sharedContext.accountManager).startStandalone()
// let _ = ApplicationSpecificNotice.setDismissedPremiumAppIconsBadge(accountManager: accountContext.sharedContext.accountManager).startStandalone()
case .animatedEmoji:
demoSubject = .animatedEmoji
case .emojiStatus:
@ -2918,8 +2915,8 @@ public final class PremiumIntroScreen: ViewControllerComponentContainer {
}
completionImpl = { [weak self] in
if let strongSelf = self {
strongSelf.view.addSubview(ConfettiView(frame: strongSelf.view.bounds))
if let self {
self.animateSuccess()
}
}
}
@ -2933,6 +2930,10 @@ public final class PremiumIntroScreen: ViewControllerComponentContainer {
self.wasDismissed?()
}
public func animateSuccess() {
self.view.addSubview(ConfettiView(frame: self.view.bounds))
}
public override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)

View File

@ -19,8 +19,6 @@ import PeerListItemComponent
import TelegramStringFormatting
import AvatarNode
//TODO:localize
private final class ReplaceBoostScreenComponent: CombinedComponent {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
@ -104,7 +102,6 @@ private final class ReplaceBoostScreenComponent: CombinedComponent {
}
static var body: Body {
// let closeButton = Child(Button.self)
let header = Child(ReplaceBoostHeaderComponent.self)
let description = Child(MultilineTextComponent.self)
let boostsBackground = Child(RoundedRectangle.self)
@ -117,7 +114,6 @@ private final class ReplaceBoostScreenComponent: CombinedComponent {
let theme = environment.theme
let strings = environment.strings
// let topInset: CGFloat = environment.navigationHeight + 22.0
let textSideInset: CGFloat = 32.0
let sideInset: CGFloat = 16.0 + environment.safeInsets.left
@ -168,8 +164,13 @@ private final class ReplaceBoostScreenComponent: CombinedComponent {
return (TelegramTextAttributes.URL, contents)
})
let channelName = state.peer?.compactDisplayTitle ?? ""
let descriptionString = "To boost **\(channelName)**, reassign a previous boost or gift **Telegram Premium** to a friend to get **3** additional boosts."
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.component.context.currentAppConfiguration.with({ $0 }))
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(
component: MultilineTextComponent(
@ -201,11 +202,11 @@ private final class ReplaceBoostScreenComponent: CombinedComponent {
if let cooldownUntil = boost.cooldownUntil, cooldownUntil > state.currentTime {
let duration = cooldownUntil - state.currentTime
let durationValue = stringForDuration(duration, position: nil)
subtitle = "Available in \(durationValue)"
subtitle = strings.ReassignBoost_AvailableIn(durationValue).string
isEnabled = false
} else {
let expiresValue = stringForDate(timestamp: boost.expires, strings: strings)
subtitle = "Boost expires on \(expiresValue)"
subtitle = strings.ReassignBoost_ExpiresOn(expiresValue).string
}
let accountContext = context.component.context
@ -242,7 +243,8 @@ private final class ReplaceBoostScreenComponent: CombinedComponent {
selectedSlotsUpdated(state.selectedSlots)
} else {
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)
}
})
@ -254,7 +256,7 @@ private final class ReplaceBoostScreenComponent: CombinedComponent {
let boosts = boosts.update(
component: List(boostItems),
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
)
@ -272,9 +274,7 @@ private final class ReplaceBoostScreenComponent: CombinedComponent {
.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 contentSize
return CGSize(width: availableSize.width, height: 226.0 + boosts.size.height + environment.safeInsets.bottom + 91.0)
}
}
}
@ -293,6 +293,8 @@ public class ReplaceBoostScreen: ViewController {
let hostView: ComponentHostView<ViewControllerComponentContainer.Environment>
private let footerView: FooterView
private var footerHeight: CGFloat = 0.0
private var bottomOffset: CGFloat = 1000.0
private(set) var isExpanded = false
private var panGestureRecognizer: UIPanGestureRecognizer?
@ -350,6 +352,7 @@ public class ReplaceBoostScreen: ViewController {
}
self.controller?.replaceBoosts?(self.selectedSlots)
}
self.footerView.updateBackgroundAlpha(1.0, transition: .immediate)
}
override func didLoad() {
@ -382,9 +385,22 @@ public class ReplaceBoostScreen: ViewController {
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) {
let contentOffset = self.scrollView.contentOffset.y
self.controller?.navigationBar?.updateBackgroundAlpha(min(30.0, contentOffset) / 30.0, transition: .immediate)
self.updateFooterAlpha()
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
@ -429,6 +445,7 @@ public class ReplaceBoostScreen: ViewController {
}
func containerLayoutUpdated(layout: ContainerViewLayout, navigationHeight: CGFloat, transition: Transition) {
let hadLayout = self.currentLayout != nil
self.currentLayout = (layout, navigationHeight)
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)
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
@ -574,7 +593,7 @@ public class ReplaceBoostScreen: ViewController {
factor = 0.15
}
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 {
return floor(max(layout.size.width, layout.size.height) * factor)
}
@ -673,6 +692,8 @@ public class ReplaceBoostScreen: ViewController {
self.bounds = bounds
self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: .immediate)
self.updateFooterAlpha()
case .ended:
guard let (currentTopInset, panOffset, scrollView, listNode) = self.panGestureArguments else {
return
@ -760,10 +781,14 @@ public class ReplaceBoostScreen: ViewController {
self.bounds = bounds
self.layer.animateBounds(from: previousBounds, to: self.bounds, duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue)
}
self.updateFooterAlpha()
case .cancelled:
self.panGestureArguments = nil
self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: Transition(.animated(duration: 0.3, curve: .easeInOut)))
self.updateFooterAlpha()
default:
break
}
@ -812,9 +837,9 @@ public class ReplaceBoostScreen: ViewController {
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)
@ -890,6 +915,11 @@ public class ReplaceBoostScreen: ViewController {
self.node.updateIsVisible(isVisible: true)
}
override open func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
self.dismissAllTooltips()
}
override open func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
@ -953,10 +983,10 @@ private final class FooterView: UIView {
fileprivate var inProgress = false
private var currentLayout: (CGSize, UIEdgeInsets, PresentationTheme, Int32)?
func update(size: CGSize, insets: UIEdgeInsets, theme: PresentationTheme, count: Int32) -> CGFloat {
private var currentLayout: (CGSize, UIEdgeInsets, PresentationTheme, PresentationStrings, Int32)?
func update(size: CGSize, insets: UIEdgeInsets, theme: PresentationTheme, strings: PresentationStrings, count: Int32) -> CGFloat {
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.separatorView.backgroundColor = theme.rootController.tabBar.separatorColor
@ -988,7 +1018,7 @@ private final class FooterView: UIView {
content: AnyComponentWithIdentity(
id: AnyHashable(0),
component: AnyComponent(ButtonTextContentComponent(
text: "Reassign Boosts",
text: strings.ReassignBoost_ReassignBoosts,
badge: Int(count),
textColor: theme.list.itemCheckColors.foregroundColor,
badgeBackground: theme.list.itemCheckColors.foregroundColor,
@ -1005,8 +1035,8 @@ private final class FooterView: UIView {
return
}
self.inProgress = true
if let (size, insets, theme, count) = self.currentLayout {
let _ = self.update(size: size, insets: insets, theme: theme, count: count)
if let (size, insets, theme, strings, count) = self.currentLayout {
let _ = self.update(size: size, insets: insets, theme: theme, strings: strings, count: count)
}
self.action()
}

View File

@ -365,12 +365,6 @@ class ThemeSettingsAppIconItemNode: ListViewItemNode, ItemListItemNode {
name = item.strings.Appearance_AppIconBlack
case "PremiumTurbo":
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:
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] = []
let strings = presentationData.strings
@ -382,7 +382,8 @@ private func themeSettingsControllerEntries(presentationData: PresentationData,
entries.append(.wallpaper(presentationData.theme, strings.Settings_ChatBackground))
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))
@ -1064,7 +1065,7 @@ public func themeSettingsController(context: AccountContext, focusOnItemTag: The
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 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))
}

View File

@ -22,7 +22,7 @@ import ShareController
import ItemListPeerActionItem
import PremiumUI
private let maxUsersDisplayedLimit: Int32 = 50
private let maxUsersDisplayedLimit: Int32 = 5
private final class ChannelStatsControllerArguments {
let context: AccountContext
@ -31,20 +31,20 @@ private final class ChannelStatsControllerArguments {
let contextAction: (MessageId, ASDisplayNode, ContextGesture?) -> Void
let copyBoostLink: (String) -> Void
let shareBoostLink: (String) -> Void
let openPeer: (EnginePeer) -> Void
let openBoost: (ChannelBoostersContext.State.Boost) -> Void
let expandBoosters: () -> Void
let openGifts: () -> Void
let createPrepaidGiveaway: (PrepaidGiveaway) -> 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.loadDetailedGraph = loadDetailedGraph
self.openMessageStats = openMessage
self.contextAction = contextAction
self.copyBoostLink = copyBoostLink
self.shareBoostLink = shareBoostLink
self.openPeer = openPeer
self.openBoost = openBoost
self.expandBoosters = expandBoosters
self.openGifts = openGifts
self.createPrepaidGiveaway = createPrepaidGiveaway
@ -118,7 +118,7 @@ private enum StatsEntry: ItemListNodeEntry {
case boostersTitle(PresentationTheme, String)
case boostersPlaceholder(PresentationTheme, String)
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 boostersInfo(PresentationTheme, String)
@ -232,7 +232,7 @@ private enum StatsEntry: ItemListNodeEntry {
return 2102
case .boosterTabs:
return 2103
case let .booster(index, _, _, _, _, _, _, _):
case let .booster(index, _, _, _):
return 2104 + index
case .boostersExpand:
return 10000
@ -439,8 +439,8 @@ private enum StatsEntry: ItemListNodeEntry {
} else {
return false
}
case let .booster(lhsIndex, lhsTheme, lhsDateTimeFormat, lhsPeer, lhsCount, lhsFlags, lhsDate, lhsExpires):
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 {
case let .booster(lhsIndex, lhsTheme, lhsDateTimeFormat, lhsBoost):
if case let .booster(rhsIndex, rhsTheme, rhsDateTimeFormat, rhsBoost) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsDateTimeFormat == rhsDateTimeFormat, lhsBoost == rhsBoost {
return true
} else {
return false
@ -548,39 +548,50 @@ private enum StatsEntry: ItemListNodeEntry {
return BoostsTabsItem(theme: presentationData.theme, boostsText: boostText, giftsText: giftText, selectedTab: giftSelected ? .gifts : .boosts, sectionId: self.section, selectionUpdated: { tab in
arguments.updateGiftsSelected(tab == .gifts)
})
case let .booster(_, _, _, peer, count, flags, date, expires):
let expiresValue = stringForDate(timestamp: expires, strings: presentationData.strings)
case let .booster(_, _, _, boost):
let count = boost.multiplier
let expiresValue = stringForDate(timestamp: boost.expires, strings: presentationData.strings)
let expiresString: String
let durationMonths = Int32(round(Float(expires - date) / (86400.0 * 30.0)))
let durationString = "\(durationMonths)m"
let durationMonths = Int32(round(Float(boost.expires - boost.date) / (86400.0 * 30.0)))
let durationString = presentationData.strings.Stats_Boosts_ShortMonth("\(durationMonths)").string
let title: String
let icon: GiftOptionItem.Icon
var label: String?
if flags.contains(.isGiveaway) {
label = "🏆 Giveaway"
} else if flags.contains(.isGift) {
label = "🎁 Gift"
if boost.flags.contains(.isGiveaway) {
label = "🏆 \(presentationData.strings.Stats_Boosts_Giveaway)"
} else if boost.flags.contains(.isGift) {
label = "🎁 \(presentationData.strings.Stats_Boosts_Gift)"
}
if let peer {
let color: GiftOptionItem.Icon.Color
if durationMonths > 11 {
color = .red
} else if durationMonths > 5 {
color = .blue
} else {
color = .green
}
if boost.flags.contains(.isUnclaimed) {
title = presentationData.strings.Stats_Boosts_Unclaimed
icon = .image(color: color, name: "Premium/Unclaimed")
expiresString = "\(durationString)\(expiresValue)"
} else if let peer = boost.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
if durationMonths > 11 {
color = .red
} else if durationMonths > 5 {
color = .blue
if let _ = label {
expiresString = expiresValue
} else {
color = .green
expiresString = presentationData.strings.Stats_Boosts_ExpiresOn(expiresValue).string
}
if flags.contains(.isUnclaimed) {
title = "Unclaimed"
} else {
if boost.flags.contains(.isUnclaimed) {
title = presentationData.strings.Stats_Boosts_Unclaimed
icon = .image(color: color, name: "Premium/Unclaimed")
} else if flags.contains(.isGiveaway) {
title = "To be distributed"
} else if boost.flags.contains(.isGiveaway) {
title = presentationData.strings.Stats_Boosts_ToBeDistributed
icon = .image(color: color, name: "Premium/ToBeDistributed")
} else {
title = "Unknown"
@ -588,9 +599,9 @@ private enum StatsEntry: ItemListNodeEntry {
}
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 ? {
arguments.openPeer(peer!)
} : nil)
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.openBoost(boost)
})
case let .boostersExpand(theme, title):
return ItemListPeerActionItem(presentationData: presentationData, icon: PresentationResourcesItemList.downArrowImage(theme), title: title, sectionId: self.section, editing: false, action: {
arguments.expandBoosters()
@ -626,7 +637,7 @@ private enum StatsEntry: ItemListNodeEntry {
default:
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)
})
}
@ -763,15 +774,14 @@ private func channelStatsControllerEntries(state: ChannelStatsControllerState, p
entries.append(.boostOverviewTitle(presentationData.theme, presentationData.strings.Stats_Boosts_OverviewHeader))
entries.append(.boostOverview(presentationData.theme, boostData))
//TODO:localize
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
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
}
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
@ -802,7 +812,7 @@ private func channelStatsControllerEntries(state: ChannelStatsControllerState, p
}
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?
@ -824,12 +834,12 @@ private func channelStatsControllerEntries(state: ChannelStatsControllerState, p
}
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
}
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))
if giveawayAvailable {
entries.append(.gifts(presentationData.theme, "Get Boosts via Gifts"))
entries.append(.giftsInfo(presentationData.theme, "Get more boosts for your channel by gifting Premium to your subscribers."))
entries.append(.gifts(presentationData.theme, presentationData.strings.Stats_Boosts_GetBoosts))
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 pushImpl: ((ViewController) -> Void)?
var navigateToProfileImpl: ((EnginePeer) -> Void)?
let arguments = ChannelStatsControllerArguments(context: context, loadDetailedGraph: { graph, x -> Signal<StatsGraph?, NoError> in
return statsContext.loadDetailedGraph(graph, x: x)
@ -954,8 +963,9 @@ public func channelStatsController(context: AccountContext, updatedPresentationD
}
presentImpl?(shareController)
},
openPeer: { peer in
navigateToProfileImpl?(peer)
openBoost: { boost in
let controller = PremiumGiftCodeScreen(context: context, subject: .boost(peerId, boost), action: {})
pushImpl?(controller)
},
expandBoosters: {
updateState { $0.withUpdatedBoostersExpanded(true) }
@ -1087,11 +1097,6 @@ public func channelStatsController(context: AccountContext, updatedPresentationD
pushImpl = { [weak controller] c in
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
}

View File

@ -10,6 +10,14 @@ public struct MyBoostStatus: Equatable {
public let date: Int32
public let expires: 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]
@ -175,7 +183,7 @@ private final class ChannelBoostersContextImpl {
var result: [ChannelBoostersContext.State.Boost] = []
for boost in cachedResult.boosts {
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)
} else {
@ -211,7 +219,7 @@ private final class ChannelBoostersContextImpl {
}
func loadMore() {
if self.isLoadingMore {
if self.isLoadingMore || !self.canLoadMore {
return
}
self.isLoadingMore = true
@ -256,7 +264,6 @@ private final class ChannelBoostersContextImpl {
switch boost {
case let .boost(flags, id, userId, giveawayMessageId, date, expires, usedGiftSlug, multiplier):
let _ = giveawayMessageId
let _ = usedGiftSlug
var boostFlags: ChannelBoostersContext.State.Boost.Flags = []
var boostPeer: EnginePeer?
if let userId = userId {
@ -274,7 +281,7 @@ private final class ChannelBoostersContextImpl {
if (flags & (1 << 3)) != 0 {
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 {
@ -305,11 +312,19 @@ private final class ChannelBoostersContextImpl {
}
strongSelf.isLoadingMore = false
strongSelf.hasLoadedOnce = true
strongSelf.canLoadMore = !boosters.isEmpty
strongSelf.canLoadMore = !boosters.isEmpty && nextOffset != nil
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 {
strongSelf.count = Int32(strongSelf.results.count)
var resultsCount: Int32 = 0
for result in strongSelf.results {
resultsCount += result.multiplier
}
strongSelf.count = resultsCount
}
strongSelf.updateState()
}))
@ -357,6 +372,7 @@ public final class ChannelBoostersContext {
public var date: Int32
public var expires: Int32
public var multiplier: Int32
public var slug: String?
}
public var boosts: [Boost]
public var isLoadingMore: Bool
@ -418,6 +434,7 @@ private final class CachedChannelBoosters: Codable {
case date
case expires
case multiplier
case slug
}
var flags: Int32
@ -426,14 +443,16 @@ private final class CachedChannelBoosters: Codable {
var date: Int32
var expires: 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.id = id
self.peerId = peerId
self.date = date
self.expires = expires
self.multiplier = multiplier
self.slug = slug
}
init(from decoder: Decoder) throws {
@ -445,6 +464,7 @@ private final class CachedChannelBoosters: Codable {
self.date = try container.decode(Int32.self, forKey: .date)
self.expires = try container.decode(Int32.self, forKey: .expires)
self.multiplier = try container.decode(Int32.self, forKey: .multiplier)
self.slug = try container.decodeIfPresent(String.self, forKey: .slug)
}
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.expires, forKey: .expires)
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) {
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
}

View File

@ -114,7 +114,7 @@ public struct CachedPremiumGiftOption: Equatable, PostboxCoding {
}
}
public enum PeerNameColor: Int32, CaseIterable {
public enum PeerNameColor: Equatable {
case red
case orange
case violet
@ -122,13 +122,49 @@ public enum PeerNameColor: Int32, CaseIterable {
case cyan
case blue
case pink
case redDash
case orangeDash
case violetDash
case greenDash
case cyanDash
case blueDash
case pinkDash
case other(Int32)
public init(rawValue: Int32) {
switch rawValue {
case 0:
self = .red
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 {

View File

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

View File

@ -223,27 +223,19 @@ public extension Peer {
if let nameColor = user.nameColor {
return nameColor
} 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:
if let nameColor = channel.nameColor {
return nameColor
} else {
return PeerNameColor(rawValue: Int32(self.id.id._internalGetInt64Value() % 7)) ?? .blue
return PeerNameColor(rawValue: Int32(self.id.id._internalGetInt64Value() % 7))
}
default:
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? {
switch self {
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 Theme: Equatable {
public final class Quote: Equatable {
public enum LineStyle {
case solid
case doubleDashed
case tripleDashed
}
public let background: UIColor
public let foreground: UIColor
public let isDashed: Bool
public let lineStyle: LineStyle
public init(
background: UIColor,
foreground: UIColor,
isDashed: Bool
lineStyle: LineStyle
) {
self.background = background
self.foreground = foreground
self.isDashed = isDashed
self.lineStyle = lineStyle
}
public static func ==(lhs: Quote, rhs: Quote) -> Bool {
@ -280,7 +285,7 @@ public final class ChatInputTextView: ChatInputTextViewImpl, NSLayoutManagerDele
if !lhs.foreground.isEqual(rhs.foreground) {
return false
}
if lhs.isDashed != rhs.isDashed {
if lhs.lineStyle != rhs.lineStyle {
return false
}
return true
@ -763,7 +768,8 @@ private final class QuoteBackgroundView: UIView {
self.backgroundView.update(
size: size,
primaryColor: theme.foreground,
secondaryColor: theme.isDashed ? .clear : nil,
secondaryColor: theme.lineStyle != .solid ? .clear : nil,
thirdColor: theme.lineStyle == .tripleDashed ? .clear : nil,
pattern: nil,
animation: .None
)

View File

@ -1213,7 +1213,7 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
}
}
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 {

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 author = message.author
let nameColors = author?.nameColor.flatMap { context.peerNameColors.get($0) }
let mainColor: UIColor
var secondaryColor: UIColor?
var tertiaryColor: UIColor?
if !incoming {
mainColor = messageTheme.accentTextColor
if let _ = author?.nameColor?.dashColors.1 {
if let _ = nameColors?.secondary {
secondaryColor = .clear
}
if let _ = nameColors?.tertiary {
tertiaryColor = .clear
}
} else {
var authorNameColor: UIColor?
authorNameColor = author?.nameColor?.color
secondaryColor = author?.nameColor?.dashColors.1
authorNameColor = nameColors?.main
secondaryColor = nameColors?.secondary
tertiaryColor = nameColors?.tertiary
if let authorNameColor {
mainColor = authorNameColor
@ -853,13 +860,13 @@ public final class ChatMessageAttachedContentNode: ASDisplayNode {
if let current = self.backgroundView {
backgroundView = current
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 {
backgroundView = MessageInlineBlockBackgroundView()
self.backgroundView = backgroundView
backgroundView.frame = backgroundFrame
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 {
if let backgroundView = self.backgroundView {

View File

@ -1270,7 +1270,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
authorNameLayout: (TextNodeLayoutArguments) -> (TextNodeLayout, () -> TextNode),
adminBadgeLayout: (TextNodeLayoutArguments) -> (TextNodeLayout, () -> TextNode),
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),
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)),
@ -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 {
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 {
let nameColor: UIColor
let nameColor = effectiveAuthor.nameColor ?? .blue
let nameColors = item.context.peerNameColors.get(nameColor)
let color: UIColor
if incoming {
nameColor = (effectiveAuthor.nameColor ?? .blue).color
color = nameColors.main
} 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 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)
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 {
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 {
nameColor = (effectiveAuthor.nameColor ?? .blue).color
color = nameColors.main
} 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 {
} else if effectiveAuthor.isScam {
@ -1913,11 +1922,11 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
} 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())
} 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 {
currentCredibilityIcon = .verified(fillColor: item.presentationData.theme.theme.list.itemCheckColors.fillColor, foregroundColor: item.presentationData.theme.theme.list.itemCheckColors.foregroundColor, sizeType: .compact)
} else if effectiveAuthor.isPremium {
currentCredibilityIcon = .premium(color: nameColor.withMultipliedAlpha(0.4))
currentCredibilityIcon = .premium(color: color.withMultipliedAlpha(0.4))
}
}
if let rawAuthorNameColor = authorNameColor {
@ -2178,7 +2187,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
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) })
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) })
if storyType != .regular {

View File

@ -6,6 +6,7 @@ import Postbox
import TelegramCore
import TelegramPresentationData
import LocalizedPeerData
import AccountContext
public enum ChatMessageForwardInfoType: Equatable {
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)
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 prefixFont = Font.regular(fontSize)
let peerFont = Font.medium(fontSize)
@ -163,8 +164,8 @@ public class ChatMessageForwardInfoNode: ASDisplayNode {
}
} else {
if incoming {
if let color = peer?.nameColor?.color {
titleColor = color
if let nameColor = peer?.nameColor {
titleColor = context.peerNameColors.get(nameColor).main
} else {
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))) {
let makeLabelLayout = TextNode.asyncLayout(self.labelNode)
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
@ -226,24 +225,24 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
textSpacing += 13.0
if unclaimed {
title = "Unclaimed Prize"
title = item.presentationData.strings.Notification_PremiumPrize_Unclaimed
} else {
title = "Congratulations!"
title = item.presentationData.strings.Notification_PremiumPrize_Title
}
var peerName = ""
if let channelId, let channel = item.message.peers[channelId] {
peerName = EnginePeer(channel).compactDisplayTitle
}
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 {
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 {
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
buttonTitle = "Open Gift Link"
buttonTitle = item.presentationData.strings.Notification_PremiumPrize_View
hasServiceMessage = false
default:
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)
//TODO:localize
let prizeTitleString = NSAttributedString(string: "Giveaway Prizes", font: titleFont, textColor: textColor)
let prizeTitleString = NSAttributedString(string: item.presentationData.strings.Chat_Giveaway_Message_PrizeTitle, font: titleFont, textColor: textColor)
var prizeTextString: NSAttributedString?
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),
bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor),
link: MarkdownAttributeSet(font: textFont, textColor: textColor),
@ -236,22 +238,22 @@ public class ChatMessageGiveawayBubbleContentNode: ChatMessageBubbleContentNode
), 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 countriesText: String
if let giveaway {
if giveaway.flags.contains(.onlyNewSubscribers) {
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 {
participantsText = "All users who join this channel after this date:"
participantsText = item.presentationData.strings.Chat_Giveaway_Message_ParticipantsNew
}
} else {
if giveaway.channelPeerIds.count > 1 {
participantsText = "All subscribers of the channels below:"
participantsText = item.presentationData.strings.Chat_Giveaway_Message_ParticipantsMany
} else {
participantsText = "All subscribers of this channel:"
participantsText = item.presentationData.strings.Chat_Giveaway_Message_Participants
}
}
if !giveaway.countries.isEmpty {
@ -270,13 +272,13 @@ public class ChatMessageGiveawayBubbleContentNode: ChatMessageBubbleContentNode
for i in 0 ..< countryNames.count {
countries.append(countryNames[i])
if i == countryNames.count - 2 {
countries.append(" and ")
countries.append(item.presentationData.strings.Chat_Giveaway_Message_CountriesLastDelimiter)
} 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 {
countriesText = ""
}
@ -289,7 +291,7 @@ public class ChatMessageGiveawayBubbleContentNode: ChatMessageBubbleContentNode
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?
if let giveaway {
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
}
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 animationName: String

View File

@ -567,7 +567,7 @@ public class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureReco
}
}
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 {

View File

@ -432,7 +432,7 @@ public class ChatMessageInteractiveInstantVideoNode: ASDisplayNode {
}
}
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

View File

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

View File

@ -780,7 +780,7 @@ public class ChatMessageStickerItemNode: ChatMessageItemView {
}
}
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

View File

@ -382,16 +382,22 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
let author = item.message.author
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 {
mainColor = messageTheme.accentTextColor
if let _ = author?.nameColor?.dashColors.1 {
if let _ = nameColors?.secondary {
secondaryColor = .clear
}
if let _ = nameColors?.tertiary {
tertiaryColor = .clear
}
} else {
var authorNameColor: UIColor?
authorNameColor = author?.nameColor?.color
secondaryColor = author?.nameColor?.dashColors.1
let authorNameColor = nameColors?.main
secondaryColor = nameColors?.secondary
tertiaryColor = nameColors?.tertiary
if let 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 {
attributedText = NSAttributedString(string: rawText, font: textFont, textColor: messageTheme.primaryTextColor)
} else {

View File

@ -1873,7 +1873,9 @@ struct ChatRecentActionsEntry: Comparable, Identifiable {
var text: String = ""
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
if index == 0, let author = author {
@ -1899,17 +1901,26 @@ struct ChatRecentActionsEntry: Comparable, Identifiable {
var text: String = ""
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)")
appendAttributedText(text: rawText, generateEntities: { index in
if index == 0, let author = author {
return [.TextMention(peerId: author.id)]
} else if index == 1 {
return [.Bold]
}
return []
}, to: &text, entities: &entities)
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
if index == 0, let author = author {
return [.TextMention(peerId: author.id)]
}
return []
}, to: &text, entities: &entities)
}
let action = TelegramMediaActionType.customText(text: text, entities: entities)
let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:])

View File

@ -92,24 +92,31 @@ private let dashBackgroundTemplateImage: UIImage = {
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
context.clear(CGRect(origin: CGPoint(), size: size))
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)
for _ in 0 ..< 2 {
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: 9.0))
context.addLine(to: CGPoint(x: 0.0, y: 9.0 + 3.0))
context.addLine(to: CGPoint(x: lineWidth, y: dashHeight))
context.addLine(to: CGPoint(x: 0.0, y: dashHeight + 3.0))
context.closePath()
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)))
@ -117,11 +124,19 @@ private func generateDashTemplateImage(isMonochrome: Bool) -> 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 = {
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 {
@ -175,6 +190,7 @@ private final class PatternContentsTarget: MultiAnimationRenderTarget {
private final class LineView: UIView {
private let backgroundView: UIImageView
private var dashBackgroundView: UIImageView?
private var dashThirdBackgroundView: UIImageView?
private var params: Params?
private var isAnimating: Bool = false
@ -183,12 +199,14 @@ private final class LineView: UIView {
var size: CGSize
var primaryColor: UIColor
var secondaryColor: UIColor?
var thirdColor: UIColor?
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.primaryColor = primaryColor
self.secondaryColor = secondaryColor
self.thirdColor = thirdColor
self.displayProgress = displayProgress
}
}
@ -231,6 +249,21 @@ private final class LineView: UIView {
}
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 {
let phaseDuration: Double = 1.0
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
}
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(
size: size,
primaryColor: primaryColor,
secondaryColor: secondaryColor,
thirdColor: thirdColor,
displayProgress: displayProgress
)
if self.params == params {
@ -305,13 +339,52 @@ private final class LineView: UIView {
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 {
self.backgroundView.alpha = 0.2
dashBackgroundView.image = dashMonochromeTemplateImage
dashBackgroundView.image = monochromeTemplateImage
dashBackgroundView.tintColor = primaryColor
} else {
self.backgroundView.alpha = 1.0
dashBackgroundView.image = dashOpaqueTemplateImage
dashBackgroundView.image = templateImage
dashBackgroundView.tintColor = secondaryColor
}
} else {
@ -365,6 +438,7 @@ public final class MessageInlineBlockBackgroundView: UIView {
var size: CGSize
var primaryColor: UIColor
var secondaryColor: UIColor?
var thirdColor: UIColor?
var pattern: Pattern?
var displayProgress: Bool
@ -372,12 +446,14 @@ public final class MessageInlineBlockBackgroundView: UIView {
size: CGSize,
primaryColor: UIColor,
secondaryColor: UIColor?,
thirdColor: UIColor?,
pattern: Pattern?,
displayProgress: Bool
) {
self.size = size
self.primaryColor = primaryColor
self.secondaryColor = secondaryColor
self.thirdColor = thirdColor
self.pattern = pattern
self.displayProgress = displayProgress
}
@ -393,6 +469,7 @@ public final class MessageInlineBlockBackgroundView: UIView {
size: params.size,
primaryColor: params.primaryColor,
secondaryColor: params.secondaryColor,
thirdColor: params.thirdColor,
pattern: params.pattern,
animation: .None
)
@ -502,6 +579,7 @@ public final class MessageInlineBlockBackgroundView: UIView {
size: CGSize,
primaryColor: UIColor,
secondaryColor: UIColor?,
thirdColor: UIColor?,
pattern: Pattern?,
animation: ListViewItemUpdateAnimation
) {
@ -509,6 +587,7 @@ public final class MessageInlineBlockBackgroundView: UIView {
size: size,
primaryColor: primaryColor,
secondaryColor: secondaryColor,
thirdColor: thirdColor,
pattern: pattern,
displayProgress: self.displayProgress
)
@ -588,6 +667,7 @@ public final class MessageInlineBlockBackgroundView: UIView {
size: lineFrame.size,
primaryColor: params.primaryColor,
secondaryColor: params.secondaryColor,
thirdColor: params.thirdColor,
displayProgress: params.displayProgress,
animation: animation
)

View File

@ -9,25 +9,26 @@ import TelegramUIPreferences
import MergeLists
import ItemListUI
import PresentationDataUtils
import AccountContext
private enum PeerNameColorEntryId: Hashable {
case color(Int32)
}
private enum PeerNameColorEntry: Comparable, Identifiable {
case color(Int, PeerNameColor, Bool)
case color(Int, PeerNameColor, PeerNameColors.Colors, Bool)
var stableId: PeerNameColorEntryId {
switch self {
case let .color(_, color, _):
case let .color(_, color, _, _):
return .color(color.rawValue)
}
}
static func ==(lhs: PeerNameColorEntry, rhs: PeerNameColorEntry) -> Bool {
switch lhs {
case let .color(lhsIndex, lhsAccentColor, lhsSelected):
if case let .color(rhsIndex, rhsAccentColor, rhsSelected) = rhs, lhsIndex == rhsIndex, lhsAccentColor == rhsAccentColor, lhsSelected == rhsSelected {
case let .color(lhsIndex, lhsColor, lhsAccentColor, lhsSelected):
if case let .color(rhsIndex, rhsColor, rhsAccentColor, rhsSelected) = rhs, lhsIndex == rhsIndex, lhsColor == rhsColor, lhsAccentColor == rhsAccentColor, lhsSelected == rhsSelected {
return true
} else {
return false
@ -37,9 +38,9 @@ private enum PeerNameColorEntry: Comparable, Identifiable {
static func <(lhs: PeerNameColorEntry, rhs: PeerNameColorEntry) -> Bool {
switch lhs {
case let .color(lhsIndex, _, _):
case let .color(lhsIndex, _, _, _):
switch rhs {
case let .color(rhsIndex, _, _):
case let .color(rhsIndex, _, _, _):
return lhsIndex < rhsIndex
}
}
@ -47,20 +48,22 @@ private enum PeerNameColorEntry: Comparable, Identifiable {
func item(action: @escaping (PeerNameColor) -> Void) -> ListViewItem {
switch self {
case let .color(_, color, selected):
return PeerNameColorIconItem(color: color, selected: selected, action: action)
case let .color(_, index, colors, selected):
return PeerNameColorIconItem(index: index, colors: colors, selected: selected, action: action)
}
}
}
private class PeerNameColorIconItem: ListViewItem {
let color: PeerNameColor
let index: PeerNameColor
let colors: PeerNameColors.Colors
let selected: Bool
let action: (PeerNameColor) -> Void
public init(color: PeerNameColor, selected: Bool, action: @escaping (PeerNameColor) -> Void) {
self.color = color
public init(index: PeerNameColor, colors: PeerNameColors.Colors, selected: Bool, action: @escaping (PeerNameColor) -> Void) {
self.index = index
self.colors = colors
self.selected = selected
self.action = action
}
@ -107,22 +110,22 @@ private class PeerNameColorIconItem: ListViewItem {
public var selectable = true
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
let bounds = CGRect(origin: CGPoint(), size: size)
context.clear(bounds)
context.setStrokeColor(nameColor.color.cgColor)
context.setStrokeColor(nameColor.main.cgColor)
context.setLineWidth(2.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
let bounds = CGRect(origin: CGPoint(), size: size)
context.clear(bounds)
@ -131,8 +134,7 @@ private func generateFillImage(nameColor: PeerNameColor) -> UIImage? {
context.addEllipse(in: circleBounds)
context.clip()
let (firstColor, secondColor) = nameColor.dashColors
if let secondColor {
if let secondColor = nameColor.secondary {
context.setFillColor(secondColor.cgColor)
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: 0.0, y: size.height))
context.closePath()
context.setFillColor(firstColor.cgColor)
context.setFillColor(nameColor.main.cgColor)
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 {
context.setFillColor(firstColor.cgColor)
context.setFillColor(nameColor.main.cgColor)
context.fill(circleBounds)
}
})
@ -198,7 +210,7 @@ private final class PeerNameColorIconItemNode : ListViewItemNode {
var updatedAccentColor = false
var updatedSelected = false
if currentItem == nil || currentItem?.color != item.color {
if currentItem == nil || currentItem?.colors != item.colors {
updatedAccentColor = true
}
if currentItem?.selected != item.selected {
@ -211,8 +223,8 @@ private final class PeerNameColorIconItemNode : ListViewItemNode {
strongSelf.item = item
if updatedAccentColor {
strongSelf.fillNode.image = generateFillImage(nameColor: item.color)
strongSelf.ringNode.image = generateRingImage(nameColor: item.color)
strongSelf.fillNode.image = generateFillImage(nameColor: item.colors)
strongSelf.ringNode.image = generateRingImage(nameColor: item.colors)
}
let center = CGPoint(x: 30.0, y: 28.0)
@ -256,12 +268,12 @@ final class PeerNameColorItem: ListViewItem, ItemListItem {
var sectionId: ItemListSectionId
let theme: PresentationTheme
let colors: [PeerNameColor]
let colors: PeerNameColors
let currentColor: PeerNameColor
let updated: (PeerNameColor) -> Void
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.colors = colors
self.currentColor = currentColor
@ -325,7 +337,7 @@ private func ensureColorVisible(listNode: ListView, color: PeerNameColor, animat
var resultNode: PeerNameColorIconItemNode?
listNode.forEachItemNode { node in
if resultNode == nil, let node = node as? PeerNameColorIconItemNode {
if node.item?.color == color {
if node.item?.index == color {
resultNode = node
}
}
@ -406,7 +418,7 @@ final class PeerNameColorItemNode: ListViewItemNode, ItemListItemNode {
let options = ListViewDeleteAndInsertOptions()
var scrollToItem: ListViewScrollToItem?
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)
self.initialized = true
}
@ -501,10 +513,13 @@ final class PeerNameColorItemNode: ListViewItemNode, ItemListItemNode {
var entries: [PeerNameColorEntry] = []
var index: Int = 0
for color in item.colors {
entries.append(.color(index, color, color == item.currentColor))
index += 1
var i: Int = 0
for index in item.colors.displayOrder {
let color = PeerNameColor(rawValue: index)
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

View File

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

View File

@ -25,7 +25,6 @@ import TelegramUIPreferences
import UndoUI
import TelegramStringFormatting
//TODO:localize
final class ShareWithPeersScreenComponent: Component {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
@ -941,9 +940,9 @@ final class ShareWithPeersScreenComponent: Component {
sectionTitle = environment.strings.Story_Privacy_WhoCanViewHeader
} else if section.id == 1 {
if case .members = component.stateContext.subject {
sectionTitle = "SUBSCRIBERS"
sectionTitle = environment.strings.BoostGift_Subscribers_SectionTitle
} else if case .channels = component.stateContext.subject {
sectionTitle = "CHANNELS"
sectionTitle = environment.strings.BoostGift_Channels_SectionTitle
} else {
sectionTitle = environment.strings.Story_Privacy_ContactsHeader
}
@ -1390,7 +1389,7 @@ final class ShareWithPeersScreenComponent: Component {
} else {
if case .members = component.stateContext.subject {
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 {
subtitle = nil
}
@ -1445,7 +1444,7 @@ final class ShareWithPeersScreenComponent: Component {
self.hapticFeedback.error()
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
}
if case .channels = component.stateContext.subject {
@ -1453,11 +1452,11 @@ final class ShareWithPeersScreenComponent: Component {
let alertController = textAlertController(
context: component.context,
forceTheme: environment.theme,
title: "Channel is Private",
text: "Are you sure you want to add a private channel? Users won't be able to join it without an invite link.",
title: environment.strings.BoostGift_Channels_PrivateChannel_Title,
text: environment.strings.BoostGift_Channels_PrivateChannel_Text,
actions: [
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()
})
]
@ -1475,7 +1474,7 @@ final class ShareWithPeersScreenComponent: Component {
self.hapticFeedback.error()
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
}
togglePeer()
@ -2018,9 +2017,9 @@ final class ShareWithPeersScreenComponent: Component {
let placeholder: String
switch component.stateContext.subject {
case .members:
placeholder = "Search Subscribers"
placeholder = environment.strings.BoostGift_Subscribers_Search
case .channels:
placeholder = "Search Channels"
placeholder = environment.strings.BoostGift_Channels_Search
case .chats:
placeholder = environment.strings.Story_Privacy_SearchChats
default:
@ -2367,13 +2366,13 @@ final class ShareWithPeersScreenComponent: Component {
case .contactsSearch:
title = ""
case .members:
title = "Gift Premium"
actionButtonTitle = "Save Recipients"
subtitle = "select up to 10 subscribers"
title = environment.strings.BoostGift_Subscribers_Title
subtitle = environment.strings.BoostGift_Subscribers_Subtitle("\(10)").string
actionButtonTitle = environment.strings.BoostGift_Subscribers_Save
case .channels:
title = "Add Channels"
actionButtonTitle = "Save Channels"
subtitle = "select up to \(component.context.userLimits.maxGiveawayChannelsCount) channels"
title = environment.strings.BoostGift_Channels_Title
subtitle = environment.strings.BoostGift_Channels_Subtitle("\(component.context.userLimits.maxGiveawayChannelsCount)").string
actionButtonTitle = environment.strings.BoostGift_Channels_Save
}
let titleComponent: AnyComponent<Empty>

View File

@ -180,8 +180,6 @@ final class MediaNavigationStripComponent: Component {
let itemHeight: CGFloat = 2.0
let minItemWidth: CGFloat = 2.0
var size = CGSize(width: availableSize.width, height: itemHeight)
var didSetCompletion = false
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))
if component.isSeeking {
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 {
continue

View File

@ -546,6 +546,14 @@ private final class StoryContainerScreenComponent: Component {
}
self.initialSeekTimestamp = 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
guard let self else {

View File

@ -95,12 +95,13 @@ final class StoryInteractionGuideComponent: Component {
self.component = component
self.state = state
let strings = component.strings
let sideInset: CGFloat = 48.0
//TODO:localize
let titleSize = self.titleLabel.update(
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: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: availableSize.height)
)
@ -114,7 +115,7 @@ final class StoryInteractionGuideComponent: Component {
let textSize = self.descriptionLabel.update(
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: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: availableSize.height)
)
@ -132,8 +133,8 @@ final class StoryInteractionGuideComponent: Component {
component: AnyComponent(
GuideItemComponent(
context: component.context,
title: "Go forward",
text: "Tap the screen",
title: strings.Story_Guide_ForwardTitle,
text: strings.Story_Guide_ForwardDescription,
animationName: "story_forward",
isPlaying: self.currentIndex == 0,
playbackCompleted: { [weak self] in
@ -151,8 +152,8 @@ final class StoryInteractionGuideComponent: Component {
component: AnyComponent(
GuideItemComponent(
context: component.context,
title: "Pause and Seek",
text: "Hold and move sideways",
title: strings.Story_Guide_PauseTitle,
text: strings.Story_Guide_PauseDescription,
animationName: "story_pause",
isPlaying: self.currentIndex == 1,
playbackCompleted: { [weak self] in
@ -170,8 +171,8 @@ final class StoryInteractionGuideComponent: Component {
component: AnyComponent(
GuideItemComponent(
context: component.context,
title: "Go back",
text: "Tap the left edge",
title: strings.Story_Guide_BackTitle,
text: strings.Story_Guide_BackDescription,
animationName: "story_back",
isPlaying: self.currentIndex == 2,
playbackCompleted: { [weak self] in
@ -189,8 +190,8 @@ final class StoryInteractionGuideComponent: Component {
component: AnyComponent(
GuideItemComponent(
context: component.context,
title: "Move between stories",
text: "Swipe left or right",
title: strings.Story_Guide_MoveTitle,
text: strings.Story_Guide_MoveDescription,
animationName: "story_move",
isPlaying: self.currentIndex == 3,
playbackCompleted: { [weak self] in
@ -222,7 +223,7 @@ final class StoryInteractionGuideComponent: Component {
let buttonSize = self.proceedButton.update(
transition: .immediate,
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
self?.handleTap()
}

View File

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

View File

@ -4708,7 +4708,7 @@ public final class StoryItemSetContainerComponent: Component {
let seekLabelSize = self.seekLabel.update(
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: {},
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?
public private(set) var userLimits: EngineConfiguration.UserLimits
private var peerNameColorsConfigurationDisposable: Disposable?
public private(set) var peerNameColors: PeerNameColors
public private(set) var isPremium: Bool
public let imageCache: AnyObject?
@ -257,6 +260,7 @@ public final class AccountContextImpl: AccountContext {
self.imageCache = DirectMediaImageCache(account: account)
self.userLimits = EngineConfiguration.UserLimits(UserLimitsConfiguration.defaultValue)
self.peerNameColors = PeerNameColors.defaultValue
self.isPremium = false
self.downloadedMediaStoreManager = DownloadedMediaStoreManagerImpl(postbox: account.postbox, accountManager: sharedContext.accountManager)
@ -336,7 +340,7 @@ public final class AccountContextImpl: AccountContext {
if !temp {
let currentCountriesConfiguration = self.currentCountriesConfiguration
self.countriesConfigurationDisposable = (self.engine.localization.getCountriesList(accountManager: sharedContext.accountManager, langCode: nil)
|> deliverOnMainQueue).start(next: { value in
|> deliverOnMainQueue).start(next: { value in
let _ = currentCountriesConfiguration.swap(CountriesConfiguration(countries: value))
})
}
@ -395,12 +399,20 @@ public final class AccountContextImpl: AccountContext {
return (isPremium, userLimits)
}
}
|> deliverOnMainQueue).start(next: { [weak self] isPremium, userLimits in
guard let strongSelf = self else {
|> deliverOnMainQueue).startStrict(next: { [weak self] isPremium, userLimits in
guard let self = self else {
return
}
strongSelf.isPremium = isPremium
strongSelf.userLimits = userLimits
self.isPremium = isPremium
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.animatedEmojiStickersDisposable?.dispose()
self.userLimitsConfigurationDisposable?.dispose()
self.peerNameColorsConfigurationDisposable?.dispose()
}
public func storeSecureIdPassword(password: String) {

View File

@ -802,11 +802,7 @@ private func extractAccountManagerState(records: AccountRecordsView<TelegramAcco
icons.append(PresentationAppIcon(name: "Premium", imageName: "Premium", isPremium: true))
icons.append(PresentationAppIcon(name: "PremiumBlack", imageName: "PremiumBlack", 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
} else {
return []

View File

@ -4807,17 +4807,16 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
case let .ongoing(_, status):
switch status {
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:
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:
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:
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, _, _, _, _):
let _ = status
content = .info(title: nil, text: "The giveaway is ended.", timeout: nil, customUndoText: "Learn More")
case .finished:
content = .info(title: nil, text: self.presentationData.strings.Chat_Giveaway_Toast_Ended, timeout: nil, customUndoText: self.presentationData.strings.Chat_Giveaway_Toast_LearnMore)
}
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 {
@ -5110,8 +5109,15 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
if let strongSelf = self {
let isPremium = peerView.peers[peerView.peerId]?.isPremium ?? false
var accountPeerColor: ChatPresentationInterfaceState.AccountPeerColor?
if peerView.peers[peerView.peerId]?.nameColor?.dashColors.1 != nil {
accountPeerColor = ChatPresentationInterfaceState.AccountPeerColor(isDashed: true)
if let nameColor = peerView.peers[peerView.peerId]?.nameColor {
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
return state.updatedIsPremium(isPremium).updatedAccountPeerColor(accountPeerColor)
@ -19577,150 +19583,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
func displayGiveawayStatusInfo(messageId: EngineMessage.Id, giveawayInfo: PremiumGiveawayInfo) {
let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Messages.Message(id: messageId))
|> 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
}
var peerName = ""
if let peerId = giveaway.channelPeerIds.first, let peer = message.peers[peerId] {
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()
if let controller = giveawayInfoController(context: self.context, updatedPresentationData: self.updatedPresentationData, message: message, giveawayInfo: giveawayInfo) {
self.present(controller, in: .window(.root))
}
})
}

View File

@ -1045,11 +1045,22 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch
var baseFontSize: CGFloat = 17.0
var keyboardAppearance: UIKeyboardAppearance = UIKeyboardAppearance.default
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(
quote: ChatInputTextView.Theme.Quote(
background: presentationInterfaceState.theme.list.itemAccentColor.withMultipliedAlpha(presentationInterfaceState.theme.overallDarkAppearance ? 0.2 : 0.1),
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 {
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(
quote: ChatInputTextView.Theme.Quote(
background: interfaceState.theme.list.itemAccentColor.withMultipliedAlpha(interfaceState.theme.overallDarkAppearance ? 0.2 : 0.1),
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
}
let _ = (context.engine.payments.checkPremiumGiftCode(slug: slug)
|> deliverOnMainQueue).startStandalone(next: { giftCode in
|> deliverOnMainQueue).startStandalone(next: { [weak navigationController] giftCode in
if let giftCode {
var dismissImpl: (() -> Void)?
let controller = PremiumGiftCodeScreen(
context: context,
giftCode: giftCode,
subject: .giftCode(giftCode),
forceDark: forceDark,
action: {
dismissImpl?()
let _ = context.engine.payments.applyPremiumGiftCode(slug: slug).startStandalone()
action: { [weak navigationController] in
let _ = (context.engine.payments.applyPremiumGiftCode(slug: slug)
|> deliverOnMainQueue).startStandalone(completed: {
dismissImpl?()
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
if peer.id != context.account.peerId {
@ -941,7 +950,7 @@ func openResolvedUrlImpl(_ resolvedUrl: ResolvedUrl, context: AccountContext, ur
}
)
dismissImpl = { [weak controller] in
controller?.dismiss()
controller?.dismissAnimated()
}
navigationController?.pushViewController(controller)
} else {

View File

@ -1621,7 +1621,8 @@ private func editingItems(data: PeerInfoScreenData?, state: PeerInfoState, chatL
}
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()
}))
}

View File

@ -55,7 +55,7 @@ public func chatInputStateStringWithAppliedEntities(_ text: String, entities: [M
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
var nsString: NSString?
@ -214,7 +214,7 @@ public func stringWithAppliedEntities(_ text: String, entities: [MessageTextEnti
case .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:
string.addAttribute(NSAttributedString.Key.foregroundColor, value: linkColor, range: range)
if underlineLinks && underlineAllLinks {