Various improvements

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 44 KiB

View File

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

View File

@ -60,7 +60,7 @@ private func loadCountryCodes() -> [(String, Int)] {
private let countryCodes: [(String, Int)] = loadCountryCodes()
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)] = []

View File

@ -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)] {

View File

@ -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 {

View File

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

View File

@ -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
}
}

View File

@ -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
}

View File

@ -264,6 +264,14 @@ private final class PremiumGiftCodeSheetContent: CombinedComponent {
)
)
))
} else if giftCode.isGiveaway {
tableItems.append(.init(
id: "to",
title: "To",
component: AnyComponent(
MultilineTextComponent(text: .plain(NSAttributedString(string: "No recipient", font: tableFont, textColor: tableTextColor)))
)
))
}
let giftTitle: String
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",

View File

@ -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:

View File

@ -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) }

View File

@ -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

View File

@ -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

View File

@ -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)
}
}

View File

@ -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
}

View File

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

View File

@ -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])
}
}

View File

@ -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?

View File

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

View File

@ -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

View File

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

View File

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

View File

@ -11,11 +11,8 @@ import AccountContext
import TelegramCore
import 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) {

View File

@ -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
}
}

View File

@ -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)
}

View File

@ -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(

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

View File

@ -738,18 +738,19 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
strongSelf.bankCardDisposable = disposable
}
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)**."
}
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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

View File

@ -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 {

View File

@ -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 {

View File

@ -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()
}
}
}