Various improvements

This commit is contained in:
Ilya Laktyushin 2023-10-15 17:07:39 +04:00
parent 4013fca50e
commit 9f7056670c
41 changed files with 2195 additions and 261 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 44 KiB

View File

@ -10110,3 +10110,13 @@ Sorry for the inconvenience.";
"Appearance.AppIconSteam" = "Steam"; "Appearance.AppIconSteam" = "Steam";
"Notification.GiftLink" = "You received a gift"; "Notification.GiftLink" = "You received a gift";
"MESSAGE_GIFTCODE" = "%1$@ sent you a Gift Code for %2$@ months of Telegram Premium";
"MESSAGE_GIVEAWAY" = "%1$@ sent you a giveaway of %2$@x %3$@mo Premium subscriptions";
"CHANNEL_MESSAGE_GIVEAWAY" = "%1$@ posted a giveaway of %2$@x %3$@mo Premium subscriptions";
"CHAT_MESSAGE_GIVEAWAY" = "%1$@ sent a giveaway of %3$@x %4$@mo Premium subscriptions to the group %2$@";
"PINNED_GIVEAWAY" = "%1$@ pinned a giveaway";
"REACT_GIVEAWAY" = "%1$@ reacted %2$@ to your giveaway";
"CHAT_REACT_GIVEAWAY" = "%1$@ reacted %3$@ in group %2$@ to your giveaway";
"Notification.GiveawayStarted" = "%1$@ just started a giveaway of Telegram Premium subscriptions for its followers.";

View File

