Various improvements

This commit is contained in:
Ilya Laktyushin 2024-03-12 17:43:21 +04:00
parent 11aa9be45c
commit 8b1bd1dec0
74 changed files with 5570 additions and 805 deletions

View file

@ -11347,6 +11347,9 @@ Sorry for the inconvenience.";
"Premium.Business.Away.Title" = "Away Messages";
"Premium.Business.Away.Text" = "Define messages that are automatically sent when you are off.";
"Premium.Business.Intro.Title" = "Intro";
"Premium.Business.Intro.Text" = "Customize the message people see before they start a chat with you.";
"Premium.Business.Chatbots.Title" = "Chatbots";
"Premium.Business.Chatbots.Text" = "Add any third party chatbots that will process customer interactions.";
@ -11360,6 +11363,7 @@ Sorry for the inconvenience.";
"Business.GreetingMessages" = "Greeting Messages";
"Business.AwayMessages" = "Away Messages";
"Business.Chatbots" = "Chatbots";
"Business.Intro" = "Intro";
"Business.LocationInfo" = "Display the location of your business on your account.";
"Business.OpeningHoursInfo" = "Show to your customers when you are open for business.";
@ -11367,6 +11371,7 @@ Sorry for the inconvenience.";
"Business.GreetingMessagesInfo" = "Create greetings that will be automatically sent to new customers.";
"Business.AwayMessagesInfo" = "Define messages that are automatically sent when you are off.";
"Business.ChatbotsInfo" = "Add any third-party chatbots that will process customer interactions.";
"Business.IntroInfo" = "Customize the message people see before they start a chat with you.";
"Business.MoreFeaturesTitle" = "MORE BUSINESS FEATURES";
"Business.MoreFeaturesInfo" = "Check this section later for new business features.";
@ -11606,3 +11611,8 @@ Sorry for the inconvenience.";
"CollectibleItemInfo.ShareInlineText.LearnMore" = "Learn more >";
"Stickers.Edit" = "EDIT";
"ChannelBoost.Table.NoAds" = "Switch Off Ads";
"ChannelBoost.NoAds" = "Switch Off Ads";
"ChannelBoost.EnableNoAdsLevelText" = "Your channel needs **Level %1$@** to switch off ads.";

View file

@ -991,17 +991,19 @@ public protocol SharedAccountContext: AnyObject {
func makePremiumLimitController(context: AccountContext, subject: PremiumLimitSubject, count: Int32, forceDark: Bool, cancel: @escaping () -> Void, action: @escaping () -> Bool) -> ViewController
func makePremiumGiftController(context: AccountContext, source: PremiumGiftSource, completion: (() -> Void)?) -> ViewController
func makePremiumPrivacyControllerController(context: AccountContext, subject: PremiumPrivacySubject, peerId: EnginePeer.Id) -> ViewController
func makePremiumBoostLevelsController(context: AccountContext, peerId: EnginePeer.Id, boostStatus: ChannelBoostStatus, myBoostStatus: MyBoostStatus, forceDark: Bool, openStats: (() -> Void)?) -> ViewController
func makePremiumBoostLevelsController(context: AccountContext, peerId: EnginePeer.Id, subject: BoostSubject, boostStatus: ChannelBoostStatus, myBoostStatus: MyBoostStatus, forceDark: Bool, openStats: (() -> Void)?) -> ViewController
func makeStickerPackScreen(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?, mainStickerPack: StickerPackReference, stickerPacks: [StickerPackReference], loadedStickerPacks: [LoadedStickerPack], isEditing: Bool, parentNavigationController: NavigationController?, sendSticker: ((FileMediaReference, UIView, CGRect) -> Bool)?) -> ViewController
func makeMediaPickerScreen(context: AccountContext, hasSearch: Bool, completion: @escaping (Any) -> Void) -> ViewController
// func makeStickerEditorScreen(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?, initialSticker: TelegramMediaFile?, targetStickerPack: StickerPackReference?) -> ViewController
func makeStickerEditorScreen(context: AccountContext, source: Any, transitionArguments: (UIView, CGRect, UIImage?)?, completion: @escaping (TelegramMediaFile) -> Void) -> ViewController
func makeStickerMediaPickerScreen(context: AccountContext, getSourceRect: @escaping () -> CGRect, completion: @escaping (Any, UIView, CGRect, UIImage?, @escaping (Bool?) -> (UIView, CGRect)?, @escaping () -> Void) -> Void, dismissed: @escaping () -> Void) -> ViewController
func makeStoryMediaPickerScreen(context: AccountContext, getSourceRect: @escaping () -> CGRect, completion: @escaping (Any, UIView, CGRect, UIImage?, @escaping (Bool?) -> (UIView, CGRect)?, @escaping () -> Void) -> Void, dismissed: @escaping () -> Void, groupsPresented: @escaping () -> Void) -> ViewController
func makeStickerPickerScreen(context: AccountContext, inputData: Promise<StickerPickerInput>, completion: @escaping (TelegramMediaFile) -> Void) -> ViewController
func makeProxySettingsController(sharedContext: SharedAccountContext, account: UnauthorizedAccount) -> ViewController
func makeInstalledStickerPacksController(context: AccountContext, mode: InstalledStickerPacksControllerMode, forceTheme: PresentationTheme?) -> ViewController

View file

@ -35,3 +35,11 @@ public final class GalleryControllerActionInteraction {
self.updateCanReadHistory = updateCanReadHistory
}
}
public protocol StickerPackScreen {
}
public protocol StickerPickerInput {
}

View file

