Various improvements
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 27 KiB |
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 44 KiB |
@ -10110,3 +10110,13 @@ Sorry for the inconvenience.";
|
||||
"Appearance.AppIconSteam" = "Steam";
|
||||
|
||||
"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.";
|
||||
|
@ -60,7 +60,7 @@ private func loadCountryCodes() -> [(String, Int)] {
|
||||
|
||||
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)
|
||||
var result: [((String, String), String, [Int])] = []
|
||||
for country in AuthorizationSequenceCountrySelectionController.countries() {
|
||||
@ -159,7 +159,7 @@ private func matchStringTokens(_ tokens: [Data], with other: [Data]) -> Bool {
|
||||
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())
|
||||
|
||||
var result: [((String, String), String, Int)] = []
|
||||
|
@ -1,5 +1,6 @@
|
||||
import Foundation
|
||||
import AppBundle
|
||||
import TelegramStringFormatting
|
||||
|
||||
public func emojiFlagForISOCountryCode(_ countryCode: String) -> String {
|
||||
if countryCode.count != 2 {
|
||||
@ -18,12 +19,7 @@ public func emojiFlagForISOCountryCode(_ countryCode: String) -> String {
|
||||
return ""
|
||||
}
|
||||
|
||||
let base : UInt32 = 127397
|
||||
var s = ""
|
||||
for v in countryCode.unicodeScalars {
|
||||
s.unicodeScalars.append(UnicodeScalar(base + v.value)!)
|
||||
}
|
||||
return String(s)
|
||||
return flagEmoji(countryCode: countryCode)
|
||||
}
|
||||
|
||||
private func loadCountriesInfo() -> [(Int, String, String)] {
|
||||
|
@ -9,6 +9,7 @@ import ItemListUI
|
||||
import LocationResources
|
||||
import AppBundle
|
||||
import LiveLocationTimerNode
|
||||
import TelegramStringFormatting
|
||||
|
||||
public enum LocationActionListItemIcon: Equatable {
|
||||
case location
|
||||
@ -280,14 +281,6 @@ final class LocationActionListItemNode: ListViewItemNode {
|
||||
strongSelf.iconNode.isHidden = true
|
||||
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
|
||||
var flag: String?
|
||||
if let venue = venue.venue, venue.provider == "city", let countryCode = venue.id {
|
||||
|
@ -106,6 +106,7 @@ swift_library(
|
||||
"//submodules/TelegramUI/Components/ShareWithPeersScreen",
|
||||
"//submodules/TelegramUI/Components/ButtonComponent",
|
||||
"//submodules/TelegramUI/Components/Utils/RoundedRectWithTailPath",
|
||||
"//submodules/CountrySelectionUI",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -38,6 +38,7 @@ final class AppIconsDemoComponent: Component {
|
||||
private var component: AppIconsDemoComponent?
|
||||
|
||||
private var containerView: UIView
|
||||
private var axisView = UIView()
|
||||
private var imageViews: [UIImageView] = []
|
||||
|
||||
private var isVisible = false
|
||||
@ -49,6 +50,7 @@ final class AppIconsDemoComponent: Component {
|
||||
super.init(frame: frame)
|
||||
|
||||
self.addSubview(self.containerView)
|
||||
self.containerView.addSubview(self.axisView)
|
||||
}
|
||||
|
||||
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.axisView.bounds = CGRect(origin: .zero, size: availableSize)
|
||||
self.axisView.center = CGPoint(x: availableSize.width, y: availableSize.height / 2.0)
|
||||
|
||||
if self.imageViews.isEmpty {
|
||||
var i = 0
|
||||
for icon in component.appIcons {
|
||||
let image: UIImage?
|
||||
switch icon.imageName {
|
||||
@ -89,31 +95,37 @@ final class AppIconsDemoComponent: Component {
|
||||
imageView.layer.cornerCurve = .continuous
|
||||
}
|
||||
imageView.image = image
|
||||
self.containerView.addSubview(imageView)
|
||||
if i == 0 {
|
||||
self.containerView.addSubview(imageView)
|
||||
} else {
|
||||
self.axisView.addSubview(imageView)
|
||||
}
|
||||
|
||||
self.imageViews.append(imageView)
|
||||
|
||||
i += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let radius: CGFloat = availableSize.width * 0.33
|
||||
let angleIncrement: CGFloat = 2 * .pi / CGFloat(self.imageViews.count - 1)
|
||||
|
||||
var i = 0
|
||||
for view in self.imageViews {
|
||||
let position: CGPoint
|
||||
switch i {
|
||||
case 0:
|
||||
position = CGPoint(x: availableSize.width * 0.5, y: availableSize.height * 0.333)
|
||||
case 1:
|
||||
position = CGPoint(x: availableSize.width * 0.333, y: availableSize.height * 0.667)
|
||||
case 2:
|
||||
position = CGPoint(x: availableSize.width * 0.667, y: availableSize.height * 0.667)
|
||||
default:
|
||||
position = CGPoint(x: availableSize.width * 0.5, y: availableSize.height * 0.5)
|
||||
}
|
||||
|
||||
if !self.animating {
|
||||
view.center = position.offsetBy(dx: availableSize.width / 2.0, dy: 0.0)
|
||||
if i == 0 {
|
||||
position = CGPoint(x: availableSize.width, y: availableSize.height / 2.0)
|
||||
} else {
|
||||
let angle = CGFloat(i - 1) * angleIncrement
|
||||
let xPosition = radius * cos(angle) + availableSize.width / 2.0
|
||||
let yPosition = radius * sin(angle) + availableSize.height / 2.0
|
||||
|
||||
position = CGPoint(x: xPosition, y: yPosition)
|
||||
}
|
||||
|
||||
view.center = position
|
||||
|
||||
i += 1
|
||||
}
|
||||
|
||||
@ -131,6 +143,48 @@ final class AppIconsDemoComponent: Component {
|
||||
}
|
||||
self.isVisible = isDisplaying
|
||||
|
||||
let rotationDuration: Double = 12.0
|
||||
if isDisplaying {
|
||||
if self.axisView.layer.animation(forKey: "rotationAnimation") == nil {
|
||||
let rotationAnimation = CABasicAnimation(keyPath: "transform.rotation")
|
||||
rotationAnimation.fromValue = 0.0
|
||||
rotationAnimation.toValue = 2.0 * CGFloat.pi
|
||||
rotationAnimation.duration = rotationDuration
|
||||
rotationAnimation.repeatCount = Float.infinity
|
||||
self.axisView.layer.add(rotationAnimation, forKey: "rotationAnimation")
|
||||
|
||||
var i = 0
|
||||
for view in self.imageViews {
|
||||
if i == 0 {
|
||||
let animation = CABasicAnimation(keyPath: "transform.scale")
|
||||
animation.duration = 2.0
|
||||
animation.fromValue = 1.0
|
||||
animation.toValue = 1.15
|
||||
animation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
|
||||
animation.autoreverses = true
|
||||
animation.repeatCount = .infinity
|
||||
view.layer.add(animation, forKey: "scale")
|
||||
} else {
|
||||
view.transform = CGAffineTransformMakeScale(0.8, 0.8)
|
||||
|
||||
let rotationAnimation = CABasicAnimation(keyPath: "transform.rotation")
|
||||
rotationAnimation.fromValue = 0.0
|
||||
rotationAnimation.toValue = -2.0 * CGFloat.pi
|
||||
rotationAnimation.duration = rotationDuration
|
||||
rotationAnimation.repeatCount = Float.infinity
|
||||
view.layer.add(rotationAnimation, forKey: "rotationAnimation")
|
||||
}
|
||||
|
||||
i += 1
|
||||
}
|
||||
}
|
||||
} else {
|
||||
self.axisView.layer.removeAllAnimations()
|
||||
for view in self.imageViews {
|
||||
view.layer.removeAllAnimations()
|
||||
}
|
||||
}
|
||||
|
||||
return availableSize
|
||||
}
|
||||
|
||||
@ -138,38 +192,37 @@ final class AppIconsDemoComponent: Component {
|
||||
func animateIn(availableSize: CGSize) {
|
||||
self.animating = true
|
||||
|
||||
let radius: CGFloat = availableSize.width * 2.5
|
||||
let angleIncrement: CGFloat = 2 * .pi / CGFloat(self.imageViews.count - 1)
|
||||
|
||||
var i = 0
|
||||
for view in self.imageViews {
|
||||
let from: CGPoint
|
||||
let delay: Double
|
||||
switch i {
|
||||
case 0:
|
||||
from = CGPoint(x: -availableSize.width * 0.333, y: -availableSize.height * 0.8)
|
||||
delay = 0.1
|
||||
case 1:
|
||||
from = CGPoint(x: -availableSize.width * 0.55, y: availableSize.height * 0.75)
|
||||
delay = 0.15
|
||||
case 2:
|
||||
from = CGPoint(x: availableSize.width * 0.9, y: availableSize.height * 0.75)
|
||||
delay = 0.0
|
||||
default:
|
||||
from = CGPoint(x: availableSize.width * 0.5, y: availableSize.height * 0.5)
|
||||
delay = 0.0
|
||||
}
|
||||
|
||||
let initialPosition = view.layer.position
|
||||
view.layer.position = initialPosition.offsetBy(dx: from.x, dy: from.y)
|
||||
|
||||
Queue.mainQueue().after(delay) {
|
||||
view.layer.position = initialPosition
|
||||
view.layer.animateScale(from: 3.0, to: 1.0, duration: 0.5, delay: 0.0, timingFunction: kCAMediaTimingFunctionSpring)
|
||||
view.layer.animatePosition(from: from, to: CGPoint(), duration: 0.5, delay: 0.0, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
|
||||
if i > 0 {
|
||||
let delay: Double = 0.033 * Double(i - 1)
|
||||
|
||||
if i == 2 {
|
||||
self.animating = false
|
||||
let angle = CGFloat(i - 1) * angleIncrement
|
||||
let xPosition = radius * cos(angle)
|
||||
let yPosition = radius * sin(angle)
|
||||
|
||||
let from = CGPoint(x: xPosition, y: yPosition)
|
||||
let initialPosition = view.layer.position
|
||||
view.layer.position = initialPosition.offsetBy(dx: xPosition, dy: yPosition)
|
||||
view.alpha = 0.0
|
||||
|
||||
Queue.mainQueue().after(delay) {
|
||||
view.alpha = 1.0
|
||||
view.layer.position = initialPosition
|
||||
view.layer.animateScale(from: 3.0, to: 0.8, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring)
|
||||
view.layer.animatePosition(from: from, to: CGPoint(), duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
|
||||
view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15)
|
||||
|
||||
if i == self.imageViews.count - 1 {
|
||||
self.animating = false
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
}
|
||||
|
||||
i += 1
|
||||
}
|
||||
}
|
||||
|
@ -19,6 +19,7 @@ import ItemListPeerActionItem
|
||||
import ShareWithPeersScreen
|
||||
import InAppPurchaseManager
|
||||
import UndoUI
|
||||
import CountrySelectionUI
|
||||
|
||||
private final class CreateGiveawayControllerArguments {
|
||||
let context: AccountContext
|
||||
@ -26,17 +27,23 @@ private final class CreateGiveawayControllerArguments {
|
||||
let dismissInput: () -> Void
|
||||
let openPeersSelection: () -> Void
|
||||
let openChannelsSelection: () -> Void
|
||||
let openCountriesSelection: () -> Void
|
||||
let openPremiumIntro: () -> 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.updateState = updateState
|
||||
self.dismissInput = dismissInput
|
||||
self.openPeersSelection = openPeersSelection
|
||||
self.openChannelsSelection = openChannelsSelection
|
||||
self.openCountriesSelection = openCountriesSelection
|
||||
self.openPremiumIntro = openPremiumIntro
|
||||
self.scrollToDate = scrollToDate
|
||||
self.setItemIdWithRevealedOptions = setItemIdWithRevealedOptions
|
||||
self.removeChannel = removeChannel
|
||||
}
|
||||
}
|
||||
|
||||
@ -76,13 +83,13 @@ private enum CreateGiveawayEntry: ItemListNodeEntry {
|
||||
case subscriptionsInfo(PresentationTheme, String)
|
||||
|
||||
case channelsHeader(PresentationTheme, String)
|
||||
case channel(Int32, PresentationTheme, EnginePeer, Int32?)
|
||||
case channel(Int32, PresentationTheme, EnginePeer, Int32?, Bool)
|
||||
case channelAdd(PresentationTheme, String)
|
||||
case channelsInfo(PresentationTheme, String)
|
||||
|
||||
case usersHeader(PresentationTheme, String)
|
||||
case usersAll(PresentationTheme, String, Bool)
|
||||
case usersNew(PresentationTheme, String, Bool)
|
||||
case usersAll(PresentationTheme, String, String, Bool)
|
||||
case usersNew(PresentationTheme, String, String, Bool)
|
||||
case usersInfo(PresentationTheme, String)
|
||||
|
||||
case timeHeader(PresentationTheme, String)
|
||||
@ -133,7 +140,7 @@ private enum CreateGiveawayEntry: ItemListNodeEntry {
|
||||
return 6
|
||||
case .channelsHeader:
|
||||
return 7
|
||||
case let .channel(index, _, _, _):
|
||||
case let .channel(index, _, _, _, _):
|
||||
return 8 + index
|
||||
case .channelAdd:
|
||||
return 100
|
||||
@ -220,8 +227,8 @@ private enum CreateGiveawayEntry: ItemListNodeEntry {
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .channel(lhsIndex, lhsTheme, lhsPeer, lhsBoosts):
|
||||
if case let .channel(rhsIndex, rhsTheme, rhsPeer, rhsBoosts) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsPeer == rhsPeer, lhsBoosts == rhsBoosts {
|
||||
case let .channel(lhsIndex, lhsTheme, lhsPeer, lhsBoosts, lhsIsRevealed):
|
||||
if case let .channel(rhsIndex, rhsTheme, rhsPeer, rhsBoosts, rhsIsRevealed) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsPeer == rhsPeer, lhsBoosts == rhsBoosts, lhsIsRevealed == rhsIsRevealed {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
@ -244,14 +251,14 @@ private enum CreateGiveawayEntry: ItemListNodeEntry {
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .usersAll(lhsTheme, lhsText, lhsSelected):
|
||||
if case let .usersAll(rhsTheme, rhsText, rhsSelected) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsSelected == rhsSelected {
|
||||
case let .usersAll(lhsTheme, lhsText, lhsSubtitle, lhsSelected):
|
||||
if case let .usersAll(rhsTheme, rhsText, rhsSubtitle, rhsSelected) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsSubtitle == rhsSubtitle, lhsSelected == rhsSelected {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .usersNew(lhsTheme, lhsText, lhsSelected):
|
||||
if case let .usersNew(rhsTheme, rhsText, rhsSelected) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsSelected == rhsSelected {
|
||||
case let .usersNew(lhsTheme, lhsText, lhsSubtitle, lhsSelected):
|
||||
if case let .usersNew(rhsTheme, rhsText, rhsSubtitle, rhsSelected) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsSubtitle == rhsSubtitle, lhsSelected == rhsSelected {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
@ -369,10 +376,14 @@ private enum CreateGiveawayEntry: ItemListNodeEntry {
|
||||
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
|
||||
case let .channelsHeader(_, text):
|
||||
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
|
||||
case let .channel(_, _, peer, boosts):
|
||||
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: {
|
||||
case let .channel(_, _, peer, boosts, isRevealed):
|
||||
return ItemListPeerItem(presentationData: presentationData, dateTimeFormat: PresentationDateTimeFormat(), nameDisplayOrder: presentationData.nameDisplayOrder, context: arguments.context, peer: peer, presence: nil, text: boosts.flatMap { .text("this channel will receive \($0) boosts", .secondary) } ?? .none, label: .none, editing: ItemListPeerItemEditing(editable: boosts == nil, editing: false, revealed: isRevealed), switchValue: nil, enabled: true, selectable: peer.id != arguments.context.account.peerId, sectionId: self.section, action: {
|
||||
// arguments.openPeer(peer)
|
||||
}, setPeerIdWithRevealedOptions: { _, _ in }, removePeer: { _ in })
|
||||
}, setPeerIdWithRevealedOptions: { lhs, rhs in
|
||||
arguments.setItemIdWithRevealedOptions(lhs, rhs)
|
||||
}, removePeer: { id in
|
||||
arguments.removeChannel(id)
|
||||
})
|
||||
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: {
|
||||
arguments.openChannelsSelection()
|
||||
@ -381,21 +392,35 @@ private enum CreateGiveawayEntry: ItemListNodeEntry {
|
||||
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
|
||||
case let .usersHeader(_, text):
|
||||
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
|
||||
case let .usersAll(_, title, isSelected):
|
||||
return GiftOptionItem(presentationData: presentationData, context: arguments.context, title: title, subtitle: nil, isSelected: isSelected, sectionId: self.section, action: {
|
||||
case let .usersAll(_, title, subtitle, isSelected):
|
||||
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
|
||||
var updatedState = state
|
||||
if !updatedState.onlyNewEligible {
|
||||
openSelection = true
|
||||
}
|
||||
updatedState.onlyNewEligible = false
|
||||
return updatedState
|
||||
}
|
||||
if openSelection {
|
||||
arguments.openCountriesSelection()
|
||||
}
|
||||
})
|
||||
case let .usersNew(_, title, isSelected):
|
||||
return GiftOptionItem(presentationData: presentationData, context: arguments.context, title: title, subtitle: nil, isSelected: isSelected, sectionId: self.section, action: {
|
||||
case let .usersNew(_, title, subtitle, isSelected):
|
||||
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
|
||||
var updatedState = state
|
||||
if updatedState.onlyNewEligible {
|
||||
openSelection = true
|
||||
}
|
||||
updatedState.onlyNewEligible = true
|
||||
return updatedState
|
||||
}
|
||||
if openSelection {
|
||||
arguments.openCountriesSelection()
|
||||
}
|
||||
})
|
||||
case let .usersInfo(_, text):
|
||||
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] = []
|
||||
|
||||
switch subject {
|
||||
@ -517,7 +542,7 @@ private func createGiveawayControllerEntries(peerId: EnginePeer.Id, subject: Cre
|
||||
let channels = [peerId] + state.channels
|
||||
for channelId in channels {
|
||||
if let channel = peers[channelId] {
|
||||
entries.append(.channel(index, presentationData.theme, channel, channel.id == peerId ? state.subscriptions : nil))
|
||||
entries.append(.channel(index, presentationData.theme, channel, channel.id == peerId ? state.subscriptions : nil, false))
|
||||
}
|
||||
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(.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(.timeHeader(presentationData.theme, "DATE WHEN GIVEAWAY ENDS".uppercased()))
|
||||
@ -602,6 +638,7 @@ private struct CreateGiveawayControllerState: Equatable {
|
||||
var onlyNewEligible: Bool
|
||||
var time: Int32
|
||||
var pickingTimeLimit = false
|
||||
var revealedItemId: EnginePeer.Id? = nil
|
||||
var updating = false
|
||||
}
|
||||
|
||||
@ -634,6 +671,7 @@ public func createGiveawayController(context: AccountContext, updatedPresentatio
|
||||
var buyActionImpl: (() -> Void)?
|
||||
var openPeersSelectionImpl: (() -> Void)?
|
||||
var openChannelsSelectionImpl: (() -> Void)?
|
||||
var openCountriesSelectionImpl: (() -> Void)?
|
||||
var openPremiumIntroImpl: (() -> Void)?
|
||||
var presentControllerImpl: ((ViewController) -> Void)?
|
||||
var pushControllerImpl: ((ViewController) -> Void)?
|
||||
@ -649,14 +687,33 @@ public func createGiveawayController(context: AccountContext, updatedPresentatio
|
||||
openPeersSelectionImpl?()
|
||||
}, openChannelsSelection: {
|
||||
openChannelsSelectionImpl?()
|
||||
}, openCountriesSelection: {
|
||||
openCountriesSelectionImpl?()
|
||||
}, openPremiumIntro: {
|
||||
openPremiumIntroImpl?()
|
||||
}, scrollToDate: {
|
||||
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 locale = localeWithStrings(context.sharedContext.currentPresentationData.with { $0 }.strings)
|
||||
|
||||
let productsAndDefaultPrice: Signal<([PremiumGiftProduct], (Int64, NSDecimalNumber)), NoError> = combineLatest(
|
||||
.single([]) |> then(context.engine.payments.premiumGiftCodeOptions(peerId: peerId)),
|
||||
context.inAppPurchaseManager?.availableProducts ?? .single([])
|
||||
@ -724,8 +781,16 @@ public func createGiveawayController(context: AccountContext, updatedPresentatio
|
||||
|
||||
let previousState = previousState.swap(state)
|
||||
var animateChanges = false
|
||||
if let previousState = previousState, previousState.pickingTimeLimit != state.pickingTimeLimit || previousState.mode != state.mode {
|
||||
animateChanges = true
|
||||
if let previousState = previousState {
|
||||
if previousState.pickingTimeLimit != state.pickingTimeLimit {
|
||||
animateChanges = true
|
||||
}
|
||||
if previousState.mode != state.mode {
|
||||
animateChanges = true
|
||||
}
|
||||
if previousState.channels.count > state.channels.count {
|
||||
animateChanges = true
|
||||
}
|
||||
}
|
||||
|
||||
var peers: [EnginePeer.Id: EnginePeer] = [:]
|
||||
@ -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 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))
|
||||
}
|
||||
@ -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 = {
|
||||
let controller = context.sharedContext.makePremiumIntroController(context: context, source: .settings, forceDark: false, dismissed: nil)
|
||||
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
|
||||
}
|
||||
|
@ -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
|
||||
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(
|
||||
id: "reason",
|
||||
title: "Reason",
|
||||
|
@ -24,6 +24,7 @@ import UniversalMediaPlayer
|
||||
import CheckNode
|
||||
import AnimationCache
|
||||
import MultiAnimationRenderer
|
||||
import TelegramNotices
|
||||
|
||||
public enum PremiumSource: Equatable {
|
||||
public static func == (lhs: PremiumSource, rhs: PremiumSource) -> Bool {
|
||||
@ -1428,12 +1429,15 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
|
||||
var selectedProductId: String?
|
||||
var validPurchases: [InAppPurchaseManager.ReceiptPurchase] = []
|
||||
|
||||
var newPerks: [String] = []
|
||||
|
||||
var isPremium: Bool?
|
||||
|
||||
private var disposable: Disposable?
|
||||
private(set) var configuration = PremiumIntroConfiguration.defaultValue
|
||||
|
||||
private var stickersDisposable: Disposable?
|
||||
private var newPerksDisposable: Disposable?
|
||||
private var preloadDisposableSet = DisposableSet()
|
||||
|
||||
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 {
|
||||
self.disposable?.dispose()
|
||||
self.preloadDisposableSet.dispose()
|
||||
self.stickersDisposable?.dispose()
|
||||
self.newPerksDisposable?.dispose()
|
||||
}
|
||||
}
|
||||
|
||||
@ -1807,7 +1826,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
|
||||
subtitleColor: subtitleColor,
|
||||
arrowColor: arrowColor,
|
||||
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
|
||||
case .appIcons:
|
||||
demoSubject = .appIcons
|
||||
let _ = ApplicationSpecificNotice.setDismissedPremiumAppIconsBadge(accountManager: accountContext.sharedContext.accountManager).startStandalone()
|
||||
case .animatedEmoji:
|
||||
demoSubject = .animatedEmoji
|
||||
case .emojiStatus:
|
||||
|
@ -1153,8 +1153,8 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
|
||||
dict[1042605427] = { return Api.payments.BankCardData.parse_bankCardData($0) }
|
||||
dict[-1222446760] = { return Api.payments.CheckedGiftCode.parse_checkedGiftCode($0) }
|
||||
dict[-1362048039] = { return Api.payments.ExportedInvoice.parse_exportedInvoice($0) }
|
||||
dict[2054937690] = { return Api.payments.GiveawayInfo.parse_giveawayInfo($0) }
|
||||
dict[952312868] = { return Api.payments.GiveawayInfo.parse_giveawayInfoResults($0) }
|
||||
dict[1130879648] = { return Api.payments.GiveawayInfo.parse_giveawayInfo($0) }
|
||||
dict[13456752] = { return Api.payments.GiveawayInfo.parse_giveawayInfoResults($0) }
|
||||
dict[-1610250415] = { return Api.payments.PaymentForm.parse_paymentForm($0) }
|
||||
dict[1891958275] = { return Api.payments.PaymentReceipt.parse_paymentReceipt($0) }
|
||||
dict[1314881805] = { return Api.payments.PaymentResult.parse_paymentResult($0) }
|
||||
|
@ -656,24 +656,27 @@ public extension Api.payments {
|
||||
}
|
||||
public extension Api.payments {
|
||||
enum GiveawayInfo: TypeConstructorDescription {
|
||||
case giveawayInfo(flags: Int32, joinedTooEarlyDate: Int32?, adminDisallowedChatId: Int64?)
|
||||
case giveawayInfoResults(flags: Int32, giftCodeSlug: String?, finishDate: Int32, winnersCount: Int32, activatedCount: Int32)
|
||||
case giveawayInfo(flags: Int32, startDate: Int32, joinedTooEarlyDate: Int32?, adminDisallowedChatId: Int64?, disallowedCountry: String?)
|
||||
case giveawayInfoResults(flags: Int32, startDate: Int32, giftCodeSlug: String?, finishDate: Int32, winnersCount: Int32, activatedCount: Int32)
|
||||
|
||||
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
|
||||
switch self {
|
||||
case .giveawayInfo(let flags, let joinedTooEarlyDate, let adminDisallowedChatId):
|
||||
case .giveawayInfo(let flags, let startDate, let joinedTooEarlyDate, let adminDisallowedChatId, let disallowedCountry):
|
||||
if boxed {
|
||||
buffer.appendInt32(2054937690)
|
||||
buffer.appendInt32(1130879648)
|
||||
}
|
||||
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 << 2) != 0 {serializeInt64(adminDisallowedChatId!, buffer: buffer, boxed: false)}
|
||||
if Int(flags) & Int(1 << 4) != 0 {serializeString(disallowedCountry!, buffer: buffer, boxed: false)}
|
||||
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 {
|
||||
buffer.appendInt32(952312868)
|
||||
buffer.appendInt32(13456752)
|
||||
}
|
||||
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)}
|
||||
serializeInt32(finishDate, buffer: buffer, boxed: false)
|
||||
serializeInt32(winnersCount, buffer: buffer, boxed: false)
|
||||
@ -684,10 +687,10 @@ public extension Api.payments {
|
||||
|
||||
public func descriptionFields() -> (String, [(String, Any)]) {
|
||||
switch self {
|
||||
case .giveawayInfo(let flags, let joinedTooEarlyDate, let adminDisallowedChatId):
|
||||
return ("giveawayInfo", [("flags", flags as Any), ("joinedTooEarlyDate", joinedTooEarlyDate as Any), ("adminDisallowedChatId", adminDisallowedChatId as Any)])
|
||||
case .giveawayInfoResults(let flags, 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)])
|
||||
case .giveawayInfo(let flags, let startDate, let joinedTooEarlyDate, let adminDisallowedChatId, let disallowedCountry):
|
||||
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 startDate, let giftCodeSlug, let finishDate, let winnersCount, let activatedCount):
|
||||
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?
|
||||
_1 = reader.readInt32()
|
||||
var _2: Int32?
|
||||
if Int(_1!) & Int(1 << 1) != 0 {_2 = reader.readInt32() }
|
||||
var _3: Int64?
|
||||
if Int(_1!) & Int(1 << 2) != 0 {_3 = reader.readInt64() }
|
||||
_2 = reader.readInt32()
|
||||
var _3: Int32?
|
||||
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 _c2 = (Int(_1!) & Int(1 << 1) == 0) || _2 != nil
|
||||
let _c3 = (Int(_1!) & Int(1 << 2) == 0) || _3 != nil
|
||||
if _c1 && _c2 && _c3 {
|
||||
return Api.payments.GiveawayInfo.giveawayInfo(flags: _1!, joinedTooEarlyDate: _2, adminDisallowedChatId: _3)
|
||||
let _c2 = _2 != nil
|
||||
let _c3 = (Int(_1!) & Int(1 << 1) == 0) || _3 != nil
|
||||
let _c4 = (Int(_1!) & Int(1 << 2) == 0) || _4 != nil
|
||||
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 {
|
||||
return nil
|
||||
@ -711,21 +720,24 @@ public extension Api.payments {
|
||||
public static func parse_giveawayInfoResults(_ reader: BufferReader) -> GiveawayInfo? {
|
||||
var _1: Int32?
|
||||
_1 = reader.readInt32()
|
||||
var _2: String?
|
||||
if Int(_1!) & Int(1 << 0) != 0 {_2 = parseString(reader) }
|
||||
var _3: Int32?
|
||||
_3 = reader.readInt32()
|
||||
var _2: Int32?
|
||||
_2 = reader.readInt32()
|
||||
var _3: String?
|
||||
if Int(_1!) & Int(1 << 0) != 0 {_3 = parseString(reader) }
|
||||
var _4: Int32?
|
||||
_4 = reader.readInt32()
|
||||
var _5: Int32?
|
||||
_5 = reader.readInt32()
|
||||
var _6: Int32?
|
||||
_6 = reader.readInt32()
|
||||
let _c1 = _1 != nil
|
||||
let _c2 = (Int(_1!) & Int(1 << 0) == 0) || _2 != nil
|
||||
let _c3 = _3 != nil
|
||||
let _c2 = _2 != nil
|
||||
let _c3 = (Int(_1!) & Int(1 << 0) == 0) || _3 != nil
|
||||
let _c4 = _4 != nil
|
||||
let _c5 = _5 != nil
|
||||
if _c1 && _c2 && _c3 && _c4 && _c5 {
|
||||
return Api.payments.GiveawayInfo.giveawayInfoResults(flags: _1!, giftCodeSlug: _2, finishDate: _3!, winnersCount: _4!, activatedCount: _5!)
|
||||
let _c6 = _6 != nil
|
||||
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 {
|
||||
return nil
|
||||
|
@ -56,6 +56,7 @@ public enum PremiumGiveawayInfo: Equatable {
|
||||
public enum DisallowReason: Equatable {
|
||||
case joinedTooEarly(Int32)
|
||||
case channelAdmin(EnginePeer.Id)
|
||||
case disallowedCountry(String)
|
||||
}
|
||||
|
||||
case notQualified
|
||||
@ -70,8 +71,8 @@ public enum PremiumGiveawayInfo: Equatable {
|
||||
case refunded
|
||||
}
|
||||
|
||||
case ongoing(status: OngoingStatus)
|
||||
case finished(status: ResultStatus, finishDate: Int32, winnersCount: Int32, activatedCount: Int32)
|
||||
case ongoing(startDate: Int32, status: OngoingStatus)
|
||||
case finished(status: ResultStatus, startDate: Int32, finishDate: Int32, winnersCount: Int32, activatedCount: Int32)
|
||||
}
|
||||
|
||||
public struct PrepaidGiveaway: Equatable {
|
||||
@ -95,19 +96,21 @@ func _internal_getPremiumGiveawayInfo(account: Account, peerId: EnginePeer.Id, m
|
||||
|> map { result -> PremiumGiveawayInfo? in
|
||||
if let result {
|
||||
switch result {
|
||||
case let .giveawayInfo(flags, joinedTooEarlyDate, adminDisallowedChatId):
|
||||
case let .giveawayInfo(flags, startDate, joinedTooEarlyDate, adminDisallowedChatId, disallowedCountry):
|
||||
if (flags & (1 << 3)) != 0 {
|
||||
return .ongoing(status: .almostOver)
|
||||
return .ongoing(startDate: startDate, status: .almostOver)
|
||||
} 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 {
|
||||
return .ongoing(status: .notAllowed(.joinedTooEarly(joinedTooEarlyDate)))
|
||||
return .ongoing(startDate: startDate, status: .notAllowed(.joinedTooEarly(joinedTooEarlyDate)))
|
||||
} 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 {
|
||||
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
|
||||
if let giftCodeSlug = giftCodeSlug {
|
||||
status = .won(slug: giftCodeSlug)
|
||||
@ -116,7 +119,7 @@ func _internal_getPremiumGiveawayInfo(account: Account, peerId: EnginePeer.Id, m
|
||||
} else {
|
||||
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 {
|
||||
return nil
|
||||
|
@ -183,6 +183,7 @@ private enum ApplicationSpecificGlobalNotice: Int32 {
|
||||
case displayStoryUnmuteTooltip = 49
|
||||
case chatReplyOptionsTip = 50
|
||||
case displayStoryInteractionGuide = 51
|
||||
case dismissedPremiumAppIconsBadge = 52
|
||||
|
||||
var key: ValueBoxKey {
|
||||
let v = ValueBoxKey(length: 4)
|
||||
@ -444,6 +445,10 @@ private struct ApplicationSpecificNoticeKeys {
|
||||
static func displayStoryInteractionGuide() -> NoticeEntryKey {
|
||||
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 {
|
||||
@ -1717,4 +1722,25 @@ public struct ApplicationSpecificNotice {
|
||||
}
|
||||
|> 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)
|
||||
}
|
||||
}
|
||||
|
@ -44,3 +44,12 @@ public func stringForDistance(strings: PresentationStrings, distance: CLLocation
|
||||
|
||||
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
|
||||
}
|
||||
|
@ -354,6 +354,8 @@ public func mediaContentKind(_ media: EngineMedia, message: EngineMessage? = nil
|
||||
}
|
||||
case .story:
|
||||
return .story
|
||||
case .giveaway:
|
||||
return .giveaway
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
|
@ -922,6 +922,10 @@ public func universalServiceMessageString(presentationData: (PresentationTheme,
|
||||
resultTitleString = strings.Conversation_StoryExpiredMentionTextOutgoing(compactPeerName)
|
||||
}
|
||||
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])
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -32,6 +32,7 @@ public struct ChatMessageBubbleContentProperties {
|
||||
public let shareButtonOffset: CGPoint?
|
||||
public let hidesHeaders: Bool
|
||||
public let avatarOffset: CGFloat?
|
||||
public let isDetached: Bool
|
||||
|
||||
public init(
|
||||
hidesSimpleAuthorHeader: Bool,
|
||||
@ -41,7 +42,8 @@ public struct ChatMessageBubbleContentProperties {
|
||||
forceAlignment: ChatMessageBubbleContentAlignment,
|
||||
shareButtonOffset: CGPoint? = nil,
|
||||
hidesHeaders: Bool = false,
|
||||
avatarOffset: CGFloat? = nil
|
||||
avatarOffset: CGFloat? = nil,
|
||||
isDetached: Bool = false
|
||||
) {
|
||||
self.hidesSimpleAuthorHeader = hidesSimpleAuthorHeader
|
||||
self.headerSpacing = headerSpacing
|
||||
@ -51,6 +53,7 @@ public struct ChatMessageBubbleContentProperties {
|
||||
self.shareButtonOffset = shareButtonOffset
|
||||
self.hidesHeaders = hidesHeaders
|
||||
self.avatarOffset = avatarOffset
|
||||
self.isDetached = isDetached
|
||||
}
|
||||
}
|
||||
|
||||
@ -169,6 +172,7 @@ open class ChatMessageBubbleContentNode: ASDisplayNode {
|
||||
return false
|
||||
}
|
||||
|
||||
public weak var itemNode: ChatMessageItemNodeProtocol?
|
||||
public weak var bubbleBackgroundNode: ChatMessageBackground?
|
||||
public weak var bubbleBackdropNode: ChatMessageBubbleBackdrop?
|
||||
|
||||
|
@ -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
|
||||
|
||||
guard let _ = self.animationInstance, let animationFrameRange = self.animationFrameRange else {
|
||||
self.scheduledPlayOnce = true
|
||||
return
|
||||
}
|
||||
if !self.isEffectivelyVisible {
|
||||
if !self.isEffectivelyVisible && !force {
|
||||
self.scheduledPlayOnce = true
|
||||
return
|
||||
}
|
||||
|
@ -36,6 +36,7 @@ import LocationUI
|
||||
import LegacyMediaPickerUI
|
||||
import ReactionSelectionNode
|
||||
import VolumeSliderContextItem
|
||||
import TelegramStringFormatting
|
||||
|
||||
enum DrawingScreenType {
|
||||
case drawing
|
||||
@ -3065,16 +3066,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
||||
if !self.didSetupStaticEmojiPack {
|
||||
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?
|
||||
if let subject = self.subject {
|
||||
if case let .asset(asset) = subject {
|
||||
@ -3095,7 +3087,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
||||
if let self {
|
||||
let emojiFile: Signal<TelegramMediaFile?, NoError>
|
||||
if let countryCode {
|
||||
let flagEmoji = flag(countryCode: countryCode)
|
||||
let flag = flagEmoji(countryCode: countryCode)
|
||||
emojiFile = self.staticEmojiPack.get()
|
||||
|> filter { result in
|
||||
if case .result = result {
|
||||
@ -3114,7 +3106,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
||||
break
|
||||
}
|
||||
}
|
||||
if let displayText, displayText.hasPrefix(flagEmoji) {
|
||||
if let displayText, displayText.hasPrefix(flag) {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
|
@ -41,6 +41,7 @@ swift_library(
|
||||
"//submodules/OverlayStatusController",
|
||||
"//submodules/UndoUI",
|
||||
"//submodules/TemporaryCachedPeerDataManager",
|
||||
"//submodules/CountrySelectionUI",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -11,11 +11,8 @@ import AccountContext
|
||||
import TelegramCore
|
||||
import Postbox
|
||||
import MultilineTextComponent
|
||||
import SolidRoundedButtonComponent
|
||||
import PresentationDataUtils
|
||||
import ButtonComponent
|
||||
import PlainButtonComponent
|
||||
import AnimatedCounterComponent
|
||||
import TokenListTextField
|
||||
import AvatarNode
|
||||
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 {
|
||||
case screenshot = 0
|
||||
case pin = 1
|
||||
@ -1465,6 +1442,9 @@ final class ShareWithPeersScreenComponent: Component {
|
||||
if peer.id.isGroupOrChannel {
|
||||
if case .channels = component.stateContext.subject, self.selectedPeers.count >= component.context.userLimits.maxGiveawayChannelsCount, index == nil {
|
||||
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
|
||||
}
|
||||
if case .channels = component.stateContext.subject {
|
||||
@ -1492,6 +1472,9 @@ final class ShareWithPeersScreenComponent: Component {
|
||||
} else {
|
||||
if case .members = component.stateContext.subject, self.selectedPeers.count >= 10, index == nil {
|
||||
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
|
||||
}
|
||||
togglePeer()
|
||||
@ -2993,6 +2976,10 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer {
|
||||
|
||||
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
|
||||
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) {
|
||||
|
@ -45,6 +45,8 @@ final class StoryInteractionGuideComponent: Component {
|
||||
private let guideItems = ComponentView<Empty>()
|
||||
private let proceedButton = ComponentView<Empty>()
|
||||
|
||||
var currentIndex = 0
|
||||
|
||||
override init(frame: CGRect) {
|
||||
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 {
|
||||
self.component = component
|
||||
self.state = state
|
||||
|
||||
let sideInset: CGFloat = 48.0
|
||||
|
||||
@ -131,7 +134,15 @@ final class StoryInteractionGuideComponent: Component {
|
||||
context: component.context,
|
||||
title: "Go forward",
|
||||
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,
|
||||
title: "Pause and Seek",
|
||||
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,
|
||||
title: "Go back",
|
||||
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,
|
||||
title: "Move between stories",
|
||||
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(
|
||||
transition: .immediate,
|
||||
transition: transition,
|
||||
component: AnyComponent(List(items)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: availableSize.height)
|
||||
@ -225,17 +260,23 @@ private final class GuideItemComponent: Component {
|
||||
let title: String
|
||||
let text: String
|
||||
let animationName: String
|
||||
let isPlaying: Bool
|
||||
let playbackCompleted: () -> Void
|
||||
|
||||
init(
|
||||
context: AccountContext,
|
||||
title: String,
|
||||
text: String,
|
||||
animationName: String
|
||||
animationName: String,
|
||||
isPlaying: Bool,
|
||||
playbackCompleted: @escaping () -> Void
|
||||
) {
|
||||
self.context = context
|
||||
self.title = title
|
||||
self.text = text
|
||||
self.animationName = animationName
|
||||
self.isPlaying = isPlaying
|
||||
self.playbackCompleted = playbackCompleted
|
||||
}
|
||||
|
||||
static func ==(lhs: GuideItemComponent, rhs: GuideItemComponent) -> Bool {
|
||||
@ -248,6 +289,9 @@ private final class GuideItemComponent: Component {
|
||||
if lhs.animationName != rhs.animationName {
|
||||
return false
|
||||
}
|
||||
if lhs.isPlaying != rhs.isPlaying {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@ -255,18 +299,33 @@ private final class GuideItemComponent: Component {
|
||||
private var component: GuideItemComponent?
|
||||
private weak var state: EmptyComponentState?
|
||||
|
||||
private let containerView = UIView()
|
||||
private let selectionView = UIView()
|
||||
|
||||
private let animation = ComponentView<Empty>()
|
||||
private let titleLabel = ComponentView<Empty>()
|
||||
private let descriptionLabel = ComponentView<Empty>()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
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) {
|
||||
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 {
|
||||
self.component = component
|
||||
|
||||
@ -285,18 +344,40 @@ private final class GuideItemComponent: Component {
|
||||
startingPosition: .begin,
|
||||
size: CGSize(width: 60.0, height: 60.0),
|
||||
renderingScale: UIScreen.main.scale,
|
||||
loop: true
|
||||
loop: false
|
||||
)
|
||||
),
|
||||
environment: {},
|
||||
containerSize: availableSize
|
||||
)
|
||||
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 {
|
||||
self.addSubview(view)
|
||||
view.externalShouldPlay = false
|
||||
self.containerView.addSubview(view)
|
||||
}
|
||||
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(
|
||||
@ -308,7 +389,7 @@ private final class GuideItemComponent: Component {
|
||||
let titleFrame = CGRect(origin: CGPoint(x: originX + 60.0, y: 25.0), size: titleSize)
|
||||
if let view = self.titleLabel.view {
|
||||
if view.superview == nil {
|
||||
self.addSubview(view)
|
||||
self.containerView.addSubview(view)
|
||||
}
|
||||
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)
|
||||
if let view = self.descriptionLabel.view {
|
||||
if view.superview == nil {
|
||||
self.addSubview(view)
|
||||
self.containerView.addSubview(view)
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ struct EditableTokenListToken {
|
||||
enum Subject {
|
||||
case peer(EnginePeer)
|
||||
case category(UIImage?)
|
||||
case emoji(String)
|
||||
}
|
||||
|
||||
let id: AnyHashable
|
||||
@ -86,6 +87,7 @@ private final class TokenNode: ASDisplayNode {
|
||||
let token: EditableTokenListToken
|
||||
let avatarNode: AvatarNode
|
||||
let categoryAvatarNode: ASImageNode
|
||||
let emojiTextNode: ImmediateTextNode
|
||||
let removeIconNode: ASImageNode
|
||||
let titleNode: ASTextNode
|
||||
let backgroundNode: ASImageNode
|
||||
@ -119,6 +121,7 @@ private final class TokenNode: ASDisplayNode {
|
||||
self.categoryAvatarNode = ASImageNode()
|
||||
self.categoryAvatarNode.displaysAsynchronously = false
|
||||
self.categoryAvatarNode.displayWithoutProcessing = true
|
||||
self.emojiTextNode = ImmediateTextNode()
|
||||
|
||||
self.removeIconNode = ASImageNode()
|
||||
self.removeIconNode.alpha = 0.0
|
||||
@ -132,6 +135,8 @@ private final class TokenNode: ASDisplayNode {
|
||||
cornerRadius = 24.0
|
||||
case .category:
|
||||
cornerRadius = 14.0
|
||||
case .emoji:
|
||||
cornerRadius = 24.0
|
||||
}
|
||||
|
||||
self.backgroundNode = ASImageNode()
|
||||
@ -160,6 +165,9 @@ private final class TokenNode: ASDisplayNode {
|
||||
case let .category(image):
|
||||
self.addSubnode(self.categoryAvatarNode)
|
||||
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)
|
||||
@ -167,7 +175,12 @@ private final class TokenNode: ASDisplayNode {
|
||||
|
||||
override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize {
|
||||
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() {
|
||||
@ -181,7 +194,13 @@ private final class TokenNode: ASDisplayNode {
|
||||
self.categoryAvatarNode.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) {
|
||||
@ -192,6 +211,7 @@ private final class TokenNode: ASDisplayNode {
|
||||
|
||||
self.avatarNode.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
|
||||
|
||||
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.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.animateScale(from: 0.01, to: 1.0, duration: 0.2)
|
||||
} 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.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.animateScale(from: 1.0, to: 0.01, duration: 0.2)
|
||||
}
|
||||
|
@ -21,6 +21,7 @@ public final class TokenListTextField: Component {
|
||||
public enum Content: Equatable {
|
||||
case peer(EnginePeer)
|
||||
case category(UIImage?)
|
||||
case emoji(String)
|
||||
|
||||
public static func ==(lhs: Content, rhs: Content) -> Bool {
|
||||
switch lhs {
|
||||
@ -36,6 +37,12 @@ public final class TokenListTextField: Component {
|
||||
} else {
|
||||
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)
|
||||
case let .category(image):
|
||||
mappedSubject = .category(image)
|
||||
case let .emoji(emoji):
|
||||
mappedSubject = .emoji(emoji)
|
||||
}
|
||||
|
||||
return EditableTokenListToken(
|
||||
|
BIN
submodules/TelegramUI/Images.xcassets/Premium/Icons/Coffee.imageset/Coffee.png
vendored
Normal file
After Width: | Height: | Size: 92 KiB |
21
submodules/TelegramUI/Images.xcassets/Premium/Icons/Coffee.imageset/Contents.json
vendored
Normal 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
|
||||
}
|
||||
}
|
21
submodules/TelegramUI/Images.xcassets/Premium/Icons/Duck.imageset/Contents.json
vendored
Normal 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
|
||||
}
|
||||
}
|
BIN
submodules/TelegramUI/Images.xcassets/Premium/Icons/Duck.imageset/Duck.png
vendored
Normal file
After Width: | Height: | Size: 48 KiB |
21
submodules/TelegramUI/Images.xcassets/Premium/Icons/Steam.imageset/Contents.json
vendored
Normal 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
|
||||
}
|
||||
}
|
BIN
submodules/TelegramUI/Images.xcassets/Premium/Icons/Steam.imageset/Steam.png
vendored
Normal file
After Width: | Height: | Size: 72 KiB |
@ -738,18 +738,19 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
strongSelf.bankCardDisposable = disposable
|
||||
}
|
||||
|
||||
var cancelImpl: (() -> Void)?
|
||||
let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }
|
||||
// var cancelImpl: (() -> Void)?
|
||||
// let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }
|
||||
let progressSignal = Signal<Never, NoError> { subscriber in
|
||||
let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: {
|
||||
cancelImpl?()
|
||||
}))
|
||||
strongSelf.present(controller, in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
|
||||
return ActionDisposable { [weak controller] in
|
||||
Queue.mainQueue().async() {
|
||||
controller?.dismiss()
|
||||
}
|
||||
}
|
||||
// let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: {
|
||||
// cancelImpl?()
|
||||
// }))
|
||||
// strongSelf.present(controller, in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
|
||||
// return ActionDisposable { [weak controller] in
|
||||
// Queue.mainQueue().async() {
|
||||
// controller?.dismiss()
|
||||
// }
|
||||
// }
|
||||
return EmptyDisposable
|
||||
}
|
||||
|> runOn(Queue.mainQueue())
|
||||
|> delay(0.15, queue: Queue.mainQueue())
|
||||
@ -761,14 +762,13 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
progressDisposable.dispose()
|
||||
}
|
||||
}
|
||||
cancelImpl = {
|
||||
disposable.set(nil)
|
||||
}
|
||||
// cancelImpl = {
|
||||
// disposable.set(nil)
|
||||
// }
|
||||
disposable.set((signal
|
||||
|> deliverOnMainQueue).startStrict(next: { [weak self] info in
|
||||
if let strongSelf = self, let info = info {
|
||||
let date = stringForDate(timestamp: giveaway.untilDate, strings: strongSelf.presentationData.strings)
|
||||
let startDate = stringForDate(timestamp: message.timestamp, strings: strongSelf.presentationData.strings)
|
||||
let untilDate = stringForDate(timestamp: giveaway.untilDate, strings: strongSelf.presentationData.strings)
|
||||
|
||||
let title: String
|
||||
let text: String
|
||||
@ -781,9 +781,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
})]
|
||||
|
||||
switch info {
|
||||
case let .ongoing(status):
|
||||
title = "About This Giveaway"
|
||||
case let .ongoing(start, status):
|
||||
let startDate = stringForDate(timestamp: start, strings: strongSelf.presentationData.strings)
|
||||
|
||||
title = "About This Giveaway"
|
||||
|
||||
let intro: String
|
||||
if case .almostOver = status {
|
||||
@ -793,33 +794,17 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
}
|
||||
|
||||
let ending: String
|
||||
if case .almostOver = status {
|
||||
if giveaway.flags.contains(.onlyNewSubscribers) {
|
||||
if giveaway.channelPeerIds.count > 1 {
|
||||
ending = "On **\(date)**, Telegram automatically selected **\(giveaway.quantity)** random users that joined **\(peerName)** and other listed channels after **\(startDate)**."
|
||||
} else {
|
||||
ending = "On **\(date)**, Telegram automatically selected **\(giveaway.quantity)** random users that joined **\(peerName)** after **\(startDate)**."
|
||||
}
|
||||
if giveaway.flags.contains(.onlyNewSubscribers) {
|
||||
if giveaway.channelPeerIds.count > 1 {
|
||||
ending = "On **\(untilDate)**, Telegram will automatically select **\(giveaway.quantity)** random users that joined **\(peerName)** and **\(giveaway.channelPeerIds.count - 1)** other listed channels after **\(startDate)**."
|
||||
} else {
|
||||
if giveaway.channelPeerIds.count > 1 {
|
||||
ending = "On **\(date)**, Telegram automatically selected **\(giveaway.quantity)** random subscribers of **\(peerName)** and other listed channels."
|
||||
} else {
|
||||
ending = "On **\(date)**, Telegram automatically selected **\(giveaway.quantity)** random subscribers of **\(peerName)**."
|
||||
}
|
||||
ending = "On **\(untilDate)**, Telegram will automatically select **\(giveaway.quantity)** random users that joined **\(peerName)** after **\(startDate)**."
|
||||
}
|
||||
} 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)**."
|
||||
}
|
||||
if giveaway.channelPeerIds.count > 1 {
|
||||
ending = "On **\(untilDate)**, Telegram will automatically select **\(giveaway.quantity)** random subscribers of **\(peerName)** and **\(giveaway.channelPeerIds.count - 1)** other listed channels."
|
||||
} else {
|
||||
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)**."
|
||||
}
|
||||
ending = "On **\(untilDate)**, Telegram will automatically select **\(giveaway.quantity)** random subscribers of **\(peerName)**."
|
||||
}
|
||||
}
|
||||
|
||||
@ -827,9 +812,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
switch status {
|
||||
case .notQualified:
|
||||
if giveaway.channelPeerIds.count > 1 {
|
||||
participation = "To take part in this giveaway please join the channel **\(peerName)** (**\(giveaway.channelPeerIds.count - 1)** other listed channels) before **\(date)**."
|
||||
participation = "To take part in this giveaway please join the channel **\(peerName)** (**\(giveaway.channelPeerIds.count - 1)** other listed channels) before **\(untilDate)**."
|
||||
} else {
|
||||
participation = "To take part in this giveaway please join the channel **\(peerName)** before **\(date)**."
|
||||
participation = "To take part in this giveaway please join the channel **\(peerName)** before **\(untilDate)**."
|
||||
}
|
||||
case let .notAllowed(reason):
|
||||
switch reason {
|
||||
@ -839,6 +824,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
case let .channelAdmin(adminId):
|
||||
let _ = adminId
|
||||
participation = "You are not eligible to participate in this giveaway, because you are an admin of participating channel (**\(peerName)**)."
|
||||
case let .disallowedCountry(countryCode):
|
||||
let _ = countryCode
|
||||
participation = "You are not eligible to participate in this giveaway, because your country is not included in the terms of the giveaway."
|
||||
}
|
||||
case .participating:
|
||||
if giveaway.channelPeerIds.count > 1 {
|
||||
@ -855,8 +843,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
}
|
||||
|
||||
text = "\(intro)\n\n\(ending)\(participation)"
|
||||
case let .finished(status, finishDate, _, activatedCount):
|
||||
let date = stringForDate(timestamp: finishDate, strings: strongSelf.presentationData.strings)
|
||||
case let .finished(status, start, finish, _, activatedCount):
|
||||
let startDate = stringForDate(timestamp: start, strings: strongSelf.presentationData.strings)
|
||||
let finishDate = stringForDate(timestamp: finish, strings: strongSelf.presentationData.strings)
|
||||
title = "Giveaway Ended"
|
||||
|
||||
let intro = "The giveaway was sponsored by the admins of **\(peerName)**, who acquired **\(giveaway.quantity) Telegram Premium** subscriptions for **\(giveaway.months)** months for its followers."
|
||||
@ -864,15 +853,15 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
var ending: String
|
||||
if giveaway.flags.contains(.onlyNewSubscribers) {
|
||||
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 {
|
||||
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 {
|
||||
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 {
|
||||
ending = "On **\(date)**, Telegram automatically selected **\(giveaway.quantity)** random subscribers of **\(peerName)**."
|
||||
ending = "On **\(finishDate)**, Telegram automatically selected **\(giveaway.quantity)** random subscribers of **\(peerName)**."
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -142,16 +142,19 @@ private func canEditMessage(accountPeerId: PeerId, limitsConfiguration: EngineCo
|
||||
} else if let _ = media as? TelegramMediaPoll {
|
||||
hasUneditableAttributes = true
|
||||
break
|
||||
} else if let _ = media as? TelegramMediaDice {
|
||||
} else if let _ = media as? TelegramMediaDice {
|
||||
hasUneditableAttributes = true
|
||||
break
|
||||
} else if let _ = media as? TelegramMediaGame {
|
||||
} else if let _ = media as? TelegramMediaGame {
|
||||
hasUneditableAttributes = true
|
||||
break
|
||||
} else if let _ = media as? TelegramMediaInvoice {
|
||||
} else if let _ = media as? TelegramMediaInvoice {
|
||||
hasUneditableAttributes = true
|
||||
break
|
||||
} else if let _ = media as? TelegramMediaStory {
|
||||
} else if let _ = media as? TelegramMediaStory {
|
||||
hasUneditableAttributes = true
|
||||
break
|
||||
} else if let _ = media as? TelegramMediaGiveaway {
|
||||
hasUneditableAttributes = true
|
||||
break
|
||||
}
|
||||
|
@ -158,7 +158,12 @@ class ChatMessageActionBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
let cachedMaskBackgroundImage = self.cachedMaskBackgroundImage
|
||||
|
||||
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)
|
||||
|
||||
@ -230,6 +235,8 @@ class ChatMessageActionBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
|
||||
if let _ = image {
|
||||
backgroundSize.height += imageSize.height + 10
|
||||
} else if isGiveaway {
|
||||
backgroundSize.height += 8.0
|
||||
}
|
||||
|
||||
return (backgroundSize.width, { boundingWidth in
|
||||
@ -510,6 +517,10 @@ class ChatMessageActionBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
}
|
||||
|
||||
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
|
||||
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 {
|
||||
@ -528,7 +539,7 @@ class ChatMessageActionBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -72,6 +72,7 @@ final class ChatMessageAttachedContentButtonNode: HighlightTrackingButtonNode {
|
||||
|
||||
self.shimmerEffectNode = ShimmerEffectForegroundNode()
|
||||
self.shimmerEffectNode.cornerRadius = 5.0
|
||||
self.shimmerEffectNode.isHidden = true
|
||||
|
||||
self.backgroundNode = ASImageNode()
|
||||
self.backgroundNode.isLayerBacked = true
|
||||
|
@ -1235,7 +1235,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
|
||||
|
||||
var allowFullWidth = false
|
||||
let chatLocationPeerId: PeerId = item.chatLocation.peerId ?? item.content.firstMessage.id.peerId
|
||||
|
||||
|
||||
do {
|
||||
let peerId = chatLocationPeerId
|
||||
|
||||
@ -1790,7 +1790,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if initialDisplayHeader && displayAuthorInfo {
|
||||
if let peer = firstMessage.peers[firstMessage.id.peerId] as? TelegramChannel, case .broadcast = peer.info, item.content.firstMessage.adAttribute == nil {
|
||||
authorNameString = EnginePeer(peer).displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder)
|
||||
@ -2399,7 +2399,12 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
|
||||
print("contentNodeWidth \(contentNodeWidth) > \(maximumNodeWidth)")
|
||||
}
|
||||
#endif
|
||||
maxContentWidth = max(maxContentWidth, contentNodeWidth)
|
||||
|
||||
if contentNodeProperties.isDetached {
|
||||
|
||||
} else {
|
||||
maxContentWidth = max(maxContentWidth, contentNodeWidth)
|
||||
}
|
||||
|
||||
contentNodePropertiesAndFinalize.append((contentNodeProperties, contentPosition, contentNodeFinalize, contentGroupId, itemSelection))
|
||||
}
|
||||
@ -2414,6 +2419,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
|
||||
var contentNodesHeight: CGFloat = 0.0
|
||||
var totalContentNodesHeight: CGFloat = 0.0
|
||||
var currentContainerGroupOverlap: CGFloat = 0.0
|
||||
var detachedContentNodesHeight: CGFloat = 0.0
|
||||
|
||||
var mosaicStatusOrigin: CGPoint?
|
||||
for i in 0 ..< contentNodePropertiesAndFinalize.count {
|
||||
@ -2455,7 +2461,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
|
||||
}
|
||||
totalContentNodesHeight += properties.headerSpacing
|
||||
}
|
||||
|
||||
|
||||
if currentContainerGroupId != contentGroupId {
|
||||
if let containerGroupId = currentContainerGroupId {
|
||||
var overlapOffset: CGFloat = 0.0
|
||||
@ -2475,11 +2481,16 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
|
||||
currentItemSelection = itemSelection
|
||||
}
|
||||
|
||||
let contentNodeOriginY = contentNodesHeight - detachedContentNodesHeight
|
||||
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
|
||||
totalContentNodesHeight += size.height
|
||||
|
||||
if properties.isDetached {
|
||||
detachedContentNodesHeight += size.height
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -2509,7 +2520,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
|
||||
}
|
||||
reactionButtonsSizeAndApply = reactionButtonsFinalize(maxContentWidth)
|
||||
}
|
||||
|
||||
|
||||
let minimalContentSize: CGSize
|
||||
if hideBackground {
|
||||
minimalContentSize = CGSize(width: 1.0, height: 1.0)
|
||||
@ -2517,24 +2528,23 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
|
||||
minimalContentSize = layoutConstants.bubble.minimumSize
|
||||
}
|
||||
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
|
||||
if minimalContentSize.height > calculatedBubbleHeight + 2.0 {
|
||||
contentVerticalOffset = floorToScreenPixels((minimalContentSize.height - calculatedBubbleHeight) / 2.0)
|
||||
}
|
||||
|
||||
let availableWidth = params.width - params.leftInset - params.rightInset
|
||||
let backgroundFrame: CGRect
|
||||
let contentOrigin: CGPoint
|
||||
let contentUpperRightCorner: CGPoint
|
||||
switch alignment {
|
||||
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)
|
||||
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:
|
||||
let availableWidth = params.width - params.leftInset - params.rightInset
|
||||
backgroundFrame = CGRect(origin: CGPoint(x: params.leftInset + floor((availableWidth - layoutBubbleSize.width) / 2.0), y: 0.0), size: layoutBubbleSize)
|
||||
backgroundFrame = CGRect(origin: CGPoint(x: params.leftInset + floor((availableWidth - layoutBubbleSize.width) / 2.0), y: detachedContentNodesHeight), size: layoutBubbleSize)
|
||||
let contentOriginX: CGFloat
|
||||
if !hideBackground {
|
||||
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)
|
||||
|
||||
var layoutSize = CGSize(width: params.width, height: layoutBubbleSize.height)
|
||||
var layoutSize = CGSize(width: params.width, height: layoutBubbleSize.height + detachedContentNodesHeight)
|
||||
if let reactionButtonsSizeAndApply = reactionButtonsSizeAndApply {
|
||||
layoutSize.height += 4.0 + reactionButtonsSizeAndApply.0.height
|
||||
}
|
||||
@ -3203,6 +3213,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
|
||||
}
|
||||
containerSupernode.addSubnode(contentNode)
|
||||
|
||||
contentNode.itemNode = strongSelf
|
||||
contentNode.bubbleBackgroundNode = strongSelf.backgroundNode
|
||||
contentNode.bubbleBackdropNode = strongSelf.backgroundWallpaperNode
|
||||
contentNode.updateIsTextSelectionActive = { [weak contextSourceNode] value in
|
||||
@ -3239,7 +3250,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
|
||||
|
||||
var shouldClipOnTransitions = true
|
||||
var contentNodeIndex = 0
|
||||
for (relativeFrame, _, useContentOrigin, apply) in contentNodeFramesPropertiesAndApply {
|
||||
for (relativeFrame, properties, useContentOrigin, apply) in contentNodeFramesPropertiesAndApply {
|
||||
apply(animation, synchronousLoads, applyInfo)
|
||||
|
||||
if contentNodeIndex >= strongSelf.contentNodes.count {
|
||||
@ -3252,7 +3263,14 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
|
||||
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
|
||||
|
||||
if case let .System(duration, _) = animation {
|
||||
|
@ -39,6 +39,7 @@ class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
private let placeholderNode: StickerShimmerEffectNode
|
||||
private let animationNode: AnimatedStickerNode
|
||||
|
||||
private let shimmerEffectNode: ShimmerEffectForegroundNode
|
||||
private let buttonNode: HighlightTrackingButtonNode
|
||||
private let buttonStarsNode: PremiumStarsNode
|
||||
private let buttonTitleNode: TextNode
|
||||
@ -93,6 +94,9 @@ class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
self.buttonNode = HighlightTrackingButtonNode()
|
||||
self.buttonNode.clipsToBounds = true
|
||||
self.buttonNode.cornerRadius = 17.0
|
||||
|
||||
self.shimmerEffectNode = ShimmerEffectForegroundNode()
|
||||
self.shimmerEffectNode.cornerRadius = 17.0
|
||||
|
||||
self.placeholderNode = StickerShimmerEffectNode()
|
||||
self.placeholderNode.isUserInteractionEnabled = false
|
||||
@ -116,6 +120,7 @@ class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
self.addSubnode(self.animationNode)
|
||||
|
||||
self.addSubnode(self.buttonNode)
|
||||
self.buttonNode.addSubnode(self.shimmerEffectNode)
|
||||
self.buttonNode.addSubnode(self.buttonStarsNode)
|
||||
self.addSubnode(self.buttonTitleNode)
|
||||
|
||||
@ -151,6 +156,26 @@ class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
return
|
||||
}
|
||||
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) {
|
||||
@ -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
|
||||
} else if self.mediaBackgroundNode.frame.contains(point) {
|
||||
return .openMessage
|
||||
@ -546,6 +573,12 @@ class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
if !item.controllerInteraction.seenOneTimeAnimatedMedia.contains(item.message.id) {
|
||||
item.controllerInteraction.seenOneTimeAnimatedMedia.insert(item.message.id)
|
||||
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 {
|
||||
|
@ -18,6 +18,7 @@ import AvatarNode
|
||||
import ChatMessageDateAndStatusNode
|
||||
import ChatMessageBubbleContentNode
|
||||
import ChatMessageItemCommon
|
||||
import UndoUI
|
||||
|
||||
private let titleFont = Font.medium(15.0)
|
||||
private let textFont = Font.regular(13.0)
|
||||
@ -35,6 +36,8 @@ class ChatMessageGiveawayBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
private let participantsTitleNode: TextNode
|
||||
private let participantsTextNode: TextNode
|
||||
|
||||
private let countriesTextNode: TextNode
|
||||
|
||||
private let dateTitleNode: TextNode
|
||||
private let dateTextNode: TextNode
|
||||
|
||||
@ -81,6 +84,8 @@ class ChatMessageGiveawayBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
self.participantsTitleNode = TextNode()
|
||||
self.participantsTextNode = TextNode()
|
||||
|
||||
self.countriesTextNode = TextNode()
|
||||
|
||||
self.dateTitleNode = TextNode()
|
||||
self.dateTextNode = TextNode()
|
||||
|
||||
@ -98,6 +103,7 @@ class ChatMessageGiveawayBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
self.addSubnode(self.prizeTextNode)
|
||||
self.addSubnode(self.participantsTitleNode)
|
||||
self.addSubnode(self.participantsTextNode)
|
||||
self.addSubnode(self.countriesTextNode)
|
||||
self.addSubnode(self.dateTitleNode)
|
||||
self.addSubnode(self.dateTextNode)
|
||||
self.addSubnode(self.buttonNode)
|
||||
@ -137,8 +143,18 @@ class ChatMessageGiveawayBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
override func didLoad() {
|
||||
super.didLoad()
|
||||
|
||||
// let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.contactTap(_:)))
|
||||
// self.view.addGestureRecognizer(tapRecognizer)
|
||||
let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.bubbleTap(_:)))
|
||||
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) {
|
||||
@ -160,6 +176,8 @@ class ChatMessageGiveawayBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
let makeParticipantsTitleLayout = TextNode.asyncLayout(self.participantsTitleNode)
|
||||
let makeParticipantsTextLayout = TextNode.asyncLayout(self.participantsTextNode)
|
||||
|
||||
let makeCountriesTextLayout = TextNode.asyncLayout(self.countriesTextNode)
|
||||
|
||||
let makeDateTitleLayout = TextNode.asyncLayout(self.dateTitleNode)
|
||||
let makeDateTextLayout = TextNode.asyncLayout(self.dateTextNode)
|
||||
|
||||
@ -215,12 +233,14 @@ class ChatMessageGiveawayBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
|
||||
let participantsTitleString = NSAttributedString(string: "Participants", font: titleFont, textColor: textColor)
|
||||
let participantsText: String
|
||||
let countriesText: String
|
||||
|
||||
if let giveaway {
|
||||
if giveaway.flags.contains(.onlyNewSubscribers) {
|
||||
if giveaway.channelPeerIds.count > 1 {
|
||||
participantsText = "All users who join the channels below after this date:"
|
||||
} else {
|
||||
participantsText = "All users who join this channel below after this date:"
|
||||
participantsText = "All users who join this channel after this date:"
|
||||
}
|
||||
} else {
|
||||
if giveaway.channelPeerIds.count > 1 {
|
||||
@ -229,12 +249,41 @@ class ChatMessageGiveawayBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
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 {
|
||||
participantsText = ""
|
||||
countriesText = ""
|
||||
}
|
||||
|
||||
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)
|
||||
var dateTextString: NSAttributedString?
|
||||
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 (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 (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)
|
||||
|
||||
if countriesTextLayout.size.height > 0.0 {
|
||||
layoutSize.height += countriesTextLayout.size.height + 7.0
|
||||
}
|
||||
layoutSize.height += channelButtonSize.height
|
||||
|
||||
if let statusSizeAndApply = statusSizeAndApply {
|
||||
@ -414,16 +468,13 @@ class ChatMessageGiveawayBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
strongSelf.updateVisibility()
|
||||
|
||||
let _ = badgeTextApply()
|
||||
|
||||
let _ = prizeTitleApply()
|
||||
let _ = prizeTextApply()
|
||||
|
||||
let _ = participantsTitleApply()
|
||||
let _ = participantsTextApply()
|
||||
|
||||
let _ = countriesTextApply()
|
||||
let _ = dateTitleApply()
|
||||
let _ = dateTextApply()
|
||||
|
||||
let _ = channelButtonApply()
|
||||
let _ = buttonApply()
|
||||
|
||||
@ -456,7 +507,14 @@ class ChatMessageGiveawayBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
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)
|
||||
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)
|
||||
originY += dateTitleLayout.size.height + smallSpacing
|
||||
@ -522,25 +580,21 @@ class ChatMessageGiveawayBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
|
||||
override func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction {
|
||||
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) {
|
||||
return .ignore
|
||||
}
|
||||
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() {
|
||||
if let item = self.item {
|
||||
let _ = item.controllerInteraction.openMessage(item.message, .default)
|
||||
self.buttonNode.startShimmering()
|
||||
Queue.mainQueue().after(0.75) {
|
||||
self.buttonNode.stopShimmering()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|