@ -60,7 +60,7 @@ private func loadCountryCodes() -> [(String, Int)] {
private let countryCodes: [(String, Int)] = loadCountryCodes() private let countryCodes: [(String, Int)] = loadCountryCodes()
func localizedCountryNamesAndCodes(strings: PresentationStrings) -> [((String, String), String, [Int])] { public func localizedCountryNamesAndCodes(strings: PresentationStrings) -> [((String, String), String, [Int])] {
let locale = localeWithStrings(strings) let locale = localeWithStrings(strings)
var result: [((String, String), String, [Int])] = [] var result: [((String, String), String, [Int])] = []
for country in AuthorizationSequenceCountrySelectionController.countries() { for country in AuthorizationSequenceCountrySelectionController.countries() {
@ -159,7 +159,7 @@ private func matchStringTokens(_ tokens: [Data], with other: [Data]) -> Bool {
return false return false
} }
private func searchCountries(items: [((String, String), String, [Int])], query: String) -> [((String, String), String, Int)] { public func searchCountries(items: [((String, String), String, [Int])], query: String) -> [((String, String), String, Int)] {
let queryTokens = stringTokens(query.lowercased()) let queryTokens = stringTokens(query.lowercased())
var result: [((String, String), String, Int)] = [] var result: [((String, String), String, Int)] = []

View File

@ -1,5 +1,6 @@
import Foundation import Foundation
import AppBundle import AppBundle
import TelegramStringFormatting
public func emojiFlagForISOCountryCode(_ countryCode: String) -> String { public func emojiFlagForISOCountryCode(_ countryCode: String) -> String {
if countryCode.count != 2 { if countryCode.count != 2 {
@ -18,12 +19,7 @@ public func emojiFlagForISOCountryCode(_ countryCode: String) -> String {
return "" return ""
} }
let base : UInt32 = 127397 return flagEmoji(countryCode: countryCode)
var s = ""
for v in countryCode.unicodeScalars {
s.unicodeScalars.append(UnicodeScalar(base + v.value)!)
}
return String(s)
} }
private func loadCountriesInfo() -> [(Int, String, String)] { private func loadCountriesInfo() -> [(Int, String, String)] {

View File

@ -9,6 +9,7 @@ import ItemListUI
import LocationResources import LocationResources
import AppBundle import AppBundle
import LiveLocationTimerNode import LiveLocationTimerNode
import TelegramStringFormatting
public enum LocationActionListItemIcon: Equatable { public enum LocationActionListItemIcon: Equatable {
case location case location
@ -280,14 +281,6 @@ final class LocationActionListItemNode: ListViewItemNode {
strongSelf.iconNode.isHidden = true strongSelf.iconNode.isHidden = true
strongSelf.venueIconNode.isHidden = false strongSelf.venueIconNode.isHidden = false
func flagEmoji(countryCode: String) -> String {
let base : UInt32 = 127397
var flagString = ""
for v in countryCode.uppercased().unicodeScalars {
flagString.unicodeScalars.append(UnicodeScalar(base + v.value)!)
}
return flagString
}
let type = venue.venue?.type let type = venue.venue?.type
var flag: String? var flag: String?
if let venue = venue.venue, venue.provider == "city", let countryCode = venue.id { if let venue = venue.venue, venue.provider == "city", let countryCode = venue.id {

View File

@ -106,6 +106,7 @@ swift_library(
"//submodules/TelegramUI/Components/ShareWithPeersScreen", "//submodules/TelegramUI/Components/ShareWithPeersScreen",
"//submodules/TelegramUI/Components/ButtonComponent", "//submodules/TelegramUI/Components/ButtonComponent",
"//submodules/TelegramUI/Components/Utils/RoundedRectWithTailPath", "//submodules/TelegramUI/Components/Utils/RoundedRectWithTailPath",
"//submodules/CountrySelectionUI",
], ],
visibility = [ visibility = [
"//visibility:public", "//visibility:public",

View File

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

View File

@ -19,6 +19,7 @@ import ItemListPeerActionItem
import ShareWithPeersScreen import ShareWithPeersScreen
import InAppPurchaseManager import InAppPurchaseManager
import UndoUI import UndoUI
import CountrySelectionUI
private final class CreateGiveawayControllerArguments { private final class CreateGiveawayControllerArguments {
let context: AccountContext let context: AccountContext
@ -26,17 +27,23 @@ private final class CreateGiveawayControllerArguments {
let dismissInput: () -> Void let dismissInput: () -> Void
let openPeersSelection: () -> Void let openPeersSelection: () -> Void
let openChannelsSelection: () -> Void let openChannelsSelection: () -> Void
let openCountriesSelection: () -> Void
let openPremiumIntro: () -> Void let openPremiumIntro: () -> Void
let scrollToDate: () -> Void let scrollToDate: () -> Void
let setItemIdWithRevealedOptions: (EnginePeer.Id?, EnginePeer.Id?) -> Void
let removeChannel: (EnginePeer.Id) -> Void
init(context: AccountContext, updateState: @escaping ((CreateGiveawayControllerState) -> CreateGiveawayControllerState) -> Void, dismissInput: @escaping () -> Void, openPeersSelection: @escaping () -> Void, openChannelsSelection: @escaping () -> Void, openPremiumIntro: @escaping () -> Void, scrollToDate: @escaping () -> Void) { init(context: AccountContext, updateState: @escaping ((CreateGiveawayControllerState) -> CreateGiveawayControllerState) -> Void, dismissInput: @escaping () -> Void, openPeersSelection: @escaping () -> Void, openChannelsSelection: @escaping () -> Void, openCountriesSelection: @escaping () -> Void, openPremiumIntro: @escaping () -> Void, scrollToDate: @escaping () -> Void, setItemIdWithRevealedOptions: @escaping (EnginePeer.Id?, EnginePeer.Id?) -> Void, removeChannel: @escaping (EnginePeer.Id) -> Void) {
self.context = context self.context = context
self.updateState = updateState self.updateState = updateState
self.dismissInput = dismissInput self.dismissInput = dismissInput
self.openPeersSelection = openPeersSelection self.openPeersSelection = openPeersSelection
self.openChannelsSelection = openChannelsSelection self.openChannelsSelection = openChannelsSelection
self.openCountriesSelection = openCountriesSelection
self.openPremiumIntro = openPremiumIntro self.openPremiumIntro = openPremiumIntro
self.scrollToDate = scrollToDate self.scrollToDate = scrollToDate
self.setItemIdWithRevealedOptions = setItemIdWithRevealedOptions
self.removeChannel = removeChannel
} }
} }
@ -76,13 +83,13 @@ private enum CreateGiveawayEntry: ItemListNodeEntry {
case subscriptionsInfo(PresentationTheme, String) case subscriptionsInfo(PresentationTheme, String)
case channelsHeader(PresentationTheme, String) case channelsHeader(PresentationTheme, String)
case channel(Int32, PresentationTheme, EnginePeer, Int32?) case channel(Int32, PresentationTheme, EnginePeer, Int32?, Bool)
case channelAdd(PresentationTheme, String) case channelAdd(PresentationTheme, String)
case channelsInfo(PresentationTheme, String) case channelsInfo(PresentationTheme, String)
case usersHeader(PresentationTheme, String) case usersHeader(PresentationTheme, String)
case usersAll(PresentationTheme, String, Bool) case usersAll(PresentationTheme, String, String, Bool)
case usersNew(PresentationTheme, String, Bool) case usersNew(PresentationTheme, String, String, Bool)
case usersInfo(PresentationTheme, String) case usersInfo(PresentationTheme, String)
case timeHeader(PresentationTheme, String) case timeHeader(PresentationTheme, String)
@ -133,7 +140,7 @@ private enum CreateGiveawayEntry: ItemListNodeEntry {
return 6 return 6
case .channelsHeader: case .channelsHeader:
return 7 return 7
case let .channel(index, _, _, _): case let .channel(index, _, _, _, _):
return 8 + index return 8 + index
case .channelAdd: case .channelAdd:
return 100 return 100
@ -220,8 +227,8 @@ private enum CreateGiveawayEntry: ItemListNodeEntry {
} else { } else {
return false return false
} }
case let .channel(lhsIndex, lhsTheme, lhsPeer, lhsBoosts): case let .channel(lhsIndex, lhsTheme, lhsPeer, lhsBoosts, lhsIsRevealed):
if case let .channel(rhsIndex, rhsTheme, rhsPeer, rhsBoosts) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsPeer == rhsPeer, lhsBoosts == rhsBoosts { if case let .channel(rhsIndex, rhsTheme, rhsPeer, rhsBoosts, rhsIsRevealed) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsPeer == rhsPeer, lhsBoosts == rhsBoosts, lhsIsRevealed == rhsIsRevealed {
return true return true
} else { } else {
return false return false
@ -244,14 +251,14 @@ private enum CreateGiveawayEntry: ItemListNodeEntry {
} else { } else {
return false return false
} }
case let .usersAll(lhsTheme, lhsText, lhsSelected): case let .usersAll(lhsTheme, lhsText, lhsSubtitle, lhsSelected):
if case let .usersAll(rhsTheme, rhsText, rhsSelected) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsSelected == rhsSelected { if case let .usersAll(rhsTheme, rhsText, rhsSubtitle, rhsSelected) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsSubtitle == rhsSubtitle, lhsSelected == rhsSelected {
return true return true
} else { } else {
return false return false
} }
case let .usersNew(lhsTheme, lhsText, lhsSelected): case let .usersNew(lhsTheme, lhsText, lhsSubtitle, lhsSelected):
if case let .usersNew(rhsTheme, rhsText, rhsSelected) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsSelected == rhsSelected { if case let .usersNew(rhsTheme, rhsText, rhsSubtitle, rhsSelected) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsSubtitle == rhsSubtitle, lhsSelected == rhsSelected {
return true return true
} else { } else {
return false return false
@ -369,10 +376,14 @@ private enum CreateGiveawayEntry: ItemListNodeEntry {
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
case let .channelsHeader(_, text): case let .channelsHeader(_, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .channel(_, _, peer, boosts): 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: false, editing: false, revealed: false), switchValue: nil, enabled: true, selectable: peer.id != arguments.context.account.peerId, sectionId: self.section, action: { return ItemListPeerItem(presentationData: presentationData, dateTimeFormat: PresentationDateTimeFormat(), nameDisplayOrder: presentationData.nameDisplayOrder, context: arguments.context, peer: peer, presence: nil, text: boosts.flatMap { .text("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) // arguments.openPeer(peer)
}, setPeerIdWithRevealedOptions: { _, _ in }, removePeer: { _ in }) }, setPeerIdWithRevealedOptions: { lhs, rhs in
arguments.setItemIdWithRevealedOptions(lhs, rhs)
}, removePeer: { id in
arguments.removeChannel(id)
})
case let .channelAdd(theme, text): case let .channelAdd(theme, text):
return ItemListPeerActionItem(presentationData: presentationData, icon: PresentationResourcesItemList.roundPlusIconImage(theme), title: text, alwaysPlain: false, hasSeparator: true, sectionId: self.section, height: .compactPeerList, color: .accent, editing: false, action: { return ItemListPeerActionItem(presentationData: presentationData, icon: PresentationResourcesItemList.roundPlusIconImage(theme), title: text, alwaysPlain: false, hasSeparator: true, sectionId: self.section, height: .compactPeerList, color: .accent, editing: false, action: {
arguments.openChannelsSelection() arguments.openChannelsSelection()
@ -381,21 +392,35 @@ private enum CreateGiveawayEntry: ItemListNodeEntry {
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
case let .usersHeader(_, text): case let .usersHeader(_, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .usersAll(_, title, isSelected): case let .usersAll(_, title, subtitle, isSelected):
return GiftOptionItem(presentationData: presentationData, context: arguments.context, title: title, subtitle: nil, isSelected: isSelected, sectionId: self.section, action: { return GiftOptionItem(presentationData: presentationData, context: arguments.context, title: title, subtitle: subtitle, subtitleActive: true, isSelected: isSelected, sectionId: self.section, action: {
var openSelection = false
arguments.updateState { state in arguments.updateState { state in
var updatedState = state var updatedState = state
if !updatedState.onlyNewEligible {
openSelection = true
}
updatedState.onlyNewEligible = false updatedState.onlyNewEligible = false
return updatedState return updatedState
} }
if openSelection {
arguments.openCountriesSelection()
}
}) })
case let .usersNew(_, title, isSelected): case let .usersNew(_, title, subtitle, isSelected):
return GiftOptionItem(presentationData: presentationData, context: arguments.context, title: title, subtitle: nil, isSelected: isSelected, sectionId: self.section, action: { return GiftOptionItem(presentationData: presentationData, context: arguments.context, title: title, subtitle: subtitle, subtitleActive: true, isSelected: isSelected, sectionId: self.section, action: {
var openSelection = false
arguments.updateState { state in arguments.updateState { state in
var updatedState = state var updatedState = state
if updatedState.onlyNewEligible {
openSelection = true
}
updatedState.onlyNewEligible = true updatedState.onlyNewEligible = true
return updatedState return updatedState
} }
if openSelection {
arguments.openCountriesSelection()
}
}) })
case let .usersInfo(_, text): case let .usersInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
@ -474,7 +499,7 @@ private struct PremiumGiftProduct: Equatable {
} }
} }
private func createGiveawayControllerEntries(peerId: EnginePeer.Id, subject: CreateGiveawaySubject, state: CreateGiveawayControllerState, presentationData: PresentationData, peers: [EnginePeer.Id: EnginePeer], products: [PremiumGiftProduct], defaultPrice: (Int64, NSDecimalNumber)) -> [CreateGiveawayEntry] { private func createGiveawayControllerEntries(peerId: EnginePeer.Id, subject: CreateGiveawaySubject, state: CreateGiveawayControllerState, presentationData: PresentationData, locale: Locale, peers: [EnginePeer.Id: EnginePeer], products: [PremiumGiftProduct], defaultPrice: (Int64, NSDecimalNumber)) -> [CreateGiveawayEntry] {
var entries: [CreateGiveawayEntry] = [] var entries: [CreateGiveawayEntry] = []
switch subject { switch subject {
@ -517,7 +542,7 @@ private func createGiveawayControllerEntries(peerId: EnginePeer.Id, subject: Cre
let channels = [peerId] + state.channels let channels = [peerId] + state.channels
for channelId in channels { for channelId in channels {
if let channel = peers[channelId] { if let channel = peers[channelId] {
entries.append(.channel(index, presentationData.theme, channel, channel.id == peerId ? state.subscriptions : nil)) entries.append(.channel(index, presentationData.theme, channel, channel.id == peerId ? state.subscriptions : nil, false))
} }
index += 1 index += 1
} }
@ -525,8 +550,19 @@ private func createGiveawayControllerEntries(peerId: EnginePeer.Id, subject: Cre
entries.append(.channelsInfo(presentationData.theme, "Choose the channels users need to be subscribed to take part in the giveaway")) entries.append(.channelsInfo(presentationData.theme, "Choose the channels users need to be subscribed to take part in the giveaway"))
entries.append(.usersHeader(presentationData.theme, "USERS ELIGIBLE FOR THE GIVEAWAY".uppercased())) entries.append(.usersHeader(presentationData.theme, "USERS ELIGIBLE FOR THE GIVEAWAY".uppercased()))
entries.append(.usersAll(presentationData.theme, "All subscribers", !state.onlyNewEligible))
entries.append(.usersNew(presentationData.theme, "Only new subscribers", state.onlyNewEligible)) let countriesText: String
if state.countries.count > 2 {
countriesText = "from \(state.countries.count) countries"
} else if !state.countries.isEmpty {
let allCountries = state.countries.map { locale.localizedString(forRegionCode: $0) ?? $0 }.joined(separator: " and ")
countriesText = "from \(allCountries)"
} else {
countriesText = "from all countries"
}
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(.usersInfo(presentationData.theme, "Choose if you want to limit the giveaway only to those who joined the channel after the giveaway started."))
entries.append(.timeHeader(presentationData.theme, "DATE WHEN GIVEAWAY ENDS".uppercased())) entries.append(.timeHeader(presentationData.theme, "DATE WHEN GIVEAWAY ENDS".uppercased()))
@ -602,6 +638,7 @@ private struct CreateGiveawayControllerState: Equatable {
var onlyNewEligible: Bool var onlyNewEligible: Bool
var time: Int32 var time: Int32
var pickingTimeLimit = false var pickingTimeLimit = false
var revealedItemId: EnginePeer.Id? = nil
var updating = false var updating = false
} }
@ -634,6 +671,7 @@ public func createGiveawayController(context: AccountContext, updatedPresentatio
var buyActionImpl: (() -> Void)? var buyActionImpl: (() -> Void)?
var openPeersSelectionImpl: (() -> Void)? var openPeersSelectionImpl: (() -> Void)?
var openChannelsSelectionImpl: (() -> Void)? var openChannelsSelectionImpl: (() -> Void)?
var openCountriesSelectionImpl: (() -> Void)?
var openPremiumIntroImpl: (() -> Void)? var openPremiumIntroImpl: (() -> Void)?
var presentControllerImpl: ((ViewController) -> Void)? var presentControllerImpl: ((ViewController) -> Void)?
var pushControllerImpl: ((ViewController) -> Void)? var pushControllerImpl: ((ViewController) -> Void)?
@ -649,14 +687,33 @@ public func createGiveawayController(context: AccountContext, updatedPresentatio
openPeersSelectionImpl?() openPeersSelectionImpl?()
}, openChannelsSelection: { }, openChannelsSelection: {
openChannelsSelectionImpl?() openChannelsSelectionImpl?()
}, openCountriesSelection: {
openCountriesSelectionImpl?()
}, openPremiumIntro: { }, openPremiumIntro: {
openPremiumIntroImpl?() openPremiumIntroImpl?()
}, scrollToDate: { }, scrollToDate: {
scrollToDateImpl?() scrollToDateImpl?()
}, setItemIdWithRevealedOptions: { itemId, fromItemId in
updateState { state in
var updatedState = state
if (itemId == nil && fromItemId == state.revealedItemId) || (itemId != nil && fromItemId == nil) {
updatedState.revealedItemId = itemId
}
return updatedState
}
},
removeChannel: { id in
updateState { state in
var updatedState = state
updatedState.channels = updatedState.channels.filter { $0 != id }
return updatedState
}
}) })
let presentationData = updatedPresentationData?.signal ?? context.sharedContext.presentationData let presentationData = updatedPresentationData?.signal ?? context.sharedContext.presentationData
let locale = localeWithStrings(context.sharedContext.currentPresentationData.with { $0 }.strings)
let productsAndDefaultPrice: Signal<([PremiumGiftProduct], (Int64, NSDecimalNumber)), NoError> = combineLatest( let productsAndDefaultPrice: Signal<([PremiumGiftProduct], (Int64, NSDecimalNumber)), NoError> = combineLatest(
.single([]) |> then(context.engine.payments.premiumGiftCodeOptions(peerId: peerId)), .single([]) |> then(context.engine.payments.premiumGiftCodeOptions(peerId: peerId)),
context.inAppPurchaseManager?.availableProducts ?? .single([]) context.inAppPurchaseManager?.availableProducts ?? .single([])
@ -724,9 +781,17 @@ public func createGiveawayController(context: AccountContext, updatedPresentatio
let previousState = previousState.swap(state) let previousState = previousState.swap(state)
var animateChanges = false var animateChanges = false
if let previousState = previousState, previousState.pickingTimeLimit != state.pickingTimeLimit || previousState.mode != state.mode { if let previousState = previousState {
if previousState.pickingTimeLimit != state.pickingTimeLimit {
animateChanges = true animateChanges = true
} }
if previousState.mode != state.mode {
animateChanges = true
}
if previousState.channels.count > state.channels.count {
animateChanges = true
}
}
var peers: [EnginePeer.Id: EnginePeer] = [:] var peers: [EnginePeer.Id: EnginePeer] = [:]
for (peerId, peer) in peersMap { for (peerId, peer) in peersMap {
@ -736,7 +801,7 @@ public func createGiveawayController(context: AccountContext, updatedPresentatio
} }
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(""), leftNavigationButton: leftNavigationButton, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true) let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(""), leftNavigationButton: leftNavigationButton, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true)
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: createGiveawayControllerEntries(peerId: peerId, subject: subject, state: state, presentationData: presentationData, peers: peers, products: products, defaultPrice: defaultPrice), style: .blocks, emptyStateItem: nil, headerItem: headerItem, footerItem: footerItem, crossfadeState: false, animateChanges: animateChanges) let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: createGiveawayControllerEntries(peerId: peerId, subject: subject, state: state, presentationData: presentationData, locale: locale, peers: peers, products: products, defaultPrice: defaultPrice), style: .blocks, emptyStateItem: nil, headerItem: headerItem, footerItem: footerItem, crossfadeState: false, animateChanges: animateChanges)
return (controllerState, (listState, arguments)) return (controllerState, (listState, arguments))
} }
@ -996,6 +1061,30 @@ public func createGiveawayController(context: AccountContext, updatedPresentatio
}) })
} }
openCountriesSelectionImpl = {
let state = stateValue.with { $0 }
let stateContext = CountriesMultiselectionScreen.StateContext(
context: context,
subject: .countries,
initialSelectedCountries: state.countries
)
let _ = (stateContext.ready |> filter { $0 } |> take(1) |> deliverOnMainQueue).startStandalone(next: { _ in
let controller = CountriesMultiselectionScreen(
context: context,
stateContext: stateContext,
completion: { countries in
updateState { state in
var updatedState = state
updatedState.countries = countries
return updatedState
}
}
)
pushControllerImpl?(controller)
})
}
openPremiumIntroImpl = { openPremiumIntroImpl = {
let controller = context.sharedContext.makePremiumIntroController(context: context, source: .settings, forceDark: false, dismissed: nil) let controller = context.sharedContext.makePremiumIntroController(context: context, source: .settings, forceDark: false, dismissed: nil)
pushControllerImpl?(controller) pushControllerImpl?(controller)
@ -1023,5 +1112,8 @@ public func createGiveawayController(context: AccountContext, updatedPresentatio
}) })
} }
let countriesConfiguration = context.currentCountriesConfiguration.with { $0 }
AuthorizationSequenceCountrySelectionController.setupCountryCodes(countries: countriesConfiguration.countries, codesByPrefix: countriesConfiguration.countriesByPrefix)
return controller return controller
} }

View File

@ -264,6 +264,14 @@ private final class PremiumGiftCodeSheetContent: CombinedComponent {
) )
) )
)) ))
} else if giftCode.isGiveaway {
tableItems.append(.init(
id: "to",
title: "To",
component: AnyComponent(
MultilineTextComponent(text: .plain(NSAttributedString(string: "No recipient", font: tableFont, textColor: tableTextColor)))
)
))
} }
let giftTitle: String let giftTitle: String
if giftCode.months == 12 { if giftCode.months == 12 {
@ -279,7 +287,12 @@ private final class PremiumGiftCodeSheetContent: CombinedComponent {
) )
)) ))
let giftReason = giftCode.isGiveaway ? "Giveaway" : "You were selected by the channel" let giftReason: String
if giftCode.toPeerId == nil {
giftReason = "Incomplete Giveaway"
} else {
giftReason = giftCode.isGiveaway ? "Giveaway" : "You were selected by the channel"
}
tableItems.append(.init( tableItems.append(.init(
id: "reason", id: "reason",
title: "Reason", title: "Reason",

View File

@ -24,6 +24,7 @@ import UniversalMediaPlayer
import CheckNode import CheckNode
import AnimationCache import AnimationCache
import MultiAnimationRenderer import MultiAnimationRenderer
import TelegramNotices
public enum PremiumSource: Equatable { public enum PremiumSource: Equatable {
public static func == (lhs: PremiumSource, rhs: PremiumSource) -> Bool { public static func == (lhs: PremiumSource, rhs: PremiumSource) -> Bool {
@ -1428,12 +1429,15 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
var selectedProductId: String? var selectedProductId: String?
var validPurchases: [InAppPurchaseManager.ReceiptPurchase] = [] var validPurchases: [InAppPurchaseManager.ReceiptPurchase] = []
var newPerks: [String] = []
var isPremium: Bool? var isPremium: Bool?
private var disposable: Disposable? private var disposable: Disposable?
private(set) var configuration = PremiumIntroConfiguration.defaultValue private(set) var configuration = PremiumIntroConfiguration.defaultValue
private var stickersDisposable: Disposable? private var stickersDisposable: Disposable?
private var newPerksDisposable: Disposable?
private var preloadDisposableSet = DisposableSet() private var preloadDisposableSet = DisposableSet()
var price: String? { var price: String? {
@ -1511,12 +1515,27 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
} }
} }
}) })
self.newPerksDisposable = (ApplicationSpecificNotice.dismissedPremiumAppIconsBadge(accountManager: context.sharedContext.accountManager)
|> deliverOnMainQueue).startStrict(next: { [weak self] dismissedPremiumAppIconsBadge in
guard let self else {
return
}
var newPerks: [String] = []
if !dismissedPremiumAppIconsBadge {
newPerks.append(PremiumPerk.appIcons.identifier)
}
self.newPerks = newPerks
self.updated()
})
} }
deinit { deinit {
self.disposable?.dispose() self.disposable?.dispose()
self.preloadDisposableSet.dispose() self.preloadDisposableSet.dispose()
self.stickersDisposable?.dispose() self.stickersDisposable?.dispose()
self.newPerksDisposable?.dispose()
} }
} }
@ -1807,7 +1826,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
subtitleColor: subtitleColor, subtitleColor: subtitleColor,
arrowColor: arrowColor, arrowColor: arrowColor,
accentColor: accentColor, accentColor: accentColor,
badge: perk.identifier == "stories" ? strings.Premium_New : nil badge: state.newPerks.contains(perk.identifier) ? strings.Premium_New : nil
) )
) )
), ),
@ -1837,6 +1856,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
demoSubject = .animatedUserpics demoSubject = .animatedUserpics
case .appIcons: case .appIcons:
demoSubject = .appIcons demoSubject = .appIcons
let _ = ApplicationSpecificNotice.setDismissedPremiumAppIconsBadge(accountManager: accountContext.sharedContext.accountManager).startStandalone()
case .animatedEmoji: case .animatedEmoji:
demoSubject = .animatedEmoji demoSubject = .animatedEmoji
case .emojiStatus: case .emojiStatus:

View File