@ -101,6 +101,21 @@ public enum PremiumPrivacySubject {
case readTime
}
public enum BoostSubject: Equatable {
case stories
case channelReactions(reactionCount: Int32)
case nameColors(colors: PeerNameColor)
case nameIcon
case profileColors(colors: PeerNameColor)
case profileIcon
case emojiStatus
case wallpaper
case customWallpaper
case audioTranscription
case emojiPack
case noAds
}
public struct PremiumConfiguration {
public static var defaultValue: PremiumConfiguration {
return PremiumConfiguration(

View file

@ -107,6 +107,7 @@ swift_library(
"//submodules/Camera",
"//submodules/TelegramUI/Components/DustEffect",
"//submodules/TelegramUI/Components/DynamicCornerRadiusView",
"//submodules/TelegramUI/Components/StickerPickerScreen",
],
visibility = [
"//visibility:public",

View file

@ -24,6 +24,7 @@ import EntityKeyboard
import TelegramUIPreferences
import FastBlur
import MediaEditor
import StickerPickerScreen
public struct DrawingResultData {
public let data: Data?
@ -493,7 +494,7 @@ private final class DrawingScreenComponent: CombinedComponent {
let context: AccountContext
let sourceHint: DrawingScreen.SourceHint?
let existingStickerPickerInputData: Promise<StickerPickerInputData>?
let existingStickerPickerInputData: Promise<StickerPickerInput>?
let isVideo: Bool
let isAvatar: Bool
let isInteractingWithEntities: Bool
@ -529,7 +530,7 @@ private final class DrawingScreenComponent: CombinedComponent {
init(
context: AccountContext,
sourceHint: DrawingScreen.SourceHint?,
existingStickerPickerInputData: Promise<StickerPickerInputData>?,
existingStickerPickerInputData: Promise<StickerPickerInput>?,
isVideo: Bool,
isAvatar: Bool,
isInteractingWithEntities: Bool,
@ -682,11 +683,11 @@ private final class DrawingScreenComponent: CombinedComponent {
var lastSize: CGFloat = 0.5
private let stickerPickerInputData: Promise<StickerPickerInputData>
private let stickerPickerInputData: Promise<StickerPickerInput>
init(
context: AccountContext,
existingStickerPickerInputData: Promise<StickerPickerInputData>?,
existingStickerPickerInputData: Promise<StickerPickerInput>?,
updateToolState: ActionSlot<DrawingToolState>,
insertEntity: ActionSlot<DrawingEntity>,
deselectEntity: ActionSlot<Void>,
@ -728,7 +729,7 @@ private final class DrawingScreenComponent: CombinedComponent {
if let existingStickerPickerInputData {
self.stickerPickerInputData = existingStickerPickerInputData
} else {
self.stickerPickerInputData = Promise<StickerPickerInputData>()
self.stickerPickerInputData = Promise<StickerPickerInput>()
let stickerPickerInputData = self.stickerPickerInputData
Queue.concurrentDefaultQueue().after(0.5, {
@ -762,7 +763,7 @@ private final class DrawingScreenComponent: CombinedComponent {
let signal = combineLatest(queue: .mainQueue(),
emojiItems,
stickerItems
) |> map { emoji, stickers -> StickerPickerInputData in
) |> map { emoji, stickers -> StickerPickerInput in
return StickerPickerInputData(emoji: emoji, stickers: stickers, gifs: nil)
}
@ -1012,7 +1013,7 @@ private final class DrawingScreenComponent: CombinedComponent {
self.currentMode = .sticker
self.updateEntitiesPlayback.invoke(false)
let controller = StickerPickerScreen(context: self.context, inputData: self.stickerPickerInputData.get(), hasInteractiveStickers: false)
let controller = StickerPickerScreen(context: self.context, inputData: self.stickerPickerInputData.get(), forceDark: true, hasInteractiveStickers: false)
if let presentGallery = self.presentGallery {
controller.presentGallery = presentGallery
}
@ -2753,7 +2754,7 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController, U
private let externalDrawingView: DrawingView?
private let externalEntitiesView: DrawingEntitiesView?
private let externalSelectionContainerView: DrawingSelectionContainerView?
private let existingStickerPickerInputData: Promise<StickerPickerInputData>?
private let existingStickerPickerInputData: Promise<StickerPickerInput>?
public var requestDismiss: () -> Void = {}
public var requestApply: () -> Void = {}
@ -2762,7 +2763,7 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController, U
public var presentGallery: (() -> Void)?
public init(context: AccountContext, sourceHint: SourceHint? = nil, size: CGSize, originalSize: CGSize, isVideo: Bool, isAvatar: Bool, drawingView: DrawingView?, entitiesView: (UIView & TGPhotoDrawingEntitiesView)?, selectionContainerView: DrawingSelectionContainerView?, existingStickerPickerInputData: Promise<StickerPickerInputData>? = nil) {
public init(context: AccountContext, sourceHint: SourceHint? = nil, size: CGSize, originalSize: CGSize, isVideo: Bool, isAvatar: Bool, drawingView: DrawingView?, entitiesView: (UIView & TGPhotoDrawingEntitiesView)?, selectionContainerView: DrawingSelectionContainerView?, existingStickerPickerInputData: Promise<StickerPickerInput>? = nil) {
self.context = context
self.sourceHint = sourceHint
self.size = size

View file

@ -31,6 +31,7 @@ swift_library(
"//submodules/TelegramCore",
"//submodules/ComponentFlow",
"//submodules/TelegramUI/Components/TabSelectorComponent",
"//submodules/Components/ComponentDisplayAdapters",
],
visibility = [
"//visibility:public",

View file

@ -60,6 +60,7 @@ public enum ItemListControllerTitle: Equatable {
case text(String)
case textWithSubtitle(String, String)
case sectionControl([String], Int)
case textWithTabs(String, [String], Int)
}
public final class ItemListControllerTabBarItem: Equatable {
@ -113,6 +114,7 @@ open class ItemListController: ViewController, KeyShortcutResponder, Presentable
private var tabBarItemInfo: ItemListControllerTabBarItem?
private var navigationButtonActions: (left: (() -> Void)?, right: (() -> Void)?, secondaryRight: (() -> Void)?) = (nil, nil, nil)
private var segmentedTitleView: ItemListControllerSegmentedTitleView?
private var tabsNavigationContentNode: ItemListControllerTabsContentNode?
private var presentationData: ItemListPresentationData
@ -318,10 +320,12 @@ open class ItemListController: ViewController, KeyShortcutResponder, Presentable
strongSelf.title = text
strongSelf.navigationItem.titleView = nil
strongSelf.segmentedTitleView = nil
strongSelf.navigationBar?.setContentNode(nil, animated: false)
case let .textWithSubtitle(title, subtitle):
strongSelf.title = ""
strongSelf.navigationItem.titleView = ItemListTextWithSubtitleTitleView(theme: controllerState.presentationData.theme, title: title, subtitle: subtitle)
strongSelf.segmentedTitleView = nil
strongSelf.navigationBar?.setContentNode(nil, animated: false)
case let .sectionControl(sections, index):
strongSelf.title = ""
if let segmentedTitleView = strongSelf.segmentedTitleView, segmentedTitleView.segments == sections {
@ -336,6 +340,21 @@ open class ItemListController: ViewController, KeyShortcutResponder, Presentable
}
}
}
strongSelf.navigationBar?.setContentNode(nil, animated: false)
case let .textWithTabs(title, sections, index):
strongSelf.title = title
if let tabsNavigationContentNode = strongSelf.tabsNavigationContentNode, tabsNavigationContentNode.segments == sections {
tabsNavigationContentNode.index = index
} else {
let tabsNavigationContentNode = ItemListControllerTabsContentNode(theme: controllerState.presentationData.theme, segments: sections, selectedIndex: index)
strongSelf.tabsNavigationContentNode = tabsNavigationContentNode
strongSelf.navigationBar?.setContentNode(tabsNavigationContentNode, animated: false)
tabsNavigationContentNode.indexUpdated = { index in
if let strongSelf = self {
strongSelf.titleControlValueChanged?(index)
}
}
}
}
}
strongSelf.navigationButtonActions = (left: controllerState.leftNavigationButton?.action, right: controllerState.rightNavigationButton?.action, secondaryRight: controllerState.secondaryRightNavigationButton?.action)

View file

@ -361,14 +361,18 @@ open class ItemListControllerNode: ASDisplayNode {
}
self.listNode.visibleContentOffsetChanged = { [weak self] offset in
guard let strongSelf = self else {
return
}
var inVoiceOver = false
if let validLayout = self?.validLayout {
inVoiceOver = validLayout.0.inVoiceOver
}
strongSelf.contentOffsetChanged?(offset, inVoiceOver)
self?.contentOffsetChanged?(offset, inVoiceOver)
if let strongSelf = self {
if let navigationContentNode = strongSelf.navigationBar.contentNode, case .expansion = navigationContentNode.mode {
strongSelf.navigationBar.updateBackgroundAlpha(1.0, transition: .immediate)
} else {
var previousContentOffsetValue: CGFloat?
if let previousContentOffset = strongSelf.previousContentOffset {
if case let .known(value) = previousContentOffset {
@ -378,30 +382,30 @@ open class ItemListControllerNode: ASDisplayNode {
}
}
switch offset {
case let .known(value):
let transition: ContainedViewLayoutTransition
if let previousContentOffsetValue = previousContentOffsetValue, value <= 0.0, previousContentOffsetValue >= 30.0 {
transition = .animated(duration: 0.2, curve: .easeInOut)
} else {
transition = .immediate
}
if let headerItemNode = strongSelf.headerItemNode {
headerItemNode.updateContentOffset(value, transition: transition)
strongSelf.navigationBar.updateBackgroundAlpha(0.0, transition: .immediate)
} else {
strongSelf.navigationBar.updateBackgroundAlpha(min(30.0, value) / 30.0, transition: transition)
}
case .unknown, .none:
if let headerItemNode = strongSelf.headerItemNode {
headerItemNode.updateContentOffset(1000.0, transition: .immediate)
strongSelf.navigationBar.updateBackgroundAlpha(0.0, transition: .immediate)
} else {
strongSelf.navigationBar.updateBackgroundAlpha(1.0, transition: .immediate)
}
case let .known(value):
let transition: ContainedViewLayoutTransition
if let previousContentOffsetValue = previousContentOffsetValue, value <= 0.0, previousContentOffsetValue >= 30.0 {
transition = .animated(duration: 0.2, curve: .easeInOut)
} else {
transition = .immediate
}
if let headerItemNode = strongSelf.headerItemNode {
headerItemNode.updateContentOffset(value, transition: transition)
strongSelf.navigationBar.updateBackgroundAlpha(0.0, transition: .immediate)
} else {
strongSelf.navigationBar.updateBackgroundAlpha(min(30.0, value) / 30.0, transition: transition)
}
case .unknown, .none:
if let headerItemNode = strongSelf.headerItemNode {
headerItemNode.updateContentOffset(1000.0, transition: .immediate)
strongSelf.navigationBar.updateBackgroundAlpha(0.0, transition: .immediate)
} else {
strongSelf.navigationBar.updateBackgroundAlpha(1.0, transition: .immediate)
}
}
strongSelf.previousContentOffset = offset
}
strongSelf.previousContentOffset = offset
}
self.listNode.beganInteractiveDragging = { [weak self] _ in

View file

@ -1,10 +1,9 @@
import Foundation
import UIKit
import SegmentedControlNode
import Display
import TelegramPresentationData
import ComponentFlow
import TabSelectorComponent
import Display
public final class ItemListControllerSegmentedTitleView: UIView {
private let tabSelector = ComponentView<Empty>()

View file

@ -0,0 +1,124 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import TelegramPresentationData
import ComponentFlow
import ComponentDisplayAdapters
import TabSelectorComponent
private let searchBarFont = Font.regular(17.0)
final class ItemListControllerTabsContentNode: NavigationBarContentNode {
private let tabSelector = ComponentView<Empty>()
var theme: PresentationTheme {
didSet {
if self.theme !== oldValue {
self.update()
}
}
}
var segments: [String] {
didSet {
if self.segments != oldValue {
self.update()
}
}
}
var index: Int {
didSet {
if self.index != oldValue {
self.update(transition: .animated(duration: 0.35, curve: .spring))
}
}
}
var indexUpdated: ((Int) -> Void)?
private var validLayout: (CGSize, CGFloat, CGFloat)?
init(theme: PresentationTheme, segments: [String], selectedIndex: Int) {
self.theme = theme
self.segments = segments
self.index = selectedIndex
super.init()
}
override func didLoad() {
super.didLoad()
}
private func update(transition: ContainedViewLayoutTransition = .immediate) {
guard let (size, leftInset, rightInset) = self.validLayout else {
return
}
self.updateLayout(size: size, leftInset: leftInset, rightInset: rightInset, transition: transition)
}
override func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition) {
let isFirstTime = self.validLayout == nil
self.validLayout = (size, leftInset, rightInset)
let mappedItems = zip(0 ..< self.segments.count, self.segments).map { index, segment in
return TabSelectorComponent.Item(
id: AnyHashable(index),
title: segment
)
}
let tabSelectorSize = self.tabSelector.update(
transition: Transition(transition),
component: AnyComponent(TabSelectorComponent(
colors: TabSelectorComponent.Colors(
foreground: self.theme.list.itemSecondaryTextColor,
selection: self.theme.list.itemAccentColor
),
customLayout: TabSelectorComponent.CustomLayout(
font: Font.medium(14.0),
spacing: 24.0,
lineSelection: true
),
items: mappedItems,
selectedId: AnyHashable(self.index),
setSelectedId: { [weak self] id in
guard let self, let index = id.base as? Int else {
return
}
self.indexUpdated?(index)
}
)),
environment: {},
containerSize: CGSize(width: size.width, height: 44.0)
)
let tabSelectorFrame = CGRect(origin: CGPoint(x: floor((size.width - tabSelectorSize.width) / 2.0), y: floor((size.height - tabSelectorSize.height) / 2.0) + 4.0), size: tabSelectorSize)
if let tabSelectorView = self.tabSelector.view {
if tabSelectorView.superview == nil {
self.view.addSubview(tabSelectorView)
}
transition.updateFrame(view: tabSelectorView, frame: tabSelectorFrame)
}
if isFirstTime {
self.requestContainerLayout(.immediate)
}
}
override var height: CGFloat {
return self.nominalHeight
}
override var clippedHeight: CGFloat {
return self.nominalHeight
}
override var nominalHeight: CGFloat {
return 54.0// + self.additionalHeight
}
override var mode: NavigationBarContentMode {
return .expansion
}
}

View file

@ -39,19 +39,28 @@ public enum ItemListDisclosureLabelStyle {
case image(image: UIImage, size: CGSize)
}
public enum ItemListDisclosureItemDetailLabelColor {
case generic
case constructive
case destructive
}
public class ItemListDisclosureItem: ListViewItem, ItemListItem {
let presentationData: ItemListPresentationData
let icon: UIImage?
let context: AccountContext?
let iconPeer: EnginePeer?
let title: String
let attributedTitle: NSAttributedString?
let titleColor: ItemListDisclosureItemTitleColor
let titleFont: ItemListDisclosureItemTitleFont
let titleIcon: UIImage?
let enabled: Bool
let label: String
let attributedLabel: NSAttributedString?
let labelStyle: ItemListDisclosureLabelStyle
let additionalDetailLabel: String?
let additionalDetailLabelColor: ItemListDisclosureItemDetailLabelColor
public let sectionId: ItemListSectionId
let style: ItemListStyle
let disclosureStyle: ItemListDisclosureStyle
@ -60,19 +69,22 @@ public class ItemListDisclosureItem: ListViewItem, ItemListItem {
public let tag: ItemListItemTag?
public let shimmeringIndex: Int?
public init(presentationData: ItemListPresentationData, icon: UIImage? = nil, context: AccountContext? = nil, iconPeer: EnginePeer? = nil, title: String, enabled: Bool = true, titleColor: ItemListDisclosureItemTitleColor = .primary, titleFont: ItemListDisclosureItemTitleFont = .regular, titleIcon: UIImage? = nil, label: String, labelStyle: ItemListDisclosureLabelStyle = .text, additionalDetailLabel: String? = nil, sectionId: ItemListSectionId, style: ItemListStyle, disclosureStyle: ItemListDisclosureStyle = .arrow, action: (() -> Void)?, clearHighlightAutomatically: Bool = true, tag: ItemListItemTag? = nil, shimmeringIndex: Int? = nil) {
public init(presentationData: ItemListPresentationData, icon: UIImage? = nil, context: AccountContext? = nil, iconPeer: EnginePeer? = nil, title: String, attributedTitle: NSAttributedString? = nil, enabled: Bool = true, titleColor: ItemListDisclosureItemTitleColor = .primary, titleFont: ItemListDisclosureItemTitleFont = .regular, titleIcon: UIImage? = nil, label: String, attributedLabel: NSAttributedString? = nil, labelStyle: ItemListDisclosureLabelStyle = .text, additionalDetailLabel: String? = nil, additionalDetailLabelColor: ItemListDisclosureItemDetailLabelColor = .generic, sectionId: ItemListSectionId, style: ItemListStyle, disclosureStyle: ItemListDisclosureStyle = .arrow, action: (() -> Void)?, clearHighlightAutomatically: Bool = true, tag: ItemListItemTag? = nil, shimmeringIndex: Int? = nil) {
self.presentationData = presentationData
self.icon = icon
self.context = context
self.iconPeer = iconPeer
self.title = title
self.attributedTitle = attributedTitle
self.titleColor = titleColor
self.titleFont = titleFont
self.titleIcon = titleIcon
self.enabled = enabled
self.labelStyle = labelStyle
self.label = label
self.attributedLabel = attributedLabel
self.additionalDetailLabel = additionalDetailLabel
self.additionalDetailLabelColor = additionalDetailLabelColor
self.sectionId = sectionId
self.style = style
self.disclosureStyle = disclosureStyle
@ -358,7 +370,7 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode {
maxTitleWidth -= 12.0
}
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.title, font: titleFont, textColor: titleColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: maxTitleWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: item.attributedTitle ?? NSAttributedString(string: item.title, font: titleFont, textColor: titleColor), backgroundColor: nil, maximumNumberOfLines: item.attributedTitle != nil ? 0 : 1, truncationType: .end, constrainedSize: CGSize(width: maxTitleWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let detailFont = Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize * 15.0 / 17.0))
@ -392,7 +404,7 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode {
multilineLabel = true
}
let (labelLayout, labelApply) = makeLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.label, font: labelFont, textColor: labelBadgeColor), backgroundColor: nil, maximumNumberOfLines: multilineLabel ? 0 : 1, truncationType: .end, constrainedSize: CGSize(width: labelConstrain, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let (labelLayout, labelApply) = makeLabelLayout(TextNodeLayoutArguments(attributedString: item.attributedLabel ?? NSAttributedString(string: item.label, font: labelFont, textColor: labelBadgeColor), backgroundColor: nil, maximumNumberOfLines: multilineLabel ? 0 : 1, truncationType: .end, constrainedSize: CGSize(width: labelConstrain, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
var additionalDetailLabelInfo: (TextNodeLayout, () -> TextNode)?
if let additionalDetailLabel = item.additionalDetailLabel {
@ -400,7 +412,16 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode {
if labelLayout.size.width != 0 {
detailRightInset += labelLayout.size.width + 12.0
}
additionalDetailLabelInfo = makeAdditionalDetailLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: additionalDetailLabel, font: detailFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - detailRightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let additionalDetailColor: UIColor
switch item.additionalDetailLabelColor {
case .generic:
additionalDetailColor = item.presentationData.theme.list.itemSecondaryTextColor
case .constructive:
additionalDetailColor = item.presentationData.theme.list.itemDisclosureActions.constructive.fillColor
case .destructive:
additionalDetailColor = item.presentationData.theme.list.itemDestructiveColor
}
additionalDetailLabelInfo = makeAdditionalDetailLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: additionalDetailLabel, font: detailFont, textColor: additionalDetailColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - detailRightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
}
let verticalInset: CGFloat

View file

@ -116,6 +116,7 @@ swift_library(
"//submodules/TelegramUI/Components/ListActionItemComponent",
"//submodules/TelegramUI/Components/EmojiStatusSelectionComponent",
"//submodules/TelegramUI/Components/EntityKeyboard",
"//submodules/TelegramUI/Components/PremiumPeerShortcutComponent",
],
visibility = [
"//visibility:public",

View file

@ -301,14 +301,13 @@ private final class BusinessListComponent: CombinedComponent {
let strings = context.component.context.sharedContext.currentPresentationData.with { $0 }.strings
let colors = [
UIColor(rgb: 0x007aff),
UIColor(rgb: 0x798aff),
UIColor(rgb: 0xac64f3),
UIColor(rgb: 0xc456ae),
UIColor(rgb: 0xe95d44),
UIColor(rgb: 0xf2822a),
UIColor(rgb: 0xe79519),
UIColor(rgb: 0xe7ad19)
UIColor(rgb: 0xef6922),
UIColor(rgb: 0xe54937),
UIColor(rgb: 0xdb374b),
UIColor(rgb: 0xbc4395),
UIColor(rgb: 0x9b4fed),
UIColor(rgb: 0x8958ff),
UIColor(rgb: 0x676bff)
]
let titleColor = theme.list.itemPrimaryTextColor
@ -397,6 +396,20 @@ private final class BusinessListComponent: CombinedComponent {
)
)
items.append(
AnyComponentWithIdentity(
id: "intro",
component: AnyComponent(ParagraphComponent(
title: strings.Premium_Business_Intro_Title,
titleColor: titleColor,
text: strings.Premium_Business_Intro_Text,
textColor: textColor,
iconName: "Premium/Business/Intro",
iconColor: colors[5]
))
)
)
items.append(
AnyComponentWithIdentity(
id: "chatbots",
@ -406,7 +419,7 @@ private final class BusinessListComponent: CombinedComponent {
text: strings.Premium_Business_Chatbots_Text,
textColor: textColor,
iconName: "Premium/Business/Chatbots",
iconColor: colors[5]
iconColor: colors[6]
))
)
)

View file

@ -20,6 +20,7 @@ import SolidRoundedButtonComponent
import BlurredBackgroundComponent
import UndoUI
import ConfettiEffect
import PremiumPeerShortcutComponent
func requiredBoostSubjectLevel(subject: BoostSubject, group: Bool, context: AccountContext, configuration: PremiumConfiguration) -> Int32 {
switch subject {
@ -55,22 +56,12 @@ func requiredBoostSubjectLevel(subject: BoostSubject, group: Bool, context: Acco
return configuration.minGroupAudioTranscriptionLevel
case .emojiPack:
return configuration.minGroupEmojiPackLevel
case .noAds:
return 30
}
}
public enum BoostSubject: Equatable {
case stories
case channelReactions(reactionCount: Int32)
case nameColors(colors: PeerNameColor)
case nameIcon
case profileColors(colors: PeerNameColor)
case profileIcon
case emojiStatus
case wallpaper
case customWallpaper
case audioTranscription
case emojiPack
extension BoostSubject {
public func requiredLevel(group: Bool, context: AccountContext, configuration: PremiumConfiguration) -> Int32 {
return requiredBoostSubjectLevel(subject: self, group: group, context: context, configuration: configuration)
}
@ -247,6 +238,7 @@ private final class LevelSectionComponent: CombinedComponent {
case customWallpaper
case audioTranscription
case emojiPack
case noAds
func title(strings: PresentationStrings, isGroup: Bool) -> String {
switch self {
@ -274,6 +266,8 @@ private final class LevelSectionComponent: CombinedComponent {
return strings.GroupBoost_Table_Group_VoiceToText
case .emojiPack:
return strings.GroupBoost_Table_Group_EmojiPack
case .noAds:
return strings.ChannelBoost_Table_NoAds
}
}
@ -303,6 +297,8 @@ private final class LevelSectionComponent: CombinedComponent {
return "Premium/BoostPerk/AudioTranscription"
case .emojiPack:
return "Premium/BoostPerk/EmojiPack"
case .noAds:
return "Premium/BoostPerk/NoAds"
}
}
}
@ -639,6 +635,8 @@ private final class SheetContent: CombinedComponent {
textString = ""
case .emojiPack:
textString = strings.GroupBoost_EnableEmojiPackLevelText("\(requiredLevel)").string
case .noAds:
textString = strings.ChannelBoost_EnableNoAdsLevelText("\(requiredLevel)").string
}
} else {
let boostsString = strings.ChannelBoost_MoreBoostsNeeded_Boosts(Int32(remaining))
@ -733,11 +731,10 @@ private final class SheetContent: CombinedComponent {
let peerShortcut = peerShortcut.update(
component: Button(
content: AnyComponent(
PeerShortcutComponent(
PremiumPeerShortcutComponent(
context: component.context,
theme: component.theme,
peer: peer
)
),
action: {
@ -1095,85 +1092,100 @@ private final class SheetContent: CombinedComponent {
isFeatures = true
}
if let nextLevels {
for level in nextLevels {
var perks: [LevelSectionComponent.Perk] = []
perks.append(.story(level))
if !isGroup {
perks.append(.reaction(level))
}
var nameColorsCount: Int32 = 0
for (colorLevel, count) in nameColorsAtLevel {
if level >= colorLevel && colorLevel == 1 {
nameColorsCount = count
}
}
if !isGroup && nameColorsCount > 0 {
perks.append(.nameColor(nameColorsCount))
}
var profileColorsCount: Int32 = 0
for (colorLevel, count) in profileColorsAtLevel {
if level >= colorLevel {
profileColorsCount += count
}
}
if profileColorsCount > 0 {
perks.append(.profileColor(profileColorsCount))
}
func layoutLevel(_ level: Int32) {
var perks: [LevelSectionComponent.Perk] = []
if isGroup && level >= requiredBoostSubjectLevel(subject: .emojiPack, group: isGroup, context: component.context, configuration: premiumConfiguration) {
perks.append(.emojiPack)
}
perks.append(.story(level))
if level >= requiredBoostSubjectLevel(subject: .profileIcon, group: isGroup, context: component.context, configuration: premiumConfiguration) {
perks.append(.profileIcon)
if !isGroup {
perks.append(.reaction(level))
}
var nameColorsCount: Int32 = 0
for (colorLevel, count) in nameColorsAtLevel {
if level >= colorLevel && colorLevel == 1 {
nameColorsCount = count
}
if isGroup && level >= requiredBoostSubjectLevel(subject: .audioTranscription, group: isGroup, context: component.context, configuration: premiumConfiguration) {
perks.append(.audioTranscription)
}
if !isGroup && nameColorsCount > 0 {
perks.append(.nameColor(nameColorsCount))
}
var profileColorsCount: Int32 = 0
for (colorLevel, count) in profileColorsAtLevel {
if level >= colorLevel {
profileColorsCount += count
}
var linkColorsCount: Int32 = 0
for (colorLevel, count) in nameColorsAtLevel {
if level >= colorLevel {
linkColorsCount += count
}
}
if profileColorsCount > 0 {
perks.append(.profileColor(profileColorsCount))
}
if isGroup && level >= requiredBoostSubjectLevel(subject: .emojiPack, group: isGroup, context: component.context, configuration: premiumConfiguration) {
perks.append(.emojiPack)
}
if level >= requiredBoostSubjectLevel(subject: .profileIcon, group: isGroup, context: component.context, configuration: premiumConfiguration) {
perks.append(.profileIcon)
}
if isGroup && level >= requiredBoostSubjectLevel(subject: .audioTranscription, group: isGroup, context: component.context, configuration: premiumConfiguration) {
perks.append(.audioTranscription)
}
var linkColorsCount: Int32 = 0
for (colorLevel, count) in nameColorsAtLevel {
if level >= colorLevel {
linkColorsCount += count
}
if !isGroup && linkColorsCount > 0 {
perks.append(.linkColor(linkColorsCount))
}
if !isGroup && level >= requiredBoostSubjectLevel(subject: .nameIcon, group: isGroup, context: component.context, configuration: premiumConfiguration) {
perks.append(.linkIcon)
}
if level >= requiredBoostSubjectLevel(subject: .emojiStatus, group: isGroup, context: component.context, configuration: premiumConfiguration) {
perks.append(.emojiStatus)
}
if level >= requiredBoostSubjectLevel(subject: .wallpaper, group: isGroup, context: component.context, configuration: premiumConfiguration) {
perks.append(.wallpaper(8))
}
if level >= requiredBoostSubjectLevel(subject: .customWallpaper, group: isGroup, context: component.context, configuration: premiumConfiguration) {
perks.append(.customWallpaper)
}
levelItems.append(
AnyComponentWithIdentity(
id: level, component: AnyComponent(
LevelSectionComponent(
theme: component.theme,
strings: component.strings,
level: level,
isFirst: !isFeatures && levelItems.isEmpty,
perks: perks.reversed(),
isGroup: isGroup
)
}
if !isGroup && linkColorsCount > 0 {
perks.append(.linkColor(linkColorsCount))
}
if !isGroup && level >= requiredBoostSubjectLevel(subject: .nameIcon, group: isGroup, context: component.context, configuration: premiumConfiguration) {
perks.append(.linkIcon)
}
if level >= requiredBoostSubjectLevel(subject: .emojiStatus, group: isGroup, context: component.context, configuration: premiumConfiguration) {
perks.append(.emojiStatus)
}
if level >= requiredBoostSubjectLevel(subject: .wallpaper, group: isGroup, context: component.context, configuration: premiumConfiguration) {
perks.append(.wallpaper(8))
}
if level >= requiredBoostSubjectLevel(subject: .customWallpaper, group: isGroup, context: component.context, configuration: premiumConfiguration) {
perks.append(.customWallpaper)
}
if !isGroup && level >= requiredBoostSubjectLevel(subject: .noAds, group: isGroup, context: component.context, configuration: premiumConfiguration) {
perks.append(.noAds)
}
levelItems.append(
AnyComponentWithIdentity(
id: level, component: AnyComponent(
LevelSectionComponent(
theme: component.theme,
strings: component.strings,
level: level,
isFirst: !isFeatures && levelItems.isEmpty,
perks: perks.reversed(),
isGroup: isGroup
)
)
)
)
}
if let nextLevels {
for level in nextLevels {
layoutLevel(level)
}
}
if !isGroup {
let noAdsLevel = requiredBoostSubjectLevel(subject: .noAds, group: false, context: component.context, configuration: premiumConfiguration)
if level < noAdsLevel {
layoutLevel(noAdsLevel)
}
}
@ -1443,6 +1455,8 @@ private final class BoostLevelsContainerComponent: CombinedComponent {
titleString = strings.GroupBoost_AudioTranscription
case .emojiPack:
titleString = strings.GroupBoost_EmojiPack
case .noAds:
titleString = strings.ChannelBoost_NoAds
}
} else {
titleString = isGroup == true ? strings.GroupBoost_Title_Current : strings.ChannelBoost_Title_Current

View file

@ -1424,6 +1424,7 @@ public class PremiumDemoScreen: ViewControllerComponentContainer {
case businessQuickReplies
case businessAwayMessage
case businessChatBots
case businessIntro
}
public enum Source: Equatable {

View file

@ -456,6 +456,7 @@ public enum PremiumPerk: CaseIterable {
case businessQuickReplies
case businessAwayMessage
case businessChatBots
case businessIntro
public static var allCases: [PremiumPerk] {
return [
@ -488,10 +489,11 @@ public enum PremiumPerk: CaseIterable {
return [
.businessLocation,
.businessHours,
.businessGreetingMessage,
.businessQuickReplies,
.businessGreetingMessage,
.businessAwayMessage,
.businessChatBots,
.businessIntro,
.businessChatBots
// .emojiStatus,
// .folderTags,
// .stories,
@ -567,6 +569,8 @@ public enum PremiumPerk: CaseIterable {
return "away_message"
case .businessChatBots:
return "business_bots"
case .businessIntro:
return "business_intro"
}
}
@ -629,6 +633,8 @@ public enum PremiumPerk: CaseIterable {
return strings.Business_AwayMessages
case .businessChatBots:
return strings.Business_Chatbots
case .businessIntro:
return strings.Business_Intro
}
}
@ -691,6 +697,8 @@ public enum PremiumPerk: CaseIterable {
return strings.Business_AwayMessagesInfo
case .businessChatBots:
return strings.Business_ChatbotsInfo
case .businessIntro:
return strings.Business_IntroInfo
}
}
@ -753,6 +761,8 @@ public enum PremiumPerk: CaseIterable {
return "Premium/BusinessPerk/Away"
case .businessChatBots:
return "Premium/BusinessPerk/Chatbots"
case .businessIntro:
return "Premium/BusinessPerk/Intro"
}
}
}
@ -782,12 +792,13 @@ struct PremiumIntroConfiguration {
.premiumStickers,
.business
], businessPerks: [
.businessLocation,
.businessHours,
.businessQuickReplies,
.businessGreetingMessage,
.businessAwayMessage,
.businessQuickReplies,
.businessChatBots,
.businessHours,
.businessLocation
.businessIntro,
.businessChatBots
// .emojiStatus,
// .folderTags,
// .stories
@ -853,6 +864,9 @@ struct PremiumIntroConfiguration {
if businessPerks.count < 4 {
businessPerks = PremiumIntroConfiguration.defaultValue.businessPerks
}
if !businessPerks.contains(.businessIntro) {
businessPerks.append(.businessIntro)
}
return PremiumIntroConfiguration(perks: perks, businessPerks: businessPerks)
} else {
@ -2134,6 +2148,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
UIColor(rgb: 0xdb374b),
UIColor(rgb: 0xbc4395),
UIColor(rgb: 0x9b4fed),
UIColor(rgb: 0x8958ff),
UIColor(rgb: 0x8958ff)
]
@ -2244,6 +2259,8 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
demoSubject = .businessAwayMessage
case .businessChatBots:
demoSubject = .businessChatBots
case .businessIntro:
demoSubject = .businessIntro
default:
fatalError()
}

View file

@ -19,6 +19,7 @@ import ConfettiEffect
import AvatarNode
import TextFormat
import RoundedRectWithTailPath
import PremiumPeerShortcutComponent
func generateCloseButtonImage(backgroundColor: UIColor, foregroundColor: UIColor) -> UIImage? {
return generateImage(CGSize(width: 30.0, height: 30.0), contextGenerator: { size, context in
@ -1140,7 +1141,7 @@ private final class LimitSheetContent: CombinedComponent {
peerShortcutChild = peerShortcut.update(
component: Button(
content: AnyComponent(
PeerShortcutComponent(
PremiumPeerShortcutComponent(
context: component.context,
theme: environment.theme,
peer: peer
@ -1894,103 +1895,6 @@ public class PremiumLimitScreen: ViewControllerComponentContainer {
}
}
final class PeerShortcutComponent: Component {
let context: AccountContext
let theme: PresentationTheme
let peer: EnginePeer
init(context: AccountContext, theme: PresentationTheme, peer: EnginePeer) {
self.context = context
self.theme = theme
self.peer = peer
}
static func ==(lhs: PeerShortcutComponent, rhs: PeerShortcutComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.theme !== rhs.theme {
return false
}
if lhs.peer != rhs.peer {
return false
}
return true
}
final class View: UIView {
private let backgroundView = UIView()
private let avatarNode: AvatarNode
private let text = ComponentView<Empty>()
private var component: PeerShortcutComponent?
private weak var state: EmptyComponentState?
override init(frame: CGRect) {
self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 18.0))
super.init(frame: frame)
self.backgroundView.clipsToBounds = true
self.backgroundView.layer.cornerRadius = 16.0
self.addSubview(self.backgroundView)
self.addSubnode(self.avatarNode)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(component: PeerShortcutComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
self.component = component
self.state = state
self.backgroundView.backgroundColor = component.theme.list.itemBlocksSeparatorColor.withAlphaComponent(0.3)
self.avatarNode.frame = CGRect(origin: CGPoint(x: 1.0, y: 1.0), size: CGSize(width: 30.0, height: 30.0))
self.avatarNode.setPeer(
context: component.context,
theme: component.context.sharedContext.currentPresentationData.with({ $0 }).theme,
peer: component.peer,
synchronousLoad: true
)
let textSize = self.text.update(
transition: .immediate,
component: AnyComponent(
MultilineTextComponent(
text: .plain(NSAttributedString(string: component.peer.compactDisplayTitle, font: Font.medium(15.0), textColor: component.theme.list.itemPrimaryTextColor, paragraphAlignment: .left))
)
),
environment: {},
containerSize: CGSize(width: availableSize.width - 50.0, height: availableSize.height)
)
let size = CGSize(width: 30.0 + textSize.width + 20.0, height: 32.0)
if let view = self.text.view {
if view.superview == nil {
self.addSubview(view)
}
let textFrame = CGRect(origin: CGPoint(x: 38.0, y: floorToScreenPixels((size.height - textSize.height) / 2.0)), size: textSize)
view.frame = textFrame
}
self.backgroundView.frame = CGRect(origin: .zero, size: size)
return size
}
}
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)
}
}
public final class BoostIconComponent: Component {
let hasIcon: Bool
let text: String

View file

@ -979,6 +979,26 @@ public class PremiumLimitsListScreen: ViewController {
)
)
availableItems[.businessIntro] = DemoPagerComponent.Item(
AnyComponentWithIdentity(
id: PremiumDemoScreen.Subject.businessIntro,
component: AnyComponent(
PageComponent(
content: AnyComponent(PhoneDemoComponent(
context: context,
position: .top,
model: .island,
videoFile: videos["business_intro"],
decoration: .business
)),
title: strings.Business_Intro,
text: strings.Business_IntroInfo,
textColor: textColor
)
)
)
)
if let order = controller.order {
var items: [DemoPagerComponent.Item] = order.compactMap { availableItems[$0] }
let initialIndex: Int

View file

@ -82,6 +82,7 @@ public final class QrCodeScanScreen: ViewController {
public enum Subject {
case authTransfer(activeSessionsContext: ActiveSessionsContext)
case peer
case cryptoAddress
case custom(info: String)
}
@ -264,6 +265,8 @@ public final class QrCodeScanScreen: ViewController {
strongSelf.controllerNode.updateFocusedRect(nil)
}))
}
case .cryptoAddress:
break
case .peer:
if let _ = URL(string: code) {
strongSelf.controllerNode.resolveCode(code: code, completion: { [weak self] result in
@ -479,6 +482,9 @@ private final class QrCodeScanScreenNode: ViewControllerTracingNode, UIScrollVie
case .peer:
title = ""
text = ""
case .cryptoAddress:
title = ""
text = ""
case let .custom(info):
title = presentationData.strings.AuthSessions_AddDevice_ScanTitle
text = info
@ -596,6 +602,8 @@ private final class QrCodeScanScreenNode: ViewControllerTracingNode, UIScrollVie
filteredCodes = codes.filter { $0.message.hasPrefix("tg://") }
case .peer:
filteredCodes = codes.filter { $0.message.hasPrefix("https://t.me/") || $0.message.hasPrefix("t.me/") }
case .cryptoAddress:
filteredCodes = codes.filter { $0.message.hasPrefix("ton://") }
case .custom:
filteredCodes = codes
}

View file

@ -34,6 +34,10 @@ public final class SolidRoundedButtonTheme: Equatable {
self.disabledForegroundColor = disabledForegroundColor
}
public func withUpdated(disabledBackgroundColor: UIColor, disabledForegroundColor: UIColor) -> SolidRoundedButtonTheme {
return SolidRoundedButtonTheme(backgroundColor: self.backgroundColor, backgroundColors: self.backgroundColors, foregroundColor: self.foregroundColor, disabledBackgroundColor: disabledBackgroundColor, disabledForegroundColor: disabledForegroundColor)
}
public static func ==(lhs: SolidRoundedButtonTheme, rhs: SolidRoundedButtonTheme) -> Bool {
if lhs.backgroundColor != rhs.backgroundColor {
return false

View file

@ -10,35 +10,41 @@ swift_library(
"-warnings-as-errors",
],
deps = [
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
"//submodules/Display:Display",
"//submodules/SSignalKit/SwiftSignalKit",
"//submodules/AsyncDisplayKit",
"//submodules/Display",
"//submodules/ComponentFlow",
"//submodules/Postbox:Postbox",
"//submodules/TelegramCore:TelegramCore",
"//submodules/TelegramPresentationData:TelegramPresentationData",
"//submodules/TelegramUIPreferences:TelegramUIPreferences",
"//submodules/AccountContext:AccountContext",
"//submodules/ItemListUI:ItemListUI",
"//submodules/AvatarNode:AvatarNode",
"//submodules/TelegramStringFormatting:TelegramStringFormatting",
"//submodules/AlertUI:AlertUI",
"//submodules/PresentationDataUtils:PresentationDataUtils",
"//submodules/MergeLists:MergeLists",
"//submodules/PhotoResources:PhotoResources",
"//submodules/GraphCore:GraphCore",
"//submodules/GraphUI:GraphUI",
"//submodules/AnimatedStickerNode:AnimatedStickerNode",
"//submodules/TelegramAnimatedStickerNode:TelegramAnimatedStickerNode",
"//submodules/ItemListPeerItem:ItemListPeerItem",
"//submodules/ItemListPeerActionItem:ItemListPeerActionItem",
"//submodules/ContextUI:ContextUI",
"//submodules/PremiumUI:PremiumUI",
"//submodules/InviteLinksUI:InviteLinksUI",
"//submodules/ShareController:ShareController",
"//submodules/Postbox",
"//submodules/TelegramCore",
"//submodules/TelegramPresentationData",
"//submodules/TelegramUIPreferences",
"//submodules/AccountContext",
"//submodules/ItemListUI",
"//submodules/AvatarNode",
"//submodules/TelegramStringFormatting",
"//submodules/AlertUI",
"//submodules/PresentationDataUtils",
"//submodules/MergeLists",
"//submodules/PhotoResources",
"//submodules/GraphCore",
"//submodules/GraphUI",
"//submodules/AnimatedStickerNode",
"//submodules/TelegramAnimatedStickerNode",
"//submodules/ItemListPeerItem",
"//submodules/ItemListPeerActionItem",
"//submodules/ContextUI",
"//submodules/PremiumUI",
"//submodules/InviteLinksUI",
"//submodules/ShareController",
"//submodules/TextFormat",
"//submodules/TelegramUI/Components/Stories/AvatarStoryIndicatorComponent",
"//submodules/TelegramUI/Components/Stories/StoryContainerScreen",
"//submodules/TelegramUI/Components/PlainButtonComponent",
"//submodules/TelegramUI/Components/EmojiTextAttachmentView",
"//submodules/Components/MultilineTextComponent",
"//submodules/Components/MultilineTextWithEntitiesComponent",
"//submodules/QrCodeUI",
"//submodules/UIKitRuntimeUtils",
],
visibility = [
"//visibility:public",

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,322 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramCore
import TelegramUIPreferences
import TelegramPresentationData
import LegacyComponents
import ItemListUI
import PresentationDataUtils
import EmojiTextAttachmentView
import TextFormat
import AccountContext
import UIKitRuntimeUtils
final class CpmSliderItem: ListViewItem, ItemListItem {
let context: AccountContext
let theme: PresentationTheme
let strings: PresentationStrings
let value: Int32
let animatedEmoji: TelegramMediaFile?
let sectionId: ItemListSectionId
let updated: (Int32) -> Void
init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, value: Int32, enabled: Bool, animatedEmoji: TelegramMediaFile?, sectionId: ItemListSectionId, updated: @escaping (Int32) -> Void) {
self.context = context
self.theme = theme
self.strings = strings
self.value = value
self.animatedEmoji = animatedEmoji
self.sectionId = sectionId
self.updated = updated
}
func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
async {
let node = CpmSliderItemNode()
let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
node.contentSize = layout.contentSize
node.insets = layout.insets
Queue.mainQueue().async {
completion(node, {
return (nil, { _ in apply() })
})
}
}
}
func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
Queue.mainQueue().async {
if let nodeValue = node() as? CpmSliderItemNode {
let makeLayout = nodeValue.asyncLayout()
async {
let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
Queue.mainQueue().async {
completion(layout, { _ in
apply()
})
}
}
}
}
}
}
private let allowedValues: [Int32] = [1, 2, 3, 4, 5]
class CpmSliderItemNode: ListViewItemNode {
private let backgroundNode: ASDisplayNode
private let topStripeNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode
private let maskNode: ASImageNode
private let minTextNode: TextNode
private let maxTextNode: TextNode
private let textNode: TextNode
private var sliderView: TGPhotoEditorSliderView?
private var animatedEmojiLayer: InlineStickerItemLayer?
private var maxAnimatedEmojiLayer: InlineStickerItemLayer?
private var item: CpmSliderItem?
private var layoutParams: ListViewItemLayoutParams?
private var reportedValue: Int32?
init() {
self.backgroundNode = ASDisplayNode()
self.backgroundNode.isLayerBacked = true
self.maskNode = ASImageNode()
self.topStripeNode = ASDisplayNode()
self.topStripeNode.isLayerBacked = true
self.bottomStripeNode = ASDisplayNode()
self.bottomStripeNode.isLayerBacked = true
self.textNode = TextNode()
self.textNode.isUserInteractionEnabled = false
self.textNode.displaysAsynchronously = false
self.minTextNode = TextNode()
self.minTextNode.isUserInteractionEnabled = false
self.minTextNode.displaysAsynchronously = false
self.maxTextNode = TextNode()
self.maxTextNode.isUserInteractionEnabled = false
self.maxTextNode.displaysAsynchronously = false
super.init(layerBacked: false, dynamicBounce: false)
self.addSubnode(self.textNode)
self.addSubnode(self.minTextNode)
self.addSubnode(self.maxTextNode)
}
override func didLoad() {
super.didLoad()
self.view.disablesInteractiveTransitionGestureRecognizer = true
let sliderView = TGPhotoEditorSliderView()
sliderView.enablePanHandling = true
sliderView.trackCornerRadius = 2.0
sliderView.lineSize = 4.0
sliderView.dotSize = 5.0
sliderView.minimumValue = 0.0
sliderView.maximumValue = 1.0
sliderView.startValue = 0.0
sliderView.displayEdges = true
sliderView.disablesInteractiveTransitionGestureRecognizer = true
if let item = self.item, let params = self.layoutParams {
sliderView.value = CGFloat(item.value) / 50.0
sliderView.backgroundColor = item.theme.list.itemBlocksBackgroundColor
sliderView.backColor = item.theme.list.itemSwitchColors.frameColor
sliderView.startColor = item.theme.list.itemSwitchColors.frameColor
sliderView.trackColor = item.theme.list.itemAccentColor
sliderView.knobImage = PresentationResourcesItemList.knobImage(item.theme)
sliderView.frame = CGRect(origin: CGPoint(x: params.leftInset + 15.0, y: 37.0), size: CGSize(width: params.width - params.leftInset - params.rightInset - 15.0 * 2.0, height: 44.0))
sliderView.hitTestEdgeInsets = UIEdgeInsets(top: -sliderView.frame.minX, left: 0.0, bottom: 0.0, right: -sliderView.frame.minX)
}
self.view.addSubview(sliderView)
sliderView.addTarget(self, action: #selector(self.sliderValueChanged), for: .valueChanged)
self.sliderView = sliderView
}
func asyncLayout() -> (_ item: CpmSliderItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
let currentItem = self.item
let makeTextLayout = TextNode.asyncLayout(self.textNode)
let makeMinTextLayout = TextNode.asyncLayout(self.minTextNode)
let makeMaxTextLayout = TextNode.asyncLayout(self.maxTextNode)
return { item, params, neighbors in
var themeUpdated = false
if currentItem?.theme !== item.theme {
themeUpdated = true
}
let contentSize: CGSize
let insets: UIEdgeInsets
let separatorHeight = UIScreenPixel
//TODO:localize
let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.value == 0 ? "No Ads" : "\(item.value) CPM", font: Font.regular(17.0), textColor: item.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .center, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets()))
let (minTextLayout, minTextApply) = makeMinTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: "No Ads", font: Font.regular(13.0), textColor: item.theme.list.itemSecondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .center, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets()))
let (maxTextLayout, maxTextApply) = makeMaxTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: "50 CPM", font: Font.regular(13.0), textColor: item.theme.list.itemSecondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .center, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets()))
contentSize = CGSize(width: params.width, height: 88.0)
insets = itemListNeighborsGroupedInsets(neighbors, params)
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
let layoutSize = layout.size
return (layout, { [weak self] in
if let strongSelf = self {
strongSelf.item = item
strongSelf.layoutParams = params
strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBlocksBackgroundColor
strongSelf.topStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor
strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor
if strongSelf.backgroundNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
}
if strongSelf.topStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1)
}
if strongSelf.bottomStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2)
}
if strongSelf.maskNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.maskNode, at: 3)
}
let hasCorners = itemListHasRoundedBlockLayout(params)
var hasTopCorners = false
var hasBottomCorners = false
switch neighbors.top {
case .sameSection(false):
strongSelf.topStripeNode.isHidden = true
default:
hasTopCorners = true
strongSelf.topStripeNode.isHidden = hasCorners
}
let bottomStripeInset: CGFloat
let bottomStripeOffset: CGFloat
switch neighbors.bottom {
case .sameSection(false):
bottomStripeInset = 0.0
bottomStripeOffset = -separatorHeight
strongSelf.bottomStripeNode.isHidden = false
default:
bottomStripeInset = 0.0
hasBottomCorners = true
strongSelf.bottomStripeNode.isHidden = hasCorners
bottomStripeOffset = 0.0
}
strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0)
strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight))
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight))
let _ = textApply()
let textFrame = CGRect(origin: CGPoint(x: floor((params.width - textLayout.size.width) / 2.0), y: 12.0), size: textLayout.size)
strongSelf.textNode.frame = textFrame
if let animatedEmoji = item.animatedEmoji {
let itemSize = floorToScreenPixels(17.0 * 20.0 / 17.0)
var itemFrame = CGRect(origin: CGPoint(x: textFrame.minX - itemSize / 2.0 - 1.0, y: textFrame.midY), size: CGSize()).insetBy(dx: -itemSize / 2.0, dy: -itemSize / 2.0)
itemFrame.origin.x = floorToScreenPixels(itemFrame.origin.x)
itemFrame.origin.y = floorToScreenPixels(itemFrame.origin.y)
let itemLayer: InlineStickerItemLayer
if let current = strongSelf.animatedEmojiLayer {
itemLayer = current
} else {
let pointSize = floor(itemSize * 1.3)
itemLayer = InlineStickerItemLayer(context: item.context, userLocation: .other, attemptSynchronousLoad: true, emoji: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: animatedEmoji.fileId.id, file: animatedEmoji, custom: nil), file: animatedEmoji, cache: item.context.animationCache, renderer: item.context.animationRenderer, placeholderColor: item.theme.list.mediaPlaceholderColor, pointSize: CGSize(width: pointSize, height: pointSize), dynamicColor: nil)
strongSelf.animatedEmojiLayer = itemLayer
strongSelf.layer.addSublayer(itemLayer)
itemLayer.isVisibleForAnimations = true
}
itemLayer.frame = itemFrame
}
let _ = minTextApply()
strongSelf.minTextNode.frame = CGRect(origin: CGPoint(x: params.leftInset + 16.0, y: 16.0), size: minTextLayout.size)
let _ = maxTextApply()
let maxTextFrame = CGRect(origin: CGPoint(x: params.width - params.rightInset - 16.0 - maxTextLayout.size.width, y: 16.0), size: maxTextLayout.size)
strongSelf.maxTextNode.frame = maxTextFrame
if let animatedEmoji = item.animatedEmoji {
let itemSize = floorToScreenPixels(13.0 * 20.0 / 17.0)
var itemFrame = CGRect(origin: CGPoint(x: maxTextFrame.minX - itemSize / 2.0 - 1.0, y: maxTextFrame.midY), size: CGSize()).insetBy(dx: -itemSize / 2.0, dy: -itemSize / 2.0)
itemFrame.origin.x = floorToScreenPixels(itemFrame.origin.x)
itemFrame.origin.y = floorToScreenPixels(itemFrame.origin.y)
let itemLayer: InlineStickerItemLayer
if let current = strongSelf.maxAnimatedEmojiLayer {
itemLayer = current
} else {
let pointSize = floor(itemSize * 1.3)
itemLayer = InlineStickerItemLayer(context: item.context, userLocation: .other, attemptSynchronousLoad: true, emoji: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: animatedEmoji.fileId.id, file: animatedEmoji, custom: nil), file: animatedEmoji, cache: item.context.animationCache, renderer: item.context.animationRenderer, placeholderColor: item.theme.list.mediaPlaceholderColor, pointSize: CGSize(width: pointSize, height: pointSize), dynamicColor: nil)
strongSelf.maxAnimatedEmojiLayer = itemLayer
strongSelf.layer.addSublayer(itemLayer)
itemLayer.isVisibleForAnimations = true
if let filter = makeMonochromeFilter() {
filter.setValue([1.0, 1.0, 1.0, 1.0] as [NSNumber], forKey: "inputColor")
filter.setValue(1.0 as NSNumber, forKey: "inputAmount")
itemLayer.filters = [filter]
}
}
itemLayer.frame = itemFrame
}
if let sliderView = strongSelf.sliderView {
if themeUpdated {
sliderView.backgroundColor = item.theme.list.itemBlocksBackgroundColor
sliderView.backColor = item.theme.list.itemSwitchColors.frameColor
sliderView.startColor = item.theme.list.itemSwitchColors.frameColor
sliderView.trackColor = item.theme.list.itemAccentColor
sliderView.knobImage = PresentationResourcesItemList.knobImage(item.theme)
}
sliderView.frame = CGRect(origin: CGPoint(x: params.leftInset + 15.0, y: 37.0), size: CGSize(width: params.width - params.leftInset - params.rightInset - 15.0 * 2.0, height: 44.0))
sliderView.hitTestEdgeInsets = UIEdgeInsets(top: -sliderView.frame.minX, left: 0.0, bottom: 0.0, right: -sliderView.frame.minX)
}
}
})
}
}
override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
}
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
}
@objc func sliderValueChanged() {
guard let item = self.item, let sliderView = self.sliderView else {
return
}
item.updated(Int32(sliderView.value * 50.0))
}
}

View file

@ -381,7 +381,7 @@ private enum StatsEntry: ItemListNodeEntry {
let .topInvitersTitle(_, text, dates):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, accessoryText: ItemListSectionHeaderAccessoryText(value: dates, color: .generic), sectionId: self.section)
case let .overview(_, stats):
return StatsOverviewItem(presentationData: presentationData, isGroup: true, stats: stats, sectionId: self.section, style: .blocks)
return StatsOverviewItem(context: arguments.context, presentationData: presentationData, isGroup: true, stats: stats, sectionId: self.section, style: .blocks)
case let .growthGraph(_, _, _, graph, type),
let .membersGraph(_, _, _, graph, type),
let .newMembersBySourceGraph(_, _, _, graph, type),

View file

@ -160,7 +160,7 @@ private enum StatsEntry: ItemListNodeEntry {
let .publicForwardsTitle(_, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .overview(_, stats, storyViews, publicShares):
return StatsOverviewItem(presentationData: presentationData, isGroup: false, stats: stats as! Stats, storyViews: storyViews, publicShares: publicShares, sectionId: self.section, style: .blocks)
return StatsOverviewItem(context: arguments.context, presentationData: presentationData, isGroup: false, stats: stats as! Stats, storyViews: storyViews, publicShares: publicShares, sectionId: self.section, style: .blocks)
case let .interactionsGraph(_, _, _, graph, type, noInitialZoom), let .reactionsGraph(_, _, _, graph, type, noInitialZoom):
return StatsGraphItem(presentationData: presentationData, graph: graph, type: type, noInitialZoom: noInitialZoom, getDetailsData: { date, completion in
let _ = arguments.loadDetailedGraph(graph, Int64(date.timeIntervalSince1970) * 1000).start(next: { graph in

View file

@ -0,0 +1,488 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import AccountContext
import TelegramPresentationData
import ItemListUI
import SolidRoundedButtonNode
import TelegramCore
import EmojiTextAttachmentView
import TextFormat
final class MonetizationBalanceItem: ListViewItem, ItemListItem {
let context: AccountContext
let presentationData: ItemListPresentationData
let stats: MonetizationStats
let animatedEmoji: TelegramMediaFile?
let address: String
let withdrawAction: () -> Void
let qrAction: () -> Void
let action: (() -> Void)?
let textUpdated: (String) -> Void
let shouldUpdateText: (String) -> Bool
let processPaste: ((String) -> Void)?
let sectionId: ItemListSectionId
let style: ItemListStyle
init(
context: AccountContext,
presentationData: ItemListPresentationData,
stats: MonetizationStats,
animatedEmoji: TelegramMediaFile?,
address: String,
withdrawAction: @escaping () -> Void,
qrAction: @escaping () -> Void,
action: (() -> Void)?,
textUpdated: @escaping (String) -> Void,
shouldUpdateText: @escaping (String) -> Bool,
processPaste: ((String) -> Void)?,
sectionId: ItemListSectionId,
style: ItemListStyle
) {
self.context = context
self.presentationData = presentationData
self.stats = stats
self.animatedEmoji = animatedEmoji
self.address = address
self.withdrawAction = withdrawAction
self.qrAction = qrAction
self.action = action
self.textUpdated = textUpdated
self.shouldUpdateText = shouldUpdateText
self.processPaste = processPaste
self.sectionId = sectionId
self.style = style
}
func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
async {
let node = MonetizationBalanceItemNode()
let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
node.contentSize = layout.contentSize
node.insets = layout.insets
Queue.mainQueue().async {
completion(node, {
return (nil, { _ in apply() })
})
}
}
}
func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
Queue.mainQueue().async {
if let nodeValue = node() as? MonetizationBalanceItemNode {
let makeLayout = nodeValue.asyncLayout()
async {
let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
Queue.mainQueue().async {
completion(layout, { _ in
apply()
})
}
}
}
}
}
var selectable: Bool = false
}
final class MonetizationBalanceItemNode: ListViewItemNode, ItemListItemNode, ASEditableTextNodeDelegate {
private let backgroundNode: ASDisplayNode
private let topStripeNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode
private let maskNode: ASImageNode
private var animatedEmojiLayer: InlineStickerItemLayer?
private let balanceTextNode: TextNode
private let valueTextNode: TextNode
private let fieldNode: ASImageNode
private let textClippingNode: ASDisplayNode
private let textNode: EditableTextNode
private let measureTextNode: TextNode
private let qrButtonNode: HighlightableButtonNode
private var withdrawButtonNode: SolidRoundedButtonNode?
private let activateArea: AccessibilityAreaNode
private var item: MonetizationBalanceItem?
override var canBeSelected: Bool {
return false
}
var tag: ItemListItemTag? {
return self.item?.tag
}
init() {
self.backgroundNode = ASDisplayNode()
self.backgroundNode.isLayerBacked = true
self.backgroundNode.backgroundColor = .white
self.maskNode = ASImageNode()
self.topStripeNode = ASDisplayNode()
self.topStripeNode.isLayerBacked = true
self.bottomStripeNode = ASDisplayNode()
self.bottomStripeNode.isLayerBacked = true
self.balanceTextNode = TextNode()
self.balanceTextNode.isUserInteractionEnabled = false
self.balanceTextNode.displaysAsynchronously = false
self.valueTextNode = TextNode()
self.valueTextNode.isUserInteractionEnabled = false
self.valueTextNode.displaysAsynchronously = false
self.fieldNode = ASImageNode()
self.fieldNode.displaysAsynchronously = false
self.fieldNode.displayWithoutProcessing = true
self.textClippingNode = ASDisplayNode()
self.textClippingNode.clipsToBounds = true
self.textNode = EditableTextNode()
self.measureTextNode = TextNode()
self.qrButtonNode = HighlightableButtonNode()
self.activateArea = AccessibilityAreaNode()
super.init(layerBacked: false, dynamicBounce: false)
self.addSubnode(self.balanceTextNode)
self.addSubnode(self.valueTextNode)
self.addSubnode(self.fieldNode)
self.addSubnode(self.qrButtonNode)
self.textClippingNode.addSubnode(self.textNode)
self.addSubnode(self.textClippingNode)
self.qrButtonNode.addTarget(self, action: #selector(self.qrButtonPressed), forControlEvents: .touchUpInside)
}
override public func didLoad() {
super.didLoad()
var textColor: UIColor = .black
if let item = self.item {
textColor = item.presentationData.theme.list.itemPrimaryTextColor
self.textNode.typingAttributes = [NSAttributedString.Key.font.rawValue: Font.regular(item.presentationData.fontSize.itemListBaseFontSize), NSAttributedString.Key.foregroundColor.rawValue: textColor]
} else {
self.textNode.typingAttributes = [NSAttributedString.Key.font.rawValue: Font.regular(17.0), NSAttributedString.Key.foregroundColor.rawValue: textColor]
}
self.textNode.clipsToBounds = true
self.textNode.delegate = self
self.textNode.hitTestSlop = UIEdgeInsets(top: -5.0, left: -5.0, bottom: -5.0, right: -5.0)
}
@objc private func qrButtonPressed() {
guard let item = self.item else {
return
}
item.qrAction()
}
func asyncLayout() -> (_ item: MonetizationBalanceItem, _ params: ListViewItemLayoutParams, _ insets: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
let currentItem = self.item
let makeBalanceTextLayout = TextNode.asyncLayout(self.balanceTextNode)
let makeValueTextLayout = TextNode.asyncLayout(self.valueTextNode)
let makeTextLayout = TextNode.asyncLayout(self.measureTextNode)
return { item, params, neighbors in
var updatedTheme: PresentationTheme?
if currentItem?.presentationData.theme !== item.presentationData.theme {
updatedTheme = item.presentationData.theme
}
let contentSize: CGSize
let insets: UIEdgeInsets
let separatorHeight = UIScreenPixel
let itemBackgroundColor: UIColor
let itemSeparatorColor: UIColor
let leftInset = 16.0 + params.leftInset
let rightInset = 16.0 + params.rightInset
let constrainedWidth = params.width - leftInset - rightInset
let integralFont = Font.with(size: 48.0, design: .round, weight: .semibold)
let fractionalFont = Font.with(size: 24.0, design: .round, weight: .semibold)
let cryptoValue = formatBalanceText(item.stats.availableBalance.cryptoAmount, decimalSeparator: item.presentationData.dateTimeFormat.decimalSeparator)
let amountString = amountAttributedString(cryptoValue, integralFont: integralFont, fractionalFont: fractionalFont, color: item.presentationData.theme.list.itemPrimaryTextColor)
let (balanceLayout, balanceApply) = makeBalanceTextLayout(TextNodeLayoutArguments(attributedString: amountString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .middle, constrainedSize: CGSize(width: constrainedWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
let value = "≈$100"
let (valueLayout, valueApply) = makeValueTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: value, font: Font.regular(17.0), textColor: item.presentationData.theme.list.itemSecondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .middle, constrainedSize: CGSize(width: constrainedWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
var measureText = item.address
if measureText.hasSuffix("\n") || measureText.isEmpty {
measureText += "|"
}
let attributedMeasureText = NSAttributedString(string: measureText, font: Font.regular(item.presentationData.fontSize.itemListBaseFontSize), textColor: .black)
let attributedText = NSAttributedString(string: item.address, font: Font.regular(item.presentationData.fontSize.itemListBaseFontSize), textColor: item.presentationData.theme.list.itemPrimaryTextColor)
let (textLayout, _) = makeTextLayout(TextNodeLayoutArguments(attributedString: attributedMeasureText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - rightInset - 12.0 - 36.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets()))
let attributedPlaceholderText = NSAttributedString(string: "Enter your TON address", font: Font.regular(item.presentationData.fontSize.itemListBaseFontSize), textColor: item.presentationData.theme.list.itemPlaceholderTextColor)
let verticalInset: CGFloat = 16.0
let fieldHeight: CGFloat = max(52.0, textLayout.size.height + 32.0)
let fieldSpacing: CGFloat = 16.0
let buttonHeight: CGFloat = 50.0
var height: CGFloat = verticalInset * 2.0 + balanceLayout.size.height + 7.0
if valueLayout.size.height > 0.0 {
height += valueLayout.size.height
height += fieldHeight + fieldSpacing + buttonHeight
}
switch item.style {
case .plain:
itemBackgroundColor = item.presentationData.theme.list.plainBackgroundColor
itemSeparatorColor = .clear
insets = UIEdgeInsets()
case .blocks:
itemBackgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor
itemSeparatorColor = item.presentationData.theme.list.itemBlocksSeparatorColor
insets = itemListNeighborsGroupedInsets(neighbors, params)
}
contentSize = CGSize(width: params.width, height: height)
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
return (ListViewItemNodeLayout(contentSize: contentSize, insets: insets), { [weak self] in
if let strongSelf = self {
strongSelf.item = item
let _ = balanceApply()
let _ = valueApply()
strongSelf.activateArea.frame = CGRect(origin: CGPoint(x: params.leftInset, y: 0.0), size: CGSize(width: params.width - params.leftInset - params.rightInset, height: layout.contentSize.height))
strongSelf.activateArea.accessibilityTraits = []
if let _ = updatedTheme {
strongSelf.topStripeNode.backgroundColor = itemSeparatorColor
strongSelf.bottomStripeNode.backgroundColor = itemSeparatorColor
strongSelf.backgroundNode.backgroundColor = itemBackgroundColor
strongSelf.fieldNode.image = generateStretchableFilledCircleImage(diameter: 18.0, color: item.presentationData.theme.list.itemInputField.backgroundColor)
strongSelf.qrButtonNode.setImage(generateTintedImage(image: UIImage(bundleImageName: "Settings/QrButtonIcon"), color: item.presentationData.theme.list.itemAccentColor), for: .normal)
}
switch item.style {
case .plain:
if strongSelf.backgroundNode.supernode != nil {
strongSelf.backgroundNode.removeFromSupernode()
}
if strongSelf.topStripeNode.supernode != nil {
strongSelf.topStripeNode.removeFromSupernode()
}
if strongSelf.bottomStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 0)
}
if strongSelf.maskNode.supernode != nil {
strongSelf.maskNode.removeFromSupernode()
}
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: leftInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - leftInset, height: separatorHeight))
case .blocks:
if strongSelf.backgroundNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
}
if strongSelf.topStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1)
}
if strongSelf.bottomStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2)
}
if strongSelf.maskNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.maskNode, at: 3)
}
let hasCorners = itemListHasRoundedBlockLayout(params)
var hasTopCorners = false
var hasBottomCorners = false
switch neighbors.top {
case .sameSection(false):
strongSelf.topStripeNode.isHidden = true
default:
hasTopCorners = true
strongSelf.topStripeNode.isHidden = hasCorners
}
let bottomStripeInset: CGFloat
switch neighbors.bottom {
case .sameSection(false):
bottomStripeInset = leftInset
strongSelf.bottomStripeNode.isHidden = false
default:
bottomStripeInset = 0.0
hasBottomCorners = true
strongSelf.bottomStripeNode.isHidden = hasCorners
}
strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0)
strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: separatorHeight))
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - bottomStripeInset, height: separatorHeight))
}
var emojiItemFrame: CGRect = .zero
var emojiItemSize: CGFloat = 0.0
if let animatedEmoji = item.animatedEmoji {
emojiItemSize = floorToScreenPixels(46.0 * 20.0 / 17.0)
emojiItemFrame = CGRect(origin: CGPoint(x: -emojiItemSize / 2.0 - 5.0, y: -3.0), size: CGSize()).insetBy(dx: -emojiItemSize / 2.0, dy: -emojiItemSize / 2.0)
emojiItemFrame.origin.x = floorToScreenPixels(emojiItemFrame.origin.x)
emojiItemFrame.origin.y = floorToScreenPixels(emojiItemFrame.origin.y)
let itemLayer: InlineStickerItemLayer
if let current = strongSelf.animatedEmojiLayer {
itemLayer = current
} else {
let pointSize = floor(emojiItemSize * 1.3)
itemLayer = InlineStickerItemLayer(context: item.context, userLocation: .other, attemptSynchronousLoad: true, emoji: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: animatedEmoji.fileId.id, file: animatedEmoji, custom: nil), file: animatedEmoji, cache: item.context.animationCache, renderer: item.context.animationRenderer, placeholderColor: item.presentationData.theme.list.mediaPlaceholderColor, pointSize: CGSize(width: pointSize, height: pointSize), dynamicColor: nil)
strongSelf.animatedEmojiLayer = itemLayer
strongSelf.layer.addSublayer(itemLayer)
itemLayer.isVisibleForAnimations = true
}
}
let balanceTotalWidth: CGFloat = emojiItemSize + balanceLayout.size.width
let balanceTextFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((params.width - balanceTotalWidth) / 2.0) + emojiItemSize, y: 13.0), size: balanceLayout.size)
strongSelf.balanceTextNode.frame = balanceTextFrame
strongSelf.animatedEmojiLayer?.frame = emojiItemFrame.offsetBy(dx: balanceTextFrame.minX, dy: balanceTextFrame.midY)
strongSelf.valueTextNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((params.width - valueLayout.size.width) / 2.0), y: balanceTextFrame.maxY - 5.0), size: valueLayout.size)
strongSelf.textNode.textView.autocapitalizationType = .none
strongSelf.textNode.textView.autocorrectionType = .no
strongSelf.textNode.textView.returnKeyType = .done
if let currentText = strongSelf.textNode.attributedText {
if currentText.string != attributedText.string || updatedTheme != nil {
strongSelf.textNode.attributedText = attributedText
}
} else {
strongSelf.textNode.attributedText = attributedText
}
if strongSelf.textNode.attributedPlaceholderText == nil || !strongSelf.textNode.attributedPlaceholderText!.isEqual(to: attributedPlaceholderText) {
strongSelf.textNode.attributedPlaceholderText = attributedPlaceholderText
}
strongSelf.textNode.keyboardAppearance = item.presentationData.theme.rootController.keyboardColor.keyboardAppearance
let textTopInset: CGFloat = 108.0
if strongSelf.animationForKey("apparentHeight") == nil {
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0)
strongSelf.textClippingNode.frame = CGRect(origin: CGPoint(x: leftInset + 12.0, y: textTopInset + 15.0), size: CGSize(width: params.width - leftInset - rightInset - 12.0 - 36.0, height: textLayout.size.height))
}
strongSelf.textNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: params.width - leftInset - rightInset - 12.0 - 36.0, height: textLayout.size.height + 1.0))
let fieldFrame = CGRect(origin: CGPoint(x: leftInset, y: textTopInset), size: CGSize(width: params.width - leftInset - rightInset, height: fieldHeight))
strongSelf.fieldNode.frame = fieldFrame
let qrButtonSize = CGSize(width: 32.0, height: 32.0)
let qrButtonFrame = CGRect(origin: CGPoint(x: fieldFrame.maxX - qrButtonSize.width - 5.0, y: fieldFrame.midY - qrButtonSize.height / 2.0), size: qrButtonSize)
strongSelf.qrButtonNode.frame = qrButtonFrame
let withdrawButtonNode: SolidRoundedButtonNode
if let currentShareButtonNode = strongSelf.withdrawButtonNode {
withdrawButtonNode = currentShareButtonNode
} else {
var buttonTheme = SolidRoundedButtonTheme(theme: item.presentationData.theme)
buttonTheme = buttonTheme.withUpdated(disabledBackgroundColor: buttonTheme.backgroundColor, disabledForegroundColor: buttonTheme.foregroundColor.withAlphaComponent(0.6))
withdrawButtonNode = SolidRoundedButtonNode(theme: buttonTheme, height: buttonHeight, cornerRadius: 11.0)
withdrawButtonNode.pressed = { [weak self] in
if let self, let item = self.item {
item.withdrawAction()
}
}
strongSelf.addSubnode(withdrawButtonNode)
strongSelf.withdrawButtonNode = withdrawButtonNode
}
if cryptoValue != "0" {
withdrawButtonNode.title = "Transfer \(cryptoValue) TON"
}
withdrawButtonNode.isEnabled = (strongSelf.textNode.attributedText?.string.count ?? 0) == walletAddressLength
let buttonWidth = contentSize.width - leftInset - rightInset
let _ = withdrawButtonNode.updateLayout(width: buttonWidth, transition: .immediate)
withdrawButtonNode.frame = CGRect(x: leftInset, y: fieldFrame.maxY + fieldSpacing, width: buttonWidth, height: buttonHeight)
}
})
}
}
public func editableTextNode(_ editableTextNode: ASEditableTextNode, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
if let item = self.item {
if text.count > 1, let processPaste = item.processPaste {
processPaste(text)
return false
}
if let action = item.action, text == "\n" {
action()
return false
}
let newText = (editableTextNode.textView.text as NSString).replacingCharacters(in: range, with: text)
if !item.shouldUpdateText(newText) {
return false
}
}
return true
}
public func editableTextNodeDidUpdateText(_ editableTextNode: ASEditableTextNode) {
if let item = self.item {
if let text = self.textNode.attributedText {
let updatedText = text.string
let updatedAttributedText = NSAttributedString(string: updatedText, font: Font.regular(item.presentationData.fontSize.itemListBaseFontSize), textColor: item.presentationData.theme.list.itemPrimaryTextColor)
if text.string != updatedAttributedText.string {
self.textNode.attributedText = updatedAttributedText
}
self.withdrawButtonNode?.isEnabled = (self.textNode.attributedText?.string.count ?? 0) == walletAddressLength
item.textUpdated(updatedText)
} else {
item.textUpdated("")
}
}
}
public func editableTextNodeShouldPaste(_ editableTextNode: ASEditableTextNode) -> Bool {
if let _ = self.item {
let text: String? = UIPasteboard.general.string
if let _ = text {
return true
}
}
return false
}
override public func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
}
override public func animateAdded(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
override public func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
}
}

View file

@ -0,0 +1,591 @@
import Foundation
import UIKit
import Display
import ComponentFlow
import SwiftSignalKit
import TelegramCore
import Markdown
import TextFormat
import TelegramPresentationData
import ViewControllerComponent
import SheetComponent
import BundleIconComponent
import BalancedTextComponent
import MultilineTextComponent
import MultilineTextWithEntitiesComponent
import SolidRoundedButtonComponent
import LottieComponent
import AccountContext
private final class SheetContent: CombinedComponent {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let context: AccountContext
let animatedEmojis: [String: TelegramMediaFile]
let openMore: () -> Void
let dismiss: () -> Void
init(
context: AccountContext,
animatedEmojis: [String: TelegramMediaFile],
openMore: @escaping () -> Void,
dismiss: @escaping () -> Void
) {
self.context = context
self.animatedEmojis = animatedEmojis
self.openMore = openMore
self.dismiss = dismiss
}
static func ==(lhs: SheetContent, rhs: SheetContent) -> Bool {
if lhs.context !== rhs.context {
return false
}
return true
}
final class State: ComponentState {
var cachedIconImage: (UIImage, PresentationTheme)?
var cachedChevronImage: (UIImage, PresentationTheme)?
let playOnce = ActionSlot<Void>()
private var didPlayAnimation = false
func playAnimationIfNeeded() {
guard !self.didPlayAnimation else {
return
}
self.didPlayAnimation = true
self.playOnce.invoke(Void())
}
}
func makeState() -> State {
return State()
}
static var body: Body {
let iconBackground = Child(Image.self)
let icon = Child(BundleIconComponent.self)
let title = Child(BalancedTextComponent.self)
let list = Child(List<Empty>.self)
let actionButton = Child(SolidRoundedButtonComponent.self)
let infoBackground = Child(RoundedRectangle.self)
let infoTitle = Child(MultilineTextWithEntitiesComponent.self)
let infoText = Child(MultilineTextComponent.self)
return { context in
let environment = context.environment[EnvironmentType.self]
let component = context.component
let state = context.state
let theme = environment.theme
// let strings = environment.strings
let sideInset: CGFloat = 16.0 + environment.safeInsets.left
let textSideInset: CGFloat = 30.0 + environment.safeInsets.left
let titleFont = Font.semibold(20.0)
let textFont = Font.regular(15.0)
let textColor = theme.actionSheet.primaryTextColor
let secondaryTextColor = theme.actionSheet.secondaryTextColor
let linkColor = theme.actionSheet.controlAccentColor
let markdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: textFont, textColor: textColor), link: MarkdownAttributeSet(font: textFont, textColor: linkColor), linkAttribute: { contents in
return (TelegramTextAttributes.URL, contents)
})
//TODO:localize
let spacing: CGFloat = 16.0
var contentSize = CGSize(width: context.availableSize.width, height: 32.0)
let iconSize = CGSize(width: 90.0, height: 90.0)
let gradientImage: UIImage
if let (current, currentTheme) = state.cachedIconImage, currentTheme === theme {
gradientImage = current
} else {
gradientImage = generateGradientFilledCircleImage(diameter: iconSize.width, colors: [
UIColor(rgb: 0x4bbb45).cgColor,
UIColor(rgb: 0x9ad164).cgColor
])!
context.state.cachedIconImage = (gradientImage, theme)
}
let iconBackground = iconBackground.update(
component: Image(image: gradientImage),
availableSize: iconSize,
transition: .immediate
)
context.add(iconBackground
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + iconBackground.size.height / 2.0))
)
let icon = icon.update(
component: BundleIconComponent(name: "Chart/Monetization", tintColor: theme.list.itemCheckColors.foregroundColor),
availableSize: CGSize(width: 90, height: 90),
transition: .immediate
)
context.add(icon
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + iconBackground.size.height / 2.0))
)
contentSize.height += iconSize.height
contentSize.height += spacing + 5.0
let title = title.update(
component: BalancedTextComponent(
text: .plain(NSAttributedString(string: "Earn From Your Channel", font: titleFont, textColor: textColor)),
horizontalAlignment: .center,
maximumNumberOfLines: 0,
lineSpacing: 0.1
),
availableSize: CGSize(width: context.availableSize.width - textSideInset * 2.0, height: context.availableSize.height),
transition: .immediate
)
context.add(title
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + title.size.height / 2.0))
)
contentSize.height += title.size.height
contentSize.height += spacing
var items: [AnyComponentWithIdentity<Empty>] = []
items.append(
AnyComponentWithIdentity(
id: "ads",
component: AnyComponent(ParagraphComponent(
title: "Telegram Ads",
titleColor: textColor,
text: "Telegram can display ads in your channel.",
textColor: secondaryTextColor,
iconName: "Chart/Ads",
iconColor: linkColor
))
)
)
items.append(
AnyComponentWithIdentity(
id: "split",
component: AnyComponent(ParagraphComponent(
title: "50:50 Revenue Split",
titleColor: textColor,
text: "You receive 50% of the ad revenue in TON.",
textColor: secondaryTextColor,
iconName: "Chart/Split",
iconColor: linkColor
))
)
)
items.append(
AnyComponentWithIdentity(
id: "withdrawal",
component: AnyComponent(ParagraphComponent(
title: "Flexible Withdrawals",
titleColor: textColor,
text: "You can withdraw your TON any time.",
textColor: secondaryTextColor,
iconName: "Chart/Withdrawal",
iconColor: linkColor
))
)
)
let list = list.update(
component: List(items),
availableSize: CGSize(width: context.availableSize.width - sideInset, height: 10000.0),
transition: context.transition
)
context.add(list
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + list.size.height / 2.0))
)
contentSize.height += list.size.height
contentSize.height += spacing - 9.0
let infoTitleString = "What's #TON?"//.replacingOccurrences(of: "#", with: "# ")
let infoTitleAttributedString = NSMutableAttributedString(string: infoTitleString, font: titleFont, textColor: textColor)
let range = (infoTitleAttributedString.string as NSString).range(of: "#")
if range.location != NSNotFound, let emojiFile = component.animatedEmojis["💎"] {
infoTitleAttributedString.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: emojiFile.fileId.id, file: emojiFile), range: range)
}
let infoTitle = infoTitle.update(
component: MultilineTextWithEntitiesComponent(
context: component.context,
animationCache: component.context.animationCache,
animationRenderer: component.context.animationRenderer,
placeholderColor: environment.theme.list.mediaPlaceholderColor,
text: .plain(infoTitleAttributedString),
horizontalAlignment: .center
),
availableSize: CGSize(width: context.availableSize.width - textSideInset * 2.0, height: context.availableSize.height),
transition: .immediate
)
if state.cachedChevronImage == nil || state.cachedChevronImage?.1 !== environment.theme {
state.cachedChevronImage = (generateTintedImage(image: UIImage(bundleImageName: "Settings/TextArrowRight"), color: linkColor)!, theme)
}
let infoString = "TON is a blockchain platform and cryptocurrency that Telegram uses for its record scalability and ultra low commissions on transactions.\n[Learn More >]()"
let infoAttributedString = parseMarkdownIntoAttributedString(infoString, attributes: markdownAttributes).mutableCopy() as! NSMutableAttributedString
if let range = infoAttributedString.string.range(of: ">"), let chevronImage = state.cachedChevronImage?.0 {
infoAttributedString.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: infoAttributedString.string))
}
let infoText = infoText.update(
component: MultilineTextComponent(
text: .plain(infoAttributedString),
horizontalAlignment: .center,
maximumNumberOfLines: 0,
lineSpacing: 0.2
),
availableSize: CGSize(width: context.availableSize.width - (textSideInset + sideInset - 2.0) * 2.0, height: context.availableSize.height),
transition: .immediate
)
let infoPadding: CGFloat = 17.0
let infoSpacing: CGFloat = 12.0
let totalInfoHeight = infoPadding + infoTitle.size.height + infoSpacing + infoText.size.height + infoPadding
let infoBackground = infoBackground.update(
component: RoundedRectangle(
color: theme.list.blocksBackgroundColor,
cornerRadius: 10.0
),
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: totalInfoHeight),
transition: .immediate
)
context.add(infoBackground
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + infoBackground.size.height / 2.0))
)
contentSize.height += infoPadding
context.add(infoTitle
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + infoTitle.size.height / 2.0))
)
contentSize.height += infoTitle.size.height
contentSize.height += infoSpacing
context.add(infoText
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + infoText.size.height / 2.0))
)
contentSize.height += infoText.size.height
contentSize.height += infoPadding
contentSize.height += spacing
let actionButton = actionButton.update(
component: SolidRoundedButtonComponent(
title: "Understood",
theme: SolidRoundedButtonComponent.Theme(
backgroundColor: theme.list.itemCheckColors.fillColor,
backgroundColors: [],
foregroundColor: theme.list.itemCheckColors.foregroundColor
),
font: .bold,
fontSize: 17.0,
height: 50.0,
cornerRadius: 10.0,
gloss: false,
iconName: nil,
animationName: nil,
iconPosition: .left,
action: {
component.dismiss()
}
),
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: 50.0),
transition: context.transition
)
context.add(actionButton
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + actionButton.size.height / 2.0))
)
contentSize.height += actionButton.size.height
contentSize.height += 22.0
contentSize.height += environment.safeInsets.bottom
state.playAnimationIfNeeded()
return contentSize
}
}
}
private final class SheetContainerComponent: CombinedComponent {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let context: AccountContext
let animatedEmojis: [String: TelegramMediaFile]
let openMore: () -> Void
init(
context: AccountContext,
animatedEmojis: [String: TelegramMediaFile],
openMore: @escaping () -> Void
) {
self.context = context
self.animatedEmojis = animatedEmojis
self.openMore = openMore
}
static func ==(lhs: SheetContainerComponent, rhs: SheetContainerComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
return true
}
static var body: Body {
let sheet = Child(SheetComponent<EnvironmentType>.self)
let animateOut = StoredActionSlot(Action<Void>.self)
let sheetExternalState = SheetComponent<EnvironmentType>.ExternalState()
return { context in
let environment = context.environment[EnvironmentType.self]
let controller = environment.controller
let sheet = sheet.update(
component: SheetComponent<EnvironmentType>(
content: AnyComponent<EnvironmentType>(SheetContent(
context: context.component.context,
animatedEmojis: context.component.animatedEmojis,
openMore: context.component.openMore,
dismiss: {
animateOut.invoke(Action { _ in
if let controller = controller() {
controller.dismiss(completion: nil)
}
})
}
)),
backgroundColor: .color(environment.theme.actionSheet.opaqueItemBackgroundColor),
followContentSizeChanges: true,
externalState: sheetExternalState,
animateOut: animateOut
),
environment: {
environment
SheetComponentEnvironment(
isDisplaying: environment.value.isVisible,
isCentered: environment.metrics.widthClass == .regular,
hasInputHeight: !environment.inputHeight.isZero,
regularMetricsSize: CGSize(width: 430.0, height: 900.0),
dismiss: { animated in
if animated {
animateOut.invoke(Action { _ in
if let controller = controller() {
controller.dismiss(completion: nil)
}
})
} else {
if let controller = controller() {
controller.dismiss(completion: nil)
}
}
}
)
},
availableSize: context.availableSize,
transition: context.transition
)
context.add(sheet
.position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0))
)
if let controller = controller(), !controller.automaticallyControlPresentationContextLayout {
let layout = ContainerViewLayout(
size: context.availableSize,
metrics: environment.metrics,
deviceMetrics: environment.deviceMetrics,
intrinsicInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: max(environment.safeInsets.bottom, sheetExternalState.contentHeight), right: 0.0),
safeInsets: UIEdgeInsets(top: 0.0, left: environment.safeInsets.left, bottom: 0.0, right: environment.safeInsets.right),
additionalInsets: .zero,
statusBarHeight: environment.statusBarHeight,
inputHeight: nil,
inputHeightIsInteractivellyChanging: false,
inVoiceOver: false
)
controller.presentationContext.containerLayoutUpdated(layout, transition: context.transition.containedViewLayoutTransition)
}
return context.availableSize
}
}
}
final class MonetizationIntroScreen: ViewControllerComponentContainer {
private let context: AccountContext
private let animatedEmojis: [String: TelegramMediaFile]
private var openMore: (() -> Void)?
init(
context: AccountContext,
animatedEmojis: [String: TelegramMediaFile],
openMore: @escaping () -> Void
) {
self.context = context
self.animatedEmojis = animatedEmojis
self.openMore = openMore
super.init(
context: context,
component: SheetContainerComponent(
context: context,
animatedEmojis: animatedEmojis,
openMore: openMore
),
navigationBarAppearance: .none,
statusBarStyle: .ignore,
theme: .default
)
self.navigationPresentation = .flatModal
}
required public init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
self.view.disablesInteractiveModalDismiss = true
}
func dismissAnimated() {
if let view = self.node.hostView.findTaggedView(tag: SheetComponent<ViewControllerComponentContainer.Environment>.View.Tag()) as? SheetComponent<ViewControllerComponentContainer.Environment>.View {
view.dismissAnimated()
}
}
}
private final class ParagraphComponent: CombinedComponent {
let title: String
let titleColor: UIColor
let text: String
let textColor: UIColor
let iconName: String
let iconColor: UIColor
public init(
title: String,
titleColor: UIColor,
text: String,
textColor: UIColor,
iconName: String,
iconColor: UIColor
) {
self.title = title
self.titleColor = titleColor
self.text = text
self.textColor = textColor
self.iconName = iconName
self.iconColor = iconColor
}
static func ==(lhs: ParagraphComponent, rhs: ParagraphComponent) -> Bool {
if lhs.title != rhs.title {
return false
}
if lhs.titleColor != rhs.titleColor {
return false
}
if lhs.text != rhs.text {
return false
}
if lhs.textColor != rhs.textColor {
return false
}
if lhs.iconName != rhs.iconName {
return false
}
if lhs.iconColor != rhs.iconColor {
return false
}
return true
}
static var body: Body {
let title = Child(MultilineTextComponent.self)
let text = Child(MultilineTextComponent.self)
let icon = Child(BundleIconComponent.self)
return { context in
let component = context.component
let leftInset: CGFloat = 64.0
let rightInset: CGFloat = 32.0
let textSideInset: CGFloat = leftInset + 8.0
let spacing: CGFloat = 5.0
let textTopInset: CGFloat = 9.0
let title = title.update(
component: MultilineTextComponent(
text: .plain(NSAttributedString(
string: component.title,
font: Font.semibold(15.0),
textColor: component.titleColor,
paragraphAlignment: .natural
)),
horizontalAlignment: .center,
maximumNumberOfLines: 1
),
availableSize: CGSize(width: context.availableSize.width - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude),
transition: .immediate
)
let textFont = Font.regular(15.0)
let boldTextFont = Font.semibold(15.0)
let textColor = component.textColor
let markdownAttributes = MarkdownAttributes(
body: MarkdownAttributeSet(font: textFont, textColor: textColor),
bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor),
link: MarkdownAttributeSet(font: textFont, textColor: textColor),
linkAttribute: { _ in
return nil
}
)
let text = text.update(
component: MultilineTextComponent(
text: .markdown(text: component.text, attributes: markdownAttributes),
horizontalAlignment: .natural,
maximumNumberOfLines: 0,
lineSpacing: 0.2
),
availableSize: CGSize(width: context.availableSize.width - leftInset - rightInset, height: context.availableSize.height),
transition: .immediate
)
let icon = icon.update(
component: BundleIconComponent(
name: component.iconName,
tintColor: component.iconColor
),
availableSize: CGSize(width: context.availableSize.width, height: context.availableSize.height),
transition: .immediate
)
context.add(title
.position(CGPoint(x: textSideInset + title.size.width / 2.0, y: textTopInset + title.size.height / 2.0))
)
context.add(text
.position(CGPoint(x: textSideInset + text.size.width / 2.0, y: textTopInset + title.size.height + spacing + text.size.height / 2.0))
)
context.add(icon
.position(CGPoint(x: 47.0, y: textTopInset + 18.0))
)
return CGSize(width: context.availableSize.width, height: textTopInset + title.size.height + text.size.height + 20.0)
}
}
}

