diff --git a/Telegram/BUILD b/Telegram/BUILD index c5e2d3508e..d460a2b6d5 100644 --- a/Telegram/BUILD +++ b/Telegram/BUILD @@ -329,9 +329,6 @@ alternate_icon_folders = [ "Premium", "PremiumBlack", "PremiumTurbo", - "PremiumCoffee", - "PremiumDuck", - "PremiumSteam", ] [ diff --git a/Telegram/Telegram-iOS/PremiumCoffee.alticon/PremiumCoffee@2x.png b/Telegram/Telegram-iOS/PremiumCoffee.alticon/PremiumCoffee@2x.png deleted file mode 100644 index abdd22ae69..0000000000 Binary files a/Telegram/Telegram-iOS/PremiumCoffee.alticon/PremiumCoffee@2x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/PremiumCoffee.alticon/PremiumCoffee@3x.png b/Telegram/Telegram-iOS/PremiumCoffee.alticon/PremiumCoffee@3x.png deleted file mode 100644 index 8a0de8458b..0000000000 Binary files a/Telegram/Telegram-iOS/PremiumCoffee.alticon/PremiumCoffee@3x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/PremiumDuck.alticon/PremiumDuck@2x.png b/Telegram/Telegram-iOS/PremiumDuck.alticon/PremiumDuck@2x.png deleted file mode 100644 index eeb7340627..0000000000 Binary files a/Telegram/Telegram-iOS/PremiumDuck.alticon/PremiumDuck@2x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/PremiumDuck.alticon/PremiumDuck@3x.png b/Telegram/Telegram-iOS/PremiumDuck.alticon/PremiumDuck@3x.png deleted file mode 100644 index dea2849f51..0000000000 Binary files a/Telegram/Telegram-iOS/PremiumDuck.alticon/PremiumDuck@3x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/PremiumSteam.alticon/PremiumSteam@2x.png b/Telegram/Telegram-iOS/PremiumSteam.alticon/PremiumSteam@2x.png deleted file mode 100644 index 4e0cf791fb..0000000000 Binary files a/Telegram/Telegram-iOS/PremiumSteam.alticon/PremiumSteam@2x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/PremiumSteam.alticon/PremiumSteam@3x.png b/Telegram/Telegram-iOS/PremiumSteam.alticon/PremiumSteam@3x.png deleted file mode 100644 index 0d59ed2b68..0000000000 Binary files a/Telegram/Telegram-iOS/PremiumSteam.alticon/PremiumSteam@3x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index feacd249f6..fda6b2b4a3 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -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."; diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index 550c56c2f0..0a228fcdb6 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -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 + } +} diff --git a/submodules/ChatPresentationInterfaceState/Sources/ChatPresentationInterfaceState.swift b/submodules/ChatPresentationInterfaceState/Sources/ChatPresentationInterfaceState.swift index 00c65ddf70..2b1855e037 100644 --- a/submodules/ChatPresentationInterfaceState/Sources/ChatPresentationInterfaceState.swift +++ b/submodules/ChatPresentationInterfaceState/Sources/ChatPresentationInterfaceState.swift @@ -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 } } diff --git a/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageActionSheetControllerNode.swift b/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageActionSheetControllerNode.swift index d28ff3534d..efd884e0fd 100644 --- a/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageActionSheetControllerNode.swift +++ b/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageActionSheetControllerNode.swift @@ -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 ) ) } diff --git a/submodules/Display/Source/TextNode.swift b/submodules/Display/Source/TextNode.swift index 945c0d271d..33a5b16f94 100644 --- a/submodules/Display/Source/TextNode.swift +++ b/submodules/Display/Source/TextNode.swift @@ -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) diff --git a/submodules/PremiumUI/BUILD b/submodules/PremiumUI/BUILD index 0cf15d47b7..b6cff081e2 100644 --- a/submodules/PremiumUI/BUILD +++ b/submodules/PremiumUI/BUILD @@ -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", diff --git a/submodules/PremiumUI/Sources/AppIconsDemoComponent.swift b/submodules/PremiumUI/Sources/AppIconsDemoComponent.swift index 595b5bb4e8..7c0fb63798 100644 --- a/submodules/PremiumUI/Sources/AppIconsDemoComponent.swift +++ b/submodules/PremiumUI/Sources/AppIconsDemoComponent.swift @@ -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 } } diff --git a/submodules/PremiumUI/Sources/CreateGiveawayController.swift b/submodules/PremiumUI/Sources/CreateGiveawayController.swift index 4be43057ca..817742e941 100644 --- a/submodules/PremiumUI/Sources/CreateGiveawayController.swift +++ b/submodules/PremiumUI/Sources/CreateGiveawayController.swift @@ -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)) diff --git a/submodules/PremiumUI/Sources/CreateGiveawayHeaderItem.swift b/submodules/PremiumUI/Sources/CreateGiveawayHeaderItem.swift index 7e540dbaa5..daeb42a2e0 100644 --- a/submodules/PremiumUI/Sources/CreateGiveawayHeaderItem.swift +++ b/submodules/PremiumUI/Sources/CreateGiveawayHeaderItem.swift @@ -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) { diff --git a/submodules/TelegramUI/Sources/GiveawayInfoAlertController.swift b/submodules/PremiumUI/Sources/GiveawayInfoController.swift similarity index 57% rename from submodules/TelegramUI/Sources/GiveawayInfoAlertController.swift rename to submodules/PremiumUI/Sources/GiveawayInfoController.swift index 8234b52419..7254a63fe7 100644 --- a/submodules/TelegramUI/Sources/GiveawayInfoAlertController.swift +++ b/submodules/PremiumUI/Sources/GiveawayInfoController.swift @@ -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)? = 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)? = nil, title: String, text: String, warning: String?, actions: [TextAlertAction]) -> AlertController { +private func giveawayInfoAlertController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = 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) diff --git a/submodules/PremiumUI/Sources/PremiumBoostScreen.swift b/submodules/PremiumUI/Sources/PremiumBoostScreen.swift index ac46b5f30a..092dce39ce 100644 --- a/submodules/PremiumUI/Sources/PremiumBoostScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumBoostScreen.swift @@ -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: {}) ], diff --git a/submodules/PremiumUI/Sources/PremiumGiftCodeScreen.swift b/submodules/PremiumUI/Sources/PremiumGiftCodeScreen.swift index ad2fb7f0a2..05735b2d65 100644 --- a/submodules/PremiumUI/Sources/PremiumGiftCodeScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumGiftCodeScreen.swift @@ -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( content: AnyComponent(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.View.Tag()) as? SheetComponent.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, 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, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/PremiumUI/Sources/PremiumIntroScreen.swift b/submodules/PremiumUI/Sources/PremiumIntroScreen.swift index c08a532aeb..e09ac732c2 100644 --- a/submodules/PremiumUI/Sources/PremiumIntroScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumIntroScreen.swift @@ -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) diff --git a/submodules/PremiumUI/Sources/ReplaceBoostScreen.swift b/submodules/PremiumUI/Sources/ReplaceBoostScreen.swift index 568c184d45..b87262f2c0 100644 --- a/submodules/PremiumUI/Sources/ReplaceBoostScreen.swift +++ b/submodules/PremiumUI/Sources/ReplaceBoostScreen.swift @@ -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 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() } diff --git a/submodules/SettingsUI/Sources/Themes/ThemeSettingsAppIconItem.swift b/submodules/SettingsUI/Sources/Themes/ThemeSettingsAppIconItem.swift index ab34ed2885..b88e7e4a58 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemeSettingsAppIconItem.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemeSettingsAppIconItem.swift @@ -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 } diff --git a/submodules/SettingsUI/Sources/Themes/ThemeSettingsController.swift b/submodules/SettingsUI/Sources/Themes/ThemeSettingsController.swift index e9f42e8d7d..81a873fc87 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemeSettingsController.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemeSettingsController.swift @@ -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)) } diff --git a/submodules/StatisticsUI/Sources/ChannelStatsController.swift b/submodules/StatisticsUI/Sources/ChannelStatsController.swift index 5c46b22f65..30fb591c75 100644 --- a/submodules/StatisticsUI/Sources/ChannelStatsController.swift +++ b/submodules/StatisticsUI/Sources/ChannelStatsController.swift @@ -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, 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, 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 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 } diff --git a/submodules/TelegramCore/Sources/State/ChannelBoost.swift b/submodules/TelegramCore/Sources/State/ChannelBoost.swift index 3a6295d86f..f1dc466a55 100644 --- a/submodules/TelegramCore/Sources/State/ChannelBoost.swift +++ b/submodules/TelegramCore/Sources/State/ChannelBoost.swift @@ -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 } diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedUserData.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedUserData.swift index 040951dec9..b693a9c933 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedUserData.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedUserData.swift @@ -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 { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelAdminEventLogs.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelAdminEventLogs.swift index 9db5461bc0..924d47c179 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelAdminEventLogs.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelAdminEventLogs.swift @@ -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 { diff --git a/submodules/TelegramCore/Sources/Utils/PeerUtils.swift b/submodules/TelegramCore/Sources/Utils/PeerUtils.swift index 407ffa5194..9f775e82cd 100644 --- a/submodules/TelegramCore/Sources/Utils/PeerUtils.swift +++ b/submodules/TelegramCore/Sources/Utils/PeerUtils.swift @@ -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: diff --git a/submodules/TelegramPresentationData/Sources/PeerNameColor.swift b/submodules/TelegramPresentationData/Sources/PeerNameColor.swift deleted file mode 100644 index 8c3ca8e668..0000000000 --- a/submodules/TelegramPresentationData/Sources/PeerNameColor.swift +++ /dev/null @@ -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)) - } - } -} diff --git a/submodules/TelegramUI/Components/Chat/ChatInputTextNode/Sources/ChatInputTextNode.swift b/submodules/TelegramUI/Components/Chat/ChatInputTextNode/Sources/ChatInputTextNode.swift index 909aa9f784..8042cee64e 100644 --- a/submodules/TelegramUI/Components/Chat/ChatInputTextNode/Sources/ChatInputTextNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatInputTextNode/Sources/ChatInputTextNode.swift @@ -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 ) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift index 902c24c9ad..2633d227df 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift @@ -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 { diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode/Sources/ChatMessageAttachedContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode/Sources/ChatMessageAttachedContentNode.swift index 20600c37c4..6ea8b4fa38 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode/Sources/ChatMessageAttachedContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode/Sources/ChatMessageAttachedContentNode.swift @@ -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 { diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift index 3871e5ae29..1e24124b19 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift @@ -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 { diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageForwardInfoNode/Sources/ChatMessageForwardInfoNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageForwardInfoNode/Sources/ChatMessageForwardInfoNode.swift index 308ca2f60f..0359b0d89b 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageForwardInfoNode/Sources/ChatMessageForwardInfoNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageForwardInfoNode/Sources/ChatMessageForwardInfoNode.swift @@ -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 } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift index c1adef884e..a66d96e2dc 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift @@ -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 diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageGiveawayBubbleContentNode/Sources/ChatMessageGiveawayBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageGiveawayBubbleContentNode/Sources/ChatMessageGiveawayBubbleContentNode.swift index b97e35b692..cb047d5613 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageGiveawayBubbleContentNode/Sources/ChatMessageGiveawayBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageGiveawayBubbleContentNode/Sources/ChatMessageGiveawayBubbleContentNode.swift @@ -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 diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInstantVideoItemNode/Sources/ChatMessageInstantVideoItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageInstantVideoItemNode/Sources/ChatMessageInstantVideoItemNode.swift index 3d66ffe92c..ec0267c3b3 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageInstantVideoItemNode/Sources/ChatMessageInstantVideoItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInstantVideoItemNode/Sources/ChatMessageInstantVideoItemNode.swift @@ -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 { diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/Sources/ChatMessageInteractiveInstantVideoNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/Sources/ChatMessageInteractiveInstantVideoNode.swift index eefebccc34..7215cdba22 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/Sources/ChatMessageInteractiveInstantVideoNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/Sources/ChatMessageInteractiveInstantVideoNode.swift @@ -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 diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageReplyInfoNode/Sources/ChatMessageReplyInfoNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageReplyInfoNode/Sources/ChatMessageReplyInfoNode.swift index 4d75dac6b5..cf692603df 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageReplyInfoNode/Sources/ChatMessageReplyInfoNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageReplyInfoNode/Sources/ChatMessageReplyInfoNode.swift @@ -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 ) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageStickerItemNode/Sources/ChatMessageStickerItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageStickerItemNode/Sources/ChatMessageStickerItemNode.swift index 069fc50e52..168cd19a7f 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageStickerItemNode/Sources/ChatMessageStickerItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageStickerItemNode/Sources/ChatMessageStickerItemNode.swift @@ -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 diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift index f77bda5024..b8680f093a 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift @@ -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 { diff --git a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsHistoryTransition.swift b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsHistoryTransition.swift index 5befa727b0..664f73eab7 100644 --- a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsHistoryTransition.swift +++ b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsHistoryTransition.swift @@ -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: [:]) diff --git a/submodules/TelegramUI/Components/Chat/MessageInlineBlockBackgroundView/Sources/MessageInlineBlockBackgroundView.swift b/submodules/TelegramUI/Components/Chat/MessageInlineBlockBackgroundView/Sources/MessageInlineBlockBackgroundView.swift index 39ad4c5d84..67f5f2a132 100644 --- a/submodules/TelegramUI/Components/Chat/MessageInlineBlockBackgroundView/Sources/MessageInlineBlockBackgroundView.swift +++ b/submodules/TelegramUI/Components/Chat/MessageInlineBlockBackgroundView/Sources/MessageInlineBlockBackgroundView.swift @@ -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 ) diff --git a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/PeerNameColorItem.swift b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/PeerNameColorItem.swift index fbf7f17659..e573d9f435 100644 --- a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/PeerNameColorItem.swift +++ b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/PeerNameColorItem.swift @@ -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 diff --git a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/PeerNameColorScreen.swift b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/PeerNameColorScreen.swift index 9d1f59cd45..fcd523753f 100644 --- a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/PeerNameColorScreen.swift +++ b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/PeerNameColorScreen.swift @@ -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, diff --git a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift index c16977a4f9..c30cf65537 100644 --- a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift +++ b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift @@ -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 diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/MediaNavigationStripComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/MediaNavigationStripComponent.swift index eaed466fe9..1fad133d46 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/MediaNavigationStripComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/MediaNavigationStripComponent.swift @@ -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 diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift index cd9f452654..7f2e080d87 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift @@ -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 { diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryInteractionGuideComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryInteractionGuideComponent.swift index b4f5af595d..197632cb9f 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryInteractionGuideComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryInteractionGuideComponent.swift @@ -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() } diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemContentComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemContentComponent.swift index 40ec787d68..33a5a51fc0 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemContentComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemContentComponent.swift @@ -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, transition: Transition) -> CGSize { let previousItem = self.component?.item diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift index 333e6297dc..99fe6f7958 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift @@ -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 ) diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Icons/Coffee.imageset/Coffee.png b/submodules/TelegramUI/Images.xcassets/Premium/Icons/Coffee.imageset/Coffee.png deleted file mode 100644 index 60c199314a..0000000000 Binary files a/submodules/TelegramUI/Images.xcassets/Premium/Icons/Coffee.imageset/Coffee.png and /dev/null differ diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Icons/Coffee.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/Icons/Coffee.imageset/Contents.json deleted file mode 100644 index bf38b88a52..0000000000 --- a/submodules/TelegramUI/Images.xcassets/Premium/Icons/Coffee.imageset/Contents.json +++ /dev/null @@ -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 - } -} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Icons/Duck.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/Icons/Duck.imageset/Contents.json deleted file mode 100644 index a7c2a54e8d..0000000000 --- a/submodules/TelegramUI/Images.xcassets/Premium/Icons/Duck.imageset/Contents.json +++ /dev/null @@ -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 - } -} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Icons/Duck.imageset/Duck.png b/submodules/TelegramUI/Images.xcassets/Premium/Icons/Duck.imageset/Duck.png deleted file mode 100644 index ac1cc232e1..0000000000 Binary files a/submodules/TelegramUI/Images.xcassets/Premium/Icons/Duck.imageset/Duck.png and /dev/null differ diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Icons/Steam.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/Icons/Steam.imageset/Contents.json deleted file mode 100644 index 3313751950..0000000000 --- a/submodules/TelegramUI/Images.xcassets/Premium/Icons/Steam.imageset/Contents.json +++ /dev/null @@ -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 - } -} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Icons/Steam.imageset/Steam.png b/submodules/TelegramUI/Images.xcassets/Premium/Icons/Steam.imageset/Steam.png deleted file mode 100644 index eaf5a8361a..0000000000 Binary files a/submodules/TelegramUI/Images.xcassets/Premium/Icons/Steam.imageset/Steam.png and /dev/null differ diff --git a/submodules/TelegramUI/Sources/AccountContext.swift b/submodules/TelegramUI/Sources/AccountContext.swift index 6d66335297..3fb0af2bbe 100644 --- a/submodules/TelegramUI/Sources/AccountContext.swift +++ b/submodules/TelegramUI/Sources/AccountContext.swift @@ -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) { diff --git a/submodules/TelegramUI/Sources/AppDelegate.swift b/submodules/TelegramUI/Sources/AppDelegate.swift index 4339f313a7..5ab0f18b1f 100644 --- a/submodules/TelegramUI/Sources/AppDelegate.swift +++ b/submodules/TelegramUI/Sources/AppDelegate.swift @@ -802,11 +802,7 @@ private func extractAccountManagerState(records: AccountRecordsView 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)) } }) } diff --git a/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift b/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift index f073a2adf6..3d0ce0ccfd 100644 --- a/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift @@ -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 ) ) } diff --git a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift index e9f59997e2..cb9ddd4ba5 100644 --- a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift +++ b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift @@ -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 { diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift index d0388072eb..6bb3c0c22b 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift @@ -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() })) } diff --git a/submodules/TextFormat/Sources/StringWithAppliedEntities.swift b/submodules/TextFormat/Sources/StringWithAppliedEntities.swift index 93f294f5ee..8b85559944 100644 --- a/submodules/TextFormat/Sources/StringWithAppliedEntities.swift +++ b/submodules/TextFormat/Sources/StringWithAppliedEntities.swift @@ -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 {