@ -1153,8 +1153,8 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
dict[1042605427] = { return Api.payments.BankCardData.parse_bankCardData($0) } dict[1042605427] = { return Api.payments.BankCardData.parse_bankCardData($0) }
dict[-1222446760] = { return Api.payments.CheckedGiftCode.parse_checkedGiftCode($0) } dict[-1222446760] = { return Api.payments.CheckedGiftCode.parse_checkedGiftCode($0) }
dict[-1362048039] = { return Api.payments.ExportedInvoice.parse_exportedInvoice($0) } dict[-1362048039] = { return Api.payments.ExportedInvoice.parse_exportedInvoice($0) }
dict[2054937690] = { return Api.payments.GiveawayInfo.parse_giveawayInfo($0) } dict[1130879648] = { return Api.payments.GiveawayInfo.parse_giveawayInfo($0) }
dict[952312868] = { return Api.payments.GiveawayInfo.parse_giveawayInfoResults($0) } dict[13456752] = { return Api.payments.GiveawayInfo.parse_giveawayInfoResults($0) }
dict[-1610250415] = { return Api.payments.PaymentForm.parse_paymentForm($0) } dict[-1610250415] = { return Api.payments.PaymentForm.parse_paymentForm($0) }
dict[1891958275] = { return Api.payments.PaymentReceipt.parse_paymentReceipt($0) } dict[1891958275] = { return Api.payments.PaymentReceipt.parse_paymentReceipt($0) }
dict[1314881805] = { return Api.payments.PaymentResult.parse_paymentResult($0) } dict[1314881805] = { return Api.payments.PaymentResult.parse_paymentResult($0) }

View File