View file

@ -0,0 +1,62 @@
import Foundation
import UIKit
let walletAddressLength: Int = 48
func formatAddress(_ address: String) -> String {
var address = address
address.insert("\n", at: address.index(address.startIndex, offsetBy: address.count / 2))
return address
}
func formatBalanceText(_ value: Int64, decimalSeparator: String, showPlus: Bool = false) -> String {
var balanceText = "\(abs(value))"
while balanceText.count < 10 {
balanceText.insert("0", at: balanceText.startIndex)
}
balanceText.insert(contentsOf: decimalSeparator, at: balanceText.index(balanceText.endIndex, offsetBy: -9))
while true {
if balanceText.hasSuffix("0") {
if balanceText.hasSuffix("\(decimalSeparator)0") {
balanceText.removeLast()
balanceText.removeLast()
break
} else {
balanceText.removeLast()
}
} else {
break
}
}
if value < 0 {
balanceText.insert("-", at: balanceText.startIndex)
} else if showPlus {
balanceText.insert("+", at: balanceText.startIndex)
}
return balanceText
}
private let invalidAddressCharacters = CharacterSet(charactersIn: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_=").inverted
func isValidAddress(_ address: String, exactLength: Bool = false) -> Bool {
if address.count > walletAddressLength || address.rangeOfCharacter(from: invalidAddressCharacters) != nil {
return false
}
if exactLength && address.count != walletAddressLength {
return false
}
return true
}
private let amountDelimeterCharacters = CharacterSet(charactersIn: "0123456789-+").inverted
func amountAttributedString(_ string: String, integralFont: UIFont, fractionalFont: UIFont, color: UIColor) -> NSAttributedString {
let result = NSMutableAttributedString()
if let range = string.rangeOfCharacter(from: amountDelimeterCharacters) {
let integralPart = String(string[..<range.lowerBound])
let fractionalPart = String(string[range.lowerBound...])
result.append(NSAttributedString(string: integralPart, font: integralFont, textColor: color))
result.append(NSAttributedString(string: fractionalPart, font: fractionalFont, textColor: color))
} else {
result.append(NSAttributedString(string: string, font: integralFont, textColor: color))
}
return result
}

View file

@ -7,6 +7,9 @@ import TelegramCore
import TelegramPresentationData
import ItemListUI
import PresentationDataUtils
import EmojiTextAttachmentView
import TextFormat
import AccountContext
protocol Stats {
@ -32,21 +35,29 @@ extension StoryStats: Stats {
}
extension MonetizationStats: Stats {
}
class StatsOverviewItem: ListViewItem, ItemListItem {
let context: AccountContext
let presentationData: ItemListPresentationData
let isGroup: Bool
let stats: Stats
let storyViews: EngineStoryItem.Views?
let publicShares: Int32?
let animatedEmoji: TelegramMediaFile?
let sectionId: ItemListSectionId
let style: ItemListStyle
init(presentationData: ItemListPresentationData, isGroup: Bool, stats: Stats, storyViews: EngineStoryItem.Views? = nil, publicShares: Int32? = nil, sectionId: ItemListSectionId, style: ItemListStyle) {
init(context: AccountContext, presentationData: ItemListPresentationData, isGroup: Bool, stats: Stats, storyViews: EngineStoryItem.Views? = nil, publicShares: Int32? = nil, animatedEmoji: TelegramMediaFile? = nil, sectionId: ItemListSectionId, style: ItemListStyle) {
self.context = context
self.presentationData = presentationData
self.isGroup = isGroup
self.stats = stats
self.storyViews = storyViews
self.publicShares = publicShares
self.animatedEmoji = animatedEmoji
self.sectionId = sectionId
self.style = style
}
@ -97,6 +108,7 @@ private final class ValueItemNode: ASDisplayNode {
private let valueNode: TextNode
private let titleNode: TextNode
private let deltaNode: TextNode
private var animatedEmojiLayer: InlineStickerItemLayer?
var currentBackgroundColor: UIColor?
var pressed: (() -> Void)?
@ -115,13 +127,13 @@ private final class ValueItemNode: ASDisplayNode {
self.addSubnode(self.deltaNode)
}
static func asyncLayout(_ current: ValueItemNode?) -> (_ width: CGFloat, _ presentationData: ItemListPresentationData, _ value: String, _ title: String, _ delta: (String, DeltaColor)?) -> (CGSize, () -> ValueItemNode) {
static func asyncLayout(_ current: ValueItemNode?) -> (_ context: AccountContext, _ width: CGFloat, _ presentationData: ItemListPresentationData, _ value: String, _ title: String, _ delta: (String, DeltaColor)?, _ animatedEmoji: TelegramMediaFile?) -> (CGSize, () -> ValueItemNode) {
let maybeMakeValueLayout = (current?.valueNode).flatMap(TextNode.asyncLayout)
let maybeMakeTitleLayout = (current?.titleNode).flatMap(TextNode.asyncLayout)
let maybeMakeDeltaLayout = (current?.deltaNode).flatMap(TextNode.asyncLayout)
return { width, presentationData, value, title, delta in
return { context, width, presentationData, value, title, delta, animatedEmoji in
let targetNode: ValueItemNode
if let current = current {
targetNode = current
@ -150,7 +162,8 @@ private final class ValueItemNode: ASDisplayNode {
makeDeltaLayout = TextNode.asyncLayout(targetNode.deltaNode)
}
let valueFont = Font.semibold(presentationData.fontSize.itemListBaseFontSize)
let fontSize = presentationData.fontSize.itemListBaseFontSize
let valueFont = Font.semibold(fontSize)
let titleFont = Font.regular(presentationData.fontSize.itemListBaseHeaderFontSize)
let deltaFont = Font.regular(presentationData.fontSize.itemListBaseHeaderFontSize)
@ -170,6 +183,7 @@ private final class ValueItemNode: ASDisplayNode {
} else {
deltaColor = presentationData.theme.list.freeTextErrorColor
}
let placeholderColor = presentationData.theme.list.mediaPlaceholderColor
let constrainedSize = CGSize(width: width, height: CGFloat.greatestFiniteMagnitude)
let (valueLayout, valueApply) = makeValueLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: value, font: valueFont, textColor: valueColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: constrainedSize, alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
@ -185,9 +199,33 @@ private final class ValueItemNode: ASDisplayNode {
let _ = titleApply()
let _ = deltaApply()
let valueFrame = CGRect(origin: .zero, size: valueLayout.size)
var valueOffset: CGFloat = 0.0
if let animatedEmoji {
let itemSize = floorToScreenPixels(fontSize * 20.0 / 17.0)
var itemFrame = CGRect(origin: CGPoint(x: itemSize / 2.0 - 1.0, y: itemSize / 2.0), size: CGSize()).insetBy(dx: -itemSize / 2.0, dy: -itemSize / 2.0)
itemFrame.origin.x = floorToScreenPixels(itemFrame.origin.x)
itemFrame.origin.y = floorToScreenPixels(itemFrame.origin.y)
let itemLayer: InlineStickerItemLayer
if let current = targetNode.animatedEmojiLayer {
itemLayer = current
} else {
let pointSize = floor(itemSize * 1.3)
itemLayer = InlineStickerItemLayer(context: context, userLocation: .other, attemptSynchronousLoad: true, emoji: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: animatedEmoji.fileId.id, file: animatedEmoji, custom: nil), file: animatedEmoji, cache: context.animationCache, renderer: context.animationRenderer, placeholderColor: placeholderColor, pointSize: CGSize(width: pointSize, height: pointSize), dynamicColor: nil)
targetNode.animatedEmojiLayer = itemLayer
targetNode.layer.addSublayer(itemLayer)
itemLayer.isVisibleForAnimations = true
}
valueOffset += 22.0
itemLayer.frame = itemFrame
}
let valueFrame = CGRect(origin: CGPoint(x: valueOffset, y: 0.0), size: valueLayout.size)
let titleFrame = CGRect(origin: CGPoint(x: 0.0, y: valueFrame.maxY), size: titleLayout.size)
let deltaFrame = CGRect(origin: CGPoint(x: valueFrame.maxX + horizontalSpacing, y: valueFrame.maxY - deltaLayout.size.height - 2.0), size: deltaLayout.size)
let deltaFrame = CGRect(origin: CGPoint(x: valueFrame.maxX + horizontalSpacing, y: valueFrame.maxY - deltaLayout.size.height - 2.0 - UIScreenPixel), size: deltaLayout.size)
targetNode.valueNode.frame = valueFrame
targetNode.titleNode.frame = titleFrame
@ -296,7 +334,7 @@ class StatsOverviewItemNode: ListViewItemNode {
insets = itemListNeighborsGroupedInsets(neighbors, params)
}
let twoColumnLayout = "".isEmpty
var twoColumnLayout = true
var topLeftItemLayoutAndApply: (CGSize, () -> ValueItemNode)?
var topRightItemLayoutAndApply: (CGSize, () -> ValueItemNode)?
@ -321,78 +359,96 @@ class StatsOverviewItemNode: ListViewItemNode {
if let stats = item.stats as? MessageStats {
topLeftItemLayoutAndApply = makeTopLeftItemLayout(
item.context,
params.width,
item.presentationData,
compactNumericCountString(stats.views),
item.presentationData.strings.Stats_Message_Views,
nil,
nil
)
topRightItemLayoutAndApply = makeTopRightItemLayout(
item.context,
params.width,
item.presentationData,
item.publicShares.flatMap { compactNumericCountString(Int($0)) } ?? "",
item.presentationData.strings.Stats_Message_PublicShares,
nil,
nil
)
middle1LeftItemLayoutAndApply = makeMiddle1LeftItemLayout(
item.context,
params.width,
item.presentationData,
compactNumericCountString(stats.reactions),
item.presentationData.strings.Stats_Message_Reactions,
nil,
nil
)
middle1RightItemLayoutAndApply = makeMiddle1RightItemLayout(
item.context,
params.width,
item.presentationData,
item.publicShares.flatMap { "\( compactNumericCountString(max(0, stats.forwards - Int($0))))" } ?? "",
item.presentationData.strings.Stats_Message_PrivateShares,
nil,
nil
)
height += topRightItemLayoutAndApply!.0.height * 2.0 + verticalSpacing
} else if let _ = item.stats as? StoryStats, let views = item.storyViews {
topLeftItemLayoutAndApply = makeTopLeftItemLayout(
item.context,
params.width,
item.presentationData,
compactNumericCountString(views.seenCount),
item.presentationData.strings.Stats_Message_Views,
nil,
nil
)
topRightItemLayoutAndApply = makeTopRightItemLayout(
item.context,
params.width,
item.presentationData,
item.publicShares.flatMap { compactNumericCountString(Int($0)) } ?? "",
item.presentationData.strings.Stats_Message_PublicShares,
nil,
nil
)
middle1LeftItemLayoutAndApply = makeMiddle1LeftItemLayout(
item.context,
params.width,
item.presentationData,
compactNumericCountString(views.reactedCount),
item.presentationData.strings.Stats_Message_Reactions,
nil,
nil
)
middle1RightItemLayoutAndApply = makeMiddle1RightItemLayout(
item.context,
params.width,
item.presentationData,
item.publicShares.flatMap { "\( compactNumericCountString(max(0, views.forwardCount - Int($0))))" } ?? "",
item.presentationData.strings.Stats_Message_PrivateShares,
nil,
nil
)
height += topRightItemLayoutAndApply!.0.height * 2.0 + verticalSpacing
} else if let stats = item.stats as? ChannelBoostStatus {
topLeftItemLayoutAndApply = makeTopLeftItemLayout(
item.context,
params.width,
item.presentationData,
"\(stats.level)",
item.presentationData.strings.Stats_Boosts_Level,
nil,
nil
)
@ -402,18 +458,22 @@ class StatsOverviewItemNode: ListViewItemNode {
}
topRightItemLayoutAndApply = makeTopRightItemLayout(
item.context,
params.width,
item.presentationData,
"\(Int(stats.premiumAudience?.value ?? 0))",
item.isGroup ? item.presentationData.strings.Stats_Boosts_PremiumMembers : item.presentationData.strings.Stats_Boosts_PremiumSubscribers,
(String(format: "%.02f%%", premiumSubscribers * 100.0), .generic)
(String(format: "%.02f%%", premiumSubscribers * 100.0), .generic),
nil
)
middle1LeftItemLayoutAndApply = makeMiddle1LeftItemLayout(
item.context,
params.width,
item.presentationData,
"\(stats.boosts)",
item.presentationData.strings.Stats_Boosts_ExistingBoosts,
nil,
nil
)
@ -424,10 +484,12 @@ class StatsOverviewItemNode: ListViewItemNode {
boostsLeft = 0
}
middle1RightItemLayoutAndApply = makeMiddle1RightItemLayout(
item.context,
params.width,
item.presentationData,
"\(boostsLeft)",
item.presentationData.strings.Stats_Boosts_BoostsToLevelUp,
nil,
nil
)
@ -447,11 +509,13 @@ class StatsOverviewItemNode: ListViewItemNode {
let followersDelta = deltaText(stats.followers)
topLeftItemLayoutAndApply = makeTopLeftItemLayout(
item.context,
params.width,
item.presentationData,
compactNumericCountString(Int(stats.followers.current)),
item.presentationData.strings.Stats_Followers,
(followersDelta.text, followersDelta.positive ? .positive : .negative)
(followersDelta.text, followersDelta.positive ? .positive : .negative),
nil
)
var enabledNotifications: Double = 0.0
@ -459,10 +523,12 @@ class StatsOverviewItemNode: ListViewItemNode {
enabledNotifications = stats.enabledNotifications.value / stats.enabledNotifications.total
}
topRightItemLayoutAndApply = makeTopRightItemLayout(
item.context,
params.width,
item.presentationData,
String(format: "%.02f%%", enabledNotifications * 100.0),
item.presentationData.strings.Stats_EnabledNotifications,
nil,
nil
)
@ -516,56 +582,68 @@ class StatsOverviewItemNode: ListViewItemNode {
if let (value, title, delta) = items[0] {
middle1LeftItemLayoutAndApply = makeMiddle1LeftItemLayout(
item.context,
params.width,
item.presentationData,
value,
title,
delta
delta,
nil
)
}
if let (value, title, delta) = items[1] {
middle1RightItemLayoutAndApply = makeMiddle1RightItemLayout(
item.context,
params.width,
item.presentationData,
value,
title,
delta
delta,
nil
)
}
if let (value, title, delta) = items[2] {
middle2LeftItemLayoutAndApply = makeMiddle2LeftItemLayout(
item.context,
params.width,
item.presentationData,
value,
title,
delta
delta,
nil
)
}
if let (value, title, delta) = items[3] {
middle2RightItemLayoutAndApply = makeMiddle2RightItemLayout(
item.context,
params.width,
item.presentationData,
value,
title,
delta
delta,
nil
)
}
if let (value, title, delta) = items[4] {
bottomLeftItemLayoutAndApply = makeBottomLeftItemLayout(
item.context,
params.width,
item.presentationData,
value,
title,
delta
delta,
nil
)
}
if let (value, title, delta) = items[5] {
bottomRightItemLayoutAndApply = makeBottomRightItemLayout(
item.context,
params.width,
item.presentationData,
value,
title,
delta
delta,
nil
)
}
@ -583,37 +661,45 @@ class StatsOverviewItemNode: ListViewItemNode {
let membersDelta = deltaText(stats.members)
topLeftItemLayoutAndApply = makeTopLeftItemLayout(
item.context,
params.width,
item.presentationData,
compactNumericCountString(Int(stats.members.current)),
item.presentationData.strings.Stats_GroupMembers,
(membersDelta.text, membersDelta.positive ? .positive : .negative)
(membersDelta.text, membersDelta.positive ? .positive : .negative),
nil
)
let messagesDelta = deltaText(stats.messages)
topRightItemLayoutAndApply = makeTopRightItemLayout(
item.context,
params.width,
item.presentationData,
compactNumericCountString(Int(stats.messages.current)),
item.presentationData.strings.Stats_GroupMessages,
(messagesDelta.text, messagesDelta.positive ? .positive : .negative)
(messagesDelta.text, messagesDelta.positive ? .positive : .negative),
nil
)
if displayBottomRow {
middle1LeftItemLayoutAndApply = makeMiddle1LeftItemLayout(
item.context,
params.width,
item.presentationData,
compactNumericCountString(Int(stats.viewers.current)),
item.presentationData.strings.Stats_GroupViewers,
(viewersDelta.text, viewersDelta.positive ? .positive : .negative)
(viewersDelta.text, viewersDelta.positive ? .positive : .negative),
nil
)
middle1RightItemLayoutAndApply = makeMiddle1RightItemLayout(
item.context,
params.width,
item.presentationData,
compactNumericCountString(Int(stats.posters.current)),
item.presentationData.strings.Stats_GroupPosters,
(postersDelta.text, postersDelta.positive ? .positive : .negative)
(postersDelta.text, postersDelta.positive ? .positive : .negative),
nil
)
}
@ -622,6 +708,40 @@ class StatsOverviewItemNode: ListViewItemNode {
} else {
height += topLeftItemLayoutAndApply!.0.height * 4.0 + verticalSpacing * 3.0
}
} else if let _ = item.stats as? MonetizationStats {
twoColumnLayout = false
topLeftItemLayoutAndApply = makeTopLeftItemLayout(
item.context,
params.width,
item.presentationData,
"54.12",
"Balance Available to Withdraw",
("≈$123", .generic),
item.animatedEmoji
)
topRightItemLayoutAndApply = makeTopRightItemLayout(
item.context,
params.width,
item.presentationData,
"84.52",
"Proceeds Since Last Withdrawal",
("≈$226", .generic),
item.animatedEmoji
)
middle1LeftItemLayoutAndApply = makeMiddle1LeftItemLayout(
item.context,
params.width,
item.presentationData,
"692.52",
"Total Lifetime Proceeds",
("≈$1858", .generic),
item.animatedEmoji
)
height += topLeftItemLayoutAndApply!.0.height * 3.0 + verticalSpacing * 2.0
}
let contentSize = CGSize(width: params.width, height: height)

View file

@ -0,0 +1,475 @@
import Foundation
import UIKit
import Display
import ComponentFlow
import SwiftSignalKit
import TelegramCore
import Markdown
import TextFormat
import TelegramPresentationData
import ViewControllerComponent
import SheetComponent
import BundleIconComponent
import BalancedTextComponent
import MultilineTextComponent
import SolidRoundedButtonComponent
import LottieComponent
import AccountContext
import TelegramStringFormatting
import PremiumPeerShortcutComponent
enum MonetizationTransaction: Equatable {
case incoming(amount: Int64, fromTimestamp: Int32, toTimestamp: Int32)
case outgoing(amount: Int64, timestamp: Int32, address: String, explorerUrl: String)
var amount: Int64 {
switch self {
case let .incoming(amount, _, _), let .outgoing(amount, _, _, _):
return amount
}
}
}
private final class SheetContent: CombinedComponent {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let context: AccountContext
let peer: EnginePeer
let transaction: MonetizationTransaction
let openExplorer: (String) -> Void
let dismiss: () -> Void
init(
context: AccountContext,
peer: EnginePeer,
transaction: MonetizationTransaction,
openExplorer: @escaping (String) -> Void,
dismiss: @escaping () -> Void
) {
self.context = context
self.peer = peer
self.transaction = transaction
self.openExplorer = openExplorer
self.dismiss = dismiss
}
static func ==(lhs: SheetContent, rhs: SheetContent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.peer != rhs.peer {
return false
}
if lhs.transaction != rhs.transaction {
return false
}
return true
}
final class State: ComponentState {
var cachedCloseImage: (UIImage, PresentationTheme)?
let playOnce = ActionSlot<Void>()
private var didPlayAnimation = false
func playAnimationIfNeeded() {
guard !self.didPlayAnimation else {
return
}
self.didPlayAnimation = true
self.playOnce.invoke(Void())
}
}
func makeState() -> State {
return State()
}
static var body: Body {
let closeButton = Child(Button.self)
let amount = Child(MultilineTextComponent.self)
let title = Child(MultilineTextComponent.self)
let date = Child(MultilineTextComponent.self)
let address = Child(MultilineTextComponent.self)
let peerShortcut = Child(PremiumPeerShortcutComponent.self)
let actionButton = Child(SolidRoundedButtonComponent.self)
return { context in
let environment = context.environment[EnvironmentType.self]
let component = context.component
let state = context.state
let theme = environment.theme
let strings = environment.strings
let dateTimeFormat = component.context.sharedContext.currentPresentationData.with { $0 }.dateTimeFormat
let sideInset: CGFloat = 16.0 + environment.safeInsets.left
let textSideInset: CGFloat = 32.0 + environment.safeInsets.left
let titleFont = Font.semibold(17.0)
let textFont = Font.regular(17.0)
let fixedFont = Font.monospace(17.0)
let textColor = theme.actionSheet.primaryTextColor
let secondaryTextColor = theme.actionSheet.secondaryTextColor
var contentSize = CGSize(width: context.availableSize.width, height: 45.0)
let closeImage: UIImage
if let (image, theme) = state.cachedCloseImage, theme === environment.theme {
closeImage = image
} else {
closeImage = generateCloseButtonImage(backgroundColor: UIColor(rgb: 0x808084, alpha: 0.1), foregroundColor: theme.actionSheet.inputClearButtonColor)!
state.cachedCloseImage = (closeImage, theme)
}
let closeButton = closeButton.update(
component: Button(
content: AnyComponent(Image(image: closeImage)),
action: { [weak component] in
component?.dismiss()
}
),
availableSize: CGSize(width: 30.0, height: 30.0),
transition: .immediate
)
context.add(closeButton
.position(CGPoint(x: context.availableSize.width - environment.safeInsets.left - closeButton.size.width, y: 28.0))
)
let amountString: NSMutableAttributedString
let dateString: String
let titleString: String
let subtitleString: String
let buttonTitle: String
let explorerUrl: String?
let integralFont = Font.with(size: 48.0, design: .round, weight: .semibold)
let fractionalFont = Font.with(size: 24.0, design: .round, weight: .semibold)
//TODO:localize
switch component.transaction {
case let .incoming(amount, fromTimestamp, toTimestamp):
amountString = amountAttributedString(formatBalanceText(amount, decimalSeparator: dateTimeFormat.decimalSeparator, showPlus: true), integralFont: integralFont, fractionalFont: fractionalFont, color: theme.list.itemDisclosureActions.constructive.fillColor).mutableCopy() as! NSMutableAttributedString
amountString.append(NSAttributedString(string: " TON", font: fractionalFont, textColor: theme.list.itemDisclosureActions.constructive.fillColor))
dateString = "\(stringForFullDate(timestamp: fromTimestamp, strings: strings, dateTimeFormat: dateTimeFormat)) \(stringForFullDate(timestamp: toTimestamp, strings: strings, dateTimeFormat: dateTimeFormat))"
titleString = "Proceeds from Ads displayed in"
subtitleString = ""
buttonTitle = strings.Common_OK
explorerUrl = nil
case let .outgoing(amount, timestamp, address, explorerUrlValue):
amountString = amountAttributedString(formatBalanceText(amount, decimalSeparator: dateTimeFormat.decimalSeparator), integralFont: integralFont, fractionalFont: fractionalFont, color: theme.list.itemDestructiveColor).mutableCopy() as! NSMutableAttributedString
amountString.append(NSAttributedString(string: " TON", font: fractionalFont, textColor: theme.list.itemDestructiveColor))
dateString = stringForFullDate(timestamp: timestamp, strings: strings, dateTimeFormat: dateTimeFormat)
titleString = "Balance Withdrawal to"
subtitleString = formatAddress(address)
buttonTitle = "View in Blockchain Explorer"
explorerUrl = explorerUrlValue
}
let amount = amount.update(
component: MultilineTextComponent(
text: .plain(amountString),
horizontalAlignment: .center,
maximumNumberOfLines: 1,
lineSpacing: 0.1
),
availableSize: CGSize(width: context.availableSize.width - textSideInset * 2.0, height: context.availableSize.height),
transition: .immediate
)
context.add(amount
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + amount.size.height / 2.0))
)
contentSize.height += amount.size.height
contentSize.height += -5.0
let date = date.update(
component: MultilineTextComponent(
text: .plain(NSAttributedString(string: dateString, font: textFont, textColor: secondaryTextColor)),
horizontalAlignment: .center,
maximumNumberOfLines: 0,
lineSpacing: 0.1
),
availableSize: CGSize(width: context.availableSize.width - textSideInset * 2.0, height: context.availableSize.height),
transition: .immediate
)
context.add(date
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + date.size.height / 2.0))
)
contentSize.height += date.size.height
contentSize.height += 32.0
let title = title.update(
component: MultilineTextComponent(
text: .plain(NSAttributedString(string: titleString, font: titleFont, textColor: textColor)),
horizontalAlignment: .center,
maximumNumberOfLines: 0,
lineSpacing: 0.1
),
availableSize: CGSize(width: context.availableSize.width - textSideInset * 2.0, height: context.availableSize.height),
transition: .immediate
)
context.add(title
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + title.size.height / 2.0))
)
contentSize.height += title.size.height
contentSize.height += 3.0
if !subtitleString.isEmpty {
let address = address.update(
component: MultilineTextComponent(
text: .plain(NSAttributedString(string: subtitleString, font: fixedFont, textColor: textColor)),
horizontalAlignment: .center,
maximumNumberOfLines: 0,
lineSpacing: 0.1
),
availableSize: CGSize(width: context.availableSize.width - textSideInset * 2.0, height: context.availableSize.height),
transition: .immediate
)
context.add(address
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + address.size.height / 2.0))
)
contentSize.height += address.size.height
contentSize.height += 50.0
} else {
contentSize.height += 5.0
let peerShortcut = peerShortcut.update(
component: PremiumPeerShortcutComponent(
context: component.context,
theme: theme,
peer: component.peer
),
availableSize: CGSize(width: context.availableSize.width - 32.0, height: context.availableSize.height),
transition: .immediate
)
context.add(peerShortcut
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + peerShortcut.size.height / 2.0))
)
contentSize.height += peerShortcut.size.height
contentSize.height += 50.0
}
let actionButton = actionButton.update(
component: SolidRoundedButtonComponent(
title: buttonTitle,
theme: SolidRoundedButtonComponent.Theme(
backgroundColor: theme.list.itemCheckColors.fillColor,
backgroundColors: [],
foregroundColor: theme.list.itemCheckColors.foregroundColor
),
font: .bold,
fontSize: 17.0,
height: 50.0,
cornerRadius: 10.0,
gloss: false,
iconName: nil,
animationName: nil,
iconPosition: .left,
action: {
component.dismiss()
if let explorerUrl {
component.openExplorer(explorerUrl)
}
}
),
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: 50.0),
transition: context.transition
)
context.add(actionButton
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + actionButton.size.height / 2.0))
)
contentSize.height += actionButton.size.height
contentSize.height += 22.0
contentSize.height += environment.safeInsets.bottom
state.playAnimationIfNeeded()
return contentSize
}
}
}
private final class SheetContainerComponent: CombinedComponent {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let context: AccountContext
let peer: EnginePeer
let transaction: MonetizationTransaction
let openExplorer: (String) -> Void
init(
context: AccountContext,
peer: EnginePeer,
transaction: MonetizationTransaction,
openExplorer: @escaping (String) -> Void
) {
self.context = context
self.peer = peer
self.transaction = transaction
self.openExplorer = openExplorer
}
static func ==(lhs: SheetContainerComponent, rhs: SheetContainerComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.peer != rhs.peer {
return false
}
if lhs.transaction != rhs.transaction {
return false
}
return true
}
static var body: Body {
let sheet = Child(SheetComponent<EnvironmentType>.self)
let animateOut = StoredActionSlot(Action<Void>.self)
let sheetExternalState = SheetComponent<EnvironmentType>.ExternalState()
return { context in
let environment = context.environment[EnvironmentType.self]
let controller = environment.controller
let sheet = sheet.update(
component: SheetComponent<EnvironmentType>(
content: AnyComponent<EnvironmentType>(SheetContent(
context: context.component.context,
peer: context.component.peer,
transaction: context.component.transaction,
openExplorer: context.component.openExplorer,
dismiss: {
animateOut.invoke(Action { _ in
if let controller = controller() {
controller.dismiss(completion: nil)
}
})
}
)),
backgroundColor: .color(environment.theme.actionSheet.opaqueItemBackgroundColor),
followContentSizeChanges: true,
externalState: sheetExternalState,
animateOut: animateOut
),
environment: {
environment
SheetComponentEnvironment(
isDisplaying: environment.value.isVisible,
isCentered: environment.metrics.widthClass == .regular,
hasInputHeight: !environment.inputHeight.isZero,
regularMetricsSize: CGSize(width: 430.0, height: 900.0),
dismiss: { animated in
if animated {
animateOut.invoke(Action { _ in
if let controller = controller() {
controller.dismiss(completion: nil)
}
})
} else {
if let controller = controller() {
controller.dismiss(completion: nil)
}
}
}
)
},
availableSize: context.availableSize,
transition: context.transition
)
context.add(sheet
.position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0))
)
if let controller = controller(), !controller.automaticallyControlPresentationContextLayout {
let layout = ContainerViewLayout(
size: context.availableSize,
metrics: environment.metrics,
deviceMetrics: environment.deviceMetrics,
intrinsicInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: max(environment.safeInsets.bottom, sheetExternalState.contentHeight), right: 0.0),
safeInsets: UIEdgeInsets(top: 0.0, left: environment.safeInsets.left, bottom: 0.0, right: environment.safeInsets.right),
additionalInsets: .zero,
statusBarHeight: environment.statusBarHeight,
inputHeight: nil,
inputHeightIsInteractivellyChanging: false,
inVoiceOver: false
)
controller.presentationContext.containerLayoutUpdated(layout, transition: context.transition.containedViewLayoutTransition)
}
return context.availableSize
}
}
}
final class TransactionInfoScreen: ViewControllerComponentContainer {
private let context: AccountContext
init(
context: AccountContext,
peer: EnginePeer,
transaction: MonetizationTransaction,
openExplorer: @escaping (String) -> Void
) {
self.context = context
super.init(
context: context,
component: SheetContainerComponent(
context: context,
peer: peer,
transaction: transaction,
openExplorer: openExplorer
),
navigationBarAppearance: .none,
statusBarStyle: .ignore,
theme: .default
)
self.navigationPresentation = .flatModal
}
required public init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
self.view.disablesInteractiveModalDismiss = true
}
func dismissAnimated() {
if let view = self.node.hostView.findTaggedView(tag: SheetComponent<ViewControllerComponentContainer.Environment>.View.Tag()) as? SheetComponent<ViewControllerComponentContainer.Environment>.View {
view.dismissAnimated()
}
}
}
func generateCloseButtonImage(backgroundColor: UIColor, foregroundColor: UIColor) -> UIImage? {
return generateImage(CGSize(width: 30.0, height: 30.0), contextGenerator: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(backgroundColor.cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(), size: size))
context.setLineWidth(2.0)
context.setLineCap(.round)
context.setStrokeColor(foregroundColor.cgColor)
context.move(to: CGPoint(x: 10.0, y: 10.0))
context.addLine(to: CGPoint(x: 20.0, y: 20.0))
context.strokePath()
context.move(to: CGPoint(x: 20.0, y: 10.0))
context.addLine(to: CGPoint(x: 10.0, y: 20.0))
context.strokePath()
})
}

View file

@ -40,7 +40,7 @@ swift_library(
"//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer",
"//submodules/StickerPeekUI:StickerPeekUI",
"//submodules/Pasteboard:Pasteboard",
"//submodules/TelegramUI/Components/Stickers/StickerPackEditTitleController"
"//submodules/TelegramUI/Components/Stickers/StickerPackEditTitleController",
],
visibility = [
"//visibility:public",

View file

@ -23,6 +23,7 @@ import AnimationCache
import MultiAnimationRenderer
import Pasteboard
import StickerPackEditTitleController
import EntityKeyboard
private enum StickerPackPreviewGridEntry: Comparable, Identifiable {
case sticker(index: Int, stableId: Int, stickerItem: StickerPackItem?, isEmpty: Bool, isPremium: Bool, isLocked: Bool, isEditing: Bool, isAdd: Bool)
@ -537,8 +538,8 @@ private final class StickerPackContainer: ASDisplayNode {
if canEdit {
menuItems.append(.action(ContextMenuActionItem(text: "Edit Sticker", icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Draw"), color: theme.contextMenu.primaryColor) }, action: { _, f in
f(.default)
if let _ = self {
if let self {
self.openEditSticker(item.file)
}
})))
if !strongSelf.isEditing {
@ -1137,6 +1138,7 @@ private final class StickerPackContainer: ASDisplayNode {
self.presentInGlobalOverlay(contextController, nil)
}
private let stickerPickerInputData = Promise<StickerPickerInput>()
private func presentAddStickerOptions() {
let actionSheet = ActionSheetController(presentationData: self.presentationData)
var items: [ActionSheetItem] = []
@ -1155,9 +1157,8 @@ private final class StickerPackContainer: ASDisplayNode {
guard let self, let controller = self.controller else {
return
}
controller.controllerNode.dismiss()
self.presentAddExistingSticker()
controller.controllerNode.dismiss()
}))
actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: self.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
@ -1165,22 +1166,158 @@ private final class StickerPackContainer: ASDisplayNode {
})
])])
self.presentInGlobalOverlay(actionSheet, nil)
let emojiItems = EmojiPagerContentComponent.emojiInputData(
context: self.context,
animationCache: self.context.animationCache,
animationRenderer: self.context.animationRenderer,
isStandalone: false,
subject: .emoji,
hasTrending: true,
topReactionItems: [],
areUnicodeEmojiEnabled: true,
areCustomEmojiEnabled: true,
chatPeerId: self.context.account.peerId,
hasSearch: true,
forceHasPremium: true
)
let stickerItems = EmojiPagerContentComponent.stickerInputData(
context: self.context,
animationCache: self.context.animationCache,
animationRenderer: self.context.animationRenderer,
stickerNamespaces: [Namespaces.ItemCollection.CloudStickerPacks],
stickerOrderedItemListCollectionIds: [Namespaces.OrderedItemList.CloudSavedStickers, Namespaces.OrderedItemList.CloudRecentStickers, Namespaces.OrderedItemList.CloudAllPremiumStickers],
chatPeerId: self.context.account.peerId,
hasSearch: true,
hasTrending: true,
forceHasPremium: true
)
let signal = combineLatest(
queue: .mainQueue(),
emojiItems,
stickerItems
) |> map { emoji, stickers -> StickerPickerInput in
return StickerPickerInputData(emoji: emoji, stickers: stickers, gifs: nil)
}
self.stickerPickerInputData.set(signal)
}
private func presentCreateSticker() {
let controller = self.context.sharedContext.makeStickerMediaPickerScreen(
context: self.context,
guard let (info, _, _) = self.currentStickerPack else {
return
}
let context = self.context
let presentationData = self.presentationData
let updatedPresentationData = self.controller?.updatedPresentationData
let navigationController = self.controller?.parentNavigationController as? NavigationController
var dismissImpl: (() -> Void)?
let mainController = context.sharedContext.makeStickerMediaPickerScreen(
context: context,
getSourceRect: { return .zero },
completion: { result, transitionView, transitionRect, transitionImage, completion, dismissed in
let editorController = context.sharedContext.makeStickerEditorScreen(
context: context,
source: result,
transitionArguments: (transitionView, transitionRect, transitionImage),
completion: { file in
dismissImpl?()
let sticker = ImportSticker(
resource: file.resource,
emojis: ["😀"],
dimensions: file.dimensions ?? PixelDimensions(width: 512, height: 512),
mimeType: file.mimeType,
keywords: ""
)
let packReference: StickerPackReference = .id(id: info.id.id, accessHash: info.accessHash)
let _ = (context.engine.stickers.addStickerToStickerSet(packReference: packReference, sticker: sticker)
|> deliverOnMainQueue).start(completed: {
let packController = StickerPackScreen(context: context, updatedPresentationData: updatedPresentationData, mainStickerPack: packReference, stickerPacks: [packReference], loadedStickerPacks: [], parentNavigationController: navigationController, sendSticker: nil, sendEmoji: nil, actionPerformed: nil, dismissed: nil, getSourceRect: nil)
(navigationController?.viewControllers.last as? ViewController)?.present(packController, in: .window(.root))
Queue.mainQueue().after(0.1) {
packController.present(UndoOverlayController(presentationData: presentationData, content: .sticker(context: context, file: file, loop: true, title: nil, text: "Sticker added to **\(info.title)** sticker set.", undoText: nil, customAction: nil), elevatedLayout: false, action: { _ in return false }), in: .current)
}
})
}
)
navigationController?.pushViewController(editorController)
},
dismissed: {}
)
self.controller?.parentNavigationController?.pushViewController(controller)
dismissImpl = { [weak mainController] in
mainController?.dismiss()
}
navigationController?.pushViewController(mainController)
}
private func presentAddExistingSticker() {
guard let (info, _, _) = self.currentStickerPack else {
return
}
let presentationData = self.presentationData
let updatedPresentationData = self.controller?.updatedPresentationData
let navigationController = self.controller?.parentNavigationController as? NavigationController
let context = self.context
let controller = self.context.sharedContext.makeStickerPickerScreen(context: self.context, inputData: self.stickerPickerInputData, completion: { file in
let sticker = ImportSticker(
resource: file.resource,
emojis: ["😀"],
dimensions: file.dimensions ?? PixelDimensions(width: 512, height: 512),
mimeType: file.mimeType,
keywords: ""
)
let packReference: StickerPackReference = .id(id: info.id.id, accessHash: info.accessHash)
let _ = (context.engine.stickers.addStickerToStickerSet(packReference: packReference, sticker: sticker)
|> deliverOnMainQueue).start(completed: {
let packController = StickerPackScreen(context: context, updatedPresentationData: updatedPresentationData, mainStickerPack: packReference, stickerPacks: [packReference], loadedStickerPacks: [], parentNavigationController: navigationController, sendSticker: nil, sendEmoji: nil, actionPerformed: nil, dismissed: nil, getSourceRect: nil)
(navigationController?.viewControllers.last as? ViewController)?.present(packController, in: .window(.root))
Queue.mainQueue().after(0.1) {
packController.present(UndoOverlayController(presentationData: presentationData, content: .sticker(context: context, file: file, loop: true, title: nil, text: "Sticker added to **\(info.title)** sticker set.", undoText: nil, customAction: nil), elevatedLayout: false, action: { _ in return false }), in: .current)
}
})
})
navigationController?.pushViewController(controller)
}
private func openEditSticker(_ initialFile: TelegramMediaFile) {
guard let (info, _, _) = self.currentStickerPack else {
return
}
let context = self.context
let updatedPresentationData = self.controller?.updatedPresentationData
let navigationController = self.controller?.parentNavigationController as? NavigationController
self.controller?.dismiss()
let controller = context.sharedContext.makeStickerEditorScreen(
context: context,
source: initialFile,
transitionArguments: nil,
completion: { file in
let sticker = ImportSticker(
resource: file.resource,
emojis: ["😀"],
dimensions: file.dimensions ?? PixelDimensions(width: 512, height: 512),
mimeType: file.mimeType,
keywords: ""
)
let packReference: StickerPackReference = .id(id: info.id.id, accessHash: info.accessHash)
let _ = (context.engine.stickers.replaceSticker(previousSticker: .stickerPack(stickerPack: .id(id: info.id.id, accessHash: info.accessHash), media: initialFile), sticker: sticker)
|> deliverOnMainQueue).start(completed: {
let packController = StickerPackScreen(context: context, updatedPresentationData: updatedPresentationData, mainStickerPack: packReference, stickerPacks: [packReference], loadedStickerPacks: [], parentNavigationController: navigationController, sendSticker: nil, sendEmoji: nil, actionPerformed: nil, dismissed: nil, getSourceRect: nil)
(navigationController?.viewControllers.last as? ViewController)?.present(packController, in: .window(.root))
})
}
)
navigationController?.pushViewController(controller)
}
private func presentEditPackTitle() {
@ -2408,7 +2545,7 @@ private final class StickerPackScreenNode: ViewControllerTracingNode {
}
}
public final class StickerPackScreenImpl: ViewController {
public final class StickerPackScreenImpl: ViewController, StickerPackScreen {
private let context: AccountContext
fileprivate var presentationData: PresentationData
fileprivate let updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?

View file

@ -526,7 +526,7 @@ if "".isEmpty {
return .single(.content(PendingMessageUploadedContentAndReuploadInfo(content: .media(.inputMediaUploadedPhoto(flags: flags, file: file, stickers: stickers, ttlSeconds: ttlSeconds), text), reuploadInfo: nil, cacheReferenceKey: nil)))
}
return network.request(Api.functions.messages.uploadMedia(peer: inputPeer, media: Api.InputMedia.inputMediaUploadedPhoto(flags: flags, file: file, stickers: stickers, ttlSeconds: ttlSeconds)))
return network.request(Api.functions.messages.uploadMedia(flags: 0, businessConnectionId: nil, peer: inputPeer, media: Api.InputMedia.inputMediaUploadedPhoto(flags: flags, file: file, stickers: stickers, ttlSeconds: ttlSeconds)))
|> mapError { _ -> PendingMessageUploadError in return .generic }
|> mapToSignal { result -> Signal<PendingMessageUploadedContentResult, PendingMessageUploadError> in
switch result {
@ -924,7 +924,7 @@ private func uploadedMediaFileContent(network: Network, postbox: Postbox, auxili
|> mapError { _ -> PendingMessageUploadError in }
|> mapToSignal { inputPeer -> Signal<PendingMessageUploadedContentResult, PendingMessageUploadError> in
if let inputPeer = inputPeer {
return network.request(Api.functions.messages.uploadMedia(peer: inputPeer, media: .inputMediaUploadedDocument(flags: flags, file: inputFile, thumb: thumbnailFile, mimeType: file.mimeType, attributes: inputDocumentAttributesFromFileAttributes(file.attributes), stickers: stickers, ttlSeconds: ttlSeconds)))
return network.request(Api.functions.messages.uploadMedia(flags: 0, businessConnectionId: nil, peer: inputPeer, media: .inputMediaUploadedDocument(flags: flags, file: inputFile, thumb: thumbnailFile, mimeType: file.mimeType, attributes: inputDocumentAttributesFromFileAttributes(file.attributes), stickers: stickers, ttlSeconds: ttlSeconds)))
|> mapError { _ -> PendingMessageUploadError in return .generic }
|> mapToSignal { result -> Signal<PendingMessageUploadedContentResult, PendingMessageUploadError> in
switch result {

View file

@ -64,7 +64,7 @@ public func standaloneUploadedImage(postbox: Postbox, network: Network, peerId:
|> mapError { _ -> StandaloneUploadMediaError in }
|> mapToSignal { inputPeer -> Signal<StandaloneUploadMediaEvent, StandaloneUploadMediaError> in
if let inputPeer = inputPeer {
return network.request(Api.functions.messages.uploadMedia(peer: inputPeer, media: Api.InputMedia.inputMediaUploadedPhoto(flags: 0, file: inputFile, stickers: nil, ttlSeconds: nil)))
return network.request(Api.functions.messages.uploadMedia(flags: 0, businessConnectionId: nil, peer: inputPeer, media: Api.InputMedia.inputMediaUploadedPhoto(flags: 0, file: inputFile, stickers: nil, ttlSeconds: nil)))
|> mapError { _ -> StandaloneUploadMediaError in return .generic }
|> mapToSignal { media -> Signal<StandaloneUploadMediaEvent, StandaloneUploadMediaError> in
switch media {
@ -158,7 +158,7 @@ public func standaloneUploadedFile(postbox: Postbox, network: Network, peerId: P
if let _ = thumbnailFile {
flags |= 1 << 2
}
return network.request(Api.functions.messages.uploadMedia(peer: inputPeer, media: Api.InputMedia.inputMediaUploadedDocument(flags: flags, file: inputFile, thumb: thumbnailFile, mimeType: mimeType, attributes: inputDocumentAttributesFromFileAttributes(attributes), stickers: nil, ttlSeconds: nil)))
return network.request(Api.functions.messages.uploadMedia(flags: 0, businessConnectionId: nil, peer: inputPeer, media: Api.InputMedia.inputMediaUploadedDocument(flags: flags, file: inputFile, thumb: thumbnailFile, mimeType: mimeType, attributes: inputDocumentAttributesFromFileAttributes(attributes), stickers: nil, ttlSeconds: nil)))
|> mapError { _ -> StandaloneUploadMediaError in return .generic }
|> mapToSignal { media -> Signal<StandaloneUploadMediaEvent, StandaloneUploadMediaError> in
switch media {

View file

@ -96,7 +96,7 @@ func applyUpdateMessage(postbox: Postbox, stateManager: AccountStateManager, mes
var updatedTimestamp: Int32?
if let apiMessage = apiMessage {
switch apiMessage {
case let .message(_, _, _, _, _, _, _, _, _, _, date, _, _, _, _, _, _, _, _, _, _, _, _, _, _):
case let .message(_, _, _, _, _, _, _, _, _, _, _, date, _, _, _, _, _, _, _, _, _, _, _, _, _, _):
updatedTimestamp = date
case .messageEmpty:
break

View file

@ -58,7 +58,7 @@ class UpdateMessageService: NSObject, MTMessageService {
self.putNext(groups)
}
case let .updateShortChatMessage(flags, id, fromId, chatId, message, pts, ptsCount, date, fwdFrom, viaBotId, replyHeader, entities, ttlPeriod):
let generatedMessage = Api.Message.message(flags: flags, flags2: 0, id: id, fromId: .peerUser(userId: fromId), fromBoostsApplied: nil, peerId: Api.Peer.peerChat(chatId: chatId), savedPeerId: nil, fwdFrom: fwdFrom, viaBotId: viaBotId, replyTo: replyHeader, date: date, message: message, media: Api.MessageMedia.messageMediaEmpty, replyMarkup: nil, entities: entities, views: nil, forwards: nil, replies: nil, editDate: nil, postAuthor: nil, groupedId: nil, reactions: nil, restrictionReason: nil, ttlPeriod: ttlPeriod, quickReplyShortcutId: nil)
let generatedMessage = Api.Message.message(flags: flags, flags2: 0, id: id, fromId: .peerUser(userId: fromId), fromBoostsApplied: nil, peerId: Api.Peer.peerChat(chatId: chatId), savedPeerId: nil, fwdFrom: fwdFrom, viaBotId: viaBotId, viaBusinessBotId: nil, replyTo: replyHeader, date: date, message: message, media: Api.MessageMedia.messageMediaEmpty, replyMarkup: nil, entities: entities, views: nil, forwards: nil, replies: nil, editDate: nil, postAuthor: nil, groupedId: nil, reactions: nil, restrictionReason: nil, ttlPeriod: ttlPeriod, quickReplyShortcutId: nil)
let update = Api.Update.updateNewMessage(message: generatedMessage, pts: pts, ptsCount: ptsCount)
let groups = groupUpdates([update], users: [], chats: [], date: date, seqRange: nil)
if groups.count != 0 {
@ -74,7 +74,7 @@ class UpdateMessageService: NSObject, MTMessageService {
let generatedPeerId = Api.Peer.peerUser(userId: userId)
let generatedMessage = Api.Message.message(flags: flags, flags2: 0, id: id, fromId: generatedFromId, fromBoostsApplied: nil, peerId: generatedPeerId, savedPeerId: nil, fwdFrom: fwdFrom, viaBotId: viaBotId, replyTo: replyHeader, date: date, message: message, media: Api.MessageMedia.messageMediaEmpty, replyMarkup: nil, entities: entities, views: nil, forwards: nil, replies: nil, editDate: nil, postAuthor: nil, groupedId: nil, reactions: nil, restrictionReason: nil, ttlPeriod: ttlPeriod, quickReplyShortcutId: nil)
let generatedMessage = Api.Message.message(flags: flags, flags2: 0, id: id, fromId: generatedFromId, fromBoostsApplied: nil, peerId: generatedPeerId, savedPeerId: nil, fwdFrom: fwdFrom, viaBotId: viaBotId, viaBusinessBotId: nil, replyTo: replyHeader, date: date, message: message, media: Api.MessageMedia.messageMediaEmpty, replyMarkup: nil, entities: entities, views: nil, forwards: nil, replies: nil, editDate: nil, postAuthor: nil, groupedId: nil, reactions: nil, restrictionReason: nil, ttlPeriod: ttlPeriod, quickReplyShortcutId: nil)
let update = Api.Update.updateNewMessage(message: generatedMessage, pts: pts, ptsCount: ptsCount)
let groups = groupUpdates([update], users: [], chats: [], date: date, seqRange: nil)
if groups.count != 0 {

View file

@ -104,7 +104,7 @@ extension Api.MessageMedia {
extension Api.Message {
var rawId: Int32 {
switch self {
case let .message(_, _, id, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _):
case let .message(_, _, id, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _):
return id
case let .messageEmpty(_, id, _):
return id
@ -115,7 +115,7 @@ extension Api.Message {
func id(namespace: MessageId.Namespace = Namespaces.Message.Cloud) -> MessageId? {
switch self {
case let .message(_, _, id, _, _, messagePeerId, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _):
case let .message(_, _, id, _, _, messagePeerId, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _):
let peerId: PeerId = messagePeerId.peerId
return MessageId(peerId: peerId, namespace: namespace, id: id)
case let .messageEmpty(_, id, peerId):
@ -132,7 +132,7 @@ extension Api.Message {
var peerId: PeerId? {
switch self {
case let .message(_, _, _, _, _, messagePeerId, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _):
case let .message(_, _, _, _, _, messagePeerId, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _):
let peerId: PeerId = messagePeerId.peerId
return peerId
case let .messageEmpty(_, _, peerId):
@ -145,7 +145,7 @@ extension Api.Message {
var timestamp: Int32? {
switch self {
case let .message(_, _, _, _, _, _, _, _, _, _, date, _, _, _, _, _, _, _, _, _, _, _, _, _, _):
case let .message(_, _, _, _, _, _, _, _, _, _, _, date, _, _, _, _, _, _, _, _, _, _, _, _, _, _):
return date
case let .messageService(_, _, _, _, _, date, _, _):
return date
@ -156,7 +156,7 @@ extension Api.Message {
var preCachedResources: [(MediaResource, Data)]? {
switch self {
case let .message(_, _, _, _, _, _, _, _, _, _, _, _, media, _, _, _, _, _, _, _, _, _, _, _, _):
case let .message(_, _, _, _, _, _, _, _, _, _, _, _, _, media, _, _, _, _, _, _, _, _, _, _, _, _):
return media?.preCachedResources
default:
return nil
@ -165,7 +165,7 @@ extension Api.Message {
var preCachedStories: [StoryId: Api.StoryItem]? {
switch self {
case let .message(_, _, _, _, _, _, _, _, _, _, _, _, media, _, _, _, _, _, _, _, _, _, _, _, _):
case let .message(_, _, _, _, _, _, _, _, _, _, _, _, _, media, _, _, _, _, _, _, _, _, _, _, _, _):
return media?.preCachedStories
default:
return nil

View file

@ -52,7 +52,7 @@ func _internal_uploadSticker(account: Account, peer: Peer, resource: MediaResour
var attributes: [Api.DocumentAttribute] = []
attributes.append(.documentAttributeSticker(flags: 0, alt: alt, stickerset: .inputStickerSetEmpty, maskCoords: nil))
attributes.append(.documentAttributeImageSize(w: dimensions.width, h: dimensions.height))
return account.network.request(Api.functions.messages.uploadMedia(peer: inputPeer, media: Api.InputMedia.inputMediaUploadedDocument(flags: flags, file: file, thumb: nil, mimeType: mimeType, attributes: attributes, stickers: nil, ttlSeconds: nil)))
return account.network.request(Api.functions.messages.uploadMedia(flags: 0, businessConnectionId: nil, peer: inputPeer, media: Api.InputMedia.inputMediaUploadedDocument(flags: flags, file: file, thumb: nil, mimeType: mimeType, attributes: attributes, stickers: nil, ttlSeconds: nil)))
|> mapError { _ -> UploadStickerError in return .generic }
|> mapToSignal { media -> Signal<UploadStickerStatus, UploadStickerError> in
switch media {
@ -384,6 +384,60 @@ func _internal_deleteStickerFromStickerSet(account: Account, sticker: FileMediaR
}
}
public enum ReplaceStickerError {
case generic
}
func _internal_replaceSticker(account: Account, previousSticker: FileMediaReference, sticker: ImportSticker) -> Signal<Never, ReplaceStickerError> {
guard let previousResource = previousSticker.media.resource as? CloudDocumentMediaResource else {
return .fail(.generic)
}
let uploadSticker: Signal<UploadStickerStatus, ReplaceStickerError>
if let resource = sticker.resource as? CloudDocumentMediaResource {
uploadSticker = .single(.complete(resource, sticker.mimeType))
} else {
uploadSticker = account.postbox.loadedPeerWithId(account.peerId)
|> castError(ReplaceStickerError.self)
|> mapToSignal { peer in
return _internal_uploadSticker(account: account, peer: peer, resource: sticker.resource, alt: sticker.emojis.first ?? "", dimensions: sticker.dimensions, mimeType: sticker.mimeType)
|> mapError { _ -> ReplaceStickerError in
return .generic
}
}
}
return uploadSticker
|> mapToSignal { uploadedSticker in
guard case let .complete(resource, _) = uploadedSticker else {
return .complete()
}
var flags: Int32 = 0
if sticker.keywords.count > 0 {
flags |= (1 << 1)
}
let inputSticker: Api.InputStickerSetItem = .inputStickerSetItem(flags: flags, document: .inputDocument(id: resource.fileId, accessHash: resource.accessHash, fileReference: Buffer(data: resource.fileReference ?? Data())), emoji: sticker.emojis.first ?? "", maskCoords: nil, keywords: sticker.keywords)
return account.network.request(Api.functions.stickers.replaceSticker(sticker: .inputDocument(id: previousResource.fileId, accessHash: previousResource.accessHash, fileReference: Buffer(data: previousResource.fileReference ?? Data())), newSticker: inputSticker))
|> mapError { error -> ReplaceStickerError in
return .generic
}
|> mapToSignal { result -> Signal<Never, ReplaceStickerError> in
guard let (info, items) = parseStickerSetInfoAndItems(apiStickerSet: result) else {
return .complete()
}
return account.postbox.transaction { transaction -> Void in
if transaction.getItemCollectionInfo(collectionId: info.id) != nil {
transaction.replaceItemCollectionItems(collectionId: info.id, items: items)
}
cacheStickerPack(transaction: transaction, info: info, items: items)
}
|> castError(ReplaceStickerError.self)
|> ignoreValues
}
}
}
func _internal_getMyStickerSets(account: Account) -> Signal<[(StickerPackCollectionInfo, StickerPackItem?)], NoError> {
return account.network.request(Api.functions.messages.getMyStickers(offsetId: 0, limit: 100))
|> map(Optional.init)

View file

@ -106,6 +106,10 @@ public extension TelegramEngine {
return _internal_deleteStickerFromStickerSet(account: self.account, sticker: sticker)
}
public func replaceSticker(previousSticker: FileMediaReference, sticker: ImportSticker) -> Signal<Never, ReplaceStickerError> {
return _internal_replaceSticker(account: self.account, previousSticker: previousSticker, sticker: sticker)
}
public func getMyStickerSets() -> Signal<[(StickerPackCollectionInfo, StickerPackItem?)], NoError> {
return _internal_getMyStickerSets(account: self.account)
}

View file

@ -437,7 +437,8 @@ swift_library(
"//submodules/TelegramUI/Components/Settings/BusinessHoursSetupScreen",
"//submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen",
"//submodules/TelegramUI/Components/Settings/QuickReplyNameAlertController",
"//submodules/TelegramUI/Components/Settings/CollectibleItemInfoScreen"
"//submodules/TelegramUI/Components/Settings/CollectibleItemInfoScreen",
"//submodules/TelegramUI/Components/StickerPickerScreen",
] + select({
"@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets,
"//build-system:ios_sim_arm64": [],

View file

@ -1615,6 +1615,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
break
}
}
maximumContentWidth = max(0.0, maximumContentWidth)
var contentPropertiesAndPrepareLayouts: [(Message, Bool, ChatMessageEntryAttributes, BubbleItemAttributes, (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize, _ avatarInset: CGFloat) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))))] = []
var addedContentNodes: [(Message, Bool, ChatMessageBubbleContentNode)]?

View file

@ -244,7 +244,7 @@ public class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode {
bubbleInsets = layoutConstants.image.bubbleInsets
}
sizeCalculation = .constrained(CGSize(width: constrainedSize.width - bubbleInsets.left - bubbleInsets.right, height: constrainedSize.height))
sizeCalculation = .constrained(CGSize(width: max(0.0, constrainedSize.width - bubbleInsets.left - bubbleInsets.right), height: constrainedSize.height))
case .mosaic:
bubbleInsets = UIEdgeInsets()
sizeCalculation = .unconstrained

View file

@ -7538,3 +7538,19 @@ private final class FadingMaskLayer: SimpleLayer {
self.fillLayer.frame = CGRect(origin: CGPoint(x: 0.0, y: gradientHeight), size: CGSize(width: self.bounds.width, height: self.bounds.height - gradientHeight))
}
}
public struct StickerPickerInputData: StickerPickerInput, Equatable {
public var emoji: EmojiPagerContentComponent
public var stickers: EmojiPagerContentComponent?
public var gifs: GifPagerContentComponent?
public init(
emoji: EmojiPagerContentComponent,
stickers: EmojiPagerContentComponent?,
gifs: GifPagerContentComponent?
) {
self.emoji = emoji
self.stickers = stickers
self.gifs = gifs
}
}

View file

@ -106,6 +106,7 @@ public final class MediaEditor {
case asset(PHAsset)
case draft(MediaEditorDraft)
case message(MessageId)
case sticker(TelegramMediaFile)
var dimensions: PixelDimensions {
switch self {
@ -117,6 +118,8 @@ public final class MediaEditor {
return draft.dimensions
case .message:
return PixelDimensions(width: 1080, height: 1920)
case .sticker:
return PixelDimensions(width: 1080, height: 1920)
}
}
}
@ -654,6 +657,19 @@ public final class MediaEditor {
)
}
}
case .sticker:
let image = generateImage(CGSize(width: 1080, height: 1920), contextGenerator: { size, context in
context.clear(CGRect(origin: .zero, size: size))
}, opaque: false, scale: 1.0)
textureSource = .single(
TextureSourceResult(
image: image,
nightImage: nil,
player: nil,
playerIsReference: false,
gradientColors: GradientColors(top: .clear, bottom: .clear)
)
)
}
self.textureSourceDisposable = (textureSource

View file

@ -55,7 +55,8 @@ swift_library(
"//submodules/WebPBinding",
"//submodules/StickerResources",
"//submodules/StickerPeekUI",
"//submodules/TelegramUI/Components/Stickers/StickerPackEditTitleController"
"//submodules/TelegramUI/Components/Stickers/StickerPackEditTitleController",
"//submodules/TelegramUI/Components/StickerPickerScreen",
],
visibility = [
"//visibility:public",

View file

@ -193,6 +193,8 @@ extension MediaEditorScreen {
if let pixel = generateSingleColorImage(size: CGSize(width: 1, height: 1), color: .black) {
innerSaveDraft(media: .image(image: pixel, dimensions: PixelDimensions(width: 1080, height: 1920)))
}
case .sticker:
break
}
if case let .draft(draft, _) = actualSubject {

View file

@ -45,6 +45,7 @@ import WebPBinding
import StickerResources
import StickerPeekUI
import StickerPackEditTitleController
import StickerPickerScreen
private let playbackButtonTag = GenericComponentViewTag()
private let muteButtonTag = GenericComponentViewTag()
@ -2054,8 +2055,14 @@ let storyMaxVideoDuration: Double = 60.0
public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate {
public enum Mode {
public enum StickerEditorMode {
case generic
case addingToPack
case editing
}
case storyEditor
case stickerEditor
case stickerEditor(mode: StickerEditorMode)
}
public enum TransitionIn {
@ -2150,7 +2157,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
let ciContext = CIContext(options: [.workingColorSpace : NSNull()])
private let stickerPickerInputData = Promise<StickerPickerInputData>()
private let stickerPickerInputData = Promise<StickerPickerInput>()
fileprivate var availableReactions: [ReactionItem] = []
private var availableReactionsDisposable: Disposable?
@ -2211,8 +2218,13 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
self.stickerTransparentView = UIImageView()
self.stickerTransparentView.clipsToBounds = true
var isStickerEditor = false
if case .stickerEditor = controller.mode {
isStickerEditor = true
}
self.entitiesContainerView = UIView(frame: CGRect(origin: .zero, size: storyDimensions))
self.entitiesView = DrawingEntitiesView(context: controller.context, size: storyDimensions, hasBin: true, isStickerEditor: controller.mode == .stickerEditor)
self.entitiesView = DrawingEntitiesView(context: controller.context, size: storyDimensions, hasBin: true, isStickerEditor: isStickerEditor)
self.entitiesView.getEntityCenterPosition = {
return CGPoint(x: storyDimensions.width / 2.0, y: storyDimensions.height / 2.0)
}
@ -2316,7 +2328,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
queue: .mainQueue(),
emojiItems,
stickerItems
) |> map { emoji, stickers -> StickerPickerInputData in
) |> map { emoji, stickers -> StickerPickerInput in
return StickerPickerInputData(emoji: emoji, stickers: stickers, gifs: nil)
} |> afterNext { [weak self] _ in
if let self {
@ -2495,7 +2507,12 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
}
}
let mediaEditor = MediaEditor(context: self.context, mode: controller.mode == .stickerEditor ? .sticker : .default, subject: effectiveSubject.editorSubject, values: initialValues, hasHistogram: true)
var isStickerEditor = false
if case .stickerEditor = controller.mode {
isStickerEditor = true
}
let mediaEditor = MediaEditor(context: self.context, mode: isStickerEditor ? .sticker : .default, subject: effectiveSubject.editorSubject, values: initialValues, hasHistogram: true)
if let initialVideoPosition = controller.initialVideoPosition {
mediaEditor.seek(initialVideoPosition, andPlay: true)
}
@ -2630,6 +2647,12 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
self.readyValue.set(.single(true))
})
})
} else if case let .sticker(sticker) = effectiveSubject {
let stickerEntity = DrawingStickerEntity(content: .file(sticker, .sticker))
stickerEntity.referenceDrawingSize = storyDimensions
stickerEntity.scale = 4.0
stickerEntity.position = CGPoint(x: storyDimensions.width / 2.0, y: storyDimensions.height / 2.0)
self.entitiesView.add(stickerEntity, announce: false)
}
self.gradientColorsDisposable = mediaEditor.gradientColors.start(next: { [weak self] colors in
@ -4066,7 +4089,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
if let controller = self.controller, case .stickerEditor = controller.mode {
hasInteractiveStickers = false
}
let controller = StickerPickerScreen(context: self.context, inputData: self.stickerPickerInputData.get(), defaultToEmoji: self.defaultToEmoji, hasGifs: hasInteractiveStickers, hasInteractiveStickers: hasInteractiveStickers)
let controller = StickerPickerScreen(context: self.context, inputData: self.stickerPickerInputData.get(), forceDark: true, defaultToEmoji: self.defaultToEmoji, hasGifs: hasInteractiveStickers, hasInteractiveStickers: hasInteractiveStickers)
controller.completion = { [weak self] content in
if let self {
if let content {
@ -4429,6 +4452,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
case asset(PHAsset)
case draft(MediaEditorDraft, Int64?)
case message([MessageId])
case sticker(TelegramMediaFile)
var dimensions: PixelDimensions {
switch self {
@ -4440,6 +4464,8 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
return draft.dimensions
case .message:
return PixelDimensions(width: 1080, height: 1920)
case .sticker:
return PixelDimensions(width: 1080, height: 1920)
}
}
@ -4455,6 +4481,8 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
return .draft(draft)
case let .message(messageIds):
return .message(messageIds.first!)
case let .sticker(sticker):
return .sticker(sticker)
}
}
@ -4474,6 +4502,8 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
return draft.isVideo
case .message:
return false
case .sticker:
return false
}
}
}
@ -5558,6 +5588,18 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
return (image, nil)
}
duration = 5.0
case .sticker:
let image = generateImage(CGSize(width: 1080, height: 1920), contextGenerator: { size, context in
context.clear(CGRect(origin: .zero, size: size))
}, opaque: false, scale: 1.0)
let tempImagePath = NSTemporaryDirectory() + "\(Int64.random(in: Int64.min ... Int64.max)).png"
if let data = image?.pngData() {
try? data.write(to: URL(fileURLWithPath: tempImagePath))
}
videoResult = .single(.imageFile(path: tempImagePath))
duration = 3.0
firstFrame = .single((image, nil))
}
let _ = combineLatest(queue: Queue.mainQueue(), firstFrame, videoResult)
@ -5672,10 +5714,133 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
self.context.account.postbox.mediaBox.storeResourceData(resource.id, data: data)
}
}
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }.withUpdated(theme: defaultDarkColorPresentationTheme)
let file = stickerFile(resource: resource, size: Int64(0), dimensions: PixelDimensions(image.size))
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }.withUpdated(theme: defaultDarkColorPresentationTheme)
var menuItems: [ContextMenuItem] = []
if case let .stickerEditor(mode) = self.mode {
switch mode {
case .generic:
menuItems.append(.action(ContextMenuActionItem(text: presentationData.strings.StickerPack_Send, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Resend"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in
f(.default)
guard let self else {
return
}
self.completion(MediaEditorScreen.Result(
media: .sticker(file: file),
mediaAreas: [],
caption: NSAttributedString(),
options: MediaEditorResultPrivacy(sendAsPeerId: nil, privacy: EngineStoryPrivacy(base: .everyone, additionallyIncludePeers: []), timeout: 0, isForwardingDisabled: false, pin: false),
stickers: [],
randomId: 0
), { [weak self] finished in
self?.node.animateOut(finished: true, saveDraft: false, completion: { [weak self] in
self?.dismiss()
Queue.mainQueue().justDispatch {
finished()
}
})
})
})))
menuItems.append(.action(ContextMenuActionItem(text: presentationData.strings.Stickers_AddToFavorites, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Fave"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in
f(.default)
guard let self else {
return
}
self.uploadSticker(file, action: .addToFavorites)
})))
menuItems.append(.action(ContextMenuActionItem(text: "Add to Sticker Set", icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/AddSticker"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, f in
guard let self else {
return
}
var contextItems: [ContextMenuItem] = []
contextItems.append(.action(ContextMenuActionItem(text: "Back", icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.contextMenu.primaryColor)
}, iconPosition: .left, action: { c, _ in
c.popItems()
})))
contextItems.append(.separator)
contextItems.append(.action(ContextMenuActionItem(text: "New Sticker Set", icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/AddCircle"), color: theme.contextMenu.primaryColor) }, iconPosition: .left, action: { [weak self] _, f in
if let self {
self.presentCreateStickerPack(file: file, completion: {
f(.default)
})
}
})))
let thumbSize = CGSize(width: 24.0, height: 24.0)
for (pack, firstItem) in self.myStickerPacks {
let thumbnailResource = pack.thumbnail?.resource ?? firstItem?.file.resource
let thumbnailIconSource: ContextMenuActionItemIconSource?
if let thumbnailResource {
var resourceId: Int64 = 0
if let resource = thumbnailResource as? CloudDocumentMediaResource {
resourceId = resource.fileId
}
let thumbnailFile = firstItem?.file ?? TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.CloudFile, id: resourceId), partialReference: nil, resource: thumbnailResource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "image/webp", size: thumbnailResource.size ?? 0, attributes: [])
let _ = freeMediaFileInteractiveFetched(account: self.context.account, userLocation: .other, fileReference: .stickerPack(stickerPack: .id(id: pack.id.id, accessHash: pack.accessHash), media: thumbnailFile)).start()
thumbnailIconSource = ContextMenuActionItemIconSource(
size: thumbSize,
signal: chatMessageStickerPackThumbnail(postbox: self.context.account.postbox, resource: thumbnailResource)
|> map { generator -> UIImage? in
return generator(TransformImageArguments(corners: ImageCorners(), imageSize: thumbSize, boundingSize: thumbSize, intrinsicInsets: .zero))?.generateImage()
}
)
} else {
thumbnailIconSource = nil
}
contextItems.append(.action(ContextMenuActionItem(text: pack.title, icon: { _ in return nil }, iconSource: thumbnailIconSource, iconPosition: .left, action: { [weak self] _, f in
guard let self else {
return
}
f(.default)
self.uploadSticker(file, action: .addToStickerPack(pack: .id(id: pack.id.id, accessHash: pack.accessHash), title: pack.title))
})))
}
let items = ContextController.Items(
id: 1,
content: .list(contextItems),
context: nil,
reactionItems: [],
selectedReactionItems: Set(),
reactionsTitle: nil,
reactionsLocked: false,
animationCache: nil,
alwaysAllowPremiumReactions: false,
allPresetReactionsAreAvailable: false,
getEmojiContent: nil,
disablePositionLock: false,
tip: nil,
tipSignal: nil,
dismissed: nil
)
c.pushItems(items: .single(items))
})))
case .editing:
menuItems.append(.action(ContextMenuActionItem(text: "Replace Sticker", icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Replace"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in
guard let self else {
return
}
f(.default)
self.uploadSticker(file, action: .upload)
})))
case .addingToPack:
menuItems.append(.action(ContextMenuActionItem(text: "Add to Sticker Set", icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/AddSticker"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, f in
guard let self else {
return
}
f(.default)
self.uploadSticker(file, action: .upload)
})))
}
}
let peekController = PeekController(
presentationData: presentationData,
content: StickerPreviewPeekContent(
@ -5684,108 +5849,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
strings: presentationData.strings,
item: .image(image),
isCreating: true,
menu: [
.action(ContextMenuActionItem(text: presentationData.strings.StickerPack_Send, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Resend"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in
f(.default)
guard let self else {
return
}
self.completion(MediaEditorScreen.Result(
media: .sticker(file: file),
mediaAreas: [],
caption: NSAttributedString(),
options: MediaEditorResultPrivacy(sendAsPeerId: nil, privacy: EngineStoryPrivacy(base: .everyone, additionallyIncludePeers: []), timeout: 0, isForwardingDisabled: false, pin: false),
stickers: [],
randomId: 0
), { [weak self] finished in
self?.node.animateOut(finished: true, saveDraft: false, completion: { [weak self] in
self?.dismiss()
Queue.mainQueue().justDispatch {
finished()
}
})
})
})),
.action(ContextMenuActionItem(text: presentationData.strings.Stickers_AddToFavorites, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Fave"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in
f(.default)
guard let self else {
return
}
self.uploadSticker(file, action: .addToFavorites)
})),
.action(ContextMenuActionItem(text: "Add to Sticker Set", icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/AddSticker"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, f in
guard let self else {
return
}
var contextItems: [ContextMenuItem] = []
contextItems.append(.action(ContextMenuActionItem(text: "Back", icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.contextMenu.primaryColor)
}, iconPosition: .left, action: { c, _ in
c.popItems()
})))
contextItems.append(.separator)
contextItems.append(.action(ContextMenuActionItem(text: "New Sticker Set", icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/AddCircle"), color: theme.contextMenu.primaryColor) }, iconPosition: .left, action: { [weak self] _, f in
if let self {
self.presentCreateStickerPack(file: file, completion: {
f(.default)
})
}
})))
let thumbSize = CGSize(width: 24.0, height: 24.0)
for (pack, firstItem) in self.myStickerPacks {
let thumbnailResource = pack.thumbnail?.resource ?? firstItem?.file.resource
let thumbnailIconSource: ContextMenuActionItemIconSource?
if let thumbnailResource {
var resourceId: Int64 = 0
if let resource = thumbnailResource as? CloudDocumentMediaResource {
resourceId = resource.fileId
}
let thumbnailFile = firstItem?.file ?? TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.CloudFile, id: resourceId), partialReference: nil, resource: thumbnailResource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "image/webp", size: thumbnailResource.size ?? 0, attributes: [])
let _ = freeMediaFileInteractiveFetched(account: self.context.account, userLocation: .other, fileReference: .stickerPack(stickerPack: .id(id: pack.id.id, accessHash: pack.accessHash), media: thumbnailFile)).start()
thumbnailIconSource = ContextMenuActionItemIconSource(
size: thumbSize,
signal: chatMessageStickerPackThumbnail(postbox: self.context.account.postbox, resource: thumbnailResource)
|> map { generator -> UIImage? in
return generator(TransformImageArguments(corners: ImageCorners(), imageSize: thumbSize, boundingSize: thumbSize, intrinsicInsets: .zero))?.generateImage()
}
)
} else {
thumbnailIconSource = nil
}
contextItems.append(.action(ContextMenuActionItem(text: pack.title, icon: { _ in return nil }, iconSource: thumbnailIconSource, iconPosition: .left, action: { [weak self] _, f in
guard let self else {
return
}
f(.default)
self.uploadSticker(file, action: .addToStickerPack(pack: .id(id: pack.id.id, accessHash: pack.accessHash), title: pack.title))
})))
}
let items = ContextController.Items(
id: 1,
content: .list(contextItems),
context: nil,
reactionItems: [],
selectedReactionItems: Set(),
reactionsTitle: nil,
reactionsLocked: false,
animationCache: nil,
alwaysAllowPremiumReactions: false,
allPresetReactionsAreAvailable: false,
getEmojiContent: nil,
disablePositionLock: false,
tip: nil,
tipSignal: nil,
dismissed: nil
)
c.pushItems(items: .single(items))
}))
],
menu: menuItems,
reactionItems: self.node.availableReactions,
openPremiumIntro: {}
),
@ -5806,6 +5870,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
case addToFavorites
case createStickerPack(title: String)
case addToStickerPack(pack: StickerPackReference, title: String)
case upload
}
private func presentCreateStickerPack(file: TelegramMediaFile, completion: @escaping () -> Void) {
@ -5938,6 +6003,8 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|> map { _ in
return status
}
case .upload:
return .single(status)
}
}
}
@ -5953,9 +6020,25 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
self.updateEditProgress(progress, cancel: { [weak self] in
self?.stickerUploadDisposable.set(nil)
})
case .complete:
case let .complete(resource, _):
let navigationController = self.navigationController as? NavigationController
self.completion(MediaEditorScreen.Result(), { [weak self] finished in
let result: MediaEditorScreen.Result
if case .upload = action {
let file = stickerFile(resource: resource, size: resource.size ?? 0, dimensions: dimensions)
result = MediaEditorScreen.Result(
media: .sticker(file: file),
mediaAreas: [],
caption: NSAttributedString(),
options: MediaEditorResultPrivacy(sendAsPeerId: nil, privacy: EngineStoryPrivacy(base: .everyone, additionallyIncludePeers: []), timeout: 0, isForwardingDisabled: false, pin: false),
stickers: [],
randomId: 0
)
} else {
result = MediaEditorScreen.Result()
}
self.completion(result, { [weak self] finished in
self?.node.animateOut(finished: true, saveDraft: false, completion: { [weak self] in
guard let self else {
return
@ -5972,7 +6055,6 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
}
case let .addToStickerPack(packReference, title):
let navigationController = self.navigationController as? NavigationController
self.dismiss()
if let navigationController {
Queue.mainQueue().after(0.2) {
let controller = self.context.sharedContext.makeStickerPackScreen(context: self.context, updatedPresentationData: nil, mainStickerPack: packReference, stickerPacks: [packReference], loadedStickerPacks: [], isEditing: false, parentNavigationController: nil, sendSticker: nil)
@ -6102,6 +6184,11 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
return image.flatMap({ .single(.image(image: $0)) }) ?? .complete()
}
}
case .sticker:
let image = generateImage(CGSize(width: 1080, height: 1920), contextGenerator: { size, context in
context.clear(CGRect(origin: .zero, size: size))
}, opaque: false, scale: 1.0)!
exportSubject = .single(.image(image: image))
}
let _ = exportSubject.start(next: { [weak self] exportSubject in

View file

@ -8792,7 +8792,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
if let previousController = navigationController.viewControllers.last as? ShareWithPeersScreen {
previousController.dismiss()
}
let controller = self.context.sharedContext.makePremiumBoostLevelsController(context: self.context, peerId: peer.id, boostStatus: boostStatus, myBoostStatus: myBoostStatus, forceDark: false, openStats: { [weak self] in
let controller = self.context.sharedContext.makePremiumBoostLevelsController(context: self.context, peerId: peer.id, subject: .stories, boostStatus: boostStatus, myBoostStatus: myBoostStatus, forceDark: false, openStats: { [weak self] in
if let self {
self.openStats(boosts: true, boostStatus: boostStatus)
}

View file

@ -0,0 +1,24 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "PremiumPeerShortcutComponent",
module_name = "PremiumPeerShortcutComponent",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/Display",
"//submodules/ComponentFlow",
"//submodules/TelegramCore",
"//submodules/TelegramPresentationData",
"//submodules/AvatarNode",
"//submodules/Components/MultilineTextComponent",
"//submodules/AccountContext",
],
visibility = [
"//visibility:public",
],
)

View file

@ -0,0 +1,106 @@
import Foundation
import UIKit
import Display
import ComponentFlow
import TelegramCore
import TelegramPresentationData
import AccountContext
import AvatarNode
import MultilineTextComponent
public final class PremiumPeerShortcutComponent: Component {
let context: AccountContext
let theme: PresentationTheme
let peer: EnginePeer
public init(context: AccountContext, theme: PresentationTheme, peer: EnginePeer) {
self.context = context
self.theme = theme
self.peer = peer
}
public static func ==(lhs: PremiumPeerShortcutComponent, rhs: PremiumPeerShortcutComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.theme !== rhs.theme {
return false
}
if lhs.peer != rhs.peer {
return false
}
return true
}
public final class View: UIView {
private let backgroundView = UIView()
private let avatarNode: AvatarNode
private let text = ComponentView<Empty>()
private var component: PremiumPeerShortcutComponent?
private weak var state: EmptyComponentState?
public override init(frame: CGRect) {
self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 18.0))
super.init(frame: frame)
self.backgroundView.clipsToBounds = true
self.backgroundView.layer.cornerRadius = 16.0
self.addSubview(self.backgroundView)
self.addSubnode(self.avatarNode)
}
public required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public func update(component: PremiumPeerShortcutComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
self.component = component
self.state = state
self.backgroundView.backgroundColor = component.theme.list.itemBlocksSeparatorColor.withAlphaComponent(0.3)
self.avatarNode.frame = CGRect(origin: CGPoint(x: 1.0, y: 1.0), size: CGSize(width: 30.0, height: 30.0))
self.avatarNode.setPeer(
context: component.context,
theme: component.context.sharedContext.currentPresentationData.with({ $0 }).theme,
peer: component.peer,
synchronousLoad: true
)
let textSize = self.text.update(
transition: .immediate,
component: AnyComponent(
MultilineTextComponent(
text: .plain(NSAttributedString(string: component.peer.compactDisplayTitle, font: Font.medium(15.0), textColor: component.theme.list.itemPrimaryTextColor, paragraphAlignment: .left))
)
),
environment: {},
containerSize: CGSize(width: availableSize.width - 50.0, height: availableSize.height)
)
let size = CGSize(width: 30.0 + textSize.width + 20.0, height: 32.0)
if let view = self.text.view {
if view.superview == nil {
self.addSubview(view)
}
let textFrame = CGRect(origin: CGPoint(x: 38.0, y: floorToScreenPixels((size.height - textSize.height) / 2.0)), size: textSize)
view.frame = textFrame
}
self.backgroundView.frame = CGRect(origin: .zero, size: size)
return size
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public 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

@ -1111,7 +1111,7 @@ final class ShareWithPeersScreenComponent: Component {
self.hapticFeedback.impact(.light)
} else {
self.postingAvailabilityDisposable.set((component.context.engine.messages.checkStoriesUploadAvailability(target: .peer(peer.id))
|> deliverOnMainQueue).start(next: { [weak self] status in
|> deliverOnMainQueue).start(next: { [weak self] status in
guard let self, let component = self.component else {
return
}
@ -1134,7 +1134,7 @@ final class ShareWithPeersScreenComponent: Component {
if let previousController = navigationController.viewControllers.last as? ShareWithPeersScreen {
previousController.dismiss()
}
let controller = component.context.sharedContext.makePremiumBoostLevelsController(context: component.context, peerId: peer.id, boostStatus: boostStatus, myBoostStatus: myBoostStatus, forceDark: true, openStats: nil)
let controller = component.context.sharedContext.makePremiumBoostLevelsController(context: component.context, peerId: peer.id, subject: .stories, boostStatus: boostStatus, myBoostStatus: myBoostStatus, forceDark: true, openStats: nil)
navigationController.pushViewController(controller)
}
self.hapticFeedback.impact(.light)

View file

@ -0,0 +1,46 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "StickerPickerScreen",
module_name = "StickerPickerScreen",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/AsyncDisplayKit",
"//submodules/Display",
"//submodules/Postbox",
"//submodules/TelegramCore",
"//submodules/SSignalKit/SwiftSignalKit",
"//submodules/ComponentFlow",
"//submodules/Components/ViewControllerComponent",
"//submodules/Components/ComponentDisplayAdapters",
"//submodules/Components/MultilineTextComponent",
"//submodules/TelegramPresentationData",
"//submodules/TelegramUIPreferences",
"//submodules/AccountContext",
"//submodules/AppBundle",
"//submodules/TelegramStringFormatting",
"//submodules/PresentationDataUtils",
"//submodules/TelegramNotices",
"//submodules/FeaturedStickersScreen",
"//submodules/Components/PagerComponent",
"//submodules/Components/BundleIconComponent",
"//submodules/TelegramUI/Components/EntityKeyboardGifContent",
"//submodules/TelegramUI/Components/EntityKeyboard",
"//submodules/TelegramUI/Components/ChatEntityKeyboardInputNode",
"//submodules/TelegramUI/Components/ButtonComponent",
"//submodules/ChatPresentationInterfaceState",
"//submodules/TelegramUI/Components/MediaEditor",
"//submodules/TelegramUI/Components/CameraButtonComponent",
"//submodules/ContextUI",
"//submodules/UndoUI",
"//submodules/GalleryUI",
],
visibility = [
"//visibility:public",
],
)

View file

@ -17,28 +17,11 @@ import ChatEntityKeyboardInputNode
import ContextUI
import ChatPresentationInterfaceState
import MediaEditor
import StickerPackPreviewUI
import EntityKeyboardGifContent
import GalleryUI
import UndoUI
import CameraButtonComponent
import BundleIconComponent
public struct StickerPickerInputData: Equatable {
var emoji: EmojiPagerContentComponent
var stickers: EmojiPagerContentComponent?
var gifs: GifPagerContentComponent?
public init(
emoji: EmojiPagerContentComponent,
stickers: EmojiPagerContentComponent?,
gifs: GifPagerContentComponent?
) {
self.emoji = emoji
self.stickers = stickers
self.gifs = gifs
}
}
import UndoUI
import GalleryUI
private final class StickerSelectionComponent: Component {
typealias EnvironmentType = Empty
@ -148,13 +131,13 @@ private final class StickerSelectionComponent: Component {
sendSticker: { [weak self] file, silent, schedule, query, clearInput, sourceView, sourceRect, sourceLayer, _ in
if let self, let controller = self.component?.getController() {
controller.forEachController { c in
if let c = c as? StickerPackScreenImpl {
if let c = c as? (ViewController & StickerPackScreen) {
c.dismiss(animated: true)
}
return true
}
controller.window?.forEachController({ c in
if let c = c as? StickerPackScreenImpl {
if let c = c as? (ViewController & StickerPackScreen) {
c.dismiss(animated: true)
}
})
@ -259,7 +242,8 @@ private final class StickerSelectionComponent: Component {
let topPanelHeight: CGFloat = 42.0
let defaultToEmoji = component.getController()?.defaultToEmoji ?? false
let controller = component.getController()
let defaultToEmoji = controller?.defaultToEmoji ?? false
let context = component.context
let stickerPeekBehavior = EmojiContentPeekBehaviorImpl(
@ -360,7 +344,7 @@ private final class StickerSelectionComponent: Component {
deviceMetrics: component.deviceMetrics,
hiddenInputHeight: 0.0,
inputHeight: 0.0,
displayBottomPanel: true,
displayBottomPanel: controller?.isFullscreen == false,
isExpanded: true,
clipContentToTopPanel: false,
useExternalSearchContainer: false
@ -523,7 +507,9 @@ public class StickerPickerScreen: ViewController {
self.containerView.clipsToBounds = true
self.containerView.backgroundColor = .clear
self.addSubnode(self.dim)
if !controller.isFullscreen {
self.addSubnode(self.dim)
}
self.view.addSubview(self.wrappingView)
self.wrappingView.addSubview(self.containerView)
@ -646,12 +632,14 @@ public class StickerPickerScreen: ViewController {
self.stickerSearchState.get(),
self.emojiSearchState.get()
)
self.contentDisposable.set(data.start(next: { [weak self] inputData, gifData, stickerSearchState, emojiSearchState in
if let strongSelf = self {
let presentationData = strongSelf.presentationData
var inputData = inputData
guard var inputData = inputData as? StickerPickerInputData else {
return
}
inputData.gifs = gifData?.component
let emoji = inputData.emoji
@ -1544,7 +1532,9 @@ public class StickerPickerScreen: ViewController {
self.dim.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:))))
self.controller?.navigationBar?.updateBackgroundAlpha(0.0, transition: .immediate)
if let controller = self.controller, !controller.isFullscreen {
controller.navigationBar?.updateBackgroundAlpha(0.0, transition: .immediate)
}
}
@objc func dimTapGesture(_ recognizer: UITapGestureRecognizer) {
@ -1555,6 +1545,9 @@ public class StickerPickerScreen: ViewController {
}
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
guard let controller = self.controller, !controller.isFullscreen else {
return false
}
if let (layout, _) = self.currentLayout {
if layout.metrics.isTablet {
return false
@ -1579,6 +1572,9 @@ public class StickerPickerScreen: ViewController {
private var isDismissing = false
func animateIn() {
guard let controller = self.controller, !controller.isFullscreen else {
return
}
ContainedViewLayoutTransition.animated(duration: 0.3, curve: .linear).updateAlpha(node: self.dim, alpha: 1.0)
let targetPosition = self.containerView.center
@ -1643,7 +1639,10 @@ public class StickerPickerScreen: ViewController {
let clipFrame: CGRect
let contentFrame: CGRect
if layout.metrics.widthClass == .compact {
if controller.isFullscreen {
clipFrame = CGRect(origin: CGPoint(), size: layout.size)
contentFrame = clipFrame
} else if layout.metrics.widthClass == .compact {
self.dim.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.25)
if isLandscape {
self.containerView.layer.cornerRadius = 0.0
@ -1753,9 +1752,14 @@ public class StickerPickerScreen: ViewController {
}
private var defaultTopInset: CGFloat {
guard let (layout, _) = self.currentLayout else{
guard let (layout, navigationBarHeight) = self.currentLayout else {
return 210.0
}
if let controller = self.controller, controller.isFullscreen {
return navigationBarHeight
}
if case .compact = layout.metrics.widthClass {
var factor: CGFloat = 0.2488
if layout.size.width <= 320.0 {
@ -1789,6 +1793,10 @@ public class StickerPickerScreen: ViewController {
return
}
guard let controller = self.controller, !controller.isFullscreen else {
return
}
let isLandscape = layout.orientation == .landscape
let edgeTopInset = isLandscape ? 0.0 : defaultTopInset
@ -1970,8 +1978,10 @@ public class StickerPickerScreen: ViewController {
private let context: AccountContext
private let theme: PresentationTheme
private let inputData: Signal<StickerPickerInputData, NoError>
private let inputData: Signal<StickerPickerInput, NoError>
fileprivate let defaultToEmoji: Bool
let isFullscreen: Bool
let hasEmoji: Bool
let hasGifs: Bool
let hasInteractiveStickers: Bool
@ -1988,17 +1998,26 @@ public class StickerPickerScreen: ViewController {
public var addReaction: () -> Void = { }
public var addCamera: () -> Void = { }
public init(context: AccountContext, inputData: Signal<StickerPickerInputData, NoError>, defaultToEmoji: Bool = false, hasGifs: Bool = false, hasInteractiveStickers: Bool = true) {
public init(context: AccountContext, inputData: Signal<StickerPickerInput, NoError>, forceDark: Bool = false, expanded: Bool = false, defaultToEmoji: Bool = false, hasEmoji: Bool = true, hasGifs: Bool = false, hasInteractiveStickers: Bool = true) {
self.context = context
self.theme = defaultDarkColorPresentationTheme
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
self.theme = forceDark ? defaultDarkColorPresentationTheme : presentationData.theme
self.inputData = inputData
self.isFullscreen = expanded
self.defaultToEmoji = defaultToEmoji
self.hasEmoji = hasEmoji
self.hasGifs = hasGifs
self.hasInteractiveStickers = hasInteractiveStickers
super.init(navigationBarPresentationData: nil)
super.init(navigationBarPresentationData: expanded ? NavigationBarPresentationData(presentationData: presentationData) : nil)
self.statusBar.statusBarStyle = .Ignore
if expanded {
//TODO:localize
self.title = "Choose Sticker"
self.navigationPresentation = .modal
}
}
required init(coder aDecoder: NSCoder) {
@ -2014,14 +2033,18 @@ public class StickerPickerScreen: ViewController {
public override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) {
self.view.endEditing(true)
if flag {
self.node.animateOut(completion: {
if self.isFullscreen {
super.dismiss(animated: flag, completion: completion)
} else {
if flag {
self.node.animateOut(completion: {
super.dismiss(animated: false, completion: {})
completion?()
})
} else {
super.dismiss(animated: false, completion: {})
completion?()
})
} else {
super.dismiss(animated: false, completion: {})
completion?()
}
}
}

View file

@ -21,10 +21,12 @@ public final class TabSelectorComponent: Component {
public struct CustomLayout: Equatable {
public var font: UIFont
public var spacing: CGFloat
public var lineSelection: Bool
public init(font: UIFont, spacing: CGFloat) {
public init(font: UIFont, spacing: CGFloat, lineSelection: Bool = false) {
self.font = font
self.spacing = spacing
self.lineSelection = lineSelection
}
}
@ -107,6 +109,8 @@ public final class TabSelectorComponent: Component {
}
func update(component: TabSelectorComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
let selectionColorUpdated = component.colors.selection != self.component?.colors.selection
self.component = component
self.state = state
@ -115,14 +119,27 @@ public final class TabSelectorComponent: Component {
let spacing: CGFloat = component.customLayout?.spacing ?? 2.0
let itemFont: UIFont
var isLineSelection = false
if let customLayout = component.customLayout {
itemFont = customLayout.font
isLineSelection = customLayout.lineSelection
} else {
itemFont = Font.semibold(14.0)
}
if self.selectionView.image == nil {
self.selectionView.image = generateStretchableFilledCircleImage(diameter: baseHeight, color: component.colors.selection)
if selectionColorUpdated {
if isLineSelection {
self.selectionView.image = generateImage(CGSize(width: 5.0, height: 3.0), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(component.colors.selection.cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(), size: CGSize(width: 4.0, height: 4.0)))
context.fillEllipse(in: CGRect(origin: CGPoint(x: size.width - 4.0, y: 0.0), size: CGSize(width: 4.0, height: 4.0)))
context.fill(CGRect(x: 2.0, y: 0.0, width: size.width - 4.0, height: 4.0))
context.fill(CGRect(x: 0.0, y: 2.0, width: size.width, height: 2.0))
})?.resizableImage(withCapInsets: UIEdgeInsets(top: 3.0, left: 3.0, bottom: 0.0, right: 3.0), resizingMode: .stretch)
} else {
self.selectionView.image = generateStretchableFilledCircleImage(diameter: baseHeight, color: component.colors.selection)
}
}
var contentWidth: CGFloat = 0.0
@ -146,7 +163,11 @@ public final class TabSelectorComponent: Component {
let itemSize = itemView.title.update(
transition: .immediate,
component: AnyComponent(PlainButtonComponent(
content: AnyComponent(Text(text: item.title, font: itemFont, color: component.colors.foreground)),
content: AnyComponent(Text(
text: item.title,
font: itemFont,
color: item.id == component.selectedId && isLineSelection ? component.colors.selection : component.colors.foreground
)),
effectAlignment: .center,
minSize: nil,
action: { [weak self] in
@ -154,7 +175,8 @@ public final class TabSelectorComponent: Component {
return
}
component.setSelectedId(itemId)
}
},
animateScale: !isLineSelection
)),
environment: {},
containerSize: CGSize(width: 200.0, height: 100.0)
@ -178,7 +200,7 @@ public final class TabSelectorComponent: Component {
}
itemTransition.setPosition(view: itemTitleView, position: itemTitleFrame.origin)
itemTransition.setBounds(view: itemTitleView, bounds: CGRect(origin: CGPoint(), size: itemTitleFrame.size))
itemTransition.setAlpha(view: itemTitleView, alpha: item.id == component.selectedId ? 1.0 : 0.4)
itemTransition.setAlpha(view: itemTitleView, alpha: item.id == component.selectedId || isLineSelection ? 1.0 : 0.4)
}
}
@ -195,7 +217,15 @@ public final class TabSelectorComponent: Component {
if let selectedBackgroundRect {
self.selectionView.alpha = 1.0
transition.setFrame(view: self.selectionView, frame: selectedBackgroundRect)
if isLineSelection {
var mappedSelectionFrame = selectedBackgroundRect.insetBy(dx: 12.0, dy: 0.0)
mappedSelectionFrame.origin.y = mappedSelectionFrame.maxY + 6.0
mappedSelectionFrame.size.height = 3.0
transition.setFrame(view: self.selectionView, frame: mappedSelectionFrame)
} else {
transition.setFrame(view: self.selectionView, frame: selectedBackgroundRect)
}
} else {
self.selectionView.alpha = 0.0
}

View file

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "ads_30.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View file

@ -0,0 +1,99 @@
%PDF-1.7
1 0 obj
<< >>
endobj
2 0 obj
<< /Length 3 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 5.334961 4.678772 cm
0.000000 0.000000 0.000000 scn
18.330002 19.142712 m
18.330002 20.972393 16.201080 21.977081 14.788693 20.813940 c
8.926420 15.986186 l
4.665000 15.986186 l
2.088591 15.986186 0.000000 13.897594 0.000000 11.321186 c
0.000000 8.970550 1.738582 7.025980 4.000000 6.703224 c
4.000000 3.821186 l
4.000000 2.349346 5.193161 1.156185 6.665000 1.156185 c
8.136839 1.156185 9.330000 2.349346 9.330000 3.821186 c
9.330000 6.323827 l
14.788692 1.828432 l
16.201078 0.665291 18.330002 1.669977 18.330002 3.499660 c
18.330002 19.142712 l
h
8.000000 6.656186 m
5.330000 6.656186 l
5.330000 3.821186 l
5.330000 3.083885 5.927700 2.486187 6.665000 2.486187 c
7.402300 2.486187 8.000000 3.083885 8.000000 3.821186 c
8.000000 6.656186 l
h
15.634185 19.787273 m
16.178917 20.235874 17.000000 19.848385 17.000000 19.142712 c
17.000000 3.499660 l
17.000000 2.793987 16.178915 2.406498 15.634184 2.855101 c
9.623762 7.804859 l
9.481418 7.922084 9.302749 7.986186 9.118342 7.986186 c
4.665000 7.986186 l
2.823130 7.986186 1.330000 9.479317 1.330000 11.321186 c
1.330000 13.163055 2.823130 14.656186 4.665000 14.656186 c
9.118342 14.656186 l
9.302745 14.656186 9.481414 14.720285 9.623762 14.837513 c
15.634185 19.787273 l
h
f*
n
Q
endstream
endobj
3 0 obj
1297
endobj
4 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 30.000000 30.000000 ]
/Resources 1 0 R
/Contents 2 0 R
/Parent 5 0 R
>>
endobj
5 0 obj
<< /Kids [ 4 0 R ]
/Count 1
/Type /Pages
>>
endobj
6 0 obj
<< /Pages 5 0 R
/Type /Catalog
>>
endobj
xref
0 7
0000000000 65535 f
0000000010 00000 n
0000000034 00000 n
0000001387 00000 n
0000001410 00000 n
0000001583 00000 n
0000001657 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 6 0 R
/Size 7
>>
startxref
1716
%%EOF

View file

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "monetization_90.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View file

@ -0,0 +1,188 @@
%PDF-1.7
1 0 obj
<< >>
endobj
2 0 obj
<< /Length 3 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 23.000000 17.500000 cm
0.000000 0.000000 0.000000 scn
37.500000 41.500000 m
37.500000 33.215729 30.784271 26.500000 22.500000 26.500000 c
14.215729 26.500000 7.500000 33.215729 7.500000 41.500000 c
7.500000 49.784271 14.215729 56.500000 22.500000 56.500000 c
30.784271 56.500000 37.500000 49.784271 37.500000 41.500000 c
h
16.055553 45.507233 m
15.908934 45.773811 16.101799 46.099998 16.406040 46.099998 c
21.299994 46.099998 l
21.299994 35.971886 l
16.055553 45.507233 l
h
23.699993 35.971840 m
28.944456 45.507229 l
29.091078 45.773815 28.898209 46.099998 28.593971 46.099998 c
23.699993 46.099998 l
23.699993 35.971840 l
h
16.406040 48.500000 m
14.276352 48.500000 12.926297 46.216698 13.952635 44.350628 c
20.046600 33.270691 l
21.110390 31.336523 23.889614 31.336515 24.953409 33.270691 c
31.047375 44.350624 l
32.073708 46.216690 30.723663 48.500000 28.593971 48.500000 c
16.406040 48.500000 l
h
4.837455 37.573509 m
5.430337 36.994904 5.441912 36.045227 4.863308 35.452347 c
3.594926 34.152664 3.000000 32.810127 3.000000 31.500000 c
3.000000 29.437628 4.521029 27.238575 7.690794 25.410986 c
7.902112 25.289146 8.120757 25.168959 8.346768 25.050571 c
11.891893 23.193604 16.893353 22.000000 22.500000 22.000000 c
28.106646 22.000000 33.108109 23.193604 36.653233 25.050571 c
40.269405 26.944759 42.000000 29.300137 42.000000 31.500000 c
42.000000 32.810127 41.405075 34.152664 40.136692 35.452347 c
39.558090 36.045227 39.569664 36.994904 40.162544 37.573509 c
40.755428 38.152111 41.705105 38.140537 42.283710 37.547653 c
43.943344 35.847065 45.000000 33.790764 45.000000 31.500000 c
45.000000 22.500000 l
45.000000 21.500000 l
45.000000 12.500000 l
45.000000 8.624733 42.029583 5.480110 38.045254 3.393078 c
33.989872 1.268829 28.491333 0.000000 22.500000 0.000000 c
16.508667 0.000000 11.010127 1.268829 6.954747 3.393078 c
2.970417 5.480110 0.000000 8.624733 0.000000 12.500000 c
0.000000 21.500000 l
0.000000 22.500000 l
0.000000 31.500000 l
0.000000 33.790764 1.056656 35.847065 2.716292 37.547653 c
3.294897 38.140537 4.244574 38.152111 4.837455 37.573509 c
h
38.045254 22.393078 m
39.540852 23.176487 40.893585 24.108913 41.999977 25.173065 c
42.000000 25.173086 l
42.000000 22.500000 l
42.000000 21.500000 l
42.000000 19.371666 40.380116 17.097767 37.000000 15.236786 c
37.000000 21.879204 l
37.358109 22.044373 37.706707 22.215744 38.045254 22.393078 c
h
34.000000 20.713173 m
32.442459 20.208160 30.765238 19.804638 29.000000 19.516380 c
29.000000 12.559616 l
30.805933 12.881241 32.486198 13.331841 34.000000 13.885990 c
34.000000 20.713173 l
h
22.500000 19.000000 m
23.688532 19.000000 24.857672 19.049931 26.000000 19.146568 c
26.000000 12.158058 l
24.866440 12.054405 23.697014 12.000000 22.500000 12.000000 c
21.651834 12.000000 20.817518 12.027317 20.000000 12.080112 c
20.000000 19.074383 l
20.821783 19.025181 21.656002 19.000000 22.500000 19.000000 c
h
22.500000 9.000000 m
23.688532 9.000000 24.857672 9.049931 26.000000 9.146568 c
26.000000 3.158058 l
24.866440 3.054405 23.697014 3.000000 22.500000 3.000000 c
21.651834 3.000000 20.817518 3.027317 20.000000 3.080112 c
20.000000 9.074383 l
20.821783 9.025181 21.656002 9.000000 22.500000 9.000000 c
h
34.000000 4.885990 m
32.486198 4.331841 30.805933 3.881241 29.000000 3.559616 c
29.000000 9.516380 l
30.765238 9.804638 32.442459 10.208160 34.000000 10.713173 c
34.000000 4.885990 l
h
17.000000 12.396725 m
17.000000 19.366760 l
14.866652 19.657177 12.848998 20.113659 11.000000 20.713173 c
11.000000 13.885990 l
12.789782 13.230812 14.812259 12.720390 17.000000 12.396725 c
h
17.000000 9.366760 m
17.000000 3.396725 l
14.812259 3.720390 12.789782 4.230812 11.000000 4.885990 c
11.000000 10.713173 l
12.848998 10.113659 14.866652 9.657177 17.000000 9.366760 c
h
6.954747 22.393078 m
7.293292 22.215744 7.641894 22.044373 8.000000 21.879204 c
8.000000 15.236786 l
4.619881 17.097767 3.000000 19.371666 3.000000 21.500000 c
3.000000 22.500000 l
3.000000 25.173086 l
3.003295 25.169918 l
4.109081 24.107075 5.460623 23.175716 6.954747 22.393078 c
h
6.954747 12.393078 m
7.293292 12.215744 7.641894 12.044373 8.000000 11.879204 c
8.000000 6.236786 l
4.619881 8.097767 3.000000 10.371666 3.000000 12.500000 c
3.000000 15.173088 l
4.106395 14.108929 5.459139 13.176491 6.954747 12.393078 c
h
37.000000 6.236786 m
40.380116 8.097767 42.000000 10.371666 42.000000 12.500000 c
42.000000 15.173088 l
40.893604 14.108929 39.540863 13.176491 38.045254 12.393078 c
37.706707 12.215744 37.358109 12.044373 37.000000 11.879204 c
37.000000 6.236786 l
h
f*
n
Q
endstream
endobj
3 0 obj
4594
endobj
4 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 90.000000 90.000000 ]
/Resources 1 0 R
/Contents 2 0 R
/Parent 5 0 R
>>
endobj
5 0 obj
<< /Kids [ 4 0 R ]
/Count 1
/Type /Pages
>>
endobj
6 0 obj
<< /Pages 5 0 R
/Type /Catalog
>>
endobj
xref
0 7
0000000000 65535 f
0000000010 00000 n
0000000034 00000 n
0000004684 00000 n
0000004707 00000 n
0000004880 00000 n
0000004954 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 6 0 R
/Size 7
>>
startxref
5013
%%EOF

View file

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "split_30.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View file

@ -0,0 +1,416 @@
%PDF-1.7
1 0 obj
<< /Type /XObject
/Length 2 0 R
/Group << /Type /Group
/S /Transparency
>>
/Subtype /Form
/Resources << >>
/BBox [ 0.000000 0.000000 30.000000 30.000000 ]
>>
stream
/DeviceRGB CS
/DeviceRGB cs
q
0.923880 -0.382683 0.382683 0.923880 3.005669 4.533266 cm
0.000000 0.000000 0.000000 scn
16.543879 16.094051 m
16.482773 16.456203 16.139654 16.700247 15.777505 16.639141 c
15.415355 16.578033 15.171310 16.234917 15.232417 15.872766 c
16.543879 16.094051 l
h
13.983027 18.887110 m
14.195807 18.587759 14.610972 18.517580 14.910323 18.730362 c
15.209674 18.943142 15.279853 19.358307 15.067072 19.657660 c
13.983027 18.887110 l
h
13.014956 21.709774 m
12.715606 21.922554 12.300441 21.852375 12.087660 21.553024 c
11.874879 21.253674 11.945057 20.838509 12.244409 20.625727 c
13.014956 21.709774 l
h
9.230065 21.875116 m
9.592216 21.814011 9.935332 22.058054 9.996439 22.420204 c
10.057546 22.782354 9.813501 23.125473 9.451351 23.186579 c
9.230065 21.875116 l
h
6.548648 23.186579 m
6.186498 23.125473 5.942453 22.782354 6.003560 22.420204 c
6.064667 22.058054 6.407784 21.814011 6.769934 21.875116 c
6.548648 23.186579 l
h
3.755590 20.625727 m
4.054941 20.838507 4.125120 21.253672 3.912338 21.553024 c
3.699557 21.852375 3.284392 21.922554 2.985041 21.709772 c
3.755590 20.625727 l
h
0.932927 19.657658 m
0.720146 19.358307 0.790325 18.943140 1.089676 18.730360 c
1.389027 18.517578 1.804192 18.587757 2.016973 18.887108 c
0.932927 19.657658 l
h
0.767583 15.872766 m
0.828690 16.234917 0.584645 16.578033 0.222495 16.639139 c
-0.139655 16.700245 -0.482772 16.456202 -0.543879 16.094051 c
0.767583 15.872766 l
h
-0.543879 13.191348 m
-0.482772 12.829198 -0.139655 12.585154 0.222495 12.646260 c
0.584646 12.707367 0.828690 13.050484 0.767583 13.412634 c
-0.543879 13.191348 l
h
2.016974 10.398291 m
1.804193 10.697641 1.389028 10.767819 1.089677 10.555038 c
0.790326 10.342257 0.720147 9.927093 0.932928 9.627742 c
2.016974 10.398291 l
h
2.985043 7.575627 m
3.284394 7.362846 3.699559 7.433024 3.912340 7.732376 c
4.125122 8.031727 4.054943 8.446892 3.755592 8.659673 c
2.985043 7.575627 l
h
6.769935 7.410283 m
6.407784 7.471390 6.064667 7.227345 6.003561 6.865195 c
5.942454 6.503046 6.186498 6.159927 6.548648 6.098822 c
6.769935 7.410283 l
h
9.451352 6.098822 m
9.813502 6.159927 10.057547 6.503046 9.996440 6.865195 c
9.935333 7.227345 9.592216 7.471390 9.230066 7.410283 c
9.451352 6.098822 l
h
12.244410 8.659674 m
11.945059 8.446893 11.874881 8.031728 12.087662 7.732377 c
12.300443 7.433026 12.715608 7.362847 13.014958 7.575628 c
12.244410 8.659674 l
h
15.067073 9.627744 m
15.279854 9.927094 15.209676 10.342259 14.910324 10.555040 c
14.610973 10.767821 14.195808 10.697643 13.983027 10.398292 c
15.067073 9.627744 l
h
15.232417 13.412635 m
15.171310 13.050485 15.415355 12.707368 15.777505 12.646261 c
16.139654 12.585155 16.482773 12.829199 16.543879 13.191349 c
15.232417 13.412635 l
h
16.665001 14.642700 m
16.665001 15.136786 16.623579 15.621709 16.543879 16.094051 c
15.232417 15.872766 l
15.299830 15.473239 15.335000 15.062332 15.335000 14.642700 c
16.665001 14.642700 l
h
15.067072 19.657660 m
14.502963 20.451275 13.808573 21.145664 13.014956 21.709774 c
12.244409 20.625727 l
12.916721 20.147842 13.505140 19.559423 13.983027 18.887110 c
15.067072 19.657660 l
h
9.451351 23.186579 m
8.979008 23.266279 8.494085 23.307701 8.000000 23.307701 c
8.000000 21.977699 l
8.419632 21.977699 8.830539 21.942530 9.230065 21.875116 c
9.451351 23.186579 l
h
8.000000 23.307701 m
7.505914 23.307701 7.020991 23.266279 6.548648 23.186579 c
6.769934 21.875116 l
7.169461 21.942530 7.580368 21.977699 8.000000 21.977699 c
8.000000 23.307701 l
h
2.985041 21.709772 m
2.191425 21.145662 1.497036 20.451273 0.932927 19.657658 c
2.016973 18.887108 l
2.494858 19.559422 3.083277 20.147840 3.755590 20.625727 c
2.985041 21.709772 l
h
-0.543879 16.094051 m
-0.623579 15.621708 -0.665000 15.136786 -0.665000 14.642700 c
0.665000 14.642700 l
0.665000 15.062332 0.700170 15.473238 0.767583 15.872766 c
-0.543879 16.094051 l
h
-0.665000 14.642700 m
-0.665000 14.148614 -0.623578 13.663692 -0.543879 13.191348 c
0.767583 13.412634 l
0.700170 13.812161 0.665000 14.223067 0.665000 14.642700 c
-0.665000 14.642700 l
h
0.932928 9.627742 m
1.497037 8.834126 2.191427 8.139736 2.985043 7.575627 c
3.755592 8.659673 l
3.083278 9.137559 2.494860 9.725977 2.016974 10.398291 c
0.932928 9.627742 l
h
6.548648 6.098822 m
7.020992 6.019121 7.505915 5.977699 8.000000 5.977699 c
8.000000 7.307700 l
7.580368 7.307700 7.169462 7.342870 6.769935 7.410283 c
6.548648 6.098822 l
h
8.000000 5.977699 m
8.494086 5.977699 8.979009 6.019121 9.451352 6.098822 c
9.230066 7.410283 l
8.830539 7.342870 8.419633 7.307700 8.000000 7.307700 c
8.000000 5.977699 l
h
13.014958 7.575628 m
13.808575 8.139737 14.502964 8.834127 15.067073 9.627744 c
13.983027 10.398292 l
13.505141 9.725979 12.916723 9.137560 12.244410 8.659674 c
13.014958 7.575628 l
h
16.543879 13.191349 m
16.623579 13.663692 16.665001 14.148615 16.665001 14.642700 c
15.335000 14.642700 l
15.335000 14.223068 15.299830 13.812161 15.232417 13.412635 c
16.543879 13.191349 l
h
f
n
Q
endstream
endobj
2 0 obj
4945
endobj
3 0 obj
<< /Type /XObject
/Length 4 0 R
/Group << /Type /Group
/S /Transparency
>>
/Subtype /Form
/Resources << >>
/BBox [ 0.000000 0.000000 30.000000 30.000000 ]
>>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 16.000000 5.000000 cm
0.000000 0.000000 0.000000 scn
0.000000 20.000000 m
10.000000 20.000000 l
10.000000 0.000000 l
0.000000 0.000000 l
0.000000 20.000000 l
h
f
n
Q
endstream
endobj
4 0 obj
233
endobj
5 0 obj
<< /XObject << /X1 1 0 R >>
/ExtGState << /E1 << /SMask << /Type /Mask
/G 3 0 R
/S /Alpha
>>
/Type /ExtGState
>> >>
>>
endobj
6 0 obj
<< /Length 7 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
/E1 gs
/X1 Do
Q
q
1.000000 0.000000 -0.000000 1.000000 5.000122 3.579651 cm
0.000000 0.000000 0.000000 scn
9.001400 1.469524 m
8.935791 0.807770 l
9.001400 1.469524 l
h
9.067010 2.131281 m
4.349816 2.598965 0.665000 6.579719 0.665000 11.420288 c
-0.665000 11.420288 l
-0.665000 5.889203 3.544995 1.342237 8.935791 0.807770 c
9.067010 2.131281 l
h
0.665000 11.420288 m
0.665000 16.260859 4.349816 20.241613 9.067010 20.709297 c
8.935791 22.032806 l
3.544996 21.498341 -0.665000 16.951374 -0.665000 11.420288 c
0.665000 11.420288 l
h
9.335000 20.420288 m
9.335000 2.420288 l
10.665000 2.420288 l
10.665000 20.420288 l
9.335000 20.420288 l
h
9.067010 20.709297 m
9.134297 20.715967 9.193896 20.694256 9.243861 20.645805 c
9.296615 20.594650 9.335000 20.515173 9.335000 20.420288 c
10.665000 20.420288 l
10.665000 21.320156 9.920688 22.130455 8.935791 22.032806 c
9.067010 20.709297 l
h
8.935791 0.807770 m
9.920687 0.710121 10.665000 1.520422 10.665000 2.420288 c
9.335000 2.420288 l
9.335000 2.325403 9.296615 2.245926 9.243861 2.194773 c
9.193896 2.146320 9.134296 2.124609 9.067010 2.131281 c
8.935791 0.807770 l
h
f
n
Q
q
1.000000 0.000000 -0.000000 1.000000 5.250000 13.790161 cm
0.000000 0.000000 0.000000 scn
9.720226 10.239613 m
9.979925 10.499311 9.979925 10.920366 9.720226 11.180065 c
9.460527 11.439764 9.039473 11.439764 8.779774 11.180065 c
9.720226 10.239613 l
h
-0.470226 1.930065 m
-0.729925 1.670366 -0.729925 1.249311 -0.470226 0.989613 c
-0.210527 0.729914 0.210527 0.729914 0.470226 0.989613 c
-0.470226 1.930065 l
h
8.779774 11.180065 m
-0.470226 1.930065 l
0.470226 0.989613 l
9.720226 10.239613 l
8.779774 11.180065 l
h
f
n
Q
q
1.000000 0.000000 -0.000000 1.000000 6.000000 9.540161 cm
0.000000 0.000000 0.000000 scn
9.470226 9.989613 m
9.729925 10.249311 9.729925 10.670366 9.470226 10.930065 c
9.210527 11.189764 8.789473 11.189764 8.529774 10.930065 c
9.470226 9.989613 l
h
-0.470226 1.930065 m
-0.729925 1.670366 -0.729925 1.249311 -0.470226 0.989613 c
-0.210527 0.729914 0.210527 0.729914 0.470226 0.989613 c
-0.470226 1.930065 l
h
8.529774 10.930065 m
-0.470226 1.930065 l
0.470226 0.989613 l
9.470226 9.989613 l
8.529774 10.930065 l
h
f
n
Q
q
1.000000 0.000000 -0.000000 1.000000 8.500000 7.040161 cm
0.000000 0.000000 0.000000 scn
6.970226 7.489613 m
7.229925 7.749311 7.229925 8.170366 6.970226 8.430065 c
6.710527 8.689764 6.289473 8.689764 6.029774 8.430065 c
6.970226 7.489613 l
h
-0.470226 1.930065 m
-0.729925 1.670366 -0.729925 1.249311 -0.470226 0.989613 c
-0.210527 0.729914 0.210527 0.729914 0.470226 0.989613 c
-0.470226 1.930065 l
h
6.029774 8.430065 m
-0.470226 1.930065 l
0.470226 0.989613 l
6.970226 7.489613 l
6.029774 8.430065 l
h
f
n
Q
q
1.000000 0.000000 -0.000000 1.000000 11.000000 4.540161 cm
0.000000 0.000000 0.000000 scn
4.470226 4.989613 m
4.729925 5.249311 4.729925 5.670366 4.470226 5.930065 c
4.210527 6.189764 3.789473 6.189764 3.529774 5.930065 c
4.470226 4.989613 l
h
-0.470226 1.930065 m
-0.729925 1.670366 -0.729925 1.249311 -0.470226 0.989613 c
-0.210527 0.729914 0.210527 0.729914 0.470226 0.989613 c
-0.470226 1.930065 l
h
3.529774 5.930065 m
-0.470226 1.930065 l
0.470226 0.989613 l
4.470226 4.989613 l
3.529774 5.930065 l
h
f
n
Q
endstream
endobj
7 0 obj
3228
endobj
8 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 30.000000 30.000000 ]
/Resources 5 0 R
/Contents 6 0 R
/Parent 9 0 R
>>
endobj
9 0 obj
<< /Kids [ 8 0 R ]
/Count 1
/Type /Pages
>>
endobj
10 0 obj
<< /Pages 9 0 R
/Type /Catalog
>>
endobj
xref
0 11
0000000000 65535 f
0000000010 00000 n
0000005203 00000 n
0000005226 00000 n
0000005707 00000 n
0000005729 00000 n
0000006027 00000 n
0000009311 00000 n
0000009334 00000 n
0000009507 00000 n
0000009581 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 10 0 R
/Size 11
>>
startxref
9641
%%EOF

View file

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "ton_30.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View file

@ -0,0 +1,117 @@
%PDF-1.7
1 0 obj
<< >>
endobj
2 0 obj
<< /Length 3 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 8.343262 7.454346 cm
0.000000 0.000000 0.000000 scn
1.432826 10.484781 m
1.133278 11.041084 1.536197 11.715655 2.168021 11.715654 c
5.991703 11.715646 l
5.991703 2.038759 l
5.966153 2.072763 5.942646 2.109401 5.921499 2.148673 c
1.432826 10.484781 l
h
7.321702 2.038787 m
7.347245 2.072783 7.370744 2.109412 7.391885 2.148674 c
11.880558 10.484761 l
12.180106 11.041063 11.777191 11.715633 11.145367 11.715634 c
7.321702 11.715643 l
7.321702 2.038787 l
h
2.168024 13.045654 m
0.529820 13.045658 -0.514872 11.296619 0.261800 9.854228 c
4.750473 1.518121 l
5.567911 0.000022 7.745473 0.000024 8.562911 1.518120 c
13.051584 9.854208 l
13.828257 11.296596 12.783570 13.045631 11.145370 13.045634 c
2.168024 13.045654 l
h
f*
n
Q
q
1.000000 0.000000 -0.000000 1.000000 5.000000 3.670044 cm
0.000000 0.000000 0.000000 scn
19.334999 11.329956 m
19.334999 6.174378 15.155578 1.994957 10.000000 1.994957 c
10.000000 0.664955 l
15.890117 0.664955 20.665001 5.439839 20.665001 11.329956 c
19.334999 11.329956 l
h
10.000000 1.994957 m
4.844422 1.994957 0.665000 6.174378 0.665000 11.329956 c
-0.665000 11.329956 l
-0.665000 5.439839 4.109883 0.664955 10.000000 0.664955 c
10.000000 1.994957 l
h
0.665000 11.329956 m
0.665000 16.485535 4.844422 20.664955 10.000000 20.664955 c
10.000000 21.994957 l
4.109883 21.994957 -0.665000 17.220074 -0.665000 11.329956 c
0.665000 11.329956 l
h
10.000000 20.664955 m
15.155578 20.664955 19.334999 16.485535 19.334999 11.329956 c
20.665001 11.329956 l
20.665001 17.220074 15.890117 21.994957 10.000000 21.994957 c
10.000000 20.664955 l
h
f
n
Q
endstream
endobj
3 0 obj
1634
endobj
4 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 30.000000 30.000000 ]
/Resources 1 0 R
/Contents 2 0 R
/Parent 5 0 R
>>
endobj
5 0 obj
<< /Kids [ 4 0 R ]
/Count 1
/Type /Pages
>>
endobj
6 0 obj
<< /Pages 5 0 R
/Type /Catalog
>>
endobj
xref
0 7
0000000000 65535 f
0000000010 00000 n
0000000034 00000 n
0000001724 00000 n
0000001747 00000 n
0000001920 00000 n
0000001994 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 6 0 R
/Size 7
>>
startxref
2053
%%EOF

View file

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "noads_30.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View file

@ -0,0 +1,111 @@
%PDF-1.7
1 0 obj
<< >>
endobj
2 0 obj
<< /Length 3 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 4.334961 3.604797 cm
0.000000 0.000000 0.000000 scn
19.329964 20.216686 m
19.329964 22.046368 17.201042 23.051056 15.788654 21.887915 c
9.926381 17.060162 l
5.940495 17.060162 l
1.135226 21.865429 l
0.875527 22.125128 0.454473 22.125128 0.194774 21.865429 c
-0.064925 21.605730 -0.064925 21.184675 0.194774 20.924976 c
20.194775 0.924976 l
20.454472 0.665277 20.875526 0.665277 21.135227 0.924976 c
21.394924 1.184675 21.394924 1.605730 21.135227 1.865429 c
19.194319 3.806337 l
19.281490 4.040243 19.329964 4.297546 19.329964 4.573635 c
19.329964 20.216686 l
h
17.999962 5.000694 m
17.999962 20.216686 l
17.999962 20.922359 17.178879 21.309849 16.634146 20.861248 c
10.623723 15.911488 l
10.481375 15.794260 10.302706 15.730161 10.118303 15.730161 c
7.270494 15.730161 l
17.999962 5.000694 l
h
0.999961 12.395161 m
0.999961 14.083057 1.896390 15.561582 3.239132 16.380619 c
4.218666 15.401085 l
3.101114 14.862392 2.329961 13.718833 2.329961 12.395161 c
2.329961 10.553291 3.823091 9.060161 5.664961 9.060161 c
10.118303 9.060161 l
10.302710 9.060161 10.481379 8.996058 10.623723 8.878834 c
11.287819 8.331931 l
17.214378 2.405373 l
16.726585 2.391582 16.223637 2.544184 15.788653 2.902407 c
10.329961 7.397801 l
10.329961 4.895161 l
10.329961 3.423321 9.136800 2.230160 7.664961 2.230160 c
6.193122 2.230160 4.999961 3.423321 4.999961 4.895161 c
4.999961 7.777199 l
2.738543 8.099955 0.999961 10.044524 0.999961 12.395161 c
h
8.999961 7.730161 m
6.329961 7.730161 l
6.329961 4.895161 l
6.329961 4.157860 6.927661 3.560162 7.664961 3.560162 c
8.402261 3.560162 8.999961 4.157860 8.999961 4.895161 c
8.999961 7.730161 l
h
f*
n
Q
endstream
endobj
3 0 obj
1696
endobj
4 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 30.000000 30.000000 ]
/Resources 1 0 R
/Contents 2 0 R
/Parent 5 0 R
>>
endobj
5 0 obj
<< /Kids [ 4 0 R ]
/Count 1
/Type /Pages
>>
endobj
6 0 obj
<< /Pages 5 0 R
/Type /Catalog
>>
endobj
xref
0 7
0000000000 65535 f
0000000010 00000 n
0000000034 00000 n
0000001786 00000 n
0000001809 00000 n
0000001982 00000 n
0000002056 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 6 0 R
/Size 7
>>
startxref
2115
%%EOF

View file

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "intro_30.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View file

@ -0,0 +1,226 @@
%PDF-1.7
1 0 obj
<< >>
endobj
2 0 obj
<< /Length 3 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 5.000000 3.620605 cm
0.000000 0.000000 0.000000 scn
7.545889 3.473349 m
7.397222 2.825180 l
7.397222 2.825180 l
7.545889 3.473349 l
h
5.266314 1.841906 m
4.941034 2.421923 l
4.941034 2.421923 l
5.266314 1.841906 l
h
2.471971 1.400564 m
2.214746 0.787325 l
2.471971 1.400564 l
h
3.613619 5.411451 m
3.220785 4.874882 l
3.613619 5.411451 l
h
10.000000 20.714394 m
15.216641 20.714394 19.334999 16.883762 19.334999 12.288486 c
20.665001 12.288486 l
20.665001 17.734751 15.829054 22.044395 10.000000 22.044395 c
10.000000 20.714394 l
h
19.334999 12.288486 m
19.334999 7.693209 15.216641 3.862577 10.000000 3.862577 c
10.000000 2.532578 l
15.829054 2.532578 20.665001 6.842221 20.665001 12.288486 c
19.334999 12.288486 l
h
10.000000 3.862577 m
9.203434 3.862577 8.431143 3.952570 7.694557 4.121517 c
7.397222 2.825180 l
8.231061 2.633926 9.103088 2.532578 10.000000 2.532578 c
10.000000 3.862577 l
h
7.694557 4.121517 m
7.296880 4.212730 6.988395 3.993872 6.866288 3.902136 c
6.721982 3.793720 6.552248 3.631365 6.405339 3.496567 c
6.085002 3.202635 5.640136 2.813988 4.941034 2.421923 c
5.591593 1.261890 l
6.417546 1.725096 6.948387 2.189802 7.304533 2.516592 c
7.495866 2.692154 7.589569 2.782000 7.665158 2.838789 c
7.762947 2.912256 7.630817 2.771601 7.397222 2.825180 c
7.694557 4.121517 l
h
4.941034 2.421923 m
4.518973 2.185225 3.984706 2.063803 3.502597 2.019852 c
3.266236 1.998304 3.056921 1.996721 2.899934 2.005190 c
2.821182 2.009439 2.761494 2.015905 2.721901 2.022039 c
2.669684 2.030128 2.683973 2.032770 2.729195 2.013802 c
2.214746 0.787325 l
2.322490 0.742132 2.437665 0.720207 2.518294 0.707716 c
2.611548 0.693270 2.716669 0.683144 2.828285 0.677122 c
3.052035 0.665051 3.325697 0.668209 3.623343 0.695343 c
4.209247 0.748756 4.947107 0.900455 5.591593 1.261890 c
4.941034 2.421923 l
h
2.729195 2.013802 m
2.749930 2.005104 2.882758 1.946646 2.968039 1.777298 c
3.061458 1.591789 3.022986 1.426357 3.001172 1.364456 c
2.982594 1.311743 2.964163 1.295038 2.989081 1.328539 c
3.009508 1.356001 3.043106 1.396257 3.097501 1.456873 c
3.303000 1.685873 3.720195 2.103415 4.080953 2.656437 c
2.967016 3.383102 l
2.678986 2.941568 2.367350 2.634583 2.107628 2.345158 c
2.045717 2.276167 1.979485 2.199697 1.921927 2.122316 c
1.868860 2.050974 1.794289 1.941309 1.746787 1.806519 c
1.696047 1.662540 1.655866 1.425915 1.780159 1.179100 c
1.896313 0.948444 2.092599 0.838560 2.214746 0.787325 c
2.729195 2.013802 l
h
4.080953 2.656437 m
4.494962 3.291088 4.613441 3.980347 4.585420 4.541803 c
4.571406 4.822596 4.520174 5.086733 4.439862 5.311483 c
4.369761 5.507658 4.241846 5.775683 4.006453 5.948020 c
3.220785 4.874882 l
3.130925 4.940670 3.145670 4.980782 3.187423 4.863937 c
3.218965 4.775669 3.248780 4.641682 3.257073 4.475510 c
3.273666 4.143038 3.201793 3.743004 2.967016 3.383102 c
4.080953 2.656437 l
h
4.006453 5.948020 m
1.882439 7.503058 0.665000 9.659539 0.665000 12.288486 c
-0.665000 12.288486 l
-0.665000 9.190428 0.792707 6.652533 3.220785 4.874882 c
4.006453 5.948020 l
h
0.665000 12.288486 m
0.665000 16.883762 4.783359 20.714394 10.000000 20.714394 c
10.000000 22.044395 l
4.170946 22.044395 -0.665000 17.734751 -0.665000 12.288486 c
0.665000 12.288486 l
h
f
n
Q
q
1.000000 0.000000 -0.000000 1.000000 14.000000 9.669922 cm
0.000000 0.000000 0.000000 scn
0.000000 1.995078 m
-0.367269 1.995078 -0.665000 1.697348 -0.665000 1.330078 c
-0.665000 0.962809 -0.367269 0.665078 0.000000 0.665078 c
0.000000 1.995078 l
h
3.000000 0.665078 m
3.367269 0.665078 3.665000 0.962809 3.665000 1.330078 c
3.665000 1.697348 3.367269 1.995078 3.000000 1.995078 c
3.000000 0.665078 l
h
0.000000 0.665078 m
3.000000 0.665078 l
3.000000 1.995078 l
0.000000 1.995078 l
0.000000 0.665078 l
h
f
n
Q
q
1.000000 0.000000 -0.000000 1.000000 13.500000 9.669922 cm
0.000000 0.000000 0.000000 scn
1.335000 1.330078 m
1.335000 0.962809 1.632731 0.665078 2.000000 0.665078 c
2.367269 0.665078 2.665000 0.962809 2.665000 1.330078 c
1.335000 1.330078 l
h
2.000000 7.830078 m
2.665000 7.830078 l
2.665000 8.197348 2.367269 8.495078 2.000000 8.495078 c
2.000000 7.830078 l
h
0.000000 8.495078 m
-0.367269 8.495078 -0.665000 8.197348 -0.665000 7.830078 c
-0.665000 7.462809 -0.367269 7.165078 0.000000 7.165078 c
0.000000 8.495078 l
h
2.665000 1.330078 m
2.665000 7.830078 l
1.335000 7.830078 l
1.335000 1.330078 l
2.665000 1.330078 l
h
2.000000 8.495078 m
0.000000 8.495078 l
0.000000 7.165078 l
2.000000 7.165078 l
2.000000 8.495078 l
h
f
n
Q
q
1.000000 0.000000 -0.000000 1.000000 13.750000 19.750000 cm
0.000000 0.000000 0.000000 scn
1.250000 0.000000 m
1.940356 0.000000 2.500000 0.559644 2.500000 1.250000 c
2.500000 1.940356 1.940356 2.500000 1.250000 2.500000 c
0.559644 2.500000 0.000000 1.940356 0.000000 1.250000 c
0.000000 0.559644 0.559644 0.000000 1.250000 0.000000 c
h
f
n
Q
endstream
endobj
3 0 obj
4852
endobj
4 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 30.000000 30.000000 ]
/Resources 1 0 R
/Contents 2 0 R
/Parent 5 0 R
>>
endobj
5 0 obj
<< /Kids [ 4 0 R ]
/Count 1
/Type /Pages
>>
endobj
6 0 obj
<< /Pages 5 0 R
/Type /Catalog
>>
endobj
xref
0 7
0000000000 65535 f
0000000010 00000 n
0000000034 00000 n
0000004942 00000 n
0000004965 00000 n
0000005138 00000 n
0000005212 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 6 0 R
/Size 7
>>
startxref
5271
%%EOF

View file

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "introsetting_30.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View file

@ -0,0 +1,182 @@
%PDF-1.7
1 0 obj
<< /Type /XObject
/Length 2 0 R
/Group << /Type /Group
/S /Transparency
>>
/Subtype /Form
/Resources << >>
/BBox [ 0.000000 0.000000 30.000000 30.000000 ]
>>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 5.000000 4.484375 cm
0.000000 0.000000 0.000000 scn
10.000000 20.135742 m
15.522848 20.135742 20.000000 16.065603 20.000000 11.044833 c
20.000000 6.024063 15.522848 1.953924 10.000000 1.953924 c
9.153261 1.953924 8.331102 2.049595 7.545889 2.229696 c
7.397357 2.263765 7.228708 2.107983 6.947171 1.847927 c
6.606689 1.533424 6.101101 1.066412 5.266314 0.598253 c
4.199766 0.000120 2.722059 0.052011 2.471971 0.156912 c
2.232201 0.257484 2.416753 0.457399 2.741760 0.809465 c
2.966608 1.053034 3.258681 1.369423 3.523984 1.776117 c
4.172771 2.770672 3.904685 3.954702 3.613619 4.167799 c
1.337573 5.834144 0.000000 8.181331 0.000000 11.044833 c
0.000000 16.065603 4.477152 20.135742 10.000000 20.135742 c
h
9.999961 15.265625 m
10.690317 15.265625 11.249961 15.825269 11.249961 16.515625 c
11.249961 17.205980 10.690317 17.765625 9.999961 17.765625 c
9.309605 17.765625 8.749961 17.205980 8.749961 16.515625 c
8.749961 15.825269 9.309605 15.265625 9.999961 15.265625 c
h
8.499961 13.680625 m
8.132691 13.680625 7.834961 13.382895 7.834961 13.015625 c
7.834961 12.648355 8.132691 12.350625 8.499961 12.350625 c
9.834961 12.350625 l
9.834961 7.180625 l
8.999961 7.180625 l
8.632691 7.180625 8.334961 6.882895 8.334961 6.515625 c
8.334961 6.148355 8.632691 5.850625 8.999961 5.850625 c
10.499961 5.850625 l
11.999961 5.850625 l
12.367230 5.850625 12.664961 6.148355 12.664961 6.515625 c
12.664961 6.882895 12.367230 7.180625 11.999961 7.180625 c
11.164961 7.180625 l
11.164961 13.015625 l
11.164961 13.382895 10.867230 13.680625 10.499961 13.680625 c
8.499961 13.680625 l
h
f*
n
Q
endstream
endobj
2 0 obj
1644
endobj
3 0 obj
<< /Type /XObject
/Length 4 0 R
/Group << /Type /Group
/S /Transparency
>>
/Subtype /Form
/Resources << >>
/BBox [ 0.000000 0.000000 30.000000 30.000000 ]
>>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 0.000000 0.000000 cm
0.000000 0.000000 0.000000 scn
0.000000 18.799999 m
0.000000 22.720367 0.000000 24.680552 0.762954 26.177933 c
1.434068 27.495068 2.504932 28.565931 3.822066 29.237045 c
5.319448 30.000000 7.279633 30.000000 11.200000 30.000000 c
18.799999 30.000000 l
22.720367 30.000000 24.680552 30.000000 26.177933 29.237045 c
27.495068 28.565931 28.565931 27.495068 29.237045 26.177933 c
30.000000 24.680552 30.000000 22.720367 30.000000 18.799999 c
30.000000 11.200001 l
30.000000 7.279633 30.000000 5.319448 29.237045 3.822067 c
28.565931 2.504932 27.495068 1.434069 26.177933 0.762955 c
24.680552 0.000000 22.720367 0.000000 18.799999 0.000000 c
11.200000 0.000000 l
7.279633 0.000000 5.319448 0.000000 3.822066 0.762955 c
2.504932 1.434069 1.434068 2.504932 0.762954 3.822067 c
0.000000 5.319448 0.000000 7.279633 0.000000 11.200001 c
0.000000 18.799999 l
h
f
n
Q
endstream
endobj
4 0 obj
944
endobj
5 0 obj
<< /XObject << /X1 1 0 R >>
/ExtGState << /E1 << /SMask << /Type /Mask
/G 3 0 R
/S /Alpha
>>
/Type /ExtGState
>> >>
>>
endobj
6 0 obj
<< /Length 7 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
/E1 gs
/X1 Do
Q
endstream
endobj
7 0 obj
46
endobj
8 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 30.000000 30.000000 ]
/Resources 5 0 R
/Contents 6 0 R
/Parent 9 0 R
>>
endobj
9 0 obj
<< /Kids [ 8 0 R ]
/Count 1
/Type /Pages
>>
endobj
10 0 obj
<< /Pages 9 0 R
/Type /Catalog
>>
endobj
xref
0 11
0000000000 65535 f
0000000010 00000 n
0000001902 00000 n
0000001925 00000 n
0000003117 00000 n
0000003139 00000 n
0000003437 00000 n
0000003539 00000 n
0000003560 00000 n
0000003733 00000 n
0000003807 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 10 0 R
/Size 11
>>
startxref
3867
%%EOF

View file

@ -14818,25 +14818,39 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
let _ = (ChatInterfaceState.update(engine: strongSelf.context.engine, peerId: peerId, threadId: threadId, { currentState in
return currentState.withUpdatedComposeInputState(textInputState)
})
|> deliverOnMainQueue).startStandalone(completed: {
if let strongSelf = self {
strongSelf.updateChatPresentationInterfaceState(animated: false, interactive: true, { $0.updatedInterfaceState({ $0.withoutSelectionState() }) })
|> deliverOnMainQueue).startStandalone(completed: { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.updateChatPresentationInterfaceState(animated: false, interactive: true, { $0.updatedInterfaceState({ $0.withoutSelectionState() }) })
if let navigationController = strongSelf.effectiveNavigationController {
let chatController = ChatControllerImpl(context: strongSelf.context, chatLocation: .peer(id: peerId))
if let navigationController = strongSelf.effectiveNavigationController {
let chatController: Signal<ChatController, NoError>
if let threadId {
chatController = chatControllerForForumThreadImpl(context: strongSelf.context, peerId: peerId, threadId: threadId)
} else {
chatController = .single(ChatControllerImpl(context: strongSelf.context, chatLocation: .peer(id: peerId)))
}
let _ = (chatController
|> deliverOnMainQueue).start(next: { [weak self, weak navigationController] chatController in
guard let strongSelf = self, let navigationController else {
return
}
var viewControllers = navigationController.viewControllers
let lastController = viewControllers.last as! ViewController
viewControllers.remove(at: viewControllers.count - 2)
viewControllers.insert(chatController, at: viewControllers.count - 1)
lastController.navigationPresentation = .modal
navigationController.setViewControllers(viewControllers, animated: false)
strongSelf.controllerNavigationDisposable.set((chatController.ready.get()
|> filter { $0 }
|> take(1)
|> deliverOnMainQueue).startStrict(next: { _ in
if let strongController = controller {
strongController.dismiss()
}
|> deliverOnMainQueue).startStrict(next: { [weak lastController] _ in
lastController?.dismiss()
}))
}
})
}
})
}

View file

@ -1725,7 +1725,7 @@ extension ChatControllerImpl {
}
let editorController = MediaEditorScreen(
context: self.context,
mode: .stickerEditor,
mode: .stickerEditor(mode: .generic),
subject: .single(.asset(asset)),
transitionIn: .gallery(
MediaEditorScreen.TransitionIn.GalleryTransitionIn(
@ -1756,8 +1756,6 @@ extension ChatControllerImpl {
} as (MediaEditorScreen.Result, @escaping (@escaping () -> Void) -> Void) -> Void
)
self.push(editorController)
},
dismissed: {}
)
@ -1768,68 +1766,4 @@ extension ChatControllerImpl {
mainController.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait)
self.push(mainController)
}
// func openStickerEditor() {
// let mainController = AttachmentController(context: self.context, updatedPresentationData: self.updatedPresentationData, chatLocation: nil, buttons: [.standalone], initialButton: .standalone, fromMenu: false, hasTextInput: false, makeEntityInputView: {
// return nil
// })
//// controller.forceSourceRect = true
//// controller.getSourceRect = getSourceRect
// mainController.requestController = { [weak self, weak mainController] _, present in
// guard let self else {
// return
// }
// let mediaPickerController = MediaPickerScreen(context: self.context, updatedPresentationData: self.updatedPresentationData, peer: nil, threadTitle: nil, chatLocation: nil, subject: .assets(nil, .createSticker))
// mediaPickerController.customSelection = { [weak self, weak mainController] controller, result in
// guard let self else {
// return
// }
// if let result = result as? PHAsset {
// controller.updateHiddenMediaId(result.localIdentifier)
// if let transitionView = controller.transitionView(for: result.localIdentifier, snapshot: false) {
// let editorController = MediaEditorScreen(
// context: self.context,
// mode: .stickerEditor,
// subject: .single(.asset(result)),
// transitionIn: .gallery(
// MediaEditorScreen.TransitionIn.GalleryTransitionIn(
// sourceView: transitionView,
// sourceRect: transitionView.bounds,
// sourceImage: controller.transitionImage(for: result.localIdentifier)
// )
// ),
// transitionOut: { finished, isNew in
// if !finished {
// return MediaEditorScreen.TransitionOut(
// destinationView: transitionView,
// destinationRect: transitionView.bounds,
// destinationCornerRadius: 0.0
// )
// }
// return nil
// }, completion: { [weak self, weak mainController] result, commit in
// mainController?.dismiss()
// self?.chatDisplayNode.dismissInput()
//
// Queue.mainQueue().after(0.1) {
// commit({})
// if case let .sticker(file) = result.media {
// self?.enqueueStickerFile(file)
// }
// }
// } as (MediaEditorScreen.Result, @escaping (@escaping () -> Void) -> Void) -> Void
// )
// editorController.dismissed = { [weak controller] in
// controller?.updateHiddenMediaId(nil)
// }
// self.push(editorController)
// }
// }
// }
// present(mediaPickerController, mediaPickerController.mediaPickerContext)
// }
// mainController.navigationPresentation = .flatModal
// mainController.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait)
// self.push(mainController)
// }
}

View file

@ -56,6 +56,9 @@ import BusinessLocationSetupScreen
import BusinessHoursSetupScreen
import AutomaticBusinessMessageSetupScreen
import CollectibleItemInfoScreen
import StickerPickerScreen
import MediaEditor
import MediaEditorScreen
private final class AccountUserInterfaceInUseContext {
let subscribers = Bag<(Bool) -> Void>()
@ -2264,7 +2267,7 @@ public final class SharedAccountContextImpl: SharedAccountContext {
return controller
}
public func makePremiumBoostLevelsController(context: AccountContext, peerId: EnginePeer.Id, boostStatus: ChannelBoostStatus, myBoostStatus: MyBoostStatus, forceDark: Bool, openStats: (() -> Void)?) -> ViewController {
public func makePremiumBoostLevelsController(context: AccountContext, peerId: EnginePeer.Id, subject: BoostSubject, boostStatus: ChannelBoostStatus, myBoostStatus: MyBoostStatus, forceDark: Bool, openStats: (() -> Void)?) -> ViewController {
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 })
var pushImpl: ((ViewController) -> Void)?
@ -2272,7 +2275,7 @@ public final class SharedAccountContextImpl: SharedAccountContext {
let controller = PremiumBoostLevelsScreen(
context: context,
peerId: peerId,
mode: .owner(subject: .stories),
mode: .owner(subject: subject),
status: boostStatus,
myBoostStatus: myBoostStatus,
openStats: openStats,
@ -2306,9 +2309,47 @@ public final class SharedAccountContextImpl: SharedAccountContext {
return StickerPackScreen(context: context, updatedPresentationData: updatedPresentationData, mainStickerPack: mainStickerPack, stickerPacks: stickerPacks, loadedStickerPacks: loadedStickerPacks, isEditing: isEditing, parentNavigationController: parentNavigationController, sendSticker: sendSticker)
}
// func makeStickerEditorScreen(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?, initialSticker: TelegramMediaFile?, targetStickerPack: StickerPackReference?) -> ViewController {
//
// }
public func makeStickerEditorScreen(context: AccountContext, source: Any, transitionArguments: (UIView, CGRect, UIImage?)?, completion: @escaping (TelegramMediaFile) -> Void) -> ViewController {
let subject: MediaEditorScreen.Subject
let mode: MediaEditorScreen.Mode.StickerEditorMode
if let file = source as? TelegramMediaFile {
subject = .sticker(file)
mode = .editing
} else if let asset = source as? PHAsset {
subject = .asset(asset)
mode = .addingToPack
} else {
fatalError()
}
let controller = MediaEditorScreen(
context: context,
mode: .stickerEditor(mode: mode),
subject: .single(subject),
transitionIn: transitionArguments.flatMap { .gallery(
MediaEditorScreen.TransitionIn.GalleryTransitionIn(
sourceView: $0.0,
sourceRect: $0.1,
sourceImage: $0.2
)
) },
transitionOut: { finished, isNew in
if !finished, let transitionArguments {
return MediaEditorScreen.TransitionOut(
destinationView: transitionArguments.0,
destinationRect: transitionArguments.0.bounds,
destinationCornerRadius: 0.0
)
}
return nil
}, completion: { result, commit in
commit({})
if case let .sticker(file) = result.media {
completion(file)
}
} as (MediaEditorScreen.Result, @escaping (@escaping () -> Void) -> Void) -> Void
)
return controller
}
public func makeMediaPickerScreen(context: AccountContext, hasSearch: Bool, completion: @escaping (Any) -> Void) -> ViewController {
return mediaPickerController(context: context, hasSearch: hasSearch, completion: completion)
@ -2321,6 +2362,17 @@ public final class SharedAccountContextImpl: SharedAccountContext {
public func makeStickerMediaPickerScreen(context: AccountContext, getSourceRect: @escaping () -> CGRect, completion: @escaping (Any, UIView, CGRect, UIImage?, @escaping (Bool?) -> (UIView, CGRect)?, @escaping () -> Void) -> Void, dismissed: @escaping () -> Void) -> ViewController {
return stickerMediaPickerController(context: context, getSourceRect: getSourceRect, completion: completion, dismissed: dismissed)
}
public func makeStickerPickerScreen(context: AccountContext, inputData: Promise<StickerPickerInput>, completion: @escaping (TelegramMediaFile) -> Void) -> ViewController {
let controller = StickerPickerScreen(context: context, inputData: inputData.get(), expanded: true, hasGifs: false, hasInteractiveStickers: false)
controller.completion = { content in
if let content, case let .file(file, _) = content {
completion(file)
}
return true
}
return controller
}
public func makeProxySettingsController(sharedContext: SharedAccountContext, account: UnauthorizedAccount) -> ViewController {
return proxySettingsController(accountManager: sharedContext.accountManager, postbox: account.postbox, network: account.network, mode: .modal, presentationData: sharedContext.currentPresentationData.with { $0 }, updatedPresentationData: sharedContext.presentationData)

View file

@ -29,6 +29,7 @@ UIView * _Nullable getPortalViewSourceView(UIView * _Nonnull portalView);
NSObject * _Nullable makeBlurFilter();
NSObject * _Nullable makeLuminanceToAlphaFilter();
NSObject * _Nullable makeMonochromeFilter();
void setLayerDisableScreenshots(CALayer * _Nonnull layer, bool disableScreenshots);
void setLayerContentsMaskMode(CALayer * _Nonnull layer, bool maskMode);

View file

@ -232,6 +232,11 @@ NSObject * _Nullable makeLuminanceToAlphaFilter() {
return [(id<GraphicsFilterProtocol>)NSClassFromString(@"CAFilter") filterWithName:@"luminanceToAlpha"];
}
NSObject * _Nullable makeMonochromeFilter() {
return [(id<GraphicsFilterProtocol>)NSClassFromString(@"CAFilter") filterWithName:@"colorMonochrome"];
}
void setLayerDisableScreenshots(CALayer * _Nonnull layer, bool disableScreenshots) {
static UITextField *textField = nil;
static UIView *secureView = nil;