@ -656,24 +656,27 @@ public extension Api.payments {
} }
public extension Api.payments { public extension Api.payments {
enum GiveawayInfo: TypeConstructorDescription { enum GiveawayInfo: TypeConstructorDescription {
case giveawayInfo(flags: Int32, joinedTooEarlyDate: Int32?, adminDisallowedChatId: Int64?) case giveawayInfo(flags: Int32, startDate: Int32, joinedTooEarlyDate: Int32?, adminDisallowedChatId: Int64?, disallowedCountry: String?)
case giveawayInfoResults(flags: Int32, giftCodeSlug: String?, finishDate: Int32, winnersCount: Int32, activatedCount: Int32) case giveawayInfoResults(flags: Int32, startDate: Int32, giftCodeSlug: String?, finishDate: Int32, winnersCount: Int32, activatedCount: Int32)
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self { switch self {
case .giveawayInfo(let flags, let joinedTooEarlyDate, let adminDisallowedChatId): case .giveawayInfo(let flags, let startDate, let joinedTooEarlyDate, let adminDisallowedChatId, let disallowedCountry):
if boxed { if boxed {
buffer.appendInt32(2054937690) buffer.appendInt32(1130879648)
} }
serializeInt32(flags, buffer: buffer, boxed: false) serializeInt32(flags, buffer: buffer, boxed: false)
serializeInt32(startDate, buffer: buffer, boxed: false)
if Int(flags) & Int(1 << 1) != 0 {serializeInt32(joinedTooEarlyDate!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 1) != 0 {serializeInt32(joinedTooEarlyDate!, buffer: buffer, boxed: false)}
if Int(flags) & Int(1 << 2) != 0 {serializeInt64(adminDisallowedChatId!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 2) != 0 {serializeInt64(adminDisallowedChatId!, buffer: buffer, boxed: false)}
if Int(flags) & Int(1 << 4) != 0 {serializeString(disallowedCountry!, buffer: buffer, boxed: false)}
break break
case .giveawayInfoResults(let flags, let giftCodeSlug, let finishDate, let winnersCount, let activatedCount): case .giveawayInfoResults(let flags, let startDate, let giftCodeSlug, let finishDate, let winnersCount, let activatedCount):
if boxed { if boxed {
buffer.appendInt32(952312868) buffer.appendInt32(13456752)
} }
serializeInt32(flags, buffer: buffer, boxed: false) serializeInt32(flags, buffer: buffer, boxed: false)
serializeInt32(startDate, buffer: buffer, boxed: false)
if Int(flags) & Int(1 << 0) != 0 {serializeString(giftCodeSlug!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 0) != 0 {serializeString(giftCodeSlug!, buffer: buffer, boxed: false)}
serializeInt32(finishDate, buffer: buffer, boxed: false) serializeInt32(finishDate, buffer: buffer, boxed: false)
serializeInt32(winnersCount, buffer: buffer, boxed: false) serializeInt32(winnersCount, buffer: buffer, boxed: false)
@ -684,10 +687,10 @@ public extension Api.payments {
public func descriptionFields() -> (String, [(String, Any)]) { public func descriptionFields() -> (String, [(String, Any)]) {
switch self { switch self {
case .giveawayInfo(let flags, let joinedTooEarlyDate, let adminDisallowedChatId): case .giveawayInfo(let flags, let startDate, let joinedTooEarlyDate, let adminDisallowedChatId, let disallowedCountry):
return ("giveawayInfo", [("flags", flags as Any), ("joinedTooEarlyDate", joinedTooEarlyDate as Any), ("adminDisallowedChatId", adminDisallowedChatId as Any)]) return ("giveawayInfo", [("flags", flags as Any), ("startDate", startDate as Any), ("joinedTooEarlyDate", joinedTooEarlyDate as Any), ("adminDisallowedChatId", adminDisallowedChatId as Any), ("disallowedCountry", disallowedCountry as Any)])
case .giveawayInfoResults(let flags, let giftCodeSlug, let finishDate, let winnersCount, let activatedCount): case .giveawayInfoResults(let flags, let startDate, let giftCodeSlug, let finishDate, let winnersCount, let activatedCount):
return ("giveawayInfoResults", [("flags", flags as Any), ("giftCodeSlug", giftCodeSlug as Any), ("finishDate", finishDate as Any), ("winnersCount", winnersCount as Any), ("activatedCount", activatedCount as Any)]) return ("giveawayInfoResults", [("flags", flags as Any), ("startDate", startDate as Any), ("giftCodeSlug", giftCodeSlug as Any), ("finishDate", finishDate as Any), ("winnersCount", winnersCount as Any), ("activatedCount", activatedCount as Any)])
} }
} }
@ -695,14 +698,20 @@ public extension Api.payments {
var _1: Int32? var _1: Int32?
_1 = reader.readInt32() _1 = reader.readInt32()
var _2: Int32? var _2: Int32?
if Int(_1!) & Int(1 << 1) != 0 {_2 = reader.readInt32() } _2 = reader.readInt32()
var _3: Int64? var _3: Int32?
if Int(_1!) & Int(1 << 2) != 0 {_3 = reader.readInt64() } if Int(_1!) & Int(1 << 1) != 0 {_3 = reader.readInt32() }
var _4: Int64?
if Int(_1!) & Int(1 << 2) != 0 {_4 = reader.readInt64() }
var _5: String?
if Int(_1!) & Int(1 << 4) != 0 {_5 = parseString(reader) }
let _c1 = _1 != nil let _c1 = _1 != nil
let _c2 = (Int(_1!) & Int(1 << 1) == 0) || _2 != nil let _c2 = _2 != nil
let _c3 = (Int(_1!) & Int(1 << 2) == 0) || _3 != nil let _c3 = (Int(_1!) & Int(1 << 1) == 0) || _3 != nil
if _c1 && _c2 && _c3 { let _c4 = (Int(_1!) & Int(1 << 2) == 0) || _4 != nil
return Api.payments.GiveawayInfo.giveawayInfo(flags: _1!, joinedTooEarlyDate: _2, adminDisallowedChatId: _3) let _c5 = (Int(_1!) & Int(1 << 4) == 0) || _5 != nil
if _c1 && _c2 && _c3 && _c4 && _c5 {
return Api.payments.GiveawayInfo.giveawayInfo(flags: _1!, startDate: _2!, joinedTooEarlyDate: _3, adminDisallowedChatId: _4, disallowedCountry: _5)
} }
else { else {
return nil return nil
@ -711,21 +720,24 @@ public extension Api.payments {
public static func parse_giveawayInfoResults(_ reader: BufferReader) -> GiveawayInfo? { public static func parse_giveawayInfoResults(_ reader: BufferReader) -> GiveawayInfo? {
var _1: Int32? var _1: Int32?
_1 = reader.readInt32() _1 = reader.readInt32()
var _2: String? var _2: Int32?
if Int(_1!) & Int(1 << 0) != 0 {_2 = parseString(reader) } _2 = reader.readInt32()
var _3: Int32? var _3: String?
_3 = reader.readInt32() if Int(_1!) & Int(1 << 0) != 0 {_3 = parseString(reader) }
var _4: Int32? var _4: Int32?
_4 = reader.readInt32() _4 = reader.readInt32()
var _5: Int32? var _5: Int32?
_5 = reader.readInt32() _5 = reader.readInt32()
var _6: Int32?
_6 = reader.readInt32()
let _c1 = _1 != nil let _c1 = _1 != nil
let _c2 = (Int(_1!) & Int(1 << 0) == 0) || _2 != nil let _c2 = _2 != nil
let _c3 = _3 != nil let _c3 = (Int(_1!) & Int(1 << 0) == 0) || _3 != nil
let _c4 = _4 != nil let _c4 = _4 != nil
let _c5 = _5 != nil let _c5 = _5 != nil
if _c1 && _c2 && _c3 && _c4 && _c5 { let _c6 = _6 != nil
return Api.payments.GiveawayInfo.giveawayInfoResults(flags: _1!, giftCodeSlug: _2, finishDate: _3!, winnersCount: _4!, activatedCount: _5!) if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 {
return Api.payments.GiveawayInfo.giveawayInfoResults(flags: _1!, startDate: _2!, giftCodeSlug: _3, finishDate: _4!, winnersCount: _5!, activatedCount: _6!)
} }
else { else {
return nil return nil

View File

@ -56,6 +56,7 @@ public enum PremiumGiveawayInfo: Equatable {
public enum DisallowReason: Equatable { public enum DisallowReason: Equatable {
case joinedTooEarly(Int32) case joinedTooEarly(Int32)
case channelAdmin(EnginePeer.Id) case channelAdmin(EnginePeer.Id)
case disallowedCountry(String)
} }
case notQualified case notQualified
@ -70,8 +71,8 @@ public enum PremiumGiveawayInfo: Equatable {
case refunded case refunded
} }
case ongoing(status: OngoingStatus) case ongoing(startDate: Int32, status: OngoingStatus)
case finished(status: ResultStatus, finishDate: Int32, winnersCount: Int32, activatedCount: Int32) case finished(status: ResultStatus, startDate: Int32, finishDate: Int32, winnersCount: Int32, activatedCount: Int32)
} }
public struct PrepaidGiveaway: Equatable { public struct PrepaidGiveaway: Equatable {
@ -95,19 +96,21 @@ func _internal_getPremiumGiveawayInfo(account: Account, peerId: EnginePeer.Id, m
|> map { result -> PremiumGiveawayInfo? in |> map { result -> PremiumGiveawayInfo? in
if let result { if let result {
switch result { switch result {
case let .giveawayInfo(flags, joinedTooEarlyDate, adminDisallowedChatId): case let .giveawayInfo(flags, startDate, joinedTooEarlyDate, adminDisallowedChatId, disallowedCountry):
if (flags & (1 << 3)) != 0 { if (flags & (1 << 3)) != 0 {
return .ongoing(status: .almostOver) return .ongoing(startDate: startDate, status: .almostOver)
} else if (flags & (1 << 0)) != 0 { } else if (flags & (1 << 0)) != 0 {
return .ongoing(status: .participating) return .ongoing(startDate: startDate, status: .participating)
} else if let disallowedCountry = disallowedCountry {
return .ongoing(startDate: startDate, status: .notAllowed(.disallowedCountry(disallowedCountry)))
} else if let joinedTooEarlyDate = joinedTooEarlyDate { } else if let joinedTooEarlyDate = joinedTooEarlyDate {
return .ongoing(status: .notAllowed(.joinedTooEarly(joinedTooEarlyDate))) return .ongoing(startDate: startDate, status: .notAllowed(.joinedTooEarly(joinedTooEarlyDate)))
} else if let adminDisallowedChatId = adminDisallowedChatId { } else if let adminDisallowedChatId = adminDisallowedChatId {
return .ongoing(status: .notAllowed(.channelAdmin(EnginePeer.Id(namespace: Namespaces.Peer.CloudChannel, id: EnginePeer.Id.Id._internalFromInt64Value(adminDisallowedChatId))))) return .ongoing(startDate: startDate, status: .notAllowed(.channelAdmin(EnginePeer.Id(namespace: Namespaces.Peer.CloudChannel, id: EnginePeer.Id.Id._internalFromInt64Value(adminDisallowedChatId)))))
} else { } else {
return .ongoing(status: .notQualified) return .ongoing(startDate: startDate, status: .notQualified)
} }
case let .giveawayInfoResults(flags, giftCodeSlug, finishDate, winnersCount, activatedCount): case let .giveawayInfoResults(flags, startDate, giftCodeSlug, finishDate, winnersCount, activatedCount):
let status: PremiumGiveawayInfo.ResultStatus let status: PremiumGiveawayInfo.ResultStatus
if let giftCodeSlug = giftCodeSlug { if let giftCodeSlug = giftCodeSlug {
status = .won(slug: giftCodeSlug) status = .won(slug: giftCodeSlug)
@ -116,7 +119,7 @@ func _internal_getPremiumGiveawayInfo(account: Account, peerId: EnginePeer.Id, m
} else { } else {
status = .notWon status = .notWon
} }
return .finished(status: status, finishDate: finishDate, winnersCount: winnersCount, activatedCount: activatedCount) return .finished(status: status, startDate: startDate, finishDate: finishDate, winnersCount: winnersCount, activatedCount: activatedCount)
} }
} else { } else {
return nil return nil

View File

@ -183,6 +183,7 @@ private enum ApplicationSpecificGlobalNotice: Int32 {
case displayStoryUnmuteTooltip = 49 case displayStoryUnmuteTooltip = 49
case chatReplyOptionsTip = 50 case chatReplyOptionsTip = 50
case displayStoryInteractionGuide = 51 case displayStoryInteractionGuide = 51
case dismissedPremiumAppIconsBadge = 52
var key: ValueBoxKey { var key: ValueBoxKey {
let v = ValueBoxKey(length: 4) let v = ValueBoxKey(length: 4)
@ -444,6 +445,10 @@ private struct ApplicationSpecificNoticeKeys {
static func displayStoryInteractionGuide() -> NoticeEntryKey { static func displayStoryInteractionGuide() -> NoticeEntryKey {
return NoticeEntryKey(namespace: noticeNamespace(namespace: globalNamespace), key: ApplicationSpecificGlobalNotice.displayStoryInteractionGuide.key) return NoticeEntryKey(namespace: noticeNamespace(namespace: globalNamespace), key: ApplicationSpecificGlobalNotice.displayStoryInteractionGuide.key)
} }
static func dismissedPremiumAppIconsBadge() -> NoticeEntryKey {
return NoticeEntryKey(namespace: noticeNamespace(namespace: globalNamespace), key: ApplicationSpecificGlobalNotice.dismissedPremiumAppIconsBadge.key)
}
} }
public struct ApplicationSpecificNotice { public struct ApplicationSpecificNotice {
@ -1717,4 +1722,25 @@ public struct ApplicationSpecificNotice {
} }
|> take(1) |> take(1)
} }
public static func setDismissedPremiumAppIconsBadge(accountManager: AccountManager<TelegramAccountManagerTypes>) -> Signal<Never, NoError> {
return accountManager.transaction { transaction -> Void in
if let entry = CodableEntry(ApplicationSpecificBoolNotice()) {
transaction.setNotice(ApplicationSpecificNoticeKeys.dismissedPremiumAppIconsBadge(), entry)
}
}
|> ignoreValues
}
public static func dismissedPremiumAppIconsBadge(accountManager: AccountManager<TelegramAccountManagerTypes>) -> Signal<Bool, NoError> {
return accountManager.noticeEntry(key: ApplicationSpecificNoticeKeys.dismissedPremiumAppIconsBadge())
|> map { view -> Bool in
if let _ = view.value?.get(ApplicationSpecificBoolNotice.self) {
return true
} else {
return false
}
}
|> take(1)
}
} }

View File

@ -44,3 +44,12 @@ public func stringForDistance(strings: PresentationStrings, distance: CLLocation
return distanceFormatter.string(fromDistance: distance) return distanceFormatter.string(fromDistance: distance)
} }
public func flagEmoji(countryCode: String) -> String {
let base : UInt32 = 127397
var flagString = ""
for v in countryCode.uppercased().unicodeScalars {
flagString.unicodeScalars.append(UnicodeScalar(base + v.value)!)
}
return flagString
}

View File

@ -354,6 +354,8 @@ public func mediaContentKind(_ media: EngineMedia, message: EngineMessage? = nil
} }
case .story: case .story:
return .story return .story
case .giveaway:
return .giveaway
default: default:
return nil return nil
} }

View File

@ -922,6 +922,10 @@ public func universalServiceMessageString(presentationData: (PresentationTheme,
resultTitleString = strings.Conversation_StoryExpiredMentionTextOutgoing(compactPeerName) resultTitleString = strings.Conversation_StoryExpiredMentionTextOutgoing(compactPeerName)
} }
attributedString = addAttributesToStringWithRanges(resultTitleString._tuple, body: bodyAttributes, argumentAttributes: [0: boldAttributes]) attributedString = addAttributesToStringWithRanges(resultTitleString._tuple, body: bodyAttributes, argumentAttributes: [0: boldAttributes])
} else if let _ = media as? TelegramMediaGiveaway {
let compactAuthorName = message.author?.compactDisplayTitle ?? ""
let resultTitleString = strings.Notification_GiveawayStarted(compactAuthorName)
attributedString = addAttributesToStringWithRanges(resultTitleString._tuple, body: bodyAttributes, argumentAttributes: [0: boldAttributes])
} }
} }

View File

@ -32,6 +32,7 @@ public struct ChatMessageBubbleContentProperties {
public let shareButtonOffset: CGPoint? public let shareButtonOffset: CGPoint?
public let hidesHeaders: Bool public let hidesHeaders: Bool
public let avatarOffset: CGFloat? public let avatarOffset: CGFloat?
public let isDetached: Bool
public init( public init(
hidesSimpleAuthorHeader: Bool, hidesSimpleAuthorHeader: Bool,
@ -41,7 +42,8 @@ public struct ChatMessageBubbleContentProperties {
forceAlignment: ChatMessageBubbleContentAlignment, forceAlignment: ChatMessageBubbleContentAlignment,
shareButtonOffset: CGPoint? = nil, shareButtonOffset: CGPoint? = nil,
hidesHeaders: Bool = false, hidesHeaders: Bool = false,
avatarOffset: CGFloat? = nil avatarOffset: CGFloat? = nil,
isDetached: Bool = false
) { ) {
self.hidesSimpleAuthorHeader = hidesSimpleAuthorHeader self.hidesSimpleAuthorHeader = hidesSimpleAuthorHeader
self.headerSpacing = headerSpacing self.headerSpacing = headerSpacing
@ -51,6 +53,7 @@ public struct ChatMessageBubbleContentProperties {
self.shareButtonOffset = shareButtonOffset self.shareButtonOffset = shareButtonOffset
self.hidesHeaders = hidesHeaders self.hidesHeaders = hidesHeaders
self.avatarOffset = avatarOffset self.avatarOffset = avatarOffset
self.isDetached = isDetached
} }
} }
@ -169,6 +172,7 @@ open class ChatMessageBubbleContentNode: ASDisplayNode {
return false return false
} }
public weak var itemNode: ChatMessageItemNodeProtocol?
public weak var bubbleBackgroundNode: ChatMessageBackground? public weak var bubbleBackgroundNode: ChatMessageBackground?
public weak var bubbleBackdropNode: ChatMessageBubbleBackdrop? public weak var bubbleBackdropNode: ChatMessageBubbleBackdrop?

View File

@ -233,14 +233,14 @@ public final class LottieComponent: Component {
} }
} }
public func playOnce(delay: Double = 0.0, completion: (() -> Void)? = nil) { public func playOnce(delay: Double = 0.0, force: Bool = false, completion: (() -> Void)? = nil) {
self.playOnceCompletion = completion self.playOnceCompletion = completion
guard let _ = self.animationInstance, let animationFrameRange = self.animationFrameRange else { guard let _ = self.animationInstance, let animationFrameRange = self.animationFrameRange else {
self.scheduledPlayOnce = true self.scheduledPlayOnce = true
return return
} }
if !self.isEffectivelyVisible { if !self.isEffectivelyVisible && !force {
self.scheduledPlayOnce = true self.scheduledPlayOnce = true
return return
} }

View File

@ -36,6 +36,7 @@ import LocationUI
import LegacyMediaPickerUI import LegacyMediaPickerUI
import ReactionSelectionNode import ReactionSelectionNode
import VolumeSliderContextItem import VolumeSliderContextItem
import TelegramStringFormatting
enum DrawingScreenType { enum DrawingScreenType {
case drawing case drawing
@ -3066,15 +3067,6 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
self.staticEmojiPack.set(self.context.engine.stickers.loadedStickerPack(reference: .name("staticemoji"), forceActualized: false)) self.staticEmojiPack.set(self.context.engine.stickers.loadedStickerPack(reference: .name("staticemoji"), forceActualized: false))
} }
func flag(countryCode: String) -> String {
let base : UInt32 = 127397
var flagString = ""
for v in countryCode.uppercased().unicodeScalars {
flagString.unicodeScalars.append(UnicodeScalar(base + v.value)!)
}
return flagString
}
var location: CLLocationCoordinate2D? var location: CLLocationCoordinate2D?
if let subject = self.subject { if let subject = self.subject {
if case let .asset(asset) = subject { if case let .asset(asset) = subject {
@ -3095,7 +3087,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
if let self { if let self {
let emojiFile: Signal<TelegramMediaFile?, NoError> let emojiFile: Signal<TelegramMediaFile?, NoError>
if let countryCode { if let countryCode {
let flagEmoji = flag(countryCode: countryCode) let flag = flagEmoji(countryCode: countryCode)
emojiFile = self.staticEmojiPack.get() emojiFile = self.staticEmojiPack.get()
|> filter { result in |> filter { result in
if case .result = result { if case .result = result {
@ -3114,7 +3106,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
break break
} }
} }
if let displayText, displayText.hasPrefix(flagEmoji) { if let displayText, displayText.hasPrefix(flag) {
return true return true
} else { } else {
return false return false

View File

@ -41,6 +41,7 @@ swift_library(
"//submodules/OverlayStatusController", "//submodules/OverlayStatusController",
"//submodules/UndoUI", "//submodules/UndoUI",
"//submodules/TemporaryCachedPeerDataManager", "//submodules/TemporaryCachedPeerDataManager",
"//submodules/CountrySelectionUI",
], ],
visibility = [ visibility = [
"//visibility:public", "//visibility:public",

View File

@ -0,0 +1,225 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import ComponentFlow
import SwiftSignalKit
import AccountContext
import TelegramCore
import MultilineTextComponent
import AvatarNode
import TelegramPresentationData
import CheckNode
import BundleIconComponent
final class CountryListItemComponent: Component {
enum SelectionState: Equatable {
case none
case editing(isSelected: Bool, isTinted: Bool)
}
let context: AccountContext
let theme: PresentationTheme
let title: String
let selectionState: SelectionState
let hasNext: Bool
let action: () -> Void
init(
context: AccountContext,
theme: PresentationTheme,
title: String,
selectionState: SelectionState,
hasNext: Bool,
action: @escaping () -> Void
) {
self.context = context
self.theme = theme
self.title = title
self.selectionState = selectionState
self.hasNext = hasNext
self.action = action
}
static func ==(lhs: CountryListItemComponent, rhs: CountryListItemComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.theme !== rhs.theme {
return false
}
if lhs.title != rhs.title {
return false
}
if lhs.selectionState != rhs.selectionState {
return false
}
if lhs.hasNext != rhs.hasNext {
return false
}
return true
}
final class View: UIView {
private let containerButton: HighlightTrackingButton
private let title = ComponentView<Empty>()
private let separatorLayer: SimpleLayer
private var checkLayer: CheckLayer?
private var component: CountryListItemComponent?
private weak var state: EmptyComponentState?
override init(frame: CGRect) {
self.separatorLayer = SimpleLayer()
self.containerButton = HighlightTrackingButton()
super.init(frame: frame)
self.layer.addSublayer(self.separatorLayer)
self.addSubview(self.containerButton)
self.containerButton.addTarget(self, action: #selector(self.pressed), for: .touchUpInside)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc private func pressed() {
guard let component = self.component else {
return
}
component.action()
}
func update(component: CountryListItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
let themeUpdated = self.component?.theme !== component.theme
var hasSelectionUpdated = false
if let previousComponent = self.component {
switch previousComponent.selectionState {
case .none:
if case .none = component.selectionState {
} else {
hasSelectionUpdated = true
}
case .editing:
if case .editing = component.selectionState {
} else {
hasSelectionUpdated = true
}
}
}
self.component = component
self.state = state
let contextInset: CGFloat = 0.0
let height: CGFloat = 44.0
let verticalInset: CGFloat = 0.0
var leftInset: CGFloat = 16.0
let rightInset: CGFloat = contextInset * 2.0 + 8.0
if case let .editing(isSelected, isTinted) = component.selectionState {
leftInset += 44.0
let checkSize: CGFloat = 22.0
let checkLayer: CheckLayer
if let current = self.checkLayer {
checkLayer = current
if themeUpdated {
var theme = CheckNodeTheme(theme: component.theme, style: .plain)
if isTinted {
theme.backgroundColor = theme.backgroundColor.mixedWith(component.theme.list.itemBlocksBackgroundColor, alpha: 0.5)
}
checkLayer.theme = theme
}
checkLayer.setSelected(isSelected, animated: !transition.animation.isImmediate)
} else {
var theme = CheckNodeTheme(theme: component.theme, style: .plain)
if isTinted {
theme.backgroundColor = theme.backgroundColor.mixedWith(component.theme.list.itemBlocksBackgroundColor, alpha: 0.5)
}
checkLayer = CheckLayer(theme: theme)
self.checkLayer = checkLayer
self.containerButton.layer.addSublayer(checkLayer)
checkLayer.frame = CGRect(origin: CGPoint(x: -checkSize, y: floor((height - verticalInset * 2.0 - checkSize) / 2.0)), size: CGSize(width: checkSize, height: checkSize))
checkLayer.setSelected(isSelected, animated: false)
checkLayer.setNeedsDisplay()
}
transition.setFrame(layer: checkLayer, frame: CGRect(origin: CGPoint(x: floor((54.0 - checkSize) * 0.5), y: floor((height - verticalInset * 2.0 - checkSize) / 2.0)), size: CGSize(width: checkSize, height: checkSize)))
} else {
if let checkLayer = self.checkLayer {
self.checkLayer = nil
transition.setPosition(layer: checkLayer, position: CGPoint(x: -checkLayer.bounds.width * 0.5, y: checkLayer.position.y), completion: { [weak checkLayer] _ in
checkLayer?.removeFromSuperlayer()
})
}
}
let previousTitleFrame = self.title.view?.frame
var previousTitleContents: UIView?
if hasSelectionUpdated && !"".isEmpty {
previousTitleContents = self.title.view?.snapshotView(afterScreenUpdates: false)
}
let titleSize = self.title.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: component.title, font: Font.regular(17.0), textColor: component.theme.list.itemPrimaryTextColor))
)),
environment: {},
containerSize: CGSize(width: availableSize.width - leftInset - rightInset, height: 100.0)
)
let centralContentHeight: CGFloat = titleSize.height
let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: floor((height - verticalInset * 2.0 - centralContentHeight) / 2.0)), size: titleSize)
if let titleView = self.title.view {
if titleView.superview == nil {
titleView.isUserInteractionEnabled = false
self.containerButton.addSubview(titleView)
}
titleView.frame = titleFrame
if let previousTitleFrame, previousTitleFrame.origin.x != titleFrame.origin.x {
transition.animatePosition(view: titleView, from: CGPoint(x: previousTitleFrame.origin.x - titleFrame.origin.x, y: 0.0), to: CGPoint(), additive: true)
}
if let previousTitleFrame, let previousTitleContents, previousTitleFrame.size != titleSize {
previousTitleContents.frame = CGRect(origin: previousTitleFrame.origin, size: previousTitleFrame.size)
self.addSubview(previousTitleContents)
transition.setFrame(view: previousTitleContents, frame: CGRect(origin: titleFrame.origin, size: previousTitleFrame.size))
transition.setAlpha(view: previousTitleContents, alpha: 0.0, completion: { [weak previousTitleContents] _ in
previousTitleContents?.removeFromSuperview()
})
transition.animateAlpha(view: titleView, from: 0.0, to: 1.0)
}
}
if themeUpdated {
self.separatorLayer.backgroundColor = component.theme.list.itemPlainSeparatorColor.cgColor
}
let separatorLeftInset = leftInset + 44.0
transition.setFrame(layer: self.separatorLayer, frame: CGRect(origin: CGPoint(x: separatorLeftInset, y: height), size: CGSize(width: availableSize.width - separatorLeftInset, height: UIScreenPixel)))
self.separatorLayer.isHidden = !component.hasNext
let containerFrame = CGRect(origin: CGPoint(x: contextInset, y: verticalInset), size: CGSize(width: availableSize.width - contextInset * 2.0, height: height - verticalInset * 2.0))
transition.setFrame(view: self.containerButton, frame: containerFrame)
return CGSize(width: availableSize.width, height: height)
}
}
func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}

View File

@ -11,11 +11,8 @@ import AccountContext
import TelegramCore import TelegramCore
import Postbox import Postbox
import MultilineTextComponent import MultilineTextComponent
import SolidRoundedButtonComponent
import PresentationDataUtils import PresentationDataUtils
import ButtonComponent import ButtonComponent
import PlainButtonComponent
import AnimatedCounterComponent
import TokenListTextField import TokenListTextField
import AvatarNode import AvatarNode
import LocalizedPeerData import LocalizedPeerData
@ -237,26 +234,6 @@ final class ShareWithPeersScreenComponent: Component {
} }
} }
final class PeerItem: Equatable {
let id: EnginePeer.Id
let peer: EnginePeer?
init(
id: EnginePeer.Id,
peer: EnginePeer?
) {
self.id = id
self.peer = peer
}
static func ==(lhs: PeerItem, rhs: PeerItem) -> Bool {
if lhs === rhs {
return true
}
return false
}
}
enum OptionId: Int, Hashable { enum OptionId: Int, Hashable {
case screenshot = 0 case screenshot = 0
case pin = 1 case pin = 1
@ -1465,6 +1442,9 @@ final class ShareWithPeersScreenComponent: Component {
if peer.id.isGroupOrChannel { if peer.id.isGroupOrChannel {
if case .channels = component.stateContext.subject, self.selectedPeers.count >= component.context.userLimits.maxGiveawayChannelsCount, index == nil { if case .channels = component.stateContext.subject, self.selectedPeers.count >= component.context.userLimits.maxGiveawayChannelsCount, index == nil {
self.hapticFeedback.error() 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), elevatedLayout: false, position: .bottom, animateInAsReplacement: false, action: { _ in return false }), in: .current)
return return
} }
if case .channels = component.stateContext.subject { if case .channels = component.stateContext.subject {
@ -1492,6 +1472,9 @@ final class ShareWithPeersScreenComponent: Component {
} else { } else {
if case .members = component.stateContext.subject, self.selectedPeers.count >= 10, index == nil { if case .members = component.stateContext.subject, self.selectedPeers.count >= 10, index == nil {
self.hapticFeedback.error() 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), elevatedLayout: false, position: .bottom, animateInAsReplacement: false, action: { _ in return false }), in: .current)
return return
} }
togglePeer() togglePeer()
@ -2993,6 +2976,10 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer {
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition) super.containerLayoutUpdated(layout, transition: transition)
var updatedLayout = layout
updatedLayout.intrinsicInsets.bottom += 66.0
self.presentationContext.containerLayoutUpdated(updatedLayout, transition: transition)
} }
override public func viewDidAppear(_ animated: Bool) { override public func viewDidAppear(_ animated: Bool) {

View File

@ -45,6 +45,8 @@ final class StoryInteractionGuideComponent: Component {
private let guideItems = ComponentView<Empty>() private let guideItems = ComponentView<Empty>()
private let proceedButton = ComponentView<Empty>() private let proceedButton = ComponentView<Empty>()
var currentIndex = 0
override init(frame: CGRect) { override init(frame: CGRect) {
self.effectView = UIVisualEffectView(effect: nil) self.effectView = UIVisualEffectView(effect: nil)
@ -91,6 +93,7 @@ final class StoryInteractionGuideComponent: Component {
func update(component: StoryInteractionGuideComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize { func update(component: StoryInteractionGuideComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
self.component = component self.component = component
self.state = state
let sideInset: CGFloat = 48.0 let sideInset: CGFloat = 48.0
@ -131,7 +134,15 @@ final class StoryInteractionGuideComponent: Component {
context: component.context, context: component.context,
title: "Go forward", title: "Go forward",
text: "Tap the screen", text: "Tap the screen",
animationName: "story_forward" animationName: "story_forward",
isPlaying: self.currentIndex == 0,
playbackCompleted: { [weak self] in
guard let self else {
return
}
self.currentIndex = 1
self.state?.updated(transition: .easeInOut(duration: 0.3))
}
) )
) )
), ),
@ -142,7 +153,15 @@ final class StoryInteractionGuideComponent: Component {
context: component.context, context: component.context,
title: "Pause and Seek", title: "Pause and Seek",
text: "Hold and move sideways", text: "Hold and move sideways",
animationName: "story_pause" animationName: "story_pause",
isPlaying: self.currentIndex == 1,
playbackCompleted: { [weak self] in
guard let self else {
return
}
self.currentIndex = 2
self.state?.updated(transition: .easeInOut(duration: 0.3))
}
) )
) )
), ),
@ -153,7 +172,15 @@ final class StoryInteractionGuideComponent: Component {
context: component.context, context: component.context,
title: "Go back", title: "Go back",
text: "Tap the left edge", text: "Tap the left edge",
animationName: "story_back" animationName: "story_back",
isPlaying: self.currentIndex == 2,
playbackCompleted: { [weak self] in
guard let self else {
return
}
self.currentIndex = 3
self.state?.updated(transition: .easeInOut(duration: 0.3))
}
) )
) )
), ),
@ -164,14 +191,22 @@ final class StoryInteractionGuideComponent: Component {
context: component.context, context: component.context,
title: "Move between stories", title: "Move between stories",
text: "Swipe left or right", text: "Swipe left or right",
animationName: "story_move" animationName: "story_move",
isPlaying: self.currentIndex == 3,
playbackCompleted: { [weak self] in
guard let self else {
return
}
self.currentIndex = 0
self.state?.updated(transition: .easeInOut(duration: 0.3))
}
) )
) )
) )
] ]
let itemsSize = self.guideItems.update( let itemsSize = self.guideItems.update(
transition: .immediate, transition: transition,
component: AnyComponent(List(items)), component: AnyComponent(List(items)),
environment: {}, environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: availableSize.height) containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: availableSize.height)
@ -225,17 +260,23 @@ private final class GuideItemComponent: Component {
let title: String let title: String
let text: String let text: String
let animationName: String let animationName: String
let isPlaying: Bool
let playbackCompleted: () -> Void
init( init(
context: AccountContext, context: AccountContext,
title: String, title: String,
text: String, text: String,
animationName: String animationName: String,
isPlaying: Bool,
playbackCompleted: @escaping () -> Void
) { ) {
self.context = context self.context = context
self.title = title self.title = title
self.text = text self.text = text
self.animationName = animationName self.animationName = animationName
self.isPlaying = isPlaying
self.playbackCompleted = playbackCompleted
} }
static func ==(lhs: GuideItemComponent, rhs: GuideItemComponent) -> Bool { static func ==(lhs: GuideItemComponent, rhs: GuideItemComponent) -> Bool {
@ -248,6 +289,9 @@ private final class GuideItemComponent: Component {
if lhs.animationName != rhs.animationName { if lhs.animationName != rhs.animationName {
return false return false
} }
if lhs.isPlaying != rhs.isPlaying {
return false
}
return true return true
} }
@ -255,18 +299,33 @@ private final class GuideItemComponent: Component {
private var component: GuideItemComponent? private var component: GuideItemComponent?
private weak var state: EmptyComponentState? private weak var state: EmptyComponentState?
private let containerView = UIView()
private let selectionView = UIView()
private let animation = ComponentView<Empty>() private let animation = ComponentView<Empty>()
private let titleLabel = ComponentView<Empty>() private let titleLabel = ComponentView<Empty>()
private let descriptionLabel = ComponentView<Empty>() private let descriptionLabel = ComponentView<Empty>()
override init(frame: CGRect) { override init(frame: CGRect) {
super.init(frame: frame) super.init(frame: frame)
self.selectionView.backgroundColor = UIColor(rgb: 0xffffff, alpha: 0.1)
self.selectionView.clipsToBounds = true
self.selectionView.layer.cornerRadius = 16.0
if #available(iOS 13.0, *) {
self.selectionView.layer.cornerCurve = .continuous
}
self.selectionView.alpha = 0.0
self.addSubview(self.containerView)
self.containerView.addSubview(self.selectionView)
} }
required init?(coder: NSCoder) { required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented") fatalError("init(coder:) has not been implemented")
} }
private var isPlaying = false
func update(component: GuideItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize { func update(component: GuideItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
self.component = component self.component = component
@ -285,18 +344,40 @@ private final class GuideItemComponent: Component {
startingPosition: .begin, startingPosition: .begin,
size: CGSize(width: 60.0, height: 60.0), size: CGSize(width: 60.0, height: 60.0),
renderingScale: UIScreen.main.scale, renderingScale: UIScreen.main.scale,
loop: true loop: false
) )
), ),
environment: {}, environment: {},
containerSize: availableSize containerSize: availableSize
) )
let animationFrame = CGRect(origin: CGPoint(x: originX - 11.0, y: 15.0), size: animationSize) let animationFrame = CGRect(origin: CGPoint(x: originX - 11.0, y: 15.0), size: animationSize)
if let view = self.animation.view { if let view = self.animation.view as? LottieComponent.View {
if view.superview == nil { if view.superview == nil {
self.addSubview(view) view.externalShouldPlay = false
self.containerView.addSubview(view)
} }
view.frame = animationFrame view.frame = animationFrame
if component.isPlaying && !self.isPlaying {
self.isPlaying = true
Queue.mainQueue().justDispatch {
let completionBlock = { [weak self] in
guard let self else {
return
}
self.isPlaying = false
Queue.mainQueue().after(0.1) {
self.component?.playbackCompleted()
}
}
view.playOnce(force: true, completion: { [weak view] in
view?.playOnce(force: true, completion: {
completionBlock()
})
})
}
}
} }
let titleSize = self.titleLabel.update( let titleSize = self.titleLabel.update(
@ -308,7 +389,7 @@ private final class GuideItemComponent: Component {
let titleFrame = CGRect(origin: CGPoint(x: originX + 60.0, y: 25.0), size: titleSize) let titleFrame = CGRect(origin: CGPoint(x: originX + 60.0, y: 25.0), size: titleSize)
if let view = self.titleLabel.view { if let view = self.titleLabel.view {
if view.superview == nil { if view.superview == nil {
self.addSubview(view) self.containerView.addSubview(view)
} }
view.frame = titleFrame view.frame = titleFrame
} }
@ -322,11 +403,18 @@ private final class GuideItemComponent: Component {
let textFrame = CGRect(origin: CGPoint(x: originX + 60.0, y: titleFrame.maxY + 2.0), size: textSize) let textFrame = CGRect(origin: CGPoint(x: originX + 60.0, y: titleFrame.maxY + 2.0), size: textSize)
if let view = self.descriptionLabel.view { if let view = self.descriptionLabel.view {
if view.superview == nil { if view.superview == nil {
self.addSubview(view) self.containerView.addSubview(view)
} }
view.frame = textFrame view.frame = textFrame
} }
self.selectionView.frame = CGRect(origin: .zero, size: size).insetBy(dx: 12.0, dy: 8.0)
transition.setAlpha(view: self.selectionView, alpha: component.isPlaying ? 1.0 : 0.0)
self.containerView.bounds = CGRect(origin: .zero, size: size)
self.containerView.center = CGPoint(x: size.width / 2.0, y: size.height / 2.0)
transition.setScale(view: self.containerView, scale: component.isPlaying ? 1.1 : 1.0)
return size return size
} }
} }

View File

@ -11,6 +11,7 @@ struct EditableTokenListToken {
enum Subject { enum Subject {
case peer(EnginePeer) case peer(EnginePeer)
case category(UIImage?) case category(UIImage?)
case emoji(String)
} }
let id: AnyHashable let id: AnyHashable
@ -86,6 +87,7 @@ private final class TokenNode: ASDisplayNode {
let token: EditableTokenListToken let token: EditableTokenListToken
let avatarNode: AvatarNode let avatarNode: AvatarNode
let categoryAvatarNode: ASImageNode let categoryAvatarNode: ASImageNode
let emojiTextNode: ImmediateTextNode
let removeIconNode: ASImageNode let removeIconNode: ASImageNode
let titleNode: ASTextNode let titleNode: ASTextNode
let backgroundNode: ASImageNode let backgroundNode: ASImageNode
@ -119,6 +121,7 @@ private final class TokenNode: ASDisplayNode {
self.categoryAvatarNode = ASImageNode() self.categoryAvatarNode = ASImageNode()
self.categoryAvatarNode.displaysAsynchronously = false self.categoryAvatarNode.displaysAsynchronously = false
self.categoryAvatarNode.displayWithoutProcessing = true self.categoryAvatarNode.displayWithoutProcessing = true
self.emojiTextNode = ImmediateTextNode()
self.removeIconNode = ASImageNode() self.removeIconNode = ASImageNode()
self.removeIconNode.alpha = 0.0 self.removeIconNode.alpha = 0.0
@ -132,6 +135,8 @@ private final class TokenNode: ASDisplayNode {
cornerRadius = 24.0 cornerRadius = 24.0
case .category: case .category:
cornerRadius = 14.0 cornerRadius = 14.0
case .emoji:
cornerRadius = 24.0
} }
self.backgroundNode = ASImageNode() self.backgroundNode = ASImageNode()
@ -160,6 +165,9 @@ private final class TokenNode: ASDisplayNode {
case let .category(image): case let .category(image):
self.addSubnode(self.categoryAvatarNode) self.addSubnode(self.categoryAvatarNode)
self.categoryAvatarNode.image = image self.categoryAvatarNode.image = image
case let .emoji(emoji):
self.addSubnode(self.emojiTextNode)
self.emojiTextNode.attributedText = NSAttributedString(string: emoji, font: Font.regular(17.0), textColor: .white)
} }
self.updateIsSelected(isSelected, animated: false) self.updateIsSelected(isSelected, animated: false)
@ -167,7 +175,12 @@ private final class TokenNode: ASDisplayNode {
override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize { override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize {
let titleSize = self.titleNode.measure(CGSize(width: constrainedSize.width - 8.0, height: constrainedSize.height)) let titleSize = self.titleNode.measure(CGSize(width: constrainedSize.width - 8.0, height: constrainedSize.height))
return CGSize(width: 22.0 + titleSize.width + 16.0, height: 28.0) var width = 22.0 + titleSize.width + 16.0
if self.emojiTextNode.supernode != nil {
let _ = self.emojiTextNode.updateLayout(constrainedSize)
width += 3.0
}
return CGSize(width: width, height: 28.0)
} }
override func layout() { override func layout() {
@ -181,7 +194,13 @@ private final class TokenNode: ASDisplayNode {
self.categoryAvatarNode.frame = self.avatarNode.frame self.categoryAvatarNode.frame = self.avatarNode.frame
self.removeIconNode.frame = self.avatarNode.frame self.removeIconNode.frame = self.avatarNode.frame
self.titleNode.frame = CGRect(origin: CGPoint(x: 29.0, y: floor((self.bounds.size.height - titleSize.height) / 2.0)), size: titleSize) var textLeftOffset: CGFloat = 29.0
if let emojiTextSize = self.emojiTextNode.cachedLayout?.size {
self.emojiTextNode.frame = CGRect(origin: CGPoint(x: 7.0, y: 4.0), size: emojiTextSize)
textLeftOffset += 3.0
}
self.titleNode.frame = CGRect(origin: CGPoint(x: textLeftOffset, y: floor((self.bounds.size.height - titleSize.height) / 2.0)), size: titleSize)
} }
func updateIsSelected(_ isSelected: Bool, animated: Bool) { func updateIsSelected(_ isSelected: Bool, animated: Bool) {
@ -192,6 +211,7 @@ private final class TokenNode: ASDisplayNode {
self.avatarNode.alpha = isSelected ? 0.0 : 1.0 self.avatarNode.alpha = isSelected ? 0.0 : 1.0
self.categoryAvatarNode.alpha = isSelected ? 0.0 : 1.0 self.categoryAvatarNode.alpha = isSelected ? 0.0 : 1.0
self.emojiTextNode.alpha = isSelected ? 0.0 : 1.0
self.removeIconNode.alpha = isSelected ? 1.0 : 0.0 self.removeIconNode.alpha = isSelected ? 1.0 : 0.0
if animated { if animated {
@ -205,6 +225,9 @@ private final class TokenNode: ASDisplayNode {
self.categoryAvatarNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2) self.categoryAvatarNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2)
self.categoryAvatarNode.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2) self.categoryAvatarNode.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2)
self.emojiTextNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2)
self.emojiTextNode.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2)
self.removeIconNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) self.removeIconNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
self.removeIconNode.layer.animateScale(from: 0.01, to: 1.0, duration: 0.2) self.removeIconNode.layer.animateScale(from: 0.01, to: 1.0, duration: 0.2)
} else { } else {
@ -217,6 +240,9 @@ private final class TokenNode: ASDisplayNode {
self.categoryAvatarNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) self.categoryAvatarNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
self.categoryAvatarNode.layer.animateScale(from: 0.01, to: 1.0, duration: 0.2) self.categoryAvatarNode.layer.animateScale(from: 0.01, to: 1.0, duration: 0.2)
self.emojiTextNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
self.emojiTextNode.layer.animateScale(from: 0.01, to: 1.0, duration: 0.2)
self.removeIconNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2) self.removeIconNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2)
self.removeIconNode.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2) self.removeIconNode.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2)
} }

View File

@ -21,6 +21,7 @@ public final class TokenListTextField: Component {
public enum Content: Equatable { public enum Content: Equatable {
case peer(EnginePeer) case peer(EnginePeer)
case category(UIImage?) case category(UIImage?)
case emoji(String)
public static func ==(lhs: Content, rhs: Content) -> Bool { public static func ==(lhs: Content, rhs: Content) -> Bool {
switch lhs { switch lhs {
@ -36,6 +37,12 @@ public final class TokenListTextField: Component {
} else { } else {
return false return false
} }
case let .emoji(lhsEmoji):
if case let .emoji(rhsEmoji) = rhs, lhsEmoji == rhsEmoji {
return true
} else {
return false
}
} }
} }
} }
@ -217,6 +224,8 @@ public final class TokenListTextField: Component {
mappedSubject = .peer(peer) mappedSubject = .peer(peer)
case let .category(image): case let .category(image):
mappedSubject = .category(image) mappedSubject = .category(image)
case let .emoji(emoji):
mappedSubject = .emoji(emoji)
} }
return EditableTokenListToken( return EditableTokenListToken(

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

View File

@ -738,18 +738,19 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
strongSelf.bankCardDisposable = disposable strongSelf.bankCardDisposable = disposable
} }
var cancelImpl: (() -> Void)? // var cancelImpl: (() -> Void)?
let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } // let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }
let progressSignal = Signal<Never, NoError> { subscriber in let progressSignal = Signal<Never, NoError> { subscriber in
let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: { // let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: {
cancelImpl?() // cancelImpl?()
})) // }))
strongSelf.present(controller, in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) // strongSelf.present(controller, in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
return ActionDisposable { [weak controller] in // return ActionDisposable { [weak controller] in
Queue.mainQueue().async() { // Queue.mainQueue().async() {
controller?.dismiss() // controller?.dismiss()
} // }
} // }
return EmptyDisposable
} }
|> runOn(Queue.mainQueue()) |> runOn(Queue.mainQueue())
|> delay(0.15, queue: Queue.mainQueue()) |> delay(0.15, queue: Queue.mainQueue())
@ -761,14 +762,13 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
progressDisposable.dispose() progressDisposable.dispose()
} }
} }
cancelImpl = { // cancelImpl = {
disposable.set(nil) // disposable.set(nil)
} // }
disposable.set((signal disposable.set((signal
|> deliverOnMainQueue).startStrict(next: { [weak self] info in |> deliverOnMainQueue).startStrict(next: { [weak self] info in
if let strongSelf = self, let info = info { if let strongSelf = self, let info = info {
let date = stringForDate(timestamp: giveaway.untilDate, strings: strongSelf.presentationData.strings) let untilDate = stringForDate(timestamp: giveaway.untilDate, strings: strongSelf.presentationData.strings)
let startDate = stringForDate(timestamp: message.timestamp, strings: strongSelf.presentationData.strings)
let title: String let title: String
let text: String let text: String
@ -781,9 +781,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
})] })]
switch info { switch info {
case let .ongoing(status): case let .ongoing(start, status):
title = "About This Giveaway" let startDate = stringForDate(timestamp: start, strings: strongSelf.presentationData.strings)
title = "About This Giveaway"
let intro: String let intro: String
if case .almostOver = status { if case .almostOver = status {
@ -793,33 +794,17 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
} }
let ending: String let ending: String
if case .almostOver = status {
if giveaway.flags.contains(.onlyNewSubscribers) { if giveaway.flags.contains(.onlyNewSubscribers) {
if giveaway.channelPeerIds.count > 1 { if giveaway.channelPeerIds.count > 1 {
ending = "On **\(date)**, Telegram automatically selected **\(giveaway.quantity)** random users that joined **\(peerName)** and other listed channels after **\(startDate)**." 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 { } else {
ending = "On **\(date)**, Telegram automatically selected **\(giveaway.quantity)** random users that joined **\(peerName)** after **\(startDate)**." ending = "On **\(untilDate)**, Telegram will automatically select **\(giveaway.quantity)** random users that joined **\(peerName)** after **\(startDate)**."
} }
} else { } else {
if giveaway.channelPeerIds.count > 1 { if giveaway.channelPeerIds.count > 1 {
ending = "On **\(date)**, Telegram automatically selected **\(giveaway.quantity)** random subscribers of **\(peerName)** and other listed channels." ending = "On **\(untilDate)**, Telegram will automatically select **\(giveaway.quantity)** random subscribers of **\(peerName)** and **\(giveaway.channelPeerIds.count - 1)** other listed channels."
} else { } else {
ending = "On **\(date)**, Telegram automatically selected **\(giveaway.quantity)** random subscribers of **\(peerName)**." ending = "On **\(untilDate)**, Telegram will automatically select **\(giveaway.quantity)** random subscribers of **\(peerName)**."
}
}
} else {
if giveaway.flags.contains(.onlyNewSubscribers) {
if giveaway.channelPeerIds.count > 1 {
ending = "On **\(date)**, Telegram will automatically select **\(giveaway.quantity)** random users that joined **\(peerName)** and **\(giveaway.channelPeerIds.count - 1)** other listed channels after **\(startDate)**."
} else {
ending = "On **\(date)**, Telegram will automatically select **\(giveaway.quantity)** random users that joined **\(peerName)** after **\(startDate)**."
}
} else {
if giveaway.channelPeerIds.count > 1 {
ending = "On **\(date)**, Telegram will automatically select **\(giveaway.quantity)** random subscribers of **\(peerName)** and **\(giveaway.channelPeerIds.count - 1)** other listed channels."
} else {
ending = "On **\(date)**, Telegram will automatically select **\(giveaway.quantity)** random subscribers of **\(peerName)**."
}
} }
} }
@ -827,9 +812,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
switch status { switch status {
case .notQualified: case .notQualified:
if giveaway.channelPeerIds.count > 1 { 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 **\(date)**." participation = "To take part in this giveaway please join the channel **\(peerName)** (**\(giveaway.channelPeerIds.count - 1)** other listed channels) before **\(untilDate)**."
} else { } else {
participation = "To take part in this giveaway please join the channel **\(peerName)** before **\(date)**." participation = "To take part in this giveaway please join the channel **\(peerName)** before **\(untilDate)**."
} }
case let .notAllowed(reason): case let .notAllowed(reason):
switch reason { switch reason {
@ -839,6 +824,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
case let .channelAdmin(adminId): case let .channelAdmin(adminId):
let _ = adminId let _ = adminId
participation = "You are not eligible to participate in this giveaway, because you are an admin of participating channel (**\(peerName)**)." 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: case .participating:
if giveaway.channelPeerIds.count > 1 { if giveaway.channelPeerIds.count > 1 {
@ -855,8 +843,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
} }
text = "\(intro)\n\n\(ending)\(participation)" text = "\(intro)\n\n\(ending)\(participation)"
case let .finished(status, finishDate, _, activatedCount): case let .finished(status, start, finish, _, activatedCount):
let date = stringForDate(timestamp: finishDate, strings: strongSelf.presentationData.strings) let startDate = stringForDate(timestamp: start, strings: strongSelf.presentationData.strings)
let finishDate = stringForDate(timestamp: finish, strings: strongSelf.presentationData.strings)
title = "Giveaway Ended" 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." 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."
@ -864,15 +853,15 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
var ending: String var ending: String
if giveaway.flags.contains(.onlyNewSubscribers) { if giveaway.flags.contains(.onlyNewSubscribers) {
if giveaway.channelPeerIds.count > 1 { if giveaway.channelPeerIds.count > 1 {
ending = "On **\(date)**, Telegram automatically selected **\(giveaway.quantity)** random users that joined **\(peerName)** and other listed channels after **\(startDate)**." ending = "On **\(finishDate)**, Telegram automatically selected **\(giveaway.quantity)** random users that joined **\(peerName)** and other listed channels after **\(startDate)**."
} else { } else {
ending = "On **\(date)**, Telegram automatically selected **\(giveaway.quantity)** random users that joined **\(peerName)** after **\(startDate)**." ending = "On **\(finishDate)**, Telegram automatically selected **\(giveaway.quantity)** random users that joined **\(peerName)** after **\(startDate)**."
} }
} else { } else {
if giveaway.channelPeerIds.count > 1 { if giveaway.channelPeerIds.count > 1 {
ending = "On **\(date)**, Telegram automatically selected **\(giveaway.quantity)** random subscribers of **\(peerName)** and other listed channels." ending = "On **\(finishDate)**, Telegram automatically selected **\(giveaway.quantity)** random subscribers of **\(peerName)** and other listed channels."
} else { } else {
ending = "On **\(date)**, Telegram automatically selected **\(giveaway.quantity)** random subscribers of **\(peerName)**." ending = "On **\(finishDate)**, Telegram automatically selected **\(giveaway.quantity)** random subscribers of **\(peerName)**."
} }
} }

View File

@ -154,6 +154,9 @@ private func canEditMessage(accountPeerId: PeerId, limitsConfiguration: EngineCo
} else if let _ = media as? TelegramMediaStory { } else if let _ = media as? TelegramMediaStory {
hasUneditableAttributes = true hasUneditableAttributes = true
break break
} else if let _ = media as? TelegramMediaGiveaway {
hasUneditableAttributes = true
break
} }
} }

View File

@ -158,7 +158,12 @@ class ChatMessageActionBubbleContentNode: ChatMessageBubbleContentNode {
let cachedMaskBackgroundImage = self.cachedMaskBackgroundImage let cachedMaskBackgroundImage = self.cachedMaskBackgroundImage
return { item, layoutConstants, _, _, _, _ in return { item, layoutConstants, _, _, _, _ in
let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: true, headerSpacing: 0.0, hidesBackground: .always, forceFullCorners: false, forceAlignment: .center) var isGiveaway = false
if let _ = item.message.media.first(where: { $0 is TelegramMediaGiveaway }) {
isGiveaway = true
}
let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: true, headerSpacing: 0.0, hidesBackground: .always, forceFullCorners: false, forceAlignment: isGiveaway ? .none : .center, isDetached: isGiveaway)
let backgroundImage = PresentationResourcesChat.chatActionPhotoBackgroundImage(item.presentationData.theme.theme, wallpaper: !item.presentationData.theme.wallpaper.isEmpty) let backgroundImage = PresentationResourcesChat.chatActionPhotoBackgroundImage(item.presentationData.theme.theme, wallpaper: !item.presentationData.theme.wallpaper.isEmpty)
@ -230,6 +235,8 @@ class ChatMessageActionBubbleContentNode: ChatMessageBubbleContentNode {
if let _ = image { if let _ = image {
backgroundSize.height += imageSize.height + 10 backgroundSize.height += imageSize.height + 10
} else if isGiveaway {
backgroundSize.height += 8.0
} }
return (backgroundSize.width, { boundingWidth in return (backgroundSize.width, { boundingWidth in
@ -510,6 +517,10 @@ class ChatMessageActionBubbleContentNode: ChatMessageBubbleContentNode {
} }
override func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction { override func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction {
if let item = self.item, item.message.media.first(where: { $0 is TelegramMediaGiveaway }) != nil {
return .none
}
let textNodeFrame = self.labelNode.textNode.frame let textNodeFrame = self.labelNode.textNode.frame
if let (index, attributes) = self.labelNode.textNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY - 10.0)), gesture == .tap { if let (index, attributes) = self.labelNode.textNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY - 10.0)), gesture == .tap {
if let url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String { if let url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String {
@ -528,7 +539,7 @@ class ChatMessageActionBubbleContentNode: ChatMessageBubbleContentNode {
return .hashtag(hashtag.peerName, hashtag.hashtag) return .hashtag(hashtag.peerName, hashtag.hashtag)
} }
} }
if let imageNode = imageNode, imageNode.frame.contains(point) { if let imageNode = self.imageNode, imageNode.frame.contains(point) {
return .openMessage return .openMessage
} }

View File

@ -72,6 +72,7 @@ final class ChatMessageAttachedContentButtonNode: HighlightTrackingButtonNode {
self.shimmerEffectNode = ShimmerEffectForegroundNode() self.shimmerEffectNode = ShimmerEffectForegroundNode()
self.shimmerEffectNode.cornerRadius = 5.0 self.shimmerEffectNode.cornerRadius = 5.0
self.shimmerEffectNode.isHidden = true
self.backgroundNode = ASImageNode() self.backgroundNode = ASImageNode()
self.backgroundNode.isLayerBacked = true self.backgroundNode.isLayerBacked = true

View File

@ -2399,7 +2399,12 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
print("contentNodeWidth \(contentNodeWidth) > \(maximumNodeWidth)") print("contentNodeWidth \(contentNodeWidth) > \(maximumNodeWidth)")
} }
#endif #endif
if contentNodeProperties.isDetached {
} else {
maxContentWidth = max(maxContentWidth, contentNodeWidth) maxContentWidth = max(maxContentWidth, contentNodeWidth)
}
contentNodePropertiesAndFinalize.append((contentNodeProperties, contentPosition, contentNodeFinalize, contentGroupId, itemSelection)) contentNodePropertiesAndFinalize.append((contentNodeProperties, contentPosition, contentNodeFinalize, contentGroupId, itemSelection))
} }
@ -2414,6 +2419,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
var contentNodesHeight: CGFloat = 0.0 var contentNodesHeight: CGFloat = 0.0
var totalContentNodesHeight: CGFloat = 0.0 var totalContentNodesHeight: CGFloat = 0.0
var currentContainerGroupOverlap: CGFloat = 0.0 var currentContainerGroupOverlap: CGFloat = 0.0
var detachedContentNodesHeight: CGFloat = 0.0
var mosaicStatusOrigin: CGPoint? var mosaicStatusOrigin: CGPoint?
for i in 0 ..< contentNodePropertiesAndFinalize.count { for i in 0 ..< contentNodePropertiesAndFinalize.count {
@ -2475,11 +2481,16 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
currentItemSelection = itemSelection currentItemSelection = itemSelection
} }
let contentNodeOriginY = contentNodesHeight - detachedContentNodesHeight
let (size, apply) = finalize(maxContentWidth) let (size, apply) = finalize(maxContentWidth)
contentNodeFramesPropertiesAndApply.append((CGRect(origin: CGPoint(x: 0.0, y: contentNodesHeight), size: size), properties, contentGroupId == nil, apply)) contentNodeFramesPropertiesAndApply.append((CGRect(origin: CGPoint(x: 0.0, y: contentNodeOriginY), size: size), properties, contentGroupId == nil, apply))
contentNodesHeight += size.height contentNodesHeight += size.height
totalContentNodesHeight += size.height totalContentNodesHeight += size.height
if properties.isDetached {
detachedContentNodesHeight += size.height
}
} }
} }
@ -2517,24 +2528,23 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
minimalContentSize = layoutConstants.bubble.minimumSize minimalContentSize = layoutConstants.bubble.minimumSize
} }
let calculatedBubbleHeight = headerSize.height + contentSize.height + layoutConstants.bubble.contentInsets.top + layoutConstants.bubble.contentInsets.bottom let calculatedBubbleHeight = headerSize.height + contentSize.height + layoutConstants.bubble.contentInsets.top + layoutConstants.bubble.contentInsets.bottom
let layoutBubbleSize = CGSize(width: max(contentSize.width, headerSize.width) + layoutConstants.bubble.contentInsets.left + layoutConstants.bubble.contentInsets.right, height: max(minimalContentSize.height, calculatedBubbleHeight)) let layoutBubbleSize = CGSize(width: max(contentSize.width, headerSize.width) + layoutConstants.bubble.contentInsets.left + layoutConstants.bubble.contentInsets.right, height: max(minimalContentSize.height, calculatedBubbleHeight - detachedContentNodesHeight))
var contentVerticalOffset: CGFloat = 0.0 var contentVerticalOffset: CGFloat = 0.0
if minimalContentSize.height > calculatedBubbleHeight + 2.0 { if minimalContentSize.height > calculatedBubbleHeight + 2.0 {
contentVerticalOffset = floorToScreenPixels((minimalContentSize.height - calculatedBubbleHeight) / 2.0) contentVerticalOffset = floorToScreenPixels((minimalContentSize.height - calculatedBubbleHeight) / 2.0)
} }
let availableWidth = params.width - params.leftInset - params.rightInset
let backgroundFrame: CGRect let backgroundFrame: CGRect
let contentOrigin: CGPoint let contentOrigin: CGPoint
let contentUpperRightCorner: CGPoint let contentUpperRightCorner: CGPoint
switch alignment { switch alignment {
case .none: case .none:
backgroundFrame = CGRect(origin: CGPoint(x: incoming ? (params.leftInset + layoutConstants.bubble.edgeInset + avatarInset) : (params.width - params.rightInset - layoutBubbleSize.width - layoutConstants.bubble.edgeInset - deliveryFailedInset), y: 0.0), size: layoutBubbleSize) backgroundFrame = CGRect(origin: CGPoint(x: incoming ? (params.leftInset + layoutConstants.bubble.edgeInset + avatarInset) : (params.width - params.rightInset - layoutBubbleSize.width - layoutConstants.bubble.edgeInset - deliveryFailedInset), y: detachedContentNodesHeight), size: layoutBubbleSize)
contentOrigin = CGPoint(x: backgroundFrame.origin.x + (incoming ? layoutConstants.bubble.contentInsets.left : layoutConstants.bubble.contentInsets.right), y: backgroundFrame.origin.y + layoutConstants.bubble.contentInsets.top + headerSize.height + contentVerticalOffset) contentOrigin = CGPoint(x: backgroundFrame.origin.x + (incoming ? layoutConstants.bubble.contentInsets.left : layoutConstants.bubble.contentInsets.right), y: backgroundFrame.origin.y + layoutConstants.bubble.contentInsets.top + headerSize.height + contentVerticalOffset)
contentUpperRightCorner = CGPoint(x: backgroundFrame.maxX - (incoming ? layoutConstants.bubble.contentInsets.right : layoutConstants.bubble.contentInsets.left), y: backgroundFrame.origin.y + layoutConstants.bubble.contentInsets.top + headerSize.height) contentUpperRightCorner = CGPoint(x: backgroundFrame.maxX - (incoming ? layoutConstants.bubble.contentInsets.right : layoutConstants.bubble.contentInsets.left), y: backgroundFrame.origin.y + layoutConstants.bubble.contentInsets.top + headerSize.height)
case .center: case .center:
let availableWidth = params.width - params.leftInset - params.rightInset backgroundFrame = CGRect(origin: CGPoint(x: params.leftInset + floor((availableWidth - layoutBubbleSize.width) / 2.0), y: detachedContentNodesHeight), size: layoutBubbleSize)
backgroundFrame = CGRect(origin: CGPoint(x: params.leftInset + floor((availableWidth - layoutBubbleSize.width) / 2.0), y: 0.0), size: layoutBubbleSize)
let contentOriginX: CGFloat let contentOriginX: CGFloat
if !hideBackground { if !hideBackground {
contentOriginX = (incoming ? layoutConstants.bubble.contentInsets.left : layoutConstants.bubble.contentInsets.right) contentOriginX = (incoming ? layoutConstants.bubble.contentInsets.left : layoutConstants.bubble.contentInsets.right)
@ -2547,7 +2557,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
let bubbleContentWidth = maxContentWidth - layoutConstants.bubble.edgeInset * 2.0 - (layoutConstants.bubble.contentInsets.right + layoutConstants.bubble.contentInsets.left) let bubbleContentWidth = maxContentWidth - layoutConstants.bubble.edgeInset * 2.0 - (layoutConstants.bubble.contentInsets.right + layoutConstants.bubble.contentInsets.left)
var layoutSize = CGSize(width: params.width, height: layoutBubbleSize.height) var layoutSize = CGSize(width: params.width, height: layoutBubbleSize.height + detachedContentNodesHeight)
if let reactionButtonsSizeAndApply = reactionButtonsSizeAndApply { if let reactionButtonsSizeAndApply = reactionButtonsSizeAndApply {
layoutSize.height += 4.0 + reactionButtonsSizeAndApply.0.height layoutSize.height += 4.0 + reactionButtonsSizeAndApply.0.height
} }
@ -3203,6 +3213,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
} }
containerSupernode.addSubnode(contentNode) containerSupernode.addSubnode(contentNode)
contentNode.itemNode = strongSelf
contentNode.bubbleBackgroundNode = strongSelf.backgroundNode contentNode.bubbleBackgroundNode = strongSelf.backgroundNode
contentNode.bubbleBackdropNode = strongSelf.backgroundWallpaperNode contentNode.bubbleBackdropNode = strongSelf.backgroundWallpaperNode
contentNode.updateIsTextSelectionActive = { [weak contextSourceNode] value in contentNode.updateIsTextSelectionActive = { [weak contextSourceNode] value in
@ -3239,7 +3250,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
var shouldClipOnTransitions = true var shouldClipOnTransitions = true
var contentNodeIndex = 0 var contentNodeIndex = 0
for (relativeFrame, _, useContentOrigin, apply) in contentNodeFramesPropertiesAndApply { for (relativeFrame, properties, useContentOrigin, apply) in contentNodeFramesPropertiesAndApply {
apply(animation, synchronousLoads, applyInfo) apply(animation, synchronousLoads, applyInfo)
if contentNodeIndex >= strongSelf.contentNodes.count { if contentNodeIndex >= strongSelf.contentNodes.count {
@ -3252,7 +3263,14 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
shouldClipOnTransitions = false shouldClipOnTransitions = false
} }
let contentNodeFrame = relativeFrame.offsetBy(dx: contentOrigin.x, dy: useContentOrigin ? contentOrigin.y : 0.0) var effectiveContentOriginX = contentOrigin.x
var effectiveContentOriginY = useContentOrigin ? contentOrigin.y : 0.0
if properties.isDetached {
effectiveContentOriginX = floorToScreenPixels((layout.size.width - relativeFrame.width) / 2.0)
effectiveContentOriginY = 0.0
}
let contentNodeFrame = relativeFrame.offsetBy(dx: effectiveContentOriginX, dy: effectiveContentOriginY)
let previousContentNodeFrame = contentNode.frame let previousContentNodeFrame = contentNode.frame
if case let .System(duration, _) = animation { if case let .System(duration, _) = animation {

View File

@ -39,6 +39,7 @@ class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
private let placeholderNode: StickerShimmerEffectNode private let placeholderNode: StickerShimmerEffectNode
private let animationNode: AnimatedStickerNode private let animationNode: AnimatedStickerNode
private let shimmerEffectNode: ShimmerEffectForegroundNode
private let buttonNode: HighlightTrackingButtonNode private let buttonNode: HighlightTrackingButtonNode
private let buttonStarsNode: PremiumStarsNode private let buttonStarsNode: PremiumStarsNode
private let buttonTitleNode: TextNode private let buttonTitleNode: TextNode
@ -94,6 +95,9 @@ class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
self.buttonNode.clipsToBounds = true self.buttonNode.clipsToBounds = true
self.buttonNode.cornerRadius = 17.0 self.buttonNode.cornerRadius = 17.0
self.shimmerEffectNode = ShimmerEffectForegroundNode()
self.shimmerEffectNode.cornerRadius = 17.0
self.placeholderNode = StickerShimmerEffectNode() self.placeholderNode = StickerShimmerEffectNode()
self.placeholderNode.isUserInteractionEnabled = false self.placeholderNode.isUserInteractionEnabled = false
self.placeholderNode.alpha = 0.75 self.placeholderNode.alpha = 0.75
@ -116,6 +120,7 @@ class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
self.addSubnode(self.animationNode) self.addSubnode(self.animationNode)
self.addSubnode(self.buttonNode) self.addSubnode(self.buttonNode)
self.buttonNode.addSubnode(self.shimmerEffectNode)
self.buttonNode.addSubnode(self.buttonStarsNode) self.buttonNode.addSubnode(self.buttonStarsNode)
self.addSubnode(self.buttonTitleNode) self.addSubnode(self.buttonTitleNode)
@ -151,6 +156,26 @@ class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
return return
} }
let _ = item.controllerInteraction.openMessage(item.message, .default) let _ = item.controllerInteraction.openMessage(item.message, .default)
self.startShimmering()
Queue.mainQueue().after(0.75) {
self.stopShimmering()
}
}
func startShimmering() {
self.shimmerEffectNode.isHidden = false
self.shimmerEffectNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
let backgroundFrame = self.buttonNode.frame
self.shimmerEffectNode.frame = CGRect(origin: .zero, size: backgroundFrame.size)
self.shimmerEffectNode.updateAbsoluteRect(CGRect(origin: .zero, size: backgroundFrame.size), within: backgroundFrame.size)
self.shimmerEffectNode.update(backgroundColor: .clear, foregroundColor: UIColor.white.withAlphaComponent(0.2), horizontal: true, effectSize: nil, globalTimeOffset: false, duration: nil)
}
func stopShimmering() {
self.shimmerEffectNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, completion: { [weak self] _ in
self?.shimmerEffectNode.isHidden = true
})
} }
private func removePlaceholder(animated: Bool) { private func removePlaceholder(animated: Bool) {
@ -500,7 +525,9 @@ class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
} }
} }
if let backgroundNode = self.backgroundNode, backgroundNode.frame.contains(point) { if self.buttonNode.frame.contains(point) {
return .ignore
} else if let backgroundNode = self.backgroundNode, backgroundNode.frame.contains(point) {
return .openMessage return .openMessage
} else if self.mediaBackgroundNode.frame.contains(point) { } else if self.mediaBackgroundNode.frame.contains(point) {
return .openMessage return .openMessage
@ -546,6 +573,12 @@ class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
if !item.controllerInteraction.seenOneTimeAnimatedMedia.contains(item.message.id) { if !item.controllerInteraction.seenOneTimeAnimatedMedia.contains(item.message.id) {
item.controllerInteraction.seenOneTimeAnimatedMedia.insert(item.message.id) item.controllerInteraction.seenOneTimeAnimatedMedia.insert(item.message.id)
self.animationNode.playOnce() self.animationNode.playOnce()
Queue.mainQueue().after(0.05) {
if let itemNode = self.itemNode, let supernode = itemNode.supernode {
supernode.addSubnode(itemNode)
}
}
} }
if !alreadySeen && self.animationNode.isPlaying { if !alreadySeen && self.animationNode.isPlaying {

View File

@ -18,6 +18,7 @@ import AvatarNode
import ChatMessageDateAndStatusNode import ChatMessageDateAndStatusNode
import ChatMessageBubbleContentNode import ChatMessageBubbleContentNode
import ChatMessageItemCommon import ChatMessageItemCommon
import UndoUI
private let titleFont = Font.medium(15.0) private let titleFont = Font.medium(15.0)
private let textFont = Font.regular(13.0) private let textFont = Font.regular(13.0)
@ -35,6 +36,8 @@ class ChatMessageGiveawayBubbleContentNode: ChatMessageBubbleContentNode {
private let participantsTitleNode: TextNode private let participantsTitleNode: TextNode
private let participantsTextNode: TextNode private let participantsTextNode: TextNode
private let countriesTextNode: TextNode
private let dateTitleNode: TextNode private let dateTitleNode: TextNode
private let dateTextNode: TextNode private let dateTextNode: TextNode
@ -81,6 +84,8 @@ class ChatMessageGiveawayBubbleContentNode: ChatMessageBubbleContentNode {
self.participantsTitleNode = TextNode() self.participantsTitleNode = TextNode()
self.participantsTextNode = TextNode() self.participantsTextNode = TextNode()
self.countriesTextNode = TextNode()
self.dateTitleNode = TextNode() self.dateTitleNode = TextNode()
self.dateTextNode = TextNode() self.dateTextNode = TextNode()
@ -98,6 +103,7 @@ class ChatMessageGiveawayBubbleContentNode: ChatMessageBubbleContentNode {
self.addSubnode(self.prizeTextNode) self.addSubnode(self.prizeTextNode)
self.addSubnode(self.participantsTitleNode) self.addSubnode(self.participantsTitleNode)
self.addSubnode(self.participantsTextNode) self.addSubnode(self.participantsTextNode)
self.addSubnode(self.countriesTextNode)
self.addSubnode(self.dateTitleNode) self.addSubnode(self.dateTitleNode)
self.addSubnode(self.dateTextNode) self.addSubnode(self.dateTextNode)
self.addSubnode(self.buttonNode) self.addSubnode(self.buttonNode)
@ -137,8 +143,18 @@ class ChatMessageGiveawayBubbleContentNode: ChatMessageBubbleContentNode {
override func didLoad() { override func didLoad() {
super.didLoad() super.didLoad()
// let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.contactTap(_:))) let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.bubbleTap(_:)))
// self.view.addGestureRecognizer(tapRecognizer) self.view.addGestureRecognizer(tapRecognizer)
}
@objc private func bubbleTap(_ gestureRecognizer: UITapGestureRecognizer) {
guard let item = self.item else {
return
}
let presentationData = item.context.sharedContext.currentPresentationData.with { $0 }
let controller = UndoOverlayController(presentationData: presentationData, content: .info(title: nil, text: "You can't participate in this giveaway.", timeout: nil), elevatedLayout: false, position: .bottom, animateInAsReplacement: false, action: { _ in return false })
item.controllerInteraction.presentController(controller, nil)
} }
private func removePlaceholder(animated: Bool) { private func removePlaceholder(animated: Bool) {
@ -160,6 +176,8 @@ class ChatMessageGiveawayBubbleContentNode: ChatMessageBubbleContentNode {
let makeParticipantsTitleLayout = TextNode.asyncLayout(self.participantsTitleNode) let makeParticipantsTitleLayout = TextNode.asyncLayout(self.participantsTitleNode)
let makeParticipantsTextLayout = TextNode.asyncLayout(self.participantsTextNode) let makeParticipantsTextLayout = TextNode.asyncLayout(self.participantsTextNode)
let makeCountriesTextLayout = TextNode.asyncLayout(self.countriesTextNode)
let makeDateTitleLayout = TextNode.asyncLayout(self.dateTitleNode) let makeDateTitleLayout = TextNode.asyncLayout(self.dateTitleNode)
let makeDateTextLayout = TextNode.asyncLayout(self.dateTextNode) let makeDateTextLayout = TextNode.asyncLayout(self.dateTextNode)
@ -215,12 +233,14 @@ class ChatMessageGiveawayBubbleContentNode: ChatMessageBubbleContentNode {
let participantsTitleString = NSAttributedString(string: "Participants", font: titleFont, textColor: textColor) let participantsTitleString = NSAttributedString(string: "Participants", font: titleFont, textColor: textColor)
let participantsText: String let participantsText: String
let countriesText: String
if let giveaway { if let giveaway {
if giveaway.flags.contains(.onlyNewSubscribers) { if giveaway.flags.contains(.onlyNewSubscribers) {
if giveaway.channelPeerIds.count > 1 { if giveaway.channelPeerIds.count > 1 {
participantsText = "All users who join the channels below after this date:" participantsText = "All users who join the channels below after this date:"
} else { } else {
participantsText = "All users who join this channel below after this date:" participantsText = "All users who join this channel after this date:"
} }
} else { } else {
if giveaway.channelPeerIds.count > 1 { if giveaway.channelPeerIds.count > 1 {
@ -229,12 +249,41 @@ class ChatMessageGiveawayBubbleContentNode: ChatMessageBubbleContentNode {
participantsText = "All subscribers of this channel:" participantsText = "All subscribers of this channel:"
} }
} }
if !giveaway.countries.isEmpty {
let locale = localeWithStrings(item.presentationData.strings)
let countryNames = giveaway.countries.map { id in
if let countryName = locale.localizedString(forRegionCode: id) {
return "\(flagEmoji(countryCode: id))\(countryName)"
} else {
return id
}
}
var countries: String = ""
if countryNames.count == 1, let country = countryNames.first {
countries = country
} else {
for i in 0 ..< countryNames.count {
countries.append(countryNames[i])
if i == countryNames.count - 2 {
countries.append(" and ")
} else if i < countryNames.count - 2 {
countries.append(", ")
}
}
}
countriesText = "from \(countries)"
} else {
countriesText = ""
}
} else { } else {
participantsText = "" participantsText = ""
countriesText = ""
} }
let participantsTextString = NSAttributedString(string: participantsText, font: textFont, textColor: textColor) let participantsTextString = NSAttributedString(string: participantsText, font: textFont, textColor: textColor)
let countriesTextString = NSAttributedString(string: countriesText, font: textFont, textColor: textColor)
let dateTitleString = NSAttributedString(string: "Winners Selection Date", font: titleFont, textColor: textColor) let dateTitleString = NSAttributedString(string: "Winners Selection Date", font: titleFont, textColor: textColor)
var dateTextString: NSAttributedString? var dateTextString: NSAttributedString?
if let giveaway { if let giveaway {
@ -255,6 +304,8 @@ class ChatMessageGiveawayBubbleContentNode: ChatMessageBubbleContentNode {
let (participantsTitleLayout, participantsTitleApply) = makeParticipantsTitleLayout(TextNodeLayoutArguments(attributedString: participantsTitleString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: maxTextWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets())) let (participantsTitleLayout, participantsTitleApply) = makeParticipantsTitleLayout(TextNodeLayoutArguments(attributedString: participantsTitleString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: maxTextWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
let (participantsTextLayout, participantsTextApply) = makeParticipantsTextLayout(TextNodeLayoutArguments(attributedString: participantsTextString, backgroundColor: nil, maximumNumberOfLines: 5, truncationType: .end, constrainedSize: CGSize(width: maxTextWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets())) let (participantsTextLayout, participantsTextApply) = makeParticipantsTextLayout(TextNodeLayoutArguments(attributedString: participantsTextString, backgroundColor: nil, maximumNumberOfLines: 5, truncationType: .end, constrainedSize: CGSize(width: maxTextWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
let (countriesTextLayout, countriesTextApply) = makeCountriesTextLayout(TextNodeLayoutArguments(attributedString: countriesTextString, backgroundColor: nil, maximumNumberOfLines: 5, truncationType: .end, constrainedSize: CGSize(width: maxTextWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
let (dateTitleLayout, dateTitleApply) = makeDateTitleLayout(TextNodeLayoutArguments(attributedString: dateTitleString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: maxTextWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets())) let (dateTitleLayout, dateTitleApply) = makeDateTitleLayout(TextNodeLayoutArguments(attributedString: dateTitleString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: maxTextWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
let (dateTextLayout, dateTextApply) = makeDateTextLayout(TextNodeLayoutArguments(attributedString: dateTextString, backgroundColor: nil, maximumNumberOfLines: 5, truncationType: .end, constrainedSize: CGSize(width: maxTextWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets())) let (dateTextLayout, dateTextApply) = makeDateTextLayout(TextNodeLayoutArguments(attributedString: dateTextString, backgroundColor: nil, maximumNumberOfLines: 5, truncationType: .end, constrainedSize: CGSize(width: maxTextWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
@ -395,6 +446,9 @@ class ChatMessageGiveawayBubbleContentNode: ChatMessageBubbleContentNode {
var layoutSize = CGSize(width: contentWidth, height: 49.0 + prizeTitleLayout.size.height + prizeTextLayout.size.height + participantsTitleLayout.size.height + participantsTextLayout.size.height + dateTitleLayout.size.height + dateTextLayout.size.height + buttonSize.height + buttonSpacing + 120.0) var layoutSize = CGSize(width: contentWidth, height: 49.0 + prizeTitleLayout.size.height + prizeTextLayout.size.height + participantsTitleLayout.size.height + participantsTextLayout.size.height + dateTitleLayout.size.height + dateTextLayout.size.height + buttonSize.height + buttonSpacing + 120.0)
if countriesTextLayout.size.height > 0.0 {
layoutSize.height += countriesTextLayout.size.height + 7.0
}
layoutSize.height += channelButtonSize.height layoutSize.height += channelButtonSize.height
if let statusSizeAndApply = statusSizeAndApply { if let statusSizeAndApply = statusSizeAndApply {
@ -414,16 +468,13 @@ class ChatMessageGiveawayBubbleContentNode: ChatMessageBubbleContentNode {
strongSelf.updateVisibility() strongSelf.updateVisibility()
let _ = badgeTextApply() let _ = badgeTextApply()
let _ = prizeTitleApply() let _ = prizeTitleApply()
let _ = prizeTextApply() let _ = prizeTextApply()
let _ = participantsTitleApply() let _ = participantsTitleApply()
let _ = participantsTextApply() let _ = participantsTextApply()
let _ = countriesTextApply()
let _ = dateTitleApply() let _ = dateTitleApply()
let _ = dateTextApply() let _ = dateTextApply()
let _ = channelButtonApply() let _ = channelButtonApply()
let _ = buttonApply() let _ = buttonApply()
@ -456,7 +507,14 @@ class ChatMessageGiveawayBubbleContentNode: ChatMessageBubbleContentNode {
originY += participantsTextLayout.size.height + smallSpacing * 2.0 + 3.0 originY += participantsTextLayout.size.height + smallSpacing * 2.0 + 3.0
strongSelf.channelButton.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((layoutSize.width - channelButtonSize.width) / 2.0), y: originY), size: channelButtonSize) strongSelf.channelButton.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((layoutSize.width - channelButtonSize.width) / 2.0), y: originY), size: channelButtonSize)
originY += channelButtonSize.height + largeSpacing originY += channelButtonSize.height
if countriesTextLayout.size.height > 0.0 {
originY += smallSpacing * 2.0 + 3.0
strongSelf.countriesTextNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((layoutSize.width - countriesTextLayout.size.width) / 2.0), y: originY), size: countriesTextLayout.size)
originY += countriesTextLayout.size.height
}
originY += largeSpacing
strongSelf.dateTitleNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((layoutSize.width - dateTitleLayout.size.width) / 2.0), y: originY), size: dateTitleLayout.size) strongSelf.dateTitleNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((layoutSize.width - dateTitleLayout.size.width) / 2.0), y: originY), size: dateTitleLayout.size)
originY += dateTitleLayout.size.height + smallSpacing originY += dateTitleLayout.size.height + smallSpacing
@ -522,7 +580,7 @@ class ChatMessageGiveawayBubbleContentNode: ChatMessageBubbleContentNode {
override func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction { override func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction {
if self.buttonNode.frame.contains(point) { if self.buttonNode.frame.contains(point) {
return .openMessage return .ignore
} }
if self.dateAndStatusNode.supernode != nil, let _ = self.dateAndStatusNode.hitTest(self.view.convert(point, to: self.dateAndStatusNode.view), with: nil) { if self.dateAndStatusNode.supernode != nil, let _ = self.dateAndStatusNode.hitTest(self.view.convert(point, to: self.dateAndStatusNode.view), with: nil) {
return .ignore return .ignore
@ -530,17 +588,13 @@ class ChatMessageGiveawayBubbleContentNode: ChatMessageBubbleContentNode {
return .none return .none
} }
@objc func contactTap(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state {
if let item = self.item {
let _ = item.controllerInteraction.openMessage(item.message, .default)
}
}
}
@objc private func buttonPressed() { @objc private func buttonPressed() {
if let item = self.item { if let item = self.item {
let _ = item.controllerInteraction.openMessage(item.message, .default) let _ = item.controllerInteraction.openMessage(item.message, .default)
self.buttonNode.startShimmering()
Queue.mainQueue().after(0.75) {
self.buttonNode.stopShimmering()
}
} }
} }