Various improvements

This commit is contained in:
Ilya Laktyushin 2024-07-13 18:13:58 +04:00
parent 3134a4ef1b
commit 4216ee3933
125 changed files with 4969 additions and 1474 deletions

View File

@ -12490,3 +12490,15 @@ Sorry for the inconvenience.";
"WebApp.Miniapp" = "miniapp";
"WebApp.Share" = "Share";
"Stars.Purchase.GiftStars" = "Gift Stars";
"Stars.Purchase.GiftInfo" = "With Stars, **%1$@** will be able to unlock content and services on Telegram. [See Examples >]()";
"Notification.StarsGift.Sent" = "%1$@ sent you a gift for %2$@";
"Notification.StarsGift.SentYou" = "You sent a gift for %@";
"Notification.StarsGift.Title_1" = "%@ Star";
"Notification.StarsGift.Title_any" = "%@ Stars";
"Notification.StarsGift.Subtitle" = "Use Stars to unlock content and services on Telegram.";
"Notification.StarsGift.SubtitleYou" = "With Stars, %@ will be able to unlock content and services on Telegram.";
"Bot.Settings" = "Bot Settings";

View File

@ -1040,7 +1040,7 @@ public protocol SharedAccountContext: AnyObject {
func makePremiumIntroController(context: AccountContext, source: PremiumIntroSource, forceDark: Bool, dismissed: (() -> Void)?) -> ViewController
func makePremiumDemoController(context: AccountContext, subject: PremiumDemoSubject, forceDark: Bool, action: @escaping () -> Void, dismissed: (() -> Void)?) -> ViewController
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 makePremiumGiftController(context: AccountContext, source: PremiumGiftSource, completion: (([EnginePeer.Id]) -> Void)?) -> ViewController
func makePremiumPrivacyControllerController(context: AccountContext, subject: PremiumPrivacySubject, peerId: EnginePeer.Id) -> ViewController
func makePremiumBoostLevelsController(context: AccountContext, peerId: EnginePeer.Id, subject: BoostSubject, boostStatus: ChannelBoostStatus, myBoostStatus: MyBoostStatus, forceDark: Bool, openStats: (() -> Void)?) -> ViewController
@ -1064,13 +1064,14 @@ public protocol SharedAccountContext: AnyObject {
func makeStoryStatsController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?, peerId: EnginePeer.Id, storyId: Int32, storyItem: EngineStoryItem, fromStory: Bool) -> ViewController
func makeStarsTransactionsScreen(context: AccountContext, starsContext: StarsContext) -> ViewController
func makeStarsPurchaseScreen(context: AccountContext, starsContext: StarsContext, options: [StarsTopUpOption], peerId: EnginePeer.Id?, requiredStars: Int64?, completion: @escaping (Int64) -> Void) -> ViewController
func makeStarsPurchaseScreen(context: AccountContext, starsContext: StarsContext, options: [Any], purpose: StarsPurchasePurpose, completion: @escaping (Int64) -> Void) -> ViewController
func makeStarsTransferScreen(context: AccountContext, starsContext: StarsContext, invoice: TelegramMediaInvoice, source: BotPaymentInvoiceSource, extendedMedia: [TelegramExtendedMedia], inputData: Signal<(StarsContext.State, BotPaymentForm, EnginePeer?)?, NoError>, completion: @escaping (Bool) -> Void) -> ViewController
func makeStarsTransactionScreen(context: AccountContext, transaction: StarsContext.State.Transaction, peer: EnginePeer) -> ViewController
func makeStarsReceiptScreen(context: AccountContext, receipt: BotPaymentReceipt) -> ViewController
func makeStarsStatisticsScreen(context: AccountContext, peerId: EnginePeer.Id, revenueContext: StarsRevenueStatsContext) -> ViewController
func makeStarsAmountScreen(context: AccountContext, initialValue: Int64?, completion: @escaping (Int64) -> Void) -> ViewController
func makeStarsWithdrawalScreen(context: AccountContext, stats: StarsRevenueStats, completion: @escaping (Int64) -> Void) -> ViewController
func makeStarsGiftScreen(context: AccountContext, message: EngineMessage) -> ViewController
func makeDebugSettingsController(context: AccountContext?) -> ViewController?

View File

@ -78,7 +78,7 @@ public enum ContactMultiselectionControllerMode {
case peerSelection(searchChatList: Bool, searchGroups: Bool, searchChannels: Bool)
case channelCreation
case chatSelection(ChatSelection)
case premiumGifting(birthdays: [EnginePeer.Id: TelegramBirthday]?, selectToday: Bool)
case premiumGifting(birthdays: [EnginePeer.Id: TelegramBirthday]?, selectToday: Bool, hasActions: Bool)
case requestedUsersSelection
}

View File

@ -49,6 +49,7 @@ public enum PremiumGiftSource: Equatable {
case attachMenu
case settings([EnginePeer.Id: TelegramBirthday]?)
case chatList([EnginePeer.Id: TelegramBirthday]?)
case stars([EnginePeer.Id: TelegramBirthday]?)
case channelBoost
case deeplink(String?)
}
@ -121,6 +122,14 @@ public enum BoostSubject: Equatable {
case noAds
}
public enum StarsPurchasePurpose: Equatable {
case generic
case transfer(peerId: EnginePeer.Id, requiredStars: Int64)
case subscription(peerId: EnginePeer.Id, requiredStars: Int64, renew: Bool)
case gift(peerId: EnginePeer.Id)
case unlockMedia(requiredStars: Int64)
}
public struct PremiumConfiguration {
public static var defaultValue: PremiumConfiguration {
return PremiumConfiguration(

View File

@ -173,6 +173,10 @@ public extension AttachmentContainable {
return nil
}
var minimizedProgress: Float? {
return nil
}
var isPanGestureEnabled: (() -> Bool)? {
return nil
}
@ -336,7 +340,9 @@ public class AttachmentController: ViewController, MinimizableController {
public private(set) var minimizedTopEdgeOffset: CGFloat?
public private(set) var minimizedBounds: CGRect?
public private(set) var minimizedIcon: UIImage?
public var minimizedIcon: UIImage? {
return self.mainController.minimizedIcon
}
private final class Node: ASDisplayNode {
private weak var controller: AttachmentController?

View File

@ -0,0 +1,157 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import TelegramPresentationData
public class WebAppCancelButtonNode: ASDisplayNode {
public enum State {
case cancel
case back
}
public let buttonNode: HighlightTrackingButtonNode
private let arrowNode: ASImageNode
private let labelNode: ImmediateTextNode
public var state: State = .cancel
private var color: UIColor?
private var _theme: PresentationTheme
public var theme: PresentationTheme {
get {
return self._theme
}
set {
self._theme = newValue
self.setState(self.state, animated: false, animateScale: false, force: true)
}
}
private let strings: PresentationStrings
private weak var colorSnapshotView: UIView?
public func updateColor(_ color: UIColor?, transition: ContainedViewLayoutTransition) {
let previousColor = self.color
self.color = color
if case let .animated(duration, curve) = transition, previousColor != color, !self.animatingStateChange {
if let snapshotView = self.view.snapshotContentTree() {
snapshotView.frame = self.bounds
self.view.addSubview(snapshotView)
self.colorSnapshotView = snapshotView
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, timingFunction: curve.timingFunction, removeOnCompletion: false, completion: { _ in
snapshotView.removeFromSuperview()
})
self.arrowNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration, timingFunction: curve.timingFunction)
self.labelNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration, timingFunction: curve.timingFunction)
}
}
self.setState(self.state, animated: false, animateScale: false, force: true)
}
public init(theme: PresentationTheme, strings: PresentationStrings) {
self._theme = theme
self.strings = strings
self.buttonNode = HighlightTrackingButtonNode()
self.arrowNode = ASImageNode()
self.arrowNode.displaysAsynchronously = false
self.labelNode = ImmediateTextNode()
self.labelNode.displaysAsynchronously = false
super.init()
self.addSubnode(self.buttonNode)
self.buttonNode.addSubnode(self.arrowNode)
self.buttonNode.addSubnode(self.labelNode)
self.buttonNode.highligthedChanged = { [weak self] highlighted in
guard let strongSelf = self else {
return
}
if highlighted {
strongSelf.arrowNode.layer.removeAnimation(forKey: "opacity")
strongSelf.arrowNode.alpha = 0.4
strongSelf.labelNode.layer.removeAnimation(forKey: "opacity")
strongSelf.labelNode.alpha = 0.4
} else {
strongSelf.arrowNode.alpha = 1.0
strongSelf.arrowNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
strongSelf.labelNode.alpha = 1.0
strongSelf.labelNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
}
}
self.setState(.cancel, animated: false, force: true)
}
public func setTheme(_ theme: PresentationTheme, animated: Bool) {
self._theme = theme
var animated = animated
if self.animatingStateChange {
animated = false
}
self.setState(self.state, animated: animated, animateScale: false, force: true)
}
private var animatingStateChange = false
public func setState(_ state: State, animated: Bool, animateScale: Bool = true, force: Bool = false) {
guard self.state != state || force else {
return
}
self.state = state
if let colorSnapshotView = self.colorSnapshotView {
self.colorSnapshotView = nil
colorSnapshotView.removeFromSuperview()
}
if animated, let snapshotView = self.buttonNode.view.snapshotContentTree() {
self.animatingStateChange = true
snapshotView.layer.sublayerTransform = self.buttonNode.subnodeTransform
self.view.addSubview(snapshotView)
let duration: Double = animateScale ? 0.25 : 0.3
if animateScale {
snapshotView.layer.animateScale(from: 1.0, to: 0.001, duration: 0.25, removeOnCompletion: false)
}
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, removeOnCompletion: false, completion: { [weak snapshotView] _ in
snapshotView?.removeFromSuperview()
self.animatingStateChange = false
})
if animateScale {
self.buttonNode.layer.animateScale(from: 0.001, to: 1.0, duration: 0.25)
}
self.buttonNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration)
}
let color = self.color ?? self.theme.rootController.navigationBar.accentTextColor
self.arrowNode.isHidden = state == .cancel
self.labelNode.attributedText = NSAttributedString(string: state == .cancel ? self.strings.Common_Close : self.strings.Common_Back, font: Font.regular(17.0), textColor: color)
let labelSize = self.labelNode.updateLayout(CGSize(width: 120.0, height: 56.0))
self.buttonNode.frame = CGRect(origin: .zero, size: CGSize(width: labelSize.width, height: self.buttonNode.frame.height))
self.arrowNode.image = NavigationBarTheme.generateBackArrowImage(color: color)
if let image = self.arrowNode.image {
self.arrowNode.frame = CGRect(origin: self.arrowNode.frame.origin, size: image.size)
}
self.labelNode.frame = CGRect(origin: self.labelNode.frame.origin, size: labelSize)
self.buttonNode.subnodeTransform = CATransform3DMakeTranslation(state == .back ? 11.0 : 0.0, 0.0, 0.0)
}
override public func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize {
self.buttonNode.frame = CGRect(origin: .zero, size: CGSize(width: self.buttonNode.frame.width, height: constrainedSize.height))
self.arrowNode.frame = CGRect(origin: CGPoint(x: -19.0, y: floorToScreenPixels((constrainedSize.height - self.arrowNode.frame.size.height) / 2.0)), size: self.arrowNode.frame.size)
self.labelNode.frame = CGRect(origin: CGPoint(x: 0.0, y: floorToScreenPixels((constrainedSize.height - self.labelNode.frame.size.height) / 2.0)), size: self.labelNode.frame.size)
return CGSize(width: 70.0, height: 56.0)
}
}

View File

@ -17,6 +17,7 @@ swift_library(
"//submodules/TelegramCore",
"//submodules/TelegramPresentationData",
"//submodules/TelegramUIPreferences",
"//submodules/PresentationDataUtils",
"//submodules/AppBundle",
"//submodules/InstantPageUI",
"//submodules/ContextUI",
@ -30,6 +31,13 @@ swift_library(
"//submodules/TelegramUI/Components/MinimizedContainer",
"//submodules/Pasteboard",
"//submodules/SaveToCameraRoll",
"//submodules/TelegramUI/Components/NavigationStackComponent",
"//submodules/LocationUI",
"//submodules/OpenInExternalAppUI",
"//submodules/GalleryUI",
"//submodules/TelegramUI/Components/ContextReferenceButtonComponent",
"//submodules/Svg",
"//submodules/PromptUI",
],
visibility = [
"//visibility:public",

View File

@ -1,7 +1,9 @@
import Foundation
import UIKit
import Display
import ComponentFlow
import SwiftSignalKit
import WebKit
final class BrowserContentState: Equatable {
enum ContentType: Equatable {
@ -9,28 +11,62 @@ final class BrowserContentState: Equatable {
case instantPage
}
struct HistoryItem: Equatable {
let url: String
let title: String
let uuid: UUID?
let webItem: WKBackForwardListItem?
init(url: String, title: String, uuid: UUID) {
self.url = url
self.title = title
self.uuid = uuid
self.webItem = nil
}
init(webItem: WKBackForwardListItem) {
self.url = webItem.url.absoluteString
self.title = webItem.title ?? ""
self.uuid = nil
self.webItem = nil
}
}
let title: String
let url: String
let estimatedProgress: Double
let readingProgress: Double
let contentType: ContentType
let favicon: UIImage?
var canGoBack: Bool
var canGoForward: Bool
let canGoBack: Bool
let canGoForward: Bool
let backList: [HistoryItem]
let forwardList: [HistoryItem]
init(
title: String,
url: String,
estimatedProgress: Double,
readingProgress: Double,
contentType: ContentType,
favicon: UIImage? = nil,
canGoBack: Bool = false,
canGoForward: Bool = false
canGoForward: Bool = false,
backList: [HistoryItem] = [],
forwardList: [HistoryItem] = []
) {
self.title = title
self.url = url
self.estimatedProgress = estimatedProgress
self.readingProgress = readingProgress
self.contentType = contentType
self.favicon = favicon
self.canGoBack = canGoBack
self.canGoForward = canGoForward
self.backList = backList
self.forwardList = forwardList
}
static func == (lhs: BrowserContentState, rhs: BrowserContentState) -> Bool {
@ -43,42 +79,80 @@ final class BrowserContentState: Equatable {
if lhs.estimatedProgress != rhs.estimatedProgress {
return false
}
if lhs.readingProgress != rhs.readingProgress {
return false
}
if lhs.contentType != rhs.contentType {
return false
}
if (lhs.favicon == nil) != (rhs.favicon == nil) {
return false
}
if lhs.canGoBack != rhs.canGoBack {
return false
}
if lhs.canGoForward != rhs.canGoForward {
return false
}
if lhs.backList != rhs.backList {
return false
}
if lhs.forwardList != rhs.forwardList {
return false
}
return true
}
func withUpdatedTitle(_ title: String) -> BrowserContentState {
return BrowserContentState(title: title, url: self.url, estimatedProgress: self.estimatedProgress, contentType: self.contentType, canGoBack: self.canGoBack, canGoForward: self.canGoForward)
return BrowserContentState(title: title, url: self.url, estimatedProgress: self.estimatedProgress, readingProgress: self.readingProgress, contentType: self.contentType, favicon: self.favicon, canGoBack: self.canGoBack, canGoForward: self.canGoForward, backList: self.backList, forwardList: self.forwardList)
}
func withUpdatedUrl(_ url: String) -> BrowserContentState {
return BrowserContentState(title: self.title, url: url, estimatedProgress: self.estimatedProgress, contentType: self.contentType, canGoBack: self.canGoBack, canGoForward: self.canGoForward)
return BrowserContentState(title: self.title, url: url, estimatedProgress: self.estimatedProgress, readingProgress: self.readingProgress, contentType: self.contentType, favicon: self.favicon, canGoBack: self.canGoBack, canGoForward: self.canGoForward, backList: self.backList, forwardList: self.forwardList)
}
func withUpdatedEstimatedProgress(_ estimatedProgress: Double) -> BrowserContentState {
return BrowserContentState(title: self.title, url: self.url, estimatedProgress: estimatedProgress, contentType: self.contentType, canGoBack: self.canGoBack, canGoForward: self.canGoForward)
return BrowserContentState(title: self.title, url: self.url, estimatedProgress: estimatedProgress, readingProgress: self.readingProgress, contentType: self.contentType, favicon: self.favicon, canGoBack: self.canGoBack, canGoForward: self.canGoForward, backList: self.backList, forwardList: self.forwardList)
}
func withUpdatedReadingProgress(_ readingProgress: Double) -> BrowserContentState {
return BrowserContentState(title: self.title, url: self.url, estimatedProgress: self.estimatedProgress, readingProgress: readingProgress, contentType: self.contentType, favicon: self.favicon, canGoBack: self.canGoBack, canGoForward: self.canGoForward, backList: self.backList, forwardList: self.forwardList)
}
func withUpdatedFavicon(_ favicon: UIImage?) -> BrowserContentState {
return BrowserContentState(title: self.title, url: self.url, estimatedProgress: self.estimatedProgress, readingProgress: self.readingProgress, contentType: self.contentType, favicon: favicon, canGoBack: self.canGoBack, canGoForward: self.canGoForward, backList: self.backList, forwardList: self.forwardList)
}
func withUpdatedCanGoBack(_ canGoBack: Bool) -> BrowserContentState {
return BrowserContentState(title: self.title, url: self.url, estimatedProgress: self.estimatedProgress, contentType: self.contentType, canGoBack: canGoBack, canGoForward: self.canGoForward)
return BrowserContentState(title: self.title, url: self.url, estimatedProgress: self.estimatedProgress, readingProgress: self.readingProgress, contentType: self.contentType, favicon: self.favicon, canGoBack: canGoBack, canGoForward: self.canGoForward, backList: self.backList, forwardList: self.forwardList)
}
func withUpdatedCanGoForward(_ canGoForward: Bool) -> BrowserContentState {
return BrowserContentState(title: self.title, url: self.url, estimatedProgress: self.estimatedProgress, contentType: self.contentType, canGoBack: self.canGoBack, canGoForward: canGoForward)
return BrowserContentState(title: self.title, url: self.url, estimatedProgress: self.estimatedProgress, readingProgress: self.readingProgress, contentType: self.contentType, favicon: self.favicon, canGoBack: self.canGoBack, canGoForward: canGoForward, backList: self.backList, forwardList: self.forwardList)
}
func withUpdatedBackList(_ backList: [HistoryItem]) -> BrowserContentState {
return BrowserContentState(title: self.title, url: self.url, estimatedProgress: self.estimatedProgress, readingProgress: self.readingProgress, contentType: self.contentType, favicon: self.favicon, canGoBack: self.canGoBack, canGoForward: self.canGoForward, backList: backList, forwardList: self.forwardList)
}
func withUpdatedForwardList(_ forwardList: [HistoryItem]) -> BrowserContentState {
return BrowserContentState(title: self.title, url: self.url, estimatedProgress: self.estimatedProgress, readingProgress: self.readingProgress, contentType: self.contentType, favicon: self.favicon, canGoBack: self.canGoBack, canGoForward: self.canGoForward, backList: self.backList, forwardList: forwardList)
}
}
protocol BrowserContent: UIView {
var uuid: UUID { get }
var currentState: BrowserContentState { get }
var state: Signal<BrowserContentState, NoError> { get }
var pushContent: (BrowserScreen.Subject) -> Void { get set }
var present: (ViewController, Any?) -> Void { get set }
var presentInGlobalOverlay: (ViewController) -> Void { get set }
var getNavigationController: () -> NavigationController? { get set }
var minimize: () -> Void { get set }
var onScrollingUpdate: (ContentScrollingUpdate) -> Void { get set }
func reload()
@ -86,6 +160,7 @@ protocol BrowserContent: UIView {
func navigateBack()
func navigateForward()
func navigateTo(historyItem: BrowserContentState.HistoryItem)
func setFontSize(_ fontSize: CGFloat)
func setForceSerif(_ force: Bool)

View File

@ -17,14 +17,30 @@ import ContextUI
import Pasteboard
import SaveToCameraRoll
import ShareController
import SafariServices
import LocationUI
import OpenInExternalAppUI
import GalleryUI
private final class InstantPageContainerNode: ASDisplayNode, UIScrollViewDelegate {
final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDelegate {
private let context: AccountContext
private let webPage: TelegramMediaWebpage
private let presentationData: PresentationData
private let theme: InstantPageTheme
private let sourceLocation: InstantPageSourceLocation
private var webPage: TelegramMediaWebpage?
let uuid: UUID
var currentState: BrowserContentState {
return self._state
}
private var _state: BrowserContentState
private let statePromise: Promise<BrowserContentState>
var state: Signal<BrowserContentState, NoError> {
return self.statePromise.get()
}
private var initialAnchor: String?
private var pendingAnchor: String?
private var initialState: InstantPageStoredState?
@ -48,34 +64,66 @@ private final class InstantPageContainerNode: ASDisplayNode, UIScrollViewDelegat
var currentAccessibilityAreas: [AccessibilityAreaNode] = []
var pushContent: (BrowserScreen.Subject) -> Void = { _ in }
var onScrollingUpdate: (ContentScrollingUpdate) -> Void = { _ in }
var openMedia: (InstantPageMedia) -> Void = { _ in }
var longPressMedia: (InstantPageMedia) -> Void = { _ in }
var minimize: () -> Void = { }
var openPeer: (EnginePeer) -> Void = { _ in }
var openUrl: (InstantPageUrlItem) -> Void = { _ in }
var activatePinchPreview: ((PinchSourceContainerNode) -> Void)?
var pinchPreviewFinished: ((InstantPageNode) -> Void)?
var present: (ViewController, Any?) -> Void = { _, _ in }
var presentInGlobalOverlay: (ViewController) -> Void = { _ in }
var push: (ViewController) -> Void = { _ in }
var getNavigationController: () -> NavigationController? = { return nil }
private var webpageDisposable: Disposable?
private let hiddenMediaDisposable = MetaDisposable()
private let loadWebpageDisposable = MetaDisposable()
private let resolveUrlDisposable = MetaDisposable()
private let updateLayoutDisposable = MetaDisposable()
private let loadProgress = ValuePromise<CGFloat>(1.0, ignoreRepeated: true)
private let readingProgress = ValuePromise<CGFloat>(1.0, ignoreRepeated: true)
private var containerLayout: (size: CGSize, insets: UIEdgeInsets)?
private var setupScrollOffsetOnLayout = false
init(context: AccountContext, webPage: TelegramMediaWebpage, sourceLocation: InstantPageSourceLocation) {
init(context: AccountContext, webPage: TelegramMediaWebpage, anchor: String?, url: String, sourceLocation: InstantPageSourceLocation) {
self.context = context
self.webPage = webPage
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
self.theme = instantPageThemeForType(.light, settings: .defaultSettings)
self.sourceLocation = sourceLocation
self.uuid = UUID()
let title: String
if case let .Loaded(content) = webPage.content {
title = content.title ?? ""
} else {
title = ""
}
self._state = BrowserContentState(title: title, url: url, estimatedProgress: 0.0, readingProgress: 0.0, contentType: .instantPage)
self.statePromise = Promise<BrowserContentState>(self._state)
self.scrollNode = ASScrollNode()
self.scrollNode.backgroundColor = self.theme.pageBackgroundColor
self.scrollNodeFooter = ASDisplayNode()
self.scrollNodeFooter.backgroundColor = self.theme.panelBackgroundColor
super.init()
super.init(frame: .zero)
self.statePromise.set(.single(self._state)
|> then(
combineLatest(
self.loadProgress.get(),
self.readingProgress.get()
)
|> map { estimatedProgress, readingProgress in
return BrowserContentState(title: title, url: url, estimatedProgress: estimatedProgress, readingProgress: readingProgress, contentType: .instantPage)
}
))
self.addSubnode(self.scrollNode)
self.scrollNode.addSubnode(self.scrollNodeFooter)
@ -101,9 +149,21 @@ private final class InstantPageContainerNode: ASDisplayNode, UIScrollViewDelegat
}
}
self.scrollNode.view.addGestureRecognizer(recognizer)
self.webpageDisposable = (actualizedWebpage(account: context.account, webpage: webPage) |> deliverOnMainQueue).start(next: { [weak self] result in
guard let self else {
return
}
self.webPage = result
self.updateWebPage(result, anchor: self.initialAnchor)
})
}
deinit {
self.webpageDisposable?.dispose()
self.hiddenMediaDisposable.dispose()
self.loadWebpageDisposable.dispose()
self.resolveUrlDisposable.dispose()
self.updateLayoutDisposable.dispose()
}
@ -184,25 +244,160 @@ private final class InstantPageContainerNode: ASDisplayNode, UIScrollViewDelegat
}
}
private func updateWebPage(_ webPage: TelegramMediaWebpage?, anchor: String?, state: InstantPageStoredState? = nil) {
if self.webPage != webPage {
if self.webPage != nil && self.currentLayout != nil {
if let snaphotView = self.scrollNode.view.snapshotView(afterScreenUpdates: false) {
self.scrollNode.view.superview?.insertSubview(snaphotView, aboveSubview: self.scrollNode.view)
snaphotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak snaphotView] _ in
snaphotView?.removeFromSuperview()
})
}
}
self.setupScrollOffsetOnLayout = self.webPage == nil
self.webPage = webPage
if let anchor = anchor {
self.initialAnchor = anchor.removingPercentEncoding
} else if let state = state {
self.initialState = state
if !state.details.isEmpty {
var storedExpandedDetails: [Int: Bool] = [:]
for state in state.details {
storedExpandedDetails[Int(clamping: state.index)] = state.expanded
}
self.currentExpandedDetails = storedExpandedDetails
}
}
self.currentLayout = nil
self.updatePageLayout()
self.scrollNode.frame = CGRect(x: 0.0, y: 0.0, width: 1.0, height: 1.0)
self.requestLayout(transition: .immediate)
if let webPage = webPage, case let .Loaded(content) = webPage.content, let instantPage = content.instantPage, instantPage.isComplete {
self.loadProgress.set(1.0)
if let anchor = self.pendingAnchor {
self.pendingAnchor = nil
self.scrollToAnchor(anchor)
}
}
}
}
private func requestLayout(transition: ContainedViewLayoutTransition) {
guard let (size, insets) = self.containerLayout else {
return
}
self.updateLayout(size: size, insets: insets, transition: transition)
}
func reload() {
}
func stop() {
}
func navigateBack() {
}
func navigateForward() {
}
func navigateTo(historyItem: BrowserContentState.HistoryItem) {
}
func setFontSize(_ fontSize: CGFloat) {
}
func setForceSerif(_ force: Bool) {
}
func setSearch(_ query: String?, completion: ((Int) -> Void)?) {
}
func scrollToPreviousSearchResult(completion: ((Int, Int) -> Void)?) {
}
func scrollToNextSearchResult(completion: ((Int, Int) -> Void)?) {
}
func scrollToTop() {
let scrollView = self.scrollNode.view
scrollView.setContentOffset(CGPoint(x: 0.0, y: -scrollView.contentInset.top), animated: true)
}
func updateLayout(size: CGSize, insets: UIEdgeInsets, transition: ComponentTransition) {
self.updateLayout(size: size, insets: insets, transition: transition.containedViewLayoutTransition)
}
func updateLayout(size: CGSize, insets: UIEdgeInsets, transition: ContainedViewLayoutTransition) {
self.containerLayout = (size, insets)
var updateVisibleItems = false
let resetContentOffset = self.scrollNode.bounds.size.width.isZero || self.setupScrollOffsetOnLayout || !(self.initialAnchor ?? "").isEmpty
var scrollInsets = insets
scrollInsets.top = 0.0
if self.scrollNode.view.contentInset != insets {
self.scrollNode.view.contentInset = scrollInsets
self.scrollNode.view.scrollIndicatorInsets = scrollInsets
}
self.scrollNode.frame = CGRect(origin: CGPoint(x: 0.0, y: insets.top), size: CGSize(width: size.width, height: size.height - insets.top))
if self.currentLayout?.contentSize.width != size.width {
self.updatePageLayout()
let scrollFrame = CGRect(origin: CGPoint(x: 0.0, y: insets.top), size: CGSize(width: size.width, height: size.height - insets.top))
let scrollFrameUpdated = self.scrollNode.bounds.size != scrollFrame.size
if scrollFrameUpdated {
let widthUpdated = self.scrollNode.bounds.size.width != scrollFrame.width
self.scrollNode.frame = scrollFrame
if widthUpdated {
self.updatePageLayout()
}
updateVisibleItems = true
}
if resetContentOffset {
var didSetScrollOffset = false
var contentOffset = CGPoint(x: 0.0, y: -self.scrollNode.view.contentInset.top)
if let state = self.initialState {
didSetScrollOffset = true
contentOffset = CGPoint(x: 0.0, y: CGFloat(state.contentOffset))
}
else if let anchor = self.initialAnchor, !anchor.isEmpty {
self.initialAnchor = nil
if let items = self.currentLayout?.items {
didSetScrollOffset = true
if let (item, lineOffset, _, _) = self.findAnchorItem(anchor, items: items) {
contentOffset = CGPoint(x: 0.0, y: item.frame.minY + lineOffset - self.scrollNode.view.contentInset.top)
}
}
} else {
didSetScrollOffset = true
}
self.scrollNode.view.contentOffset = contentOffset
if didSetScrollOffset {
//update scroll event
if self.currentLayout != nil {
self.setupScrollOffsetOnLayout = false
}
}
}
if updateVisibleItems {
self.updateVisibleItems(visibleBounds: self.scrollNode.view.bounds)
}
}
private func updatePageLayout() {
guard let (size, insets) = self.containerLayout else {
guard let (size, insets) = self.containerLayout, let webPage = self.webPage else {
return
}
@ -355,32 +550,9 @@ private final class InstantPageContainerNode: ASDisplayNode, UIScrollViewDelegat
}, longPressMedia: { [weak self] media in
self?.longPressMedia(media)
}, activatePinchPreview: { [weak self] sourceNode in
let _ = self
// guard let strongSelf = self, let controller = strongSelf.controller else {
// return
// }
// let pinchController = PinchController(sourceNode: sourceNode, getContentAreaInScreenSpace: {
// guard let strongSelf = self else {
// return CGRect()
// }
//
// let localRect = CGRect(origin: CGPoint(x: 0.0, y: strongSelf.navigationBar.frame.maxY), size: CGSize(width: strongSelf.bounds.width, height: strongSelf.bounds.height - strongSelf.navigationBar.frame.maxY))
// return strongSelf.view.convert(localRect, to: nil)
// })
// controller.window?.presentInGlobalOverlay(pinchController)
self?.activatePinchPreview(sourceNode: sourceNode)
}, pinchPreviewFinished: { [weak self] itemNode in
let _ = self
// guard let strongSelf = self else {
// return
// }
// for (_, listItemNode) in strongSelf.visibleItemsWithNodes {
// if let listItemNode = listItemNode as? InstantPagePeerReferenceNode {
// if listItemNode.frame.intersects(itemNode.frame) && listItemNode.frame.maxY <= itemNode.frame.maxY + 2.0 {
// listItemNode.layer.animateAlpha(from: 0.0, to: listItemNode.alpha, duration: 0.25)
// break
// }
// }
// }
self?.pinchPreviewFinished(itemNode: itemNode)
}, openPeer: { [weak self] peerId in
self?.openPeer(peerId)
}, openUrl: { [weak self] url in
@ -547,6 +719,13 @@ private final class InstantPageContainerNode: ASDisplayNode, UIScrollViewDelegat
))
}
self.previousScrollingOffset = ScrollingOffsetState(value: scrollView.contentOffset.y, isDraggingOrDecelerating: isInteracting)
var readingProgress: CGFloat = 0.0
if !scrollView.contentSize.height.isZero {
let value = (scrollView.contentOffset.y + scrollView.contentInset.top) / (scrollView.contentSize.height - scrollView.bounds.size.height + scrollView.contentInset.top)
readingProgress = max(0.0, min(1.0, value))
}
self.readingProgress.set(readingProgress)
}
private func scrollableContentOffset(item: InstantPageScrollableItem) -> CGPoint {
@ -645,6 +824,230 @@ private final class InstantPageContainerNode: ASDisplayNode, UIScrollViewDelegat
return nil
}
private func openUrl(_ url: InstantPageUrlItem) {
var baseUrl = url.url
var anchor: String?
if let anchorRange = url.url.range(of: "#") {
anchor = String(baseUrl[anchorRange.upperBound...]).removingPercentEncoding
baseUrl = String(baseUrl[..<anchorRange.lowerBound])
}
if let webPage = self.webPage, case let .Loaded(content) = webPage.content, let page = content.instantPage, page.url == baseUrl, let anchor = anchor {
self.scrollToAnchor(anchor)
return
}
self.loadProgress.set(0.0)
self.loadProgress.set(0.02)
self.loadWebpageDisposable.set(nil)
self.resolveUrlDisposable.set((self.context.sharedContext.resolveUrl(context: self.context, peerId: nil, url: url.url, skipUrlAuth: true)
|> deliverOnMainQueue).start(next: { [weak self] result in
if let strongSelf = self {
strongSelf.loadProgress.set(0.07)
switch result {
case let .externalUrl(externalUrl):
if let webpageId = url.webpageId {
var anchor: String?
if let anchorRange = externalUrl.range(of: "#") {
anchor = String(externalUrl[anchorRange.upperBound...])
}
strongSelf.loadWebpageDisposable.set((webpagePreviewWithProgress(account: strongSelf.context.account, urls: [externalUrl], webpageId: webpageId)
|> deliverOnMainQueue).start(next: { result in
if let strongSelf = self {
switch result {
case let .result(webpageResult):
if let webpageResult = webpageResult, case .Loaded = webpageResult.webpage.content {
strongSelf.loadProgress.set(1.0)
strongSelf.pushContent(.instantPage(webPage: webpageResult.webpage, anchor: anchor, sourceLocation: strongSelf.sourceLocation))
}
break
case let .progress(progress):
strongSelf.loadProgress.set(CGFloat(0.07 + progress * (1.0 - 0.07)))
}
}
}))
} else {
strongSelf.loadProgress.set(1.0)
strongSelf.pushContent(.webPage(url: externalUrl))
}
case let .instantView(webpage, anchor):
strongSelf.loadProgress.set(1.0)
strongSelf.pushContent(.instantPage(webPage: webpage, anchor: anchor, sourceLocation: strongSelf.sourceLocation))
default:
strongSelf.loadProgress.set(1.0)
strongSelf.minimize()
strongSelf.context.sharedContext.openResolvedUrl(result, context: strongSelf.context, urlContext: .generic, navigationController: strongSelf.getNavigationController(), forceExternal: false, openPeer: { peer, navigation in
switch navigation {
case let .chat(_, subject, peekData):
if let navigationController = strongSelf.getNavigationController() {
strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(peer), subject: subject, peekData: peekData))
}
case let .withBotStartPayload(botStart):
if let navigationController = strongSelf.getNavigationController() {
strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(peer), botStart: botStart, keepStack: .always))
}
case let .withAttachBot(attachBotStart):
if let navigationController = strongSelf.getNavigationController() {
strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(peer), attachBotStart: attachBotStart))
}
case let .withBotApp(botAppStart):
if let navigationController = strongSelf.getNavigationController() {
strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(peer), botAppStart: botAppStart))
}
case .info:
let _ = (strongSelf.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peer.id))
|> deliverOnMainQueue).start(next: { peer in
if let strongSelf = self, let peer = peer {
if let controller = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) {
strongSelf.getNavigationController()?.pushViewController(controller)
}
}
})
default:
break
}
},
sendFile: nil,
sendSticker: nil,
sendEmoji: nil,
requestMessageActionUrlAuth: nil,
joinVoiceChat: nil,
present: { c, a in
self?.present(c, a)
}, dismissInput: { [weak self] in
self?.endEditing(true)
}, contentContext: nil, progress: nil, completion: nil)
}
}
}))
}
private func openUrlIn(_ url: InstantPageUrlItem) {
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
let actionSheet = OpenInActionSheetController(context: self.context, item: .url(url: url.url), openUrl: { [weak self] url in
if let self {
self.context.sharedContext.openExternalUrl(context: self.context, urlContext: .generic, url: url, forceExternal: true, presentationData: presentationData, navigationController: nil, dismissInput: {})
}
})
self.present(actionSheet, nil)
}
private func openMedia(_ media: InstantPageMedia) {
guard let items = self.currentLayout?.items, let webPage = self.webPage else {
return
}
func mediasFromItems(_ items: [InstantPageItem]) -> [InstantPageMedia] {
var medias: [InstantPageMedia] = []
for item in items {
if let detailsItem = item as? InstantPageDetailsItem {
medias.append(contentsOf: mediasFromItems(detailsItem.items))
} else {
if let item = item as? InstantPageImageItem, item.interactive {
medias.append(contentsOf: item.medias)
} else if let item = item as? InstantPagePlayableVideoItem, item.interactive {
medias.append(contentsOf: item.medias)
}
}
}
return medias
}
if case let .geo(map) = media.media {
let controllerParams = LocationViewParams(sendLiveLocation: { _ in
}, stopLiveLocation: { _ in
}, openUrl: { _ in }, openPeer: { _ in
}, showAll: false)
let peer = TelegramUser(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(0)), accessHash: nil, firstName: "", lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil)
let message = Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: 0, id: 0), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 0, flags: [], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: peer, text: "", attributes: [], media: [map], peers: SimpleDictionary(), associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:])
let controller = LocationViewController(context: self.context, subject: EngineMessage(message), params: controllerParams)
self.push(controller)
return
}
if case let .file(file) = media.media, (file.isVoice || file.isMusic) {
var medias: [InstantPageMedia] = []
var initialIndex = 0
for item in items {
for itemMedia in item.medias {
if case let .file(itemFile) = itemMedia.media, (itemFile.isVoice || itemFile.isMusic) {
if itemMedia.index == media.index {
initialIndex = medias.count
}
medias.append(itemMedia)
}
}
}
self.context.sharedContext.mediaManager.setPlaylist((self.context.account, InstantPageMediaPlaylist(webPage: webPage, items: medias, initialItemIndex: initialIndex)), type: file.isVoice ? .voice : .music, control: .playback(.play))
return
}
var fromPlayingVideo = false
var entries: [InstantPageGalleryEntry] = []
if case let .webpage(webPage) = media.media {
entries.append(InstantPageGalleryEntry(index: 0, pageId: webPage.webpageId, media: media, caption: nil, credit: nil, location: nil))
} else if case let .file(file) = media.media, file.isAnimated {
fromPlayingVideo = true
entries.append(InstantPageGalleryEntry(index: Int32(media.index), pageId: webPage.webpageId, media: media, caption: media.caption, credit: media.credit, location: nil))
} else {
fromPlayingVideo = true
var medias: [InstantPageMedia] = mediasFromItems(items)
medias = medias.filter { item in
switch item.media {
case .image, .file:
return true
default:
return false
}
}
for media in medias {
entries.append(InstantPageGalleryEntry(index: Int32(media.index), pageId: webPage.webpageId, media: media, caption: media.caption, credit: media.credit, location: InstantPageGalleryEntryLocation(position: Int32(entries.count), totalCount: Int32(medias.count))))
}
}
var centralIndex: Int?
for i in 0 ..< entries.count {
if entries[i].media == media {
centralIndex = i
break
}
}
if let centralIndex = centralIndex {
let controller = InstantPageGalleryController(context: self.context, userLocation: self.sourceLocation.userLocation, webPage: webPage, entries: entries, centralIndex: centralIndex, fromPlayingVideo: fromPlayingVideo, replaceRootController: { _, _ in
}, baseNavigationController: self.getNavigationController())
self.hiddenMediaDisposable.set((controller.hiddenMedia |> deliverOnMainQueue).start(next: { [weak self] entry in
if let strongSelf = self {
for (_, itemNode) in strongSelf.visibleItemsWithNodes {
itemNode.updateHiddenMedia(media: entry?.media)
}
}
}))
controller.openUrl = { [weak self] url in
self?.openUrl(url)
}
self.present(controller, InstantPageGalleryControllerPresentationArguments(transitionArguments: { [weak self] entry -> GalleryTransitionArguments? in
if let strongSelf = self {
for (_, itemNode) in strongSelf.visibleItemsWithNodes {
if let transitionNode = itemNode.transitionNode(media: entry.media) {
return GalleryTransitionArguments(transitionNode: transitionNode, addToTransitionSurface: { view in
if let strongSelf = self {
strongSelf.scrollNode.view.superview?.insertSubview(view, aboveSubview: strongSelf.scrollNode.view)
}
})
}
}
}
return nil
}))
}
}
private func longPressMedia(_ media: InstantPageMedia) {
let controller = makeContextMenuController(actions: [ContextMenuAction(content: .text(title: self.presentationData.strings.Conversation_ContextMenuCopy, accessibilityLabel: self.presentationData.strings.Conversation_ContextMenuCopy), action: { [weak self] in
if let self, let image = media.media._asMedia() as? TelegramMediaImage {
@ -657,74 +1060,94 @@ private final class InstantPageContainerNode: ASDisplayNode, UIScrollViewDelegat
let _ = saveToCameraRoll(context: self.context, postbox: self.context.account.postbox, userLocation: self.sourceLocation.userLocation, mediaReference: .standalone(media: media)).start()
}
}), ContextMenuAction(content: .text(title: self.presentationData.strings.Conversation_ContextMenuShare, accessibilityLabel: self.presentationData.strings.Conversation_ContextMenuShare), action: { [weak self] in
if let self, let image = media.media._asMedia() as? TelegramMediaImage {
self.present(ShareController(context: self.context, subject: .image(image.representations.map({ ImageRepresentationWithReference(representation: $0, reference: MediaResourceReference.media(media: .webPage(webPage: WebpageReference(self.webPage), media: image), resource: $0.resource)) }))), nil)
if let self, let webPage = self.webPage, let image = media.media._asMedia() as? TelegramMediaImage {
self.present(ShareController(context: self.context, subject: .image(image.representations.map({ ImageRepresentationWithReference(representation: $0, reference: MediaResourceReference.media(media: .webPage(webPage: WebpageReference(webPage), media: image), resource: $0.resource)) }))), nil)
}
})], catchTapsOutside: true)
self.present(controller, ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak self] in
if let self {
for (_, itemNode) in self.visibleItemsWithNodes {
if let (node, _, _) = itemNode.transitionNode(media: media) {
return (self.scrollNode, node.convert(node.bounds, to: self.scrollNode), self, self.bounds)
}
}
if let _ = self {
// for (_, itemNode) in self.visibleItemsWithNodes {
// if let (node, _, _) = itemNode.transitionNode(media: media) {
// return (self.scrollNode, node.convert(node.bounds, to: self.scrollNode), self, self.bounds)
// }
// }
}
return nil
}))
}
private func activatePinchPreview(sourceNode: PinchSourceContainerNode) {
let pinchController = PinchController(sourceNode: sourceNode, getContentAreaInScreenSpace: { [weak self] in
guard let self else {
return CGRect()
}
let localRect = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: self.bounds.width, height: self.bounds.height))
return self.convert(localRect, to: nil)
})
self.presentInGlobalOverlay(pinchController)
}
private func pinchPreviewFinished(itemNode: ASDisplayNode) {
for (_, listItemNode) in self.visibleItemsWithNodes {
if let listItemNode = listItemNode as? InstantPagePeerReferenceNode {
if listItemNode.frame.intersects(itemNode.frame) && listItemNode.frame.maxY <= itemNode.frame.maxY + 2.0 {
listItemNode.layer.animateAlpha(from: 0.0, to: listItemNode.alpha, duration: 0.25)
break
}
}
}
}
@objc private func tapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) {
switch recognizer.state {
case .ended:
if let (gesture, _) = recognizer.lastRecognizedGestureAndLocation {
if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation {
switch gesture {
case .tap:
break
// if let url = self.urlForTapLocation(location) {
// self.openUrl(url)
// }
if let url = self.urlForTapLocation(location) {
self.openUrl(url)
}
case .longTap:
break
// if let theme = self.theme, let url = self.urlForTapLocation(location) {
// let canOpenIn = availableOpenInOptions(context: self.context, item: .url(url: url.url)).count > 1
// let openText = canOpenIn ? self.strings.Conversation_FileOpenIn : self.strings.Conversation_LinkDialogOpen
// let actionSheet = ActionSheetController(instantPageTheme: theme)
// actionSheet.setItemGroups([ActionSheetItemGroup(items: [
// ActionSheetTextItem(title: url.url),
// ActionSheetButtonItem(title: openText, color: .accent, action: { [weak self, weak actionSheet] in
// actionSheet?.dismissAnimated()
// if let strongSelf = self {
// if canOpenIn {
// strongSelf.openUrlIn(url)
// } else {
// strongSelf.openUrl(url)
// }
// }
// }),
// ActionSheetButtonItem(title: self.strings.ShareMenu_CopyShareLink, color: .accent, action: { [weak actionSheet] in
// actionSheet?.dismissAnimated()
// UIPasteboard.general.string = url.url
// }),
// ActionSheetButtonItem(title: self.strings.Conversation_AddToReadingList, color: .accent, action: { [weak actionSheet] in
// actionSheet?.dismissAnimated()
// if let link = URL(string: url.url) {
// let _ = try? SSReadingList.default()?.addItem(with: link, title: nil, previewText: nil)
// }
// })
// ]), ActionSheetItemGroup(items: [
// ActionSheetButtonItem(title: self.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
// actionSheet?.dismissAnimated()
// })
// ])])
// self.present(actionSheet, nil)
// } else if let (item, parentOffset) = self.textItemAtLocation(location) {
// let textFrame = item.frame
// var itemRects = item.lineRects()
// for i in 0 ..< itemRects.count {
// itemRects[i] = itemRects[i].offsetBy(dx: parentOffset.x + textFrame.minX, dy: parentOffset.y + textFrame.minY).insetBy(dx: -2.0, dy: -2.0)
// }
// self.updateTextSelectionRects(itemRects, text: item.plainText())
// }
if let url = self.urlForTapLocation(location) {
let canOpenIn = availableOpenInOptions(context: self.context, item: .url(url: url.url)).count > 1
let openText = canOpenIn ? self.presentationData.strings.Conversation_FileOpenIn : self.presentationData.strings.Conversation_LinkDialogOpen
let actionSheet = ActionSheetController(instantPageTheme: self.theme)
actionSheet.setItemGroups([ActionSheetItemGroup(items: [
ActionSheetTextItem(title: url.url),
ActionSheetButtonItem(title: openText, color: .accent, action: { [weak self, weak actionSheet] in
actionSheet?.dismissAnimated()
if let strongSelf = self {
if canOpenIn {
strongSelf.openUrlIn(url)
} else {
strongSelf.openUrl(url)
}
}
}),
ActionSheetButtonItem(title: self.presentationData.strings.ShareMenu_CopyShareLink, color: .accent, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
UIPasteboard.general.string = url.url
}),
ActionSheetButtonItem(title: self.presentationData.strings.Conversation_AddToReadingList, color: .accent, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
if let link = URL(string: url.url) {
let _ = try? SSReadingList.default()?.addItem(with: link, title: nil, previewText: nil)
}
})
]), ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: self.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
})
])])
self.present(actionSheet, nil)
} else if let (item, parentOffset) = self.textItemAtLocation(location) {
let textFrame = item.frame
var itemRects = item.lineRects()
for i in 0 ..< itemRects.count {
itemRects[i] = itemRects[i].offsetBy(dx: parentOffset.x + textFrame.minX, dy: parentOffset.y + textFrame.minY).insetBy(dx: -2.0, dy: -2.0)
}
self.updateTextSelectionRects(itemRects, text: item.plainText())
}
default:
break
}
@ -917,7 +1340,7 @@ private final class InstantPageContainerNode: ASDisplayNode, UIScrollViewDelegat
}
self.scrollNode.view.setContentOffset(CGPoint(x: 0.0, y: targetY), animated: true)
}
} else if case let .Loaded(content) = self.webPage.content, let instantPage = content.instantPage, !instantPage.isComplete {
} else if case let .Loaded(content) = self.webPage?.content, let instantPage = content.instantPage, !instantPage.isComplete {
// self.loadProgress.set(0.5)
self.pendingAnchor = anchor
}
@ -952,89 +1375,3 @@ private final class InstantPageContainerNode: ASDisplayNode, UIScrollViewDelegat
self.updateVisibleItems(visibleBounds: self.scrollNode.view.bounds, animated: animated)
}
}
final class BrowserInstantPageContent: UIView, BrowserContent {
var onScrollingUpdate: (ContentScrollingUpdate) -> Void = { _ in }
private var _state: BrowserContentState
private let statePromise: Promise<BrowserContentState>
private let webPage: TelegramMediaWebpage
private var initialized = false
private let instantPageNode: InstantPageContainerNode
var state: Signal<BrowserContentState, NoError> {
return self.statePromise.get()
}
init(context: AccountContext, webPage: TelegramMediaWebpage, url: String, sourceLocation: InstantPageSourceLocation) {
self.webPage = webPage
let title: String
if case let .Loaded(content) = webPage.content {
title = content.title ?? ""
} else {
title = ""
}
self._state = BrowserContentState(title: title, url: url, estimatedProgress: 0.0, contentType: .instantPage)
self.statePromise = Promise<BrowserContentState>(self._state)
self.instantPageNode = InstantPageContainerNode(context: context, webPage: webPage, sourceLocation: sourceLocation)
super.init(frame: .zero)
self.addSubnode(self.instantPageNode)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func reload() {
}
func stop() {
}
func navigateBack() {
}
func navigateForward() {
}
func setFontSize(_ fontSize: CGFloat) {
}
func setForceSerif(_ force: Bool) {
}
func setSearch(_ query: String?, completion: ((Int) -> Void)?) {
}
func scrollToPreviousSearchResult(completion: ((Int, Int) -> Void)?) {
}
func scrollToNextSearchResult(completion: ((Int, Int) -> Void)?) {
}
func scrollToTop() {
let scrollView = self.instantPageNode.scrollNode.view
scrollView.setContentOffset(CGPoint(x: 0.0, y: -scrollView.contentInset.top), animated: true)
}
func updateLayout(size: CGSize, insets: UIEdgeInsets, transition: ComponentTransition) {
// let layout = ContainerViewLayout(size: size, metrics: LayoutMetrics(widthClass: .compact, heightClass: .compact, orientation: .portrait), deviceMetrics: .iPhoneX, intrinsicInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: insets.bottom, right: 0.0), safeInsets: UIEdgeInsets(top: 0.0, left: insets.left, bottom: 0.0, right: insets.right), additionalInsets: .zero, statusBarHeight: nil, inputHeight: nil, inputHeightIsInteractivellyChanging: false, inVoiceOver: false)
self.instantPageNode.updateLayout(size: size, insets: insets, transition: transition.containedViewLayoutTransition)
self.instantPageNode.frame = CGRect(x: 0.0, y: 0.0, width: size.width, height: size.height)
//transition.updateFrame(view: self.webView, frame: CGRect(origin: CGPoint(x: 0.0, y: 56.0), size: CGSize(width: size.width, height: size.height - 56.0)))
}
}

View File

@ -219,7 +219,7 @@ final class BrowserNavigationBarComponent: CombinedComponent {
let maxCenterInset = max(centerLeftInset, centerRightInset)
if !leftItemList.isEmpty || !rightItemList.isEmpty {
availableWidth -= 20.0
availableWidth -= 28.0
}
let centerItem = context.component.centerItem.flatMap { item in

View File

@ -16,6 +16,7 @@ import OpenInExternalAppUI
import MultilineTextComponent
import MinimizedContainer
import InstantPageUI
import NavigationStackComponent
private let settingsTag = GenericComponentViewTag()
@ -26,6 +27,7 @@ private final class BrowserScreenComponent: CombinedComponent {
let contentState: BrowserContentState?
let presentationState: BrowserPresentationState
let performAction: ActionSlot<BrowserScreen.Action>
let performHoldAction: (UIView, ContextGesture?, BrowserScreen.Action) -> Void
let panelCollapseFraction: CGFloat
init(
@ -33,12 +35,14 @@ private final class BrowserScreenComponent: CombinedComponent {
contentState: BrowserContentState?,
presentationState: BrowserPresentationState,
performAction: ActionSlot<BrowserScreen.Action>,
performHoldAction: @escaping (UIView, ContextGesture?, BrowserScreen.Action) -> Void,
panelCollapseFraction: CGFloat
) {
self.context = context
self.contentState = contentState
self.presentationState = presentationState
self.performAction = performAction
self.performHoldAction = performHoldAction
self.panelCollapseFraction = panelCollapseFraction
}
@ -72,7 +76,8 @@ private final class BrowserScreenComponent: CombinedComponent {
return { context in
let environment = context.environment[ViewControllerComponentContainer.Environment.self].value
let performAction = context.component.performAction
let performHoldAction = context.component.performHoldAction
let navigationContent: AnyComponentWithIdentity<Empty>?
var navigationLeftItems: [AnyComponentWithIdentity<Empty>]
var navigationRightItems: [AnyComponentWithIdentity<Empty>]
@ -172,7 +177,7 @@ private final class BrowserScreenComponent: CombinedComponent {
leftItems: navigationLeftItems,
rightItems: navigationRightItems,
centerItem: navigationContent,
readingProgress: 0.0,
readingProgress: context.component.contentState?.readingProgress ?? 0.0,
loadingProgress: context.component.contentState?.estimatedProgress,
collapseFraction: collapseFraction
),
@ -206,7 +211,8 @@ private final class BrowserScreenComponent: CombinedComponent {
textColor: environment.theme.rootController.navigationBar.primaryTextColor,
canGoBack: context.component.contentState?.canGoBack ?? false,
canGoForward: context.component.contentState?.canGoForward ?? false,
performAction: performAction
performAction: performAction,
performHoldAction: performHoldAction
)
)
)
@ -275,17 +281,18 @@ public class BrowserScreen: ViewController, MinimizableController {
private weak var controller: BrowserScreen?
private let context: AccountContext
private let contentContainerView: UIView
fileprivate var content: BrowserContent?
private let contentContainerView = UIView()
fileprivate let contentNavigationContainer = ComponentView<Empty>()
fileprivate var content: [BrowserContent] = []
private var contentState: BrowserContentState?
private var contentStateDisposable: Disposable?
fileprivate var contentState: BrowserContentState?
private var contentStateDisposable = MetaDisposable()
private var presentationState: BrowserPresentationState
private let performAction: ActionSlot<BrowserScreen.Action>
private let performAction = ActionSlot<BrowserScreen.Action>()
fileprivate let componentHost: ComponentView<ViewControllerComponentContainer.Environment>
fileprivate let componentHost = ComponentView<ViewControllerComponentContainer.Environment>()
private var presentationData: PresentationData
private var validLayout: (ContainerViewLayout, CGFloat)?
@ -296,41 +303,13 @@ public class BrowserScreen: ViewController, MinimizableController {
self.presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
self.presentationState = BrowserPresentationState(fontSize: 100, fontIsSerif: false, isSearching: false, searchResultIndex: 0, searchResultCount: 0, searchQueryIsEmpty: true)
self.performAction = ActionSlot()
self.contentContainerView = UIView()
self.contentContainerView.clipsToBounds = true
self.componentHost = ComponentView<ViewControllerComponentContainer.Environment>()
super.init()
let content: BrowserContent
switch controller.subject {
case let .webPage(url):
content = BrowserWebContent(context: controller.context, url: url)
case let .instantPage(webPage, sourceLocation):
content = BrowserInstantPageContent(context: controller.context, webPage: webPage, url: webPage.content.url ?? "", sourceLocation: sourceLocation)
}
self.content = content
self.contentStateDisposable = (content.state
|> deliverOnMainQueue).start(next: { [weak self] state in
guard let strongSelf = self else {
return
}
strongSelf.controller?.title = state.title
strongSelf.contentState = state
strongSelf.requestLayout(transition: .easeInOut(duration: 0.25))
}).strict()
self.content?.onScrollingUpdate = { [weak self] update in
self?.onContentScrollingUpdate(update)
}
self.pushContent(controller.subject, transition: .immediate)
self.performAction.connect { [weak self] action in
guard let self, let content = self.content, let url = self.contentState?.url else {
guard let self, let content = self.content.last, let url = self.contentState?.url else {
return
}
switch action {
@ -341,7 +320,11 @@ public class BrowserScreen: ViewController, MinimizableController {
case .stop:
content.stop()
case .navigateBack:
content.navigateBack()
if content.currentState.canGoBack {
content.navigateBack()
} else {
self.popContent(transition: .spring(duration: 0.4))
}
case .navigateForward:
content.navigateForward()
case .share:
@ -458,22 +441,139 @@ public class BrowserScreen: ViewController, MinimizableController {
}
deinit {
self.contentStateDisposable?.dispose()
self.contentStateDisposable.dispose()
}
override func didLoad() {
super.didLoad()
self.contentContainerView.clipsToBounds = true
self.view.addSubview(self.contentContainerView)
if let content = self.content {
self.contentContainerView.addSubview(content)
}
}
func updatePresentationState(animated: Bool = false, _ f: (BrowserPresentationState) -> BrowserPresentationState) {
self.presentationState = f(self.presentationState)
self.requestLayout(transition: animated ? .easeInOut(duration: 0.2) : .immediate)
}
func pushContent(_ content: BrowserScreen.Subject, transition: ComponentTransition) {
let browserContent: BrowserContent
switch content {
case let .webPage(url):
browserContent = BrowserWebContent(context: self.context, url: url)
case let .instantPage(webPage, anchor, sourceLocation):
let instantPageContent = BrowserInstantPageContent(context: self.context, webPage: webPage, anchor: anchor, url: webPage.content.url ?? "", sourceLocation: sourceLocation)
instantPageContent.openPeer = { [weak self] peer in
guard let self else {
return
}
self.openPeer(peer)
}
browserContent = instantPageContent
}
browserContent.pushContent = { [weak self] content in
guard let self else {
return
}
self.pushContent(content, transition: .spring(duration: 0.4))
}
browserContent.present = { [weak self] c, a in
guard let self, let controller = self.controller else {
return
}
controller.present(c, in: .window(.root), with: a)
}
browserContent.presentInGlobalOverlay = { [weak self] c in
guard let self, let controller = self.controller else {
return
}
controller.presentInGlobalOverlay(c)
}
browserContent.getNavigationController = { [weak self] in
return self?.controller?.navigationController as? NavigationController
}
browserContent.minimize = { [weak self] in
guard let self else {
return
}
self.minimize()
}
self.content.append(browserContent)
self.requestLayout(transition: transition)
self.setupContentStateUpdates()
}
func popContent(transition: ComponentTransition) {
self.content.removeLast()
self.requestLayout(transition: transition)
self.setupContentStateUpdates()
}
func openPeer(_ peer: EnginePeer) {
guard let controller = self.controller, let navigationController = controller.navigationController as? NavigationController else {
return
}
self.minimize()
self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(peer), animated: true))
}
private func setupContentStateUpdates() {
for content in self.content {
content.onScrollingUpdate = { _ in }
}
guard let content = self.content.last else {
self.controller?.title = ""
self.contentState = nil
self.contentStateDisposable.set(nil)
self.requestLayout(transition: .easeInOut(duration: 0.25))
return
}
var previousState = BrowserContentState(title: "", url: "", estimatedProgress: 1.0, readingProgress: 0.0, contentType: .webPage, canGoBack: false, canGoForward: false, backList: [], forwardList: [])
if self.content.count > 1 {
for content in self.content.prefix(upTo: self.content.count - 1) {
var backList = previousState.backList
backList.append(BrowserContentState.HistoryItem(url: content.currentState.url, title: content.currentState.title, uuid: content.uuid))
previousState = previousState.withUpdatedBackList(backList)
}
}
self.contentStateDisposable.set((content.state
|> deliverOnMainQueue).startStrict(next: { [weak self] state in
guard let self else {
return
}
var backList = state.backList
backList.insert(contentsOf: previousState.backList, at: 0)
var canGoBack = state.canGoBack
if !backList.isEmpty {
canGoBack = true
}
let previousState = self.contentState
let state = state.withUpdatedCanGoBack(canGoBack).withUpdatedBackList(backList)
self.controller?.title = state.title
self.contentState = state
let transition: ComponentTransition
if let previousState, previousState.withUpdatedReadingProgress(state.readingProgress) == state {
transition = .immediate
} else {
transition = .easeInOut(duration: 0.25)
}
self.requestLayout(transition: transition)
}))
content.onScrollingUpdate = { [weak self] update in
self?.onContentScrollingUpdate(update)
}
}
func minimize() {
guard let controller = self.controller, let navigationController = controller.navigationController as? NavigationController else {
@ -598,7 +698,7 @@ public class BrowserScreen: ViewController, MinimizableController {
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let result = super.hitTest(point, with: event)
if result == self.componentHost.view, let content = self.content {
if result == self.componentHost.view, let content = self.content.last {
return content.hitTest(self.view.convert(point, to: content), with: event)
}
return result
@ -654,6 +754,51 @@ public class BrowserScreen: ViewController, MinimizableController {
}
}
func navigateTo(_ item: BrowserContentState.HistoryItem) {
if let _ = item.webItem {
if let last = self.content.last {
last.navigateTo(historyItem: item)
}
} else if let uuid = item.uuid {
var newContent = self.content
while newContent.last?.uuid != uuid {
newContent.removeLast()
}
self.content = newContent
self.requestLayout(transition: .spring(duration: 0.4))
}
}
func performHoldAction(view: UIView, gesture: ContextGesture?, action: BrowserScreen.Action) {
guard let controller = self.controller, let contentState = self.contentState else {
return
}
let source: ContextContentSource = .reference(BrowserReferenceContentSource(controller: controller, sourceView: view))
var items: [ContextMenuItem] = []
switch action {
case .navigateBack:
for item in contentState.backList {
items.append(.action(ContextMenuActionItem(text: item.title, textLayout: .secondLineWithValue(item.url), icon: { _ in return nil }, action: { [weak self] (_, action) in
self?.navigateTo(item)
action(.default)
})))
}
case .navigateForward:
for item in contentState.forwardList {
items.append(.action(ContextMenuActionItem(text: item.title, textLayout: .secondLineWithValue(item.url), icon: { _ in return nil }, action: { [weak self] (_, action) in
self?.navigateTo(item)
action(.default)
})))
}
default:
return
}
let contextController = ContextController(presentationData: self.presentationData, source: source, items: .single(ContextController.Items(content: .list(items))))
self.controller?.present(contextController, in: .window(.root))
}
func requestLayout(transition: ComponentTransition) {
if let (layout, navigationBarHeight) = self.validLayout {
self.containerLayoutUpdated(layout: layout, navigationBarHeight: navigationBarHeight, transition: transition)
@ -694,6 +839,11 @@ public class BrowserScreen: ViewController, MinimizableController {
contentState: self.contentState,
presentationState: self.presentationState,
performAction: self.performAction,
performHoldAction: { [weak self] view, gesture, action in
if let self {
self.performHoldAction(view: view, gesture: gesture, action: action)
}
},
panelCollapseFraction: self.scrollingPanelOffsetFraction
)
),
@ -711,12 +861,48 @@ public class BrowserScreen: ViewController, MinimizableController {
transition.setFrame(view: componentView, frame: CGRect(origin: .zero, size: componentSize))
}
transition.setFrame(view: self.contentContainerView, frame: CGRect(origin: .zero, size: layout.size))
if let content = self.content {
let collapsedHeight: CGFloat = 24.0
let topInset: CGFloat = environment.statusBarHeight + navigationBarHeight * (1.0 - self.scrollingPanelOffsetFraction) + collapsedHeight * self.scrollingPanelOffsetFraction
let bottomInset = 49.0 + layout.intrinsicInsets.bottom
content.updateLayout(size: layout.size, insets: UIEdgeInsets(top: topInset, left: layout.safeInsets.left, bottom: bottomInset, right: layout.safeInsets.right), transition: transition)
transition.setFrame(view: content, frame: CGRect(origin: .zero, size: layout.size))
var items: [AnyComponentWithIdentity<Empty>] = []
for content in self.content {
items.append(
AnyComponentWithIdentity(id: content.uuid, component: AnyComponent(
BrowserContentComponent(
content: content,
insets: UIEdgeInsets(
top: environment.statusBarHeight,
left: layout.safeInsets.left,
bottom: layout.intrinsicInsets.bottom,
right: layout.safeInsets.right
),
navigationBarHeight: navigationBarHeight,
scrollingPanelOffsetFraction: self.scrollingPanelOffsetFraction
)
))
)
}
let _ = self.contentNavigationContainer.update(
transition: transition,
component: AnyComponent(
NavigationStackComponent(
items: items,
requestPop: { [weak self] in
guard let self else {
return
}
self.popContent(transition: .spring(duration: 0.4))
}
)
),
environment: {},
containerSize: layout.size
)
let navigationFrame = CGRect(origin: .zero, size: layout.size)
if let view = self.contentNavigationContainer.view {
if view.superview == nil {
self.contentContainerView.addSubview(view)
}
transition.setFrame(view: view, frame: navigationFrame)
}
self.navigationBarHeight = environment.navigationHeight
@ -726,7 +912,7 @@ public class BrowserScreen: ViewController, MinimizableController {
public enum Subject {
case webPage(url: String)
case instantPage(webPage: TelegramMediaWebpage, sourceLocation: InstantPageSourceLocation)
case instantPage(webPage: TelegramMediaWebpage, anchor: String?, sourceLocation: InstantPageSourceLocation)
}
private let context: AccountContext
@ -743,7 +929,7 @@ public class BrowserScreen: ViewController, MinimizableController {
self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .allButUpsideDown)
self.scrollToTop = { [weak self] in
(self?.displayNode as? Node)?.content?.scrollToTop()
self?.node.content.last?.scrollToTop()
}
}
@ -751,6 +937,10 @@ public class BrowserScreen: ViewController, MinimizableController {
preconditionFailure()
}
private var node: Node {
return self.displayNode as! Node
}
override public func loadDisplayNode() {
self.displayNode = Node(controller: self)
@ -760,11 +950,30 @@ public class BrowserScreen: ViewController, MinimizableController {
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
(self.displayNode as! Node).containerLayoutUpdated(layout: layout, navigationBarHeight: self.navigationLayout(layout: layout).navigationFrame.height, transition: ComponentTransition(transition))
self.node.containerLayoutUpdated(layout: layout, navigationBarHeight: self.navigationLayout(layout: layout).navigationFrame.height, transition: ComponentTransition(transition))
}
public var isMinimized = false
public var isMinimizable = true
public var minimizedIcon: UIImage? {
if let contentState = self.node.contentState {
switch contentState.contentType {
case .webPage:
return contentState.favicon
case .instantPage:
return UIImage(bundleImageName: "Chat/Message/AttachedContentInstantIcon")?.withRenderingMode(.alwaysTemplate)
}
}
return nil
}
public var minimizedProgress: Float? {
if let contentState = self.node.contentState {
return Float(contentState.readingProgress)
}
return nil
}
}
private final class BrowserReferenceContentSource: ContextReferenceContentSource {
@ -780,3 +989,70 @@ private final class BrowserReferenceContentSource: ContextReferenceContentSource
return ContextControllerReferenceViewInfo(referenceView: self.sourceView, contentAreaInScreenSpace: UIScreen.main.bounds)
}
}
private final class BrowserContentComponent: Component {
let content: BrowserContent
let insets: UIEdgeInsets
let navigationBarHeight: CGFloat
let scrollingPanelOffsetFraction: CGFloat
init(
content: BrowserContent,
insets: UIEdgeInsets,
navigationBarHeight: CGFloat,
scrollingPanelOffsetFraction: CGFloat
) {
self.content = content
self.insets = insets
self.navigationBarHeight = navigationBarHeight
self.scrollingPanelOffsetFraction = scrollingPanelOffsetFraction
}
static func ==(lhs: BrowserContentComponent, rhs: BrowserContentComponent) -> Bool {
if lhs.content.uuid != rhs.content.uuid {
return false
}
if lhs.insets != rhs.insets {
return false
}
if lhs.navigationBarHeight != rhs.navigationBarHeight {
return false
}
if lhs.scrollingPanelOffsetFraction != rhs.scrollingPanelOffsetFraction {
return false
}
return true
}
final class View: UIView {
init() {
super.init(frame: CGRect())
}
required init?(coder aDecoder: NSCoder) {
preconditionFailure()
}
func update(component: BrowserContentComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize {
if component.content.superview !== self {
self.addSubview(component.content)
}
let collapsedHeight: CGFloat = 24.0
let topInset: CGFloat = component.insets.top + component.navigationBarHeight * (1.0 - component.scrollingPanelOffsetFraction) + collapsedHeight * component.scrollingPanelOffsetFraction
let bottomInset = 49.0 + component.insets.bottom
component.content.updateLayout(size: availableSize, insets: UIEdgeInsets(top: topInset, left: component.insets.left, bottom: bottomInset, right: component.insets.right), transition: transition)
transition.setFrame(view: component.content, frame: CGRect(origin: .zero, size: availableSize))
return availableSize
}
}
func makeView() -> View {
return View()
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, transition: transition)
}
}

View File

@ -5,6 +5,7 @@ import ComponentFlow
import BlurredBackgroundComponent
import BundleIconComponent
import TelegramPresentationData
import ContextReferenceButtonComponent
final class BrowserToolbarComponent: CombinedComponent {
let backgroundColor: UIColor
@ -123,17 +124,20 @@ final class NavigationToolbarContentComponent: CombinedComponent {
let canGoBack: Bool
let canGoForward: Bool
let performAction: ActionSlot<BrowserScreen.Action>
let performHoldAction: (UIView, ContextGesture?, BrowserScreen.Action) -> Void
init(
textColor: UIColor,
canGoBack: Bool,
canGoForward: Bool,
performAction: ActionSlot<BrowserScreen.Action>
performAction: ActionSlot<BrowserScreen.Action>,
performHoldAction: @escaping (UIView, ContextGesture?, BrowserScreen.Action) -> Void
) {
self.textColor = textColor
self.canGoBack = canGoBack
self.canGoForward = canGoForward
self.performAction = performAction
self.performHoldAction = performHoldAction
}
static func ==(lhs: NavigationToolbarContentComponent, rhs: NavigationToolbarContentComponent) -> Bool {
@ -150,32 +154,41 @@ final class NavigationToolbarContentComponent: CombinedComponent {
}
static var body: Body {
let back = Child(Button.self)
let forward = Child(Button.self)
let back = Child(ContextReferenceButtonComponent.self)
let forward = Child(ContextReferenceButtonComponent.self)
let share = Child(Button.self)
let openIn = Child(Button.self)
return { context in
let availableSize = context.availableSize
let performAction = context.component.performAction
let performHoldAction = context.component.performHoldAction
let sideInset: CGFloat = 5.0
let buttonSize = CGSize(width: 50.0, height: availableSize.height)
let spacing = (availableSize.width - buttonSize.width * 4.0 - sideInset * 2.0) / 3.0
let canGoBack = context.component.canGoBack
let back = back.update(
component: Button(
component: ContextReferenceButtonComponent(
content: AnyComponent(
BundleIconComponent(
name: "Instant View/Back",
tintColor: context.component.textColor
tintColor: canGoBack ? context.component.textColor : context.component.textColor.withAlphaComponent(0.4)
)
),
isEnabled: context.component.canGoBack,
action: {
performAction.invoke(.navigateBack)
minSize: buttonSize,
action: { view, gesture in
guard canGoBack else {
return
}
if let gesture {
performHoldAction(view, gesture, .navigateBack)
} else {
performAction.invoke(.navigateBack)
}
}
).minSize(buttonSize),
),
availableSize: buttonSize,
transition: .easeInOut(duration: 0.2)
)
@ -183,19 +196,27 @@ final class NavigationToolbarContentComponent: CombinedComponent {
.position(CGPoint(x: sideInset + back.size.width / 2.0, y: availableSize.height / 2.0))
)
let canGoForward = context.component.canGoForward
let forward = forward.update(
component: Button(
component: ContextReferenceButtonComponent(
content: AnyComponent(
BundleIconComponent(
name: "Instant View/Forward",
tintColor: context.component.textColor
tintColor: canGoForward ? context.component.textColor : context.component.textColor.withAlphaComponent(0.4)
)
),
isEnabled: context.component.canGoForward,
action: {
performAction.invoke(.navigateForward)
minSize: buttonSize,
action: { view, gesture in
guard canGoForward else {
return
}
if let gesture {
performHoldAction(view, gesture, .navigateForward)
} else {
performAction.invoke(.navigateForward)
}
}
).minSize(buttonSize),
),
availableSize: buttonSize,
transition: .easeInOut(duration: 0.2)
)

View File

@ -1,14 +1,20 @@
import Foundation
import UIKit
import Display
import ComponentFlow
import TelegramCore
import Postbox
import SwiftSignalKit
import TelegramPresentationData
import TelegramUIPreferences
import PresentationDataUtils
import AccountContext
import WebKit
import AppBundle
import PromptUI
import SafariServices
import ShareController
import UndoUI
private final class IpfsSchemeHandler: NSObject, WKURLSchemeHandler {
private final class PendingTask {
@ -80,19 +86,36 @@ private final class IpfsSchemeHandler: NSObject, WKURLSchemeHandler {
}
}
final class BrowserWebContent: UIView, BrowserContent, UIScrollViewDelegate {
final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKUIDelegate, UIScrollViewDelegate {
private let context: AccountContext
private let webView: WKWebView
let uuid: UUID
private var _state: BrowserContentState
private let statePromise: Promise<BrowserContentState>
var currentState: BrowserContentState {
return self._state
}
var state: Signal<BrowserContentState, NoError> {
return self.statePromise.get()
}
private let faviconDisposable = MetaDisposable()
var pushContent: (BrowserScreen.Subject) -> Void = { _ in }
var onScrollingUpdate: (ContentScrollingUpdate) -> Void = { _ in }
var minimize: () -> Void = { }
var present: (ViewController, Any?) -> Void = { _, _ in }
var presentInGlobalOverlay: (ViewController) -> Void = { _ in }
var getNavigationController: () -> NavigationController? = { return nil }
init(context: AccountContext, url: String) {
self.context = context
self.uuid = UUID()
let configuration = WKWebViewConfiguration()
if context.sharedContext.immediateExperimentalUISettings.browserExperiment {
@ -101,7 +124,7 @@ final class BrowserWebContent: UIView, BrowserContent, UIScrollViewDelegate {
}
self.webView = WKWebView(frame: CGRect(), configuration: configuration)
self.webView.allowsLinkPreview = false
self.webView.allowsLinkPreview = true
if #available(iOSApplicationExtension 11.0, iOS 11.0, *) {
self.webView.scrollView.contentInsetAdjustmentBehavior = .never
@ -115,13 +138,15 @@ final class BrowserWebContent: UIView, BrowserContent, UIScrollViewDelegate {
title = parsedUrl.host ?? ""
}
self._state = BrowserContentState(title: title, url: url, estimatedProgress: 0.0, contentType: .webPage)
self._state = BrowserContentState(title: title, url: url, estimatedProgress: 0.0, readingProgress: 0.0, contentType: .webPage)
self.statePromise = Promise<BrowserContentState>(self._state)
super.init(frame: .zero)
self.webView.allowsBackForwardNavigationGestures = true
self.webView.scrollView.delegate = self
self.webView.navigationDelegate = self
self.webView.uiDelegate = self
self.webView.addObserver(self, forKeyPath: #keyPath(WKWebView.title), options: [], context: nil)
self.webView.addObserver(self, forKeyPath: #keyPath(WKWebView.url), options: [], context: nil)
self.webView.addObserver(self, forKeyPath: #keyPath(WKWebView.estimatedProgress), options: [], context: nil)
@ -141,6 +166,8 @@ final class BrowserWebContent: UIView, BrowserContent, UIScrollViewDelegate {
self.webView.removeObserver(self, forKeyPath: #keyPath(WKWebView.estimatedProgress))
self.webView.removeObserver(self, forKeyPath: #keyPath(WKWebView.canGoBack))
self.webView.removeObserver(self, forKeyPath: #keyPath(WKWebView.canGoForward))
self.faviconDisposable.dispose()
}
func setFontSize(_ fontSize: CGFloat) {
@ -262,6 +289,12 @@ final class BrowserWebContent: UIView, BrowserContent, UIScrollViewDelegate {
self.webView.goForward()
}
func navigateTo(historyItem: BrowserContentState.HistoryItem) {
if let webItem = historyItem.webItem {
self.webView.go(to: webItem)
}
}
func scrollToTop() {
self.webView.scrollView.setContentOffset(CGPoint(x: 0.0, y: -self.webView.scrollView.contentInset.top), animated: true)
}
@ -277,25 +310,25 @@ final class BrowserWebContent: UIView, BrowserContent, UIScrollViewDelegate {
transition.setFrame(view: self.webView, frame: CGRect(origin: CGPoint(x: 0.0, y: insets.top), size: CGSize(width: size.width, height: size.height - insets.top)))
}
private func updateState(_ f: (BrowserContentState) -> BrowserContentState) {
let updated = f(self._state)
self._state = updated
self.statePromise.set(.single(self._state))
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
let updateState: ((BrowserContentState) -> BrowserContentState) -> Void = { f in
let updated = f(self._state)
self._state = updated
self.statePromise.set(.single(self._state))
}
if keyPath == "title" {
updateState { $0.withUpdatedTitle(self.webView.title ?? "") }
self.updateState { $0.withUpdatedTitle(self.webView.title ?? "") }
} else if keyPath == "URL" {
updateState { $0.withUpdatedUrl(self.webView.url?.absoluteString ?? "") }
self.updateState { $0.withUpdatedUrl(self.webView.url?.absoluteString ?? "") }
self.didSetupSearch = false
} else if keyPath == "estimatedProgress" {
updateState { $0.withUpdatedEstimatedProgress(self.webView.estimatedProgress) }
self.updateState { $0.withUpdatedEstimatedProgress(self.webView.estimatedProgress) }
} else if keyPath == "canGoBack" {
updateState { $0.withUpdatedCanGoBack(self.webView.canGoBack) }
self.updateState { $0.withUpdatedCanGoBack(self.webView.canGoBack) }
self.webView.disablesInteractiveTransitionGestureRecognizer = self.webView.canGoBack
} else if keyPath == "canGoForward" {
updateState { $0.withUpdatedCanGoForward(self.webView.canGoForward) }
self.updateState { $0.withUpdatedCanGoForward(self.webView.canGoForward) }
}
}
@ -344,5 +377,234 @@ final class BrowserWebContent: UIView, BrowserContent, UIScrollViewDelegate {
))
}
self.previousScrollingOffset = ScrollingOffsetState(value: scrollView.contentOffset.y, isDraggingOrDecelerating: isInteracting)
var readingProgress: CGFloat = 0.0
if !scrollView.contentSize.height.isZero {
let value = (scrollView.contentOffset.y + scrollView.contentInset.top) / (scrollView.contentSize.height - scrollView.bounds.size.height + scrollView.contentInset.top)
readingProgress = max(0.0, min(1.0, value))
}
self.updateState {
$0.withUpdatedReadingProgress(readingProgress)
}
}
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
self.updateState {
$0
.withUpdatedBackList(webView.backForwardList.backList.map { BrowserContentState.HistoryItem(webItem: $0) })
.withUpdatedForwardList(webView.backForwardList.forwardList.map { BrowserContentState.HistoryItem(webItem: $0) })
}
self.parseFavicon()
}
@available(iOSApplicationExtension 15.0, iOS 15.0, *)
func webView(_ webView: WKWebView, requestMediaCapturePermissionFor origin: WKSecurityOrigin, initiatedByFrame frame: WKFrameInfo, type: WKMediaCaptureType, decisionHandler: @escaping (WKPermissionDecision) -> Void) {
decisionHandler(.prompt)
}
func webView(_ webView: WKWebView, runJavaScriptAlertPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping () -> Void) {
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
var completed = false
let alertController = textAlertController(context: self.context, updatedPresentationData: nil, title: nil, text: message, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {
if !completed {
completed = true
completionHandler()
}
})])
alertController.dismissed = { byOutsideTap in
if byOutsideTap {
if !completed {
completed = true
completionHandler()
}
}
}
self.present(alertController, nil)
}
func webView(_ webView: WKWebView, runJavaScriptConfirmPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping (Bool) -> Void) {
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
var completed = false
let alertController = textAlertController(context: self.context, updatedPresentationData: nil, title: nil, text: message, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {
if !completed {
completed = true
completionHandler(false)
}
}), TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {
if !completed {
completed = true
completionHandler(true)
}
})])
alertController.dismissed = { byOutsideTap in
if byOutsideTap {
if !completed {
completed = true
completionHandler(false)
}
}
}
self.present(alertController, nil)
}
func webView(_ webView: WKWebView, runJavaScriptTextInputPanelWithPrompt prompt: String, defaultText: String?, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping (String?) -> Void) {
var completed = false
let promptController = promptController(sharedContext: self.context.sharedContext, updatedPresentationData: nil, text: prompt, value: defaultText, apply: { value in
if !completed {
completed = true
if let value = value {
completionHandler(value)
} else {
completionHandler(nil)
}
}
})
promptController.dismissed = { byOutsideTap in
if byOutsideTap {
if !completed {
completed = true
completionHandler(nil)
}
}
}
self.present(promptController, nil)
}
@available(iOS 13.0, *)
func webView(_ webView: WKWebView, contextMenuConfigurationForElement elementInfo: WKContextMenuElementInfo, completionHandler: @escaping (UIContextMenuConfiguration?) -> Void) {
guard let url = elementInfo.linkURL else {
completionHandler(nil)
return
}
//TODO:localize
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
let configuration = UIContextMenuConfiguration(identifier: nil, previewProvider: nil) { [weak self] _ in
return UIMenu(title: "", children: [
UIAction(title: "Open", image: generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Browser"), color: presentationData.theme.contextMenu.primaryColor), handler: { [weak self] _ in
self?.open(url: url.absoluteString, new: false)
}),
UIAction(title: "Open in New Tab", image: generateTintedImage(image: UIImage(bundleImageName: "Instant View/NewTab"), color: presentationData.theme.contextMenu.primaryColor), handler: { [weak self] _ in
self?.open(url: url.absoluteString, new: true)
}),
UIAction(title: "Add to Reading List", image: generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/ReadingList"), color: presentationData.theme.contextMenu.primaryColor), handler: { _ in
let _ = try? SSReadingList.default()?.addItem(with: url, title: nil, previewText: nil)
}),
UIAction(title: "Copy Link", image: generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Copy"), color: presentationData.theme.contextMenu.primaryColor), handler: { [weak self] _ in
UIPasteboard.general.string = url.absoluteString
self?.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil)
}),
UIAction(title: "Share", image: generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Forward"), color: presentationData.theme.contextMenu.primaryColor), handler: { [weak self] _ in
self?.share(url: url.absoluteString)
})
])
}
completionHandler(configuration)
}
private func open(url: String, new: Bool) {
let subject: BrowserScreen.Subject = .webPage(url: url)
if new, let navigationController = self.getNavigationController() {
self.minimize()
let controller = BrowserScreen(context: self.context, subject: subject)
navigationController.pushViewController(controller)
} else {
self.pushContent(subject)
}
}
private func share(url: String) {
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
let shareController = ShareController(context: self.context, subject: .url(url))
shareController.actionCompleted = { [weak self] in
self?.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil)
}
self.present(shareController, nil)
}
private func parseFavicon() {
struct Favicon: Equatable, Hashable {
let url: String
let dimensions: PixelDimensions?
func hash(into hasher: inout Hasher) {
hasher.combine(self.url)
if let dimensions = self.dimensions {
hasher.combine(dimensions.width)
hasher.combine(dimensions.height)
}
}
}
let js = """
var favicons = [];
var nodeList = document.getElementsByTagName('link');
for (var i = 0; i < nodeList.length; i++)
{
if((nodeList[i].getAttribute('rel') == 'icon')||(nodeList[i].getAttribute('rel') == 'shortcut icon'))
{
const node = nodeList[i];
favicons.push({
url: node.getAttribute('href'),
sizes: node.getAttribute('sizes')
});
}
}
favicons;
"""
self.webView.evaluateJavaScript(js, completionHandler: { [weak self] jsResult, _ in
guard let self, let favicons = jsResult as? [Any] else {
return
}
var result = Set<Favicon>();
for favicon in favicons {
if let faviconDict = favicon as? [String: Any], let urlString = faviconDict["url"] as? String {
if let url = URL(string: urlString, relativeTo: self.webView.url) {
let sizesString = faviconDict["sizes"] as? String;
let sizeStrings = sizesString?.components(separatedBy: "x") ?? []
if (sizeStrings.count == 2) {
let width = Int(sizeStrings[0])
let height = Int(sizeStrings[1])
let dimensions: PixelDimensions?
if let width, let height {
dimensions = PixelDimensions(width: Int32(width), height: Int32(height))
} else {
dimensions = nil
}
result.insert(Favicon(url: url.absoluteString, dimensions: dimensions))
} else {
result.insert(Favicon(url: url.absoluteString, dimensions: nil))
}
}
}
}
if result.isEmpty, let webViewUrl = self.webView.url {
let schemeAndHostUrl = webViewUrl.deletingPathExtension()
let url = schemeAndHostUrl.appendingPathComponent("favicon.ico")
result.insert(Favicon(url: url.absoluteString, dimensions: nil))
}
var largestIcon = result.first(where: { $0.url.lowercased().contains(".svg") })
if largestIcon == nil {
largestIcon = result.first
for icon in result {
let maxSize = largestIcon?.dimensions?.width ?? 0
if let width = icon.dimensions?.width, width > maxSize {
largestIcon = icon
}
}
}
if let favicon = largestIcon {
self.faviconDisposable.set((fetchFavicon(context: self.context, url: favicon.url, size: CGSize(width: 20.0, height: 20.0))
|> deliverOnMainQueue).startStrict(next: { [weak self] favicon in
guard let self else {
return
}
self.updateState { $0.withUpdatedFavicon(favicon) }
}))
}
})
}
}

View File

@ -0,0 +1,38 @@
import Foundation
import UIKit
import Display
import SwiftSignalKit
import TelegramCore
import AccountContext
import Svg
private var faviconCache: [String: UIImage] = [:]
func fetchFavicon(context: AccountContext, url: String, size: CGSize) -> Signal<UIImage?, NoError> {
if let icon = faviconCache[url] {
return .single(icon)
}
return context.engine.resources.httpData(url: url)
|> map(Optional.init)
|> `catch` { _ -> Signal<Data?, NoError> in
return .single(nil)
}
|> map { data in
if let data {
if let image = UIImage(data: data) {
return image
} else if url.lowercased().contains(".svg"), let preparedData = prepareSvgImage(data, false), let image = renderPreparedImage(preparedData, size, .clear, UIScreenScale, false) {
return image
}
return nil
} else {
return nil
}
}
|> beforeNext { image in
if let image {
Queue.mainQueue().async {
faviconCache[url] = image
}
}
}
}

View File

@ -9,7 +9,7 @@ public final class Button: Component {
public let isEnabled: Bool
public let isExclusive: Bool
public let action: () -> Void
public let holdAction: (() -> Void)?
public let holdAction: ((UIView) -> Void)?
public let highlightedAction: ActionSlot<Bool>?
convenience public init(
@ -39,7 +39,7 @@ public final class Button: Component {
isEnabled: Bool = true,
isExclusive: Bool = true,
action: @escaping () -> Void,
holdAction: (() -> Void)?,
holdAction: ((UIView) -> Void)?,
highlightedAction: ActionSlot<Bool>?
) {
self.content = content
@ -82,7 +82,7 @@ public final class Button: Component {
}
public func withHoldAction(_ holdAction: (() -> Void)?) -> Button {
public func withHoldAction(_ holdAction: ((UIView) -> Void)?) -> Button {
return Button(
content: self.content,
minSize: self.minSize,
@ -228,7 +228,7 @@ public final class Button: Component {
return
}
strongSelf.holdActionTimer?.invalidate()
strongSelf.component?.holdAction?()
strongSelf.component?.holdAction?(strongSelf)
strongSelf.beginExecuteHoldActionTimer()
})
self.holdActionTimer = holdActionTimer
@ -246,7 +246,7 @@ public final class Button: Component {
guard let strongSelf = self else {
return
}
strongSelf.component?.holdAction?()
strongSelf.component?.holdAction?(strongSelf)
})
self.holdActionTimer = holdActionTimer
RunLoop.main.add(holdActionTimer, forMode: .common)

View File

@ -21,10 +21,12 @@ private let tagImage: UIImage? = {
}()
private final class StarsButtonEffectLayer: SimpleLayer {
let emitterLayer = CAEmitterLayer()
override init() {
super.init()
self.backgroundColor = UIColor.lightGray.withAlphaComponent(0.2).cgColor
self.addSublayer(self.emitterLayer)
}
override init(layer: Any) {
@ -35,7 +37,45 @@ private final class StarsButtonEffectLayer: SimpleLayer {
fatalError("init(coder:) has not been implemented")
}
private func setup() {
let color = UIColor(rgb: 0xffbe27)
let emitter = CAEmitterCell()
emitter.name = "emitter"
emitter.contents = UIImage(bundleImageName: "Premium/Stars/Particle")?.cgImage
emitter.birthRate = 25.0
emitter.lifetime = 2.0
emitter.velocity = 12.0
emitter.velocityRange = 3
emitter.scale = 0.1
emitter.scaleRange = 0.08
emitter.alphaRange = 0.1
emitter.emissionRange = .pi * 2.0
emitter.setValue(3.0, forKey: "mass")
emitter.setValue(2.0, forKey: "massRange")
let staticColors: [Any] = [
color.withAlphaComponent(0.0).cgColor,
color.cgColor,
color.cgColor,
color.withAlphaComponent(0.0).cgColor
]
let staticColorBehavior = CAEmitterCell.createEmitterBehavior(type: "colorOverLife")
staticColorBehavior.setValue(staticColors, forKey: "colors")
emitter.setValue([staticColorBehavior], forKey: "emitterBehaviors")
self.emitterLayer.emitterCells = [emitter]
}
func update(size: CGSize) {
if self.emitterLayer.emitterCells == nil {
self.setup()
}
self.emitterLayer.emitterShape = .circle
self.emitterLayer.emitterSize = CGSize(width: size.width * 0.7, height: size.height * 0.7)
self.emitterLayer.emitterMode = .surface
self.emitterLayer.frame = CGRect(origin: .zero, size: size)
self.emitterLayer.emitterPosition = CGPoint(x: size.width / 2.0, y: size.height / 2.0)
}
}

View File

@ -569,7 +569,7 @@ private func contactListNodeEntries(accountPeer: EnginePeer?, peers: [ContactLis
if !topPeers.isEmpty {
var index: Int = 0
var sectionId: Int = 1
for (title, peerIds) in sections {
for (title, peerIds, hasActions) in sections {
var allSelected = true
if let selectedPeerIndices = selectionState?.selectedPeerIndices, !selectedPeerIndices.isEmpty {
for peerId in peerIds {
@ -617,7 +617,7 @@ private func contactListNodeEntries(accountPeer: EnginePeer?, peers: [ContactLis
}
let presence = presences[peer.id]
entries.append(.peer(index, .peer(peer: peer._asPeer(), isGlobal: false, participantCount: nil), presence, header, selection, theme, strings, dateTimeFormat, sortOrder, displayOrder, false, true, true, nil, false))
entries.append(.peer(index, .peer(peer: peer._asPeer(), isGlobal: false, participantCount: nil), presence, header, selection, theme, strings, dateTimeFormat, sortOrder, displayOrder, false, hasActions, true, nil, false))
index += 1
}
@ -629,7 +629,7 @@ private func contactListNodeEntries(accountPeer: EnginePeer?, peers: [ContactLis
if !sections.isEmpty, let selectionState {
var hasNonBirthdayPeers = false
var allBirthdayPeerIds = Set<EnginePeer.Id>()
for (_, peerIds) in sections {
for (_, peerIds, _) in sections {
for peerId in peerIds {
allBirthdayPeerIds.insert(peerId)
}
@ -865,7 +865,7 @@ public enum ContactListPresentation {
public enum TopPeers {
case none
case recent
case custom([(title: String, peerIds: [EnginePeer.Id])])
case custom([(title: String, peerIds: [EnginePeer.Id], hasActions: Bool)])
}
case orderedByPresence(options: [ContactListAdditionalOption])
@ -1711,7 +1711,7 @@ public final class ContactListNode: ASDisplayNode {
}
case let .custom(sections):
var peerIds: [EnginePeer.Id] = []
for (_, sectionPeers) in sections {
for (_, sectionPeers, _) in sections {
peerIds.append(contentsOf: sectionPeers)
}
topPeers = combineLatest(

View File

@ -26,6 +26,7 @@ public protocol MinimizableController: ViewController {
var isMinimized: Bool { get set }
var isMinimizable: Bool { get }
var minimizedIcon: UIImage? { get }
var minimizedProgress: Float? { get }
func makeContentSnapshotView() -> UIView?
func shouldDismissImmediately() -> Bool
@ -52,6 +53,10 @@ public extension MinimizableController {
return nil
}
var minimizedProgress: Float? {
return nil
}
func makeContentSnapshotView() -> UIView? {
return self.displayNode.view.snapshotView(afterScreenUpdates: false)
}

View File

@ -813,6 +813,16 @@ public extension CALayer {
}
}
public extension CAEmitterCell {
static func createEmitterBehavior(type: String) -> NSObject {
let selector = ["behaviorWith", "Type:"].joined(separator: "")
let behaviorClass = NSClassFromString(["CA", "Emitter", "Behavior"].joined(separator: "")) as! NSObject.Type
let behaviorWithType = behaviorClass.method(for: NSSelectorFromString(selector))!
let castedBehaviorWithType = unsafeBitCast(behaviorWithType, to:(@convention(c)(Any?, Selector, Any?) -> NSObject).self)
return castedBehaviorWithType(behaviorClass, NSSelectorFromString(selector), type)
}
}
public extension CALayer {
func snapshotContentTreeAsView(unhide: Bool = false) -> UIView? {
let wasHidden = self.isHidden

View File

@ -30,6 +30,8 @@ private func makeEntityView(context: AccountContext, entity: DrawingEntity) -> D
return DrawingLocationEntityView(context: context, entity: entity)
} else if let entity = entity as? DrawingLinkEntity {
return DrawingLinkEntityView(context: context, entity: entity)
} else if let entity = entity as? DrawingWeatherEntity {
return DrawingWeatherEntityView(context: context, entity: entity)
} else {
return nil
}
@ -59,6 +61,9 @@ private func prepareForRendering(entityView: DrawingEntityView) {
if let entityView = entityView as? DrawingLinkEntityView {
entityView.entity.renderImage = entityView.getRenderImage()
}
if let entityView = entityView as? DrawingWeatherEntityView {
entityView.entity.renderImage = entityView.getRenderImage()
}
}
public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView {
@ -397,6 +402,14 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView {
location.width = floor(self.size.width * 0.85)
location.scale = zoomScale
}
} else if let weather = entity as? DrawingWeatherEntity {
weather.position = center
if setup {
weather.rotation = rotation
weather.referenceDrawingSize = self.size
weather.width = floor(self.size.width * 0.85)
weather.scale = zoomScale
}
}
}

View File

@ -0,0 +1,643 @@
import Foundation
import UIKit
import Display
import SwiftSignalKit
import AccountContext
import TelegramCore
import AnimatedStickerNode
import TelegramAnimatedStickerNode
import StickerResources
import MediaEditor
private func generateIcon(style: DrawingWeatherEntity.Style) -> UIImage? {
guard let image = UIImage(bundleImageName: "Chat/Attach Menu/Location") else {
return nil
}
return generateImage(image.size, contextGenerator: { size, context in
context.clear(CGRect(origin: .zero, size: size))
if let cgImage = image.cgImage {
context.clip(to: CGRect(origin: .zero, size: size), mask: cgImage)
}
if [.black, .white].contains(style) {
let green: UIColor
let blue: UIColor
if case .black = style {
green = UIColor(rgb: 0x3EF588)
blue = UIColor(rgb: 0x4FAAFF)
} else {
green = UIColor(rgb: 0x1EBD5E)
blue = UIColor(rgb: 0x1C92FF)
}
var locations: [CGFloat] = [0.0, 1.0]
let colorsArray = [green.cgColor, blue.cgColor] as NSArray
let colorSpace = CGColorSpaceCreateDeviceRGB()
let gradient = CGGradient(colorsSpace: colorSpace, colors: colorsArray, locations: &locations)!
context.drawLinearGradient(gradient, start: CGPoint(x: size.width, y: size.height), end: CGPoint(x: 0.0, y: 0.0), options: CGGradientDrawingOptions())
} else {
context.setFillColor(UIColor.white.cgColor)
context.fill(CGRect(origin: .zero, size: size))
}
})
}
public final class DrawingWeatherEntityView: DrawingEntityView, UITextViewDelegate {
private var weatherEntity: DrawingWeatherEntity {
return self.entity as! DrawingWeatherEntity
}
let backgroundView: UIView
let blurredBackgroundView: BlurredBackgroundView
let textView: DrawingTextView
let iconView: UIImageView
private let imageNode: TransformImageNode
private var animationNode: AnimatedStickerNode?
private var didSetUpAnimationNode = false
private let stickerFetchedDisposable = MetaDisposable()
private let cachedDisposable = MetaDisposable()
init(context: AccountContext, entity: DrawingWeatherEntity) {
self.backgroundView = UIView()
self.backgroundView.clipsToBounds = true
self.blurredBackgroundView = BlurredBackgroundView(color: UIColor(white: 0.0, alpha: 0.25), enableBlur: true)
self.blurredBackgroundView.clipsToBounds = true
self.textView = DrawingTextView(frame: .zero)
self.textView.clipsToBounds = false
self.textView.backgroundColor = .clear
self.textView.isEditable = false
self.textView.isSelectable = false
self.textView.contentInset = .zero
self.textView.showsHorizontalScrollIndicator = false
self.textView.showsVerticalScrollIndicator = false
self.textView.scrollsToTop = false
self.textView.isScrollEnabled = false
self.textView.textContainerInset = .zero
self.textView.minimumZoomScale = 1.0
self.textView.maximumZoomScale = 1.0
self.textView.keyboardAppearance = .dark
self.textView.autocorrectionType = .default
self.textView.spellCheckingType = .no
self.textView.textContainer.maximumNumberOfLines = 2
self.textView.textContainer.lineBreakMode = .byTruncatingTail
self.iconView = UIImageView()
self.imageNode = TransformImageNode()
super.init(context: context, entity: entity)
self.textView.delegate = self
self.addSubview(self.backgroundView)
self.addSubview(self.blurredBackgroundView)
self.addSubview(self.textView)
self.addSubview(self.iconView)
self.update(animated: false)
self.setup()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private var textSize: CGSize = .zero
public override func sizeThatFits(_ size: CGSize) -> CGSize {
var result = self.textView.sizeThatFits(CGSize(width: self.weatherEntity.width, height: .greatestFiniteMagnitude))
self.textSize = result
let widthExtension: CGFloat
if self.weatherEntity.icon != nil {
widthExtension = result.height * 0.77
} else {
widthExtension = result.height * 0.65
}
result.width = floorToScreenPixels(max(224.0, ceil(result.width) + 20.0) + widthExtension)
result.height = ceil(result.height * 1.2);
return result;
}
public override func sizeToFit() {
let center = self.center
let transform = self.transform
self.transform = .identity
super.sizeToFit()
self.center = center
self.transform = transform
}
public override func layoutSubviews() {
super.layoutSubviews()
let iconSize: CGFloat
let iconOffset: CGFloat
if self.weatherEntity.icon != nil {
iconSize = min(80.0, floor(self.bounds.height * 0.7))
iconOffset = 0.2
} else {
iconSize = min(76.0, floor(self.bounds.height * 0.6))
iconOffset = 0.3
}
self.iconView.frame = CGRect(origin: CGPoint(x: floorToScreenPixels(iconSize * iconOffset), y: floorToScreenPixels((self.bounds.height - iconSize) / 2.0)), size: CGSize(width: iconSize, height: iconSize))
self.imageNode.frame = self.iconView.frame.offsetBy(dx: 0.0, dy: 2.0)
let imageSize = CGSize(width: iconSize, height: iconSize)
self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets()))()
self.textView.frame = CGRect(origin: CGPoint(x: self.bounds.width - self.textSize.width - 6.0, y: floorToScreenPixels((self.bounds.height - self.textSize.height) / 2.0)), size: self.textSize)
self.backgroundView.frame = self.bounds
self.blurredBackgroundView.frame = self.bounds
self.blurredBackgroundView.update(size: self.bounds.size, transition: .immediate)
}
override func selectedTapAction() -> Bool {
let values = [self.entity.scale, self.entity.scale * 0.93, self.entity.scale]
let keyTimes = [0.0, 0.33, 1.0]
self.layer.animateKeyframes(values: values as [NSNumber], keyTimes: keyTimes as [NSNumber], duration: 0.3, keyPath: "transform.scale")
let updatedStyle: DrawingWeatherEntity.Style
switch self.weatherEntity.style {
case .white:
updatedStyle = .black
case .black:
updatedStyle = .transparent
case .transparent:
if self.weatherEntity.hasCustomColor {
updatedStyle = .custom
} else {
updatedStyle = .white
}
case .custom:
updatedStyle = .white
case .blur:
updatedStyle = .white
}
self.weatherEntity.style = updatedStyle
self.update()
return true
}
private var displayFontSize: CGFloat {
var textFontSize: CGFloat = 0.07
let textLength = self.weatherEntity.temperature.count
if textLength > 10 {
textFontSize = max(0.01, 0.07 - CGFloat(textLength - 10) / 100.0)
}
let minFontSize = max(10.0, max(self.weatherEntity.referenceDrawingSize.width, self.weatherEntity.referenceDrawingSize.height) * 0.025)
let maxFontSize = max(10.0, max(self.weatherEntity.referenceDrawingSize.width, self.weatherEntity.referenceDrawingSize.height) * 0.25)
let fontSize = minFontSize + (maxFontSize - minFontSize) * textFontSize
return fontSize
}
private func updateText() {
let text = NSMutableAttributedString(string: self.weatherEntity.temperature.uppercased())
let range = NSMakeRange(0, text.length)
let fontSize = self.displayFontSize
self.textView.drawingLayoutManager.textContainers.first?.lineFragmentPadding = floor(fontSize * 0.24)
let font = Font.with(size: fontSize, design: .camera, weight: .semibold)
text.addAttribute(.font, value: font, range: range)
text.addAttribute(.kern, value: -3.5 as NSNumber, range: range)
self.textView.font = font
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.alignment = .left
text.addAttribute(.paragraphStyle, value: paragraphStyle, range: range)
let textColor: UIColor
switch self.weatherEntity.style {
case .white:
textColor = .black
case .black, .transparent, .blur:
textColor = .white
case .custom:
let color = self.weatherEntity.color.toUIColor()
if color.lightness > 0.705 {
textColor = .black
} else {
textColor = .white
}
}
text.addAttribute(.foregroundColor, value: textColor, range: range)
self.textView.attributedText = text
self.textView.visualText = text
}
private var currentStyle: DrawingWeatherEntity.Style?
public override func update(animated: Bool = false) {
self.center = self.weatherEntity.position
self.transform = CGAffineTransformScale(CGAffineTransformMakeRotation(self.weatherEntity.rotation), self.weatherEntity.scale, self.weatherEntity.scale)
self.textView.frameInsets = UIEdgeInsets(top: 0.15, left: 0.0, bottom: 0.15, right: 0.0)
switch self.weatherEntity.style {
case .white:
self.textView.textColor = .black
self.backgroundView.backgroundColor = .white
self.backgroundView.isHidden = false
self.blurredBackgroundView.isHidden = true
case .black:
self.textView.textColor = .white
self.backgroundView.backgroundColor = .black
self.backgroundView.isHidden = false
self.blurredBackgroundView.isHidden = true
case .transparent:
self.textView.textColor = .white
self.backgroundView.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.2)
self.backgroundView.isHidden = false
self.blurredBackgroundView.isHidden = true
case .custom:
let color = self.weatherEntity.color.toUIColor()
let textColor: UIColor
if color.lightness > 0.705 {
textColor = .black
} else {
textColor = .white
}
self.textView.textColor = textColor
self.backgroundView.backgroundColor = color
self.backgroundView.isHidden = false
self.blurredBackgroundView.isHidden = true
case .blur:
self.textView.textColor = .white
self.backgroundView.isHidden = true
self.backgroundView.backgroundColor = UIColor(rgb: 0xffffff)
self.blurredBackgroundView.isHidden = false
}
self.textView.textAlignment = .left
self.updateText()
self.sizeToFit()
if self.currentStyle != self.weatherEntity.style {
self.currentStyle = self.weatherEntity.style
self.iconView.image = generateIcon(style: self.weatherEntity.style)
}
self.backgroundView.layer.cornerRadius = self.textSize.height * 0.2
self.blurredBackgroundView.layer.cornerRadius = self.backgroundView.layer.cornerRadius
if #available(iOS 13.0, *) {
self.backgroundView.layer.cornerCurve = .continuous
self.blurredBackgroundView.layer.cornerCurve = .continuous
}
super.update(animated: animated)
}
private func setup() {
if let file = self.weatherEntity.icon {
self.iconView.isHidden = true
self.addSubnode(self.imageNode)
if let dimensions = file.dimensions {
if file.isAnimatedSticker || file.isVideoSticker || file.mimeType == "video/webm" {
if self.animationNode == nil {
let animationNode = DefaultAnimatedStickerNodeImpl()
animationNode.autoplay = false
self.animationNode = animationNode
animationNode.started = { [weak self, weak animationNode] in
self?.imageNode.isHidden = true
let _ = animationNode
// if let animationNode = animationNode {
// let _ = (animationNode.status
// |> take(1)
// |> deliverOnMainQueue).start(next: { [weak self] status in
// self?.started?(status.duration)
// })
// }
}
self.addSubnode(animationNode)
if file.isCustomTemplateEmoji {
animationNode.dynamicColor = UIColor(rgb: 0xffffff)
}
}
self.imageNode.setSignal(chatMessageAnimatedSticker(postbox: self.context.account.postbox, userLocation: .other, file: file, small: false, size: dimensions.cgSize.aspectFitted(CGSize(width: 256.0, height: 256.0))))
self.stickerFetchedDisposable.set(freeMediaFileResourceInteractiveFetched(account: self.context.account, userLocation: .other, fileReference: stickerPackFileReference(file), resource: file.resource).start())
} else {
if let animationNode = self.animationNode {
animationNode.visibility = false
self.animationNode = nil
animationNode.removeFromSupernode()
self.imageNode.isHidden = false
self.didSetUpAnimationNode = false
}
self.imageNode.setSignal(chatMessageSticker(account: self.context.account, userLocation: .other, file: file, small: false, synchronousLoad: false))
self.stickerFetchedDisposable.set(freeMediaFileResourceInteractiveFetched(account: self.context.account, userLocation: .other, fileReference: stickerPackFileReference(file), resource: chatMessageStickerResource(file: file, small: false)).start())
}
self.setNeedsLayout()
}
}
}
override func updateSelectionView() {
guard let selectionView = self.selectionView as? DrawingWeatherEntitySelectionView else {
return
}
self.pushIdentityTransformForMeasurement()
selectionView.transform = .identity
let bounds = self.selectionBounds
let center = bounds.center
let scale = self.superview?.superview?.layer.value(forKeyPath: "transform.scale.x") as? CGFloat ?? 1.0
selectionView.center = self.convert(center, to: selectionView.superview)
selectionView.bounds = CGRect(origin: .zero, size: CGSize(width: (bounds.width * self.weatherEntity.scale) * scale + selectionView.selectionInset * 2.0, height: (bounds.height * self.weatherEntity.scale) * scale + selectionView.selectionInset * 2.0))
selectionView.transform = CGAffineTransformMakeRotation(self.weatherEntity.rotation)
self.popIdentityTransformForMeasurement()
}
override func makeSelectionView() -> DrawingEntitySelectionView? {
if let selectionView = self.selectionView {
return selectionView
}
let selectionView = DrawingWeatherEntitySelectionView()
selectionView.entityView = self
return selectionView
}
func getRenderImage() -> UIImage? {
let rect = self.bounds
UIGraphicsBeginImageContextWithOptions(rect.size, false, 2.0)
self.drawHierarchy(in: rect, afterScreenUpdates: true)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return image
}
func getRenderSubEntities() -> [DrawingEntity] {
return []
}
}
final class DrawingWeatherEntitySelectionView: DrawingEntitySelectionView {
private let border = SimpleShapeLayer()
private let leftHandle = SimpleShapeLayer()
private let rightHandle = SimpleShapeLayer()
private var longPressGestureRecognizer: UILongPressGestureRecognizer?
override init(frame: CGRect) {
let handleBounds = CGRect(origin: .zero, size: entitySelectionViewHandleSize)
let handles = [
self.leftHandle,
self.rightHandle
]
super.init(frame: frame)
self.backgroundColor = .clear
self.isOpaque = false
self.border.lineCap = .round
self.border.fillColor = UIColor.clear.cgColor
self.border.strokeColor = UIColor(rgb: 0xffffff, alpha: 0.75).cgColor
self.layer.addSublayer(self.border)
for handle in handles {
handle.bounds = handleBounds
handle.fillColor = UIColor(rgb: 0x0a60ff).cgColor
handle.strokeColor = UIColor(rgb: 0xffffff).cgColor
handle.rasterizationScale = UIScreen.main.scale
handle.shouldRasterize = true
self.layer.addSublayer(handle)
}
self.snapTool.onSnapUpdated = { [weak self] type, snapped in
if let self, let entityView = self.entityView {
entityView.onSnapUpdated(type, snapped)
}
}
let longPressGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(self.handleLongPress(_:)))
self.addGestureRecognizer(longPressGestureRecognizer)
self.longPressGestureRecognizer = longPressGestureRecognizer
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
var scale: CGFloat = 1.0 {
didSet {
self.setNeedsLayout()
}
}
override var selectionInset: CGFloat {
return 15.0
}
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
private let snapTool = DrawingEntitySnapTool()
@objc private func handleLongPress(_ gestureRecognizer: UILongPressGestureRecognizer) {
if case .began = gestureRecognizer.state {
self.longPressed()
}
}
private var currentHandle: CALayer?
override func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) {
guard let entityView = self.entityView, let entity = entityView.entity as? DrawingWeatherEntity else {
return
}
let location = gestureRecognizer.location(in: self)
switch gestureRecognizer.state {
case .began:
self.tapGestureRecognizer?.isEnabled = false
self.tapGestureRecognizer?.isEnabled = true
self.longPressGestureRecognizer?.isEnabled = false
self.longPressGestureRecognizer?.isEnabled = true
self.snapTool.maybeSkipFromStart(entityView: entityView, position: entity.position)
if let sublayers = self.layer.sublayers {
for layer in sublayers {
if layer.frame.contains(location) {
self.currentHandle = layer
self.snapTool.maybeSkipFromStart(entityView: entityView, rotation: entity.rotation)
entityView.onInteractionUpdated(true)
return
}
}
}
self.currentHandle = self.layer
entityView.onInteractionUpdated(true)
case .changed:
if self.currentHandle == nil {
self.currentHandle = self.layer
}
let delta = gestureRecognizer.translation(in: entityView.superview)
let parentLocation = gestureRecognizer.location(in: self.superview)
let velocity = gestureRecognizer.velocity(in: entityView.superview)
var updatedScale = entity.scale
var updatedPosition = entity.position
var updatedRotation = entity.rotation
if self.currentHandle === self.leftHandle || self.currentHandle === self.rightHandle {
if gestureRecognizer.numberOfTouches > 1 {
return
}
var deltaX = gestureRecognizer.translation(in: self).x
if self.currentHandle === self.leftHandle {
deltaX *= -1.0
}
let scaleDelta = (self.bounds.size.width + deltaX * 2.0) / self.bounds.size.width
updatedScale = max(0.01, updatedScale * scaleDelta)
let newAngle: CGFloat
if self.currentHandle === self.leftHandle {
newAngle = atan2(self.center.y - parentLocation.y, self.center.x - parentLocation.x)
} else {
newAngle = atan2(parentLocation.y - self.center.y, parentLocation.x - self.center.x)
}
var delta = newAngle - updatedRotation
if delta < -.pi {
delta = 2.0 * .pi + delta
}
let velocityValue = sqrt(velocity.x * velocity.x + velocity.y * velocity.y) / 1000.0
updatedRotation = self.snapTool.update(entityView: entityView, velocity: velocityValue, delta: delta, updatedRotation: newAngle, skipMultiplier: 1.0)
} else if self.currentHandle === self.layer {
updatedPosition.x += delta.x
updatedPosition.y += delta.y
updatedPosition = self.snapTool.update(entityView: entityView, velocity: velocity, delta: delta, updatedPosition: updatedPosition, size: entityView.frame.size)
}
entity.scale = updatedScale
entity.position = updatedPosition
entity.rotation = updatedRotation
entityView.update()
gestureRecognizer.setTranslation(.zero, in: entityView)
case .ended, .cancelled:
self.snapTool.reset()
if self.currentHandle != nil {
self.snapTool.rotationReset()
}
entityView.onInteractionUpdated(false)
default:
break
}
entityView.onPositionUpdated(entity.position)
}
override func handlePinch(_ gestureRecognizer: UIPinchGestureRecognizer) {
guard let entityView = self.entityView as? DrawingWeatherEntityView, let entity = entityView.entity as? DrawingWeatherEntity else {
return
}
switch gestureRecognizer.state {
case .began, .changed:
if case .began = gestureRecognizer.state {
entityView.onInteractionUpdated(true)
}
let scale = gestureRecognizer.scale
entity.scale = max(0.1, entity.scale * scale)
entityView.update()
gestureRecognizer.scale = 1.0
case .ended, .cancelled:
entityView.onInteractionUpdated(false)
default:
break
}
}
override func handleRotate(_ gestureRecognizer: UIRotationGestureRecognizer) {
guard let entityView = self.entityView as? DrawingWeatherEntityView, let entity = entityView.entity as? DrawingWeatherEntity else {
return
}
let velocity = gestureRecognizer.velocity
var updatedRotation = entity.rotation
var rotation: CGFloat = 0.0
switch gestureRecognizer.state {
case .began:
self.snapTool.maybeSkipFromStart(entityView: entityView, rotation: entity.rotation)
entityView.onInteractionUpdated(true)
case .changed:
rotation = gestureRecognizer.rotation
updatedRotation += rotation
updatedRotation = self.snapTool.update(entityView: entityView, velocity: velocity, delta: rotation, updatedRotation: updatedRotation)
entity.rotation = updatedRotation
entityView.update()
gestureRecognizer.rotation = 0.0
case .ended, .cancelled:
self.snapTool.rotationReset()
entityView.onInteractionUpdated(false)
default:
break
}
entityView.onPositionUpdated(entity.position)
}
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
return self.bounds.insetBy(dx: -22.0, dy: -22.0).contains(point)
}
override func layoutSubviews() {
let inset = self.selectionInset - 10.0
let bounds = CGRect(origin: .zero, size: CGSize(width: entitySelectionViewHandleSize.width / self.scale, height: entitySelectionViewHandleSize.height / self.scale))
let handleSize = CGSize(width: 9.0 / self.scale, height: 9.0 / self.scale)
let handlePath = CGPath(ellipseIn: CGRect(origin: CGPoint(x: (bounds.width - handleSize.width) / 2.0, y: (bounds.height - handleSize.height) / 2.0), size: handleSize), transform: nil)
let lineWidth = (1.0 + UIScreenPixel) / self.scale
let handles = [
self.leftHandle,
self.rightHandle
]
for handle in handles {
handle.path = handlePath
handle.bounds = bounds
handle.lineWidth = lineWidth
}
self.leftHandle.position = CGPoint(x: inset, y: self.bounds.midY)
self.rightHandle.position = CGPoint(x: self.bounds.maxX - inset, y: self.bounds.midY)
let width: CGFloat = self.bounds.width - inset * 2.0
let height: CGFloat = self.bounds.height - inset * 2.0
let cornerRadius: CGFloat = 12.0 - self.scale
let perimeter: CGFloat = 2.0 * (width + height - cornerRadius * (4.0 - .pi))
let count = 12
let relativeDashLength: CGFloat = 0.25
let dashLength = perimeter / CGFloat(count)
self.border.lineDashPattern = [dashLength * relativeDashLength, dashLength * relativeDashLength] as [NSNumber]
self.border.lineWidth = 2.0 / self.scale
self.border.path = UIBezierPath(roundedRect: CGRect(origin: CGPoint(x: inset, y: inset), size: CGSize(width: width, height: height)), cornerRadius: cornerRadius).cgPath
}
}

View File

@ -632,6 +632,7 @@ private final class PendingInAppPurchaseState: Codable {
case giftCode
case giveaway
case stars
case starsGift
}
case subscription
@ -641,6 +642,7 @@ private final class PendingInAppPurchaseState: Codable {
case giftCode(peerIds: [EnginePeer.Id], boostPeer: EnginePeer.Id?)
case giveaway(boostPeer: EnginePeer.Id, additionalPeerIds: [EnginePeer.Id], countries: [String], onlyNewSubscribers: Bool, showWinners: Bool, prizeDescription: String?, randomId: Int64, untilDate: Int32)
case stars(count: Int64)
case starsGift(peerId: EnginePeer.Id, count: Int64)
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
@ -674,7 +676,14 @@ private final class PendingInAppPurchaseState: Codable {
untilDate: try container.decode(Int32.self, forKey: .untilDate)
)
case .stars:
self = .stars(count: try container.decode(Int64.self, forKey: .stars))
self = .stars(
count: try container.decode(Int64.self, forKey: .stars)
)
case .starsGift:
self = .starsGift(
peerId: EnginePeer.Id(try container.decode(Int64.self, forKey: .peer)),
count: try container.decode(Int64.self, forKey: .stars)
)
default:
throw DecodingError.generic
}
@ -710,6 +719,10 @@ private final class PendingInAppPurchaseState: Codable {
case let .stars(count):
try container.encode(PurposeType.stars.rawValue, forKey: .type)
try container.encode(count, forKey: .stars)
case let .starsGift(peerId, count):
try container.encode(PurposeType.starsGift.rawValue, forKey: .type)
try container.encode(peerId.toInt64(), forKey: .peer)
try container.encode(count, forKey: .stars)
}
}
@ -729,6 +742,8 @@ private final class PendingInAppPurchaseState: Codable {
self = .giveaway(boostPeer: boostPeer, additionalPeerIds: additionalPeerIds, countries: countries, onlyNewSubscribers: onlyNewSubscribers, showWinners: showWinners, prizeDescription: prizeDescription, randomId: randomId, untilDate: untilDate)
case let .stars(count, _, _):
self = .stars(count: count)
case let .starsGift(peerId, count, _, _):
self = .starsGift(peerId: peerId, count: count)
}
}
@ -749,6 +764,8 @@ private final class PendingInAppPurchaseState: Codable {
return .giveaway(boostPeer: boostPeer, additionalPeerIds: additionalPeerIds, countries: countries, onlyNewSubscribers: onlyNewSubscribers, showWinners: showWinners, prizeDescription: prizeDescription, randomId: randomId, untilDate: untilDate, currency: currency, amount: amount)
case let .stars(count):
return .stars(count: count, currency: currency, amount: amount)
case let .starsGift(peerId, count):
return .starsGift(peerId: peerId, count: count, currency: currency, amount: amount)
}
}
}

View File

@ -198,7 +198,7 @@ public class InstantPageGalleryController: ViewController, StandalonePresentable
private let replaceRootController: (ViewController, Promise<Bool>?) -> Void
private let baseNavigationController: NavigationController?
var openUrl: ((InstantPageUrlItem) -> Void)?
public var openUrl: ((InstantPageUrlItem) -> Void)?
private var innerOpenUrl: (InstantPageUrlItem) -> Void
private var openUrlOptions: (InstantPageUrlItem) -> Void

View File

@ -27,7 +27,7 @@ public final class InstantPageImageItem: InstantPageItem {
return [self.media]
}
let interactive: Bool
public let interactive: Bool
let roundCorners: Bool
let fit: Bool

View File

@ -141,30 +141,30 @@ struct InstantPagePlaylistLocation: Equatable, SharedMediaPlaylistLocation {
}
}
final class InstantPageMediaPlaylist: SharedMediaPlaylist {
public final class InstantPageMediaPlaylist: SharedMediaPlaylist {
private let webPage: TelegramMediaWebpage
private let items: [InstantPageMedia]
private let initialItemIndex: Int
var location: SharedMediaPlaylistLocation {
public var location: SharedMediaPlaylistLocation {
return InstantPagePlaylistLocation(webpageId: self.webPage.webpageId)
}
var currentItemDisappeared: (() -> Void)?
public var currentItemDisappeared: (() -> Void)?
private var currentItem: InstantPageMedia?
private var playedToEnd: Bool = false
private var order: MusicPlaybackSettingsOrder = .regular
private(set) var looping: MusicPlaybackSettingsLooping = .none
public private(set) var looping: MusicPlaybackSettingsLooping = .none
let id: SharedMediaPlaylistId
public let id: SharedMediaPlaylistId
private let stateValue = Promise<SharedMediaPlaylistState>()
var state: Signal<SharedMediaPlaylistState, NoError> {
public var state: Signal<SharedMediaPlaylistState, NoError> {
return self.stateValue.get()
}
init(webPage: TelegramMediaWebpage, items: [InstantPageMedia], initialItemIndex: Int) {
public init(webPage: TelegramMediaWebpage, items: [InstantPageMedia], initialItemIndex: Int) {
assert(Queue.mainQueue().isCurrent())
self.id = InstantPageMediaPlaylistId(webpageId: webPage.webpageId)
@ -176,7 +176,7 @@ final class InstantPageMediaPlaylist: SharedMediaPlaylist {
self.control(.next)
}
func control(_ action: SharedMediaPlaylistControlAction) {
public func control(_ action: SharedMediaPlaylistControlAction) {
assert(Queue.mainQueue().isCurrent())
switch action {
@ -228,14 +228,14 @@ final class InstantPageMediaPlaylist: SharedMediaPlaylist {
}
}
func setOrder(_ order: MusicPlaybackSettingsOrder) {
public func setOrder(_ order: MusicPlaybackSettingsOrder) {
if self.order != order {
self.order = order
self.updateState()
}
}
func setLooping(_ looping: MusicPlaybackSettingsLooping) {
public func setLooping(_ looping: MusicPlaybackSettingsLooping) {
if self.looping != looping {
self.looping = looping
self.updateState()
@ -246,6 +246,6 @@ final class InstantPageMediaPlaylist: SharedMediaPlaylist {
self.stateValue.set(.single(SharedMediaPlaylistState(loading: false, playedToEnd: self.playedToEnd, item: self.currentItem.flatMap({ InstantPageMediaPlaylistItem(webPage: self.webPage, item: $0) }), nextItem: nil, previousItem: nil, order: self.order, looping: self.looping)))
}
func onItemPlaybackStarted(_ item: SharedMediaPlaylistItem) {
public func onItemPlaybackStarted(_ item: SharedMediaPlaylistItem) {
}
}

View File

@ -46,7 +46,7 @@ private enum JoinState: Equatable {
}
}
final class InstantPagePeerReferenceNode: ASDisplayNode, InstantPageNode {
public final class InstantPagePeerReferenceNode: ASDisplayNode, InstantPageNode {
private let context: AccountContext
let safeInset: CGFloat
private let transparent: Bool
@ -197,7 +197,7 @@ final class InstantPagePeerReferenceNode: ASDisplayNode, InstantPageNode {
self.joinDisposable.dispose()
}
func update(strings: PresentationStrings, theme: InstantPageTheme) {
public func update(strings: PresentationStrings, theme: InstantPageTheme) {
if self.strings !== strings || self.theme !== theme {
let themeUpdated = self.theme !== theme
self.strings = strings
@ -206,7 +206,7 @@ final class InstantPagePeerReferenceNode: ASDisplayNode, InstantPageNode {
}
}
func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) {
public func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) {
}
private func applyThemeAndStrings(themeUpdated: Bool) {
@ -263,7 +263,7 @@ final class InstantPagePeerReferenceNode: ASDisplayNode, InstantPageNode {
}
}
override func layout() {
public override func layout() {
super.layout()
let size = self.bounds.size
@ -290,14 +290,14 @@ final class InstantPagePeerReferenceNode: ASDisplayNode, InstantPageNode {
}
}
func transitionNode(media: InstantPageMedia) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? {
public func transitionNode(media: InstantPageMedia) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? {
return nil
}
func updateHiddenMedia(media: InstantPageMedia?) {
public func updateHiddenMedia(media: InstantPageMedia?) {
}
func updateIsVisible(_ isVisible: Bool) {
public func updateIsVisible(_ isVisible: Bool) {
}
@objc func buttonPressed() {

View File

@ -16,7 +16,7 @@ public final class InstantPagePlayableVideoItem: InstantPageItem {
return [self.media]
}
let interactive: Bool
public let interactive: Bool
public let wantsNode: Bool = true
public let separatesTiles: Bool = false

View File

@ -327,7 +327,7 @@ extension ActionSheetControllerTheme {
}
}
extension ActionSheetController {
public extension ActionSheetController {
convenience init(instantPageTheme: InstantPageTheme) {
self.init(theme: ActionSheetControllerTheme(instantPageTheme: instantPageTheme), allowInputInset: false)
}

View File

@ -12,14 +12,6 @@ struct ArbitraryRandomNumberGenerator : RandomNumberGenerator {
func next() -> UInt64 { return UInt64(drand48() * Double(UInt64.max)) }
}
func createEmitterBehavior(type: String) -> NSObject {
let selector = ["behaviorWith", "Type:"].joined(separator: "")
let behaviorClass = NSClassFromString(["CA", "Emitter", "Behavior"].joined(separator: "")) as! NSObject.Type
let behaviorWithType = behaviorClass.method(for: NSSelectorFromString(selector))!
let castedBehaviorWithType = unsafeBitCast(behaviorWithType, to:(@convention(c)(Any?, Selector, Any?) -> NSObject).self)
return castedBehaviorWithType(behaviorClass, NSSelectorFromString(selector), type)
}
func generateMaskImage(size originalSize: CGSize, position: CGPoint, inverse: Bool) -> UIImage? {
var size = originalSize
var position = position
@ -123,10 +115,10 @@ public class InvisibleInkDustView: UIView {
emitter.setValue(2.0, forKey: "massRange")
self.emitter = emitter
let fingerAttractor = createEmitterBehavior(type: "simpleAttractor")
let fingerAttractor = CAEmitterCell.createEmitterBehavior(type: "simpleAttractor")
fingerAttractor.setValue("fingerAttractor", forKey: "name")
let alphaBehavior = createEmitterBehavior(type: "valueOverLife")
let alphaBehavior = CAEmitterCell.createEmitterBehavior(type: "valueOverLife")
alphaBehavior.setValue("color.alpha", forKey: "keyPath")
alphaBehavior.setValue([0.0, 0.0, 1.0, 0.0, -1.0], forKey: "values")
alphaBehavior.setValue(true, forKey: "additive")
@ -435,10 +427,10 @@ public class InvisibleInkDustNode: ASDisplayNode {
emitter.setValue(2.0, forKey: "massRange")
self.emitter = emitter
let fingerAttractor = createEmitterBehavior(type: "simpleAttractor")
let fingerAttractor = CAEmitterCell.createEmitterBehavior(type: "simpleAttractor")
fingerAttractor.setValue("fingerAttractor", forKey: "name")
let alphaBehavior = createEmitterBehavior(type: "valueOverLife")
let alphaBehavior = CAEmitterCell.createEmitterBehavior(type: "valueOverLife")
alphaBehavior.setValue("color.alpha", forKey: "keyPath")
alphaBehavior.setValue([0.0, 0.0, 1.0, 0.0, -1.0], forKey: "values")
alphaBehavior.setValue(true, forKey: "additive")

View File

@ -40,12 +40,12 @@ public class MediaDustLayer: CALayer {
emitter.setValue(0.01, forKey: "massRange")
self.emitter = emitter
let alphaBehavior = createEmitterBehavior(type: "valueOverLife")
let alphaBehavior = CAEmitterCell.createEmitterBehavior(type: "valueOverLife")
alphaBehavior.setValue("color.alpha", forKey: "keyPath")
alphaBehavior.setValue([0, 0, 1, 0, -1, 0, 0, 1, 0, -1, 0, 0, 1, 0, -1, 0, 0, 1, 0, -1, 0, 0, 1, 0, -1, 0, 0, 1, 0, -1, 0, 0, 1, 0, -1, 0, 0, 1, 0, -1], forKey: "values")
alphaBehavior.setValue(true, forKey: "additive")
let scaleBehavior = createEmitterBehavior(type: "valueOverLife")
let scaleBehavior = CAEmitterCell.createEmitterBehavior(type: "valueOverLife")
scaleBehavior.setValue("scale", forKey: "keyPath")
scaleBehavior.setValue([0.0, 0.5], forKey: "values")
scaleBehavior.setValue([0.0, 0.05], forKey: "locations")
@ -154,31 +154,31 @@ public class MediaDustNode: ASDisplayNode {
emitter.setValue(0.01, forKey: "massRange")
self.emitter = emitter
let alphaBehavior = createEmitterBehavior(type: "valueOverLife")
let alphaBehavior = CAEmitterCell.createEmitterBehavior(type: "valueOverLife")
alphaBehavior.setValue("color.alpha", forKey: "keyPath")
alphaBehavior.setValue([0, 0, 1, 0, -1, 0, 0, 1, 0, -1, 0, 0, 1, 0, -1, 0, 0, 1, 0, -1, 0, 0, 1, 0, -1, 0, 0, 1, 0, -1, 0, 0, 1, 0, -1, 0, 0, 1, 0, -1], forKey: "values")
alphaBehavior.setValue(true, forKey: "additive")
let scaleBehavior = createEmitterBehavior(type: "valueOverLife")
let scaleBehavior = CAEmitterCell.createEmitterBehavior(type: "valueOverLife")
scaleBehavior.setValue("scale", forKey: "keyPath")
scaleBehavior.setValue([0.0, 0.5], forKey: "values")
scaleBehavior.setValue([0.0, 0.05], forKey: "locations")
let randomAttractor0 = createEmitterBehavior(type: "simpleAttractor")
let randomAttractor0 = CAEmitterCell.createEmitterBehavior(type: "simpleAttractor")
randomAttractor0.setValue("randomAttractor0", forKey: "name")
randomAttractor0.setValue(20, forKey: "falloff")
randomAttractor0.setValue(35, forKey: "radius")
randomAttractor0.setValue(5, forKey: "stiffness")
randomAttractor0.setValue(NSValue(cgPoint: .zero), forKey: "position")
let randomAttractor1 = createEmitterBehavior(type: "simpleAttractor")
let randomAttractor1 = CAEmitterCell.createEmitterBehavior(type: "simpleAttractor")
randomAttractor1.setValue("randomAttractor1", forKey: "name")
randomAttractor1.setValue(20, forKey: "falloff")
randomAttractor1.setValue(35, forKey: "radius")
randomAttractor1.setValue(5, forKey: "stiffness")
randomAttractor1.setValue(NSValue(cgPoint: .zero), forKey: "position")
let fingerAttractor = createEmitterBehavior(type: "simpleAttractor")
let fingerAttractor = CAEmitterCell.createEmitterBehavior(type: "simpleAttractor")
fingerAttractor.setValue("fingerAttractor", forKey: "name")
let behaviors = [randomAttractor0, randomAttractor1, fingerAttractor, alphaBehavior, scaleBehavior]

View File

@ -10,11 +10,17 @@ import LocationResources
import ShimmerEffect
public final class ItemListVenueItem: ListViewItem, ItemListItem {
public enum InfoIcon {
case info
case goTo
}
let presentationData: ItemListPresentationData
let engine: TelegramEngine
let venue: TelegramMediaMap?
let title: String?
let subtitle: String?
let icon: InfoIcon
let style: ItemListStyle
let action: (() -> Void)?
let infoAction: (() -> Void)?
@ -22,12 +28,13 @@ public final class ItemListVenueItem: ListViewItem, ItemListItem {
public let sectionId: ItemListSectionId
let header: ListViewItemHeader?
public init(presentationData: ItemListPresentationData, engine: TelegramEngine, venue: TelegramMediaMap?, title: String? = nil, subtitle: String? = nil, sectionId: ItemListSectionId = 0, style: ItemListStyle, action: (() -> Void)?, infoAction: (() -> Void)? = nil, header: ListViewItemHeader? = nil) {
public init(presentationData: ItemListPresentationData, engine: TelegramEngine, venue: TelegramMediaMap?, title: String? = nil, subtitle: String? = nil, icon: ItemListVenueItem.InfoIcon = .info, sectionId: ItemListSectionId = 0, style: ItemListStyle, action: (() -> Void)?, infoAction: (() -> Void)? = nil, header: ListViewItemHeader? = nil) {
self.presentationData = presentationData
self.engine = engine
self.venue = venue
self.title = title
self.subtitle = subtitle
self.icon = icon
self.sectionId = sectionId
self.style = style
self.action = action
@ -274,7 +281,15 @@ public class ItemListVenueItemNode: ListViewItemNode, ItemListItemNode {
strongSelf.bottomStripeNode.backgroundColor = itemSeparatorColor
strongSelf.backgroundNode.backgroundColor = itemBackgroundColor
strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.list.itemHighlightedBackgroundColor
strongSelf.infoButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Location/InfoIcon"), color: item.presentationData.theme.list.itemAccentColor), for: .normal)
let iconName: String
switch item.icon {
case .info:
iconName = "Location/InfoIcon"
case .goTo:
iconName = "Location/GoTo"
}
strongSelf.infoButton.setImage(generateTintedImage(image: UIImage(bundleImageName: iconName), color: item.presentationData.theme.list.itemAccentColor), for: .normal)
}
let transition = ContainedViewLayoutTransition.immediate

View File

@ -189,6 +189,7 @@ public final class LocationMapNode: ASDisplayNode, MKMapViewDelegateTarget {
public static let defaultMapSpan = MKCoordinateSpan(latitudeDelta: 0.016, longitudeDelta: 0.016)
public static let viewMapSpan = MKCoordinateSpan(latitudeDelta: 0.008, longitudeDelta: 0.008)
public static let globalMapSpan = MKCoordinateSpan(latitudeDelta: 0.1, longitudeDelta: 0.1)
class ProximityCircleRenderer: MKCircleRenderer {
override func draw(_ mapRect: MKMapRect, zoomScale: MKZoomScale, in context: CGContext) {

View File

@ -24,7 +24,7 @@ class LocationPickerInteraction {
let toggleMapModeSelection: () -> Void
let updateMapMode: (LocationMapMode) -> Void
let goToUserLocation: () -> Void
let goToCoordinate: (CLLocationCoordinate2D) -> Void
let goToCoordinate: (CLLocationCoordinate2D, Bool) -> Void
let openSearch: () -> Void
let updateSearchQuery: (String) -> Void
let dismissSearch: () -> Void
@ -33,7 +33,7 @@ class LocationPickerInteraction {
let openHomeWorkInfo: () -> Void
let showPlacesInThisArea: () -> Void
init(sendLocation: @escaping (CLLocationCoordinate2D, String?, MapGeoAddress?) -> Void, sendLiveLocation: @escaping (CLLocationCoordinate2D) -> Void, sendVenue: @escaping (TelegramMediaMap, Int64?, String?) -> Void, toggleMapModeSelection: @escaping () -> Void, updateMapMode: @escaping (LocationMapMode) -> Void, goToUserLocation: @escaping () -> Void, goToCoordinate: @escaping (CLLocationCoordinate2D) -> Void, openSearch: @escaping () -> Void, updateSearchQuery: @escaping (String) -> Void, dismissSearch: @escaping () -> Void, dismissInput: @escaping () -> Void, updateSendActionHighlight: @escaping (Bool) -> Void, openHomeWorkInfo: @escaping () -> Void, showPlacesInThisArea: @escaping ()-> Void) {
init(sendLocation: @escaping (CLLocationCoordinate2D, String?, MapGeoAddress?) -> Void, sendLiveLocation: @escaping (CLLocationCoordinate2D) -> Void, sendVenue: @escaping (TelegramMediaMap, Int64?, String?) -> Void, toggleMapModeSelection: @escaping () -> Void, updateMapMode: @escaping (LocationMapMode) -> Void, goToUserLocation: @escaping () -> Void, goToCoordinate: @escaping (CLLocationCoordinate2D, Bool) -> Void, openSearch: @escaping () -> Void, updateSearchQuery: @escaping (String) -> Void, dismissSearch: @escaping () -> Void, dismissInput: @escaping () -> Void, updateSendActionHighlight: @escaping (Bool) -> Void, openHomeWorkInfo: @escaping () -> Void, showPlacesInThisArea: @escaping ()-> Void) {
self.sendLocation = sendLocation
self.sendLiveLocation = sendLiveLocation
self.sendVenue = sendVenue
@ -231,14 +231,14 @@ public final class LocationPickerController: ViewController, AttachmentContainab
return
}
strongSelf.controllerNode.goToUserLocation()
}, goToCoordinate: { [weak self] coordinate in
}, goToCoordinate: { [weak self] coordinate, zoomOut in
guard let strongSelf = self else {
return
}
strongSelf.controllerNode.updateState { state in
var state = state
state.displayingMapModeOptions = false
state.selectedLocation = .location(coordinate, nil)
state.selectedLocation = .location(coordinate, nil, zoomOut)
state.searchingVenuesAround = false
return state
}

View File

@ -219,7 +219,7 @@ private func preparedTransition(from fromEntries: [LocationPickerEntry], to toEn
enum LocationPickerLocation: Equatable {
case none
case selecting
case location(CLLocationCoordinate2D, String?)
case location(CLLocationCoordinate2D, String?, Bool)
case venue(TelegramMediaMap, Int64?, String?)
var isCustom: Bool {
@ -245,8 +245,8 @@ enum LocationPickerLocation: Equatable {
} else {
return false
}
case let .location(lhsCoordinate, lhsAddress):
if case let .location(rhsCoordinate, rhsAddress) = rhs, locationCoordinatesAreEqual(lhsCoordinate, rhsCoordinate), lhsAddress == rhsAddress {
case let .location(lhsCoordinate, lhsAddress, lhsGlobal):
if case let .location(rhsCoordinate, rhsAddress, rhsGlobal) = rhs, locationCoordinatesAreEqual(lhsCoordinate, rhsCoordinate), lhsAddress == rhsAddress, lhsGlobal == rhsGlobal {
return true
} else {
return false
@ -589,7 +589,7 @@ final class LocationPickerControllerNode: ViewControllerTracingNode, CLLocationM
var entries: [LocationPickerEntry] = []
switch state.selectedLocation {
case let .location(coordinate, address):
case let .location(coordinate, address, _):
let title: String
switch strongSelf.mode {
case .share:
@ -722,12 +722,13 @@ final class LocationPickerControllerNode: ViewControllerTracingNode, CLLocationM
strongSelf.headerNode.mapNode.resetAnnotationSelection()
case .selecting:
strongSelf.headerNode.mapNode.resetAnnotationSelection()
case let .location(coordinate, address):
case let .location(coordinate, address, global):
var updateMap = false
let span = global ? LocationMapNode.globalMapSpan : LocationMapNode.defaultMapSpan
switch previousState.selectedLocation {
case .none, .venue:
updateMap = true
case let .location(previousCoordinate, _):
case let .location(previousCoordinate, _, _):
if !locationCoordinatesAreEqual(previousCoordinate, coordinate) {
updateMap = true
}
@ -735,7 +736,7 @@ final class LocationPickerControllerNode: ViewControllerTracingNode, CLLocationM
break
}
if updateMap {
strongSelf.headerNode.mapNode.setMapCenter(coordinate: coordinate, isUserLocation: false, hidePicker: false, animated: true)
strongSelf.headerNode.mapNode.setMapCenter(coordinate: coordinate, span: span, isUserLocation: false, hidePicker: false, animated: true)
strongSelf.headerNode.mapNode.switchToPicking(animated: false)
}
@ -849,11 +850,11 @@ final class LocationPickerControllerNode: ViewControllerTracingNode, CLLocationM
))
}
if case let .location(coordinate, address) = state.selectedLocation, address == nil {
if case let .location(coordinate, address, global) = state.selectedLocation, address == nil {
setupGeocoding(coordinate, { [weak self] geoAddress, address, cityName, streetName, countryCode, isStreet in
self?.updateState { state in
var state = state
state.selectedLocation = .location(coordinate, address)
state.selectedLocation = .location(coordinate, address, global)
state.geoAddress = geoAddress
state.city = cityName
state.street = streetName
@ -938,7 +939,7 @@ final class LocationPickerControllerNode: ViewControllerTracingNode, CLLocationM
strongSelf.updateState { state in
var state = state
if case .selecting = state.selectedLocation {
state.selectedLocation = .location(coordinate, nil)
state.selectedLocation = .location(coordinate, nil, false)
state.searchingVenuesAround = false
}
return state
@ -1231,7 +1232,7 @@ final class LocationPickerControllerNode: ViewControllerTracingNode, CLLocationM
}
func requestPlacesAtSelectedLocation() {
if case let .location(coordinate, _) = self.state.selectedLocation {
if case let .location(coordinate, _, _) = self.state.selectedLocation {
self.headerNode.mapNode.setMapCenter(coordinate: coordinate, animated: true)
self.searchVenuesPromise.set(.single(coordinate))
self.updateState { state in

View File

@ -23,6 +23,7 @@ private struct LocationSearchEntry: Identifiable, Comparable {
let resultId: String?
let title: String?
let distance: Double
let story: Bool
var stableId: String {
return self.location.venue?.id ?? ""
@ -50,6 +51,9 @@ private struct LocationSearchEntry: Identifiable, Comparable {
if lhs.distance != rhs.distance {
return false
}
if lhs.story != rhs.story {
return false
}
return true
}
@ -57,7 +61,7 @@ private struct LocationSearchEntry: Identifiable, Comparable {
return lhs.index < rhs.index
}
func item(engine: TelegramEngine, presentationData: PresentationData, sendVenue: @escaping (TelegramMediaMap, Int64?, String?) -> Void) -> ListViewItem {
func item(engine: TelegramEngine, presentationData: PresentationData, sendVenue: @escaping (TelegramMediaMap, Int64?, String?) -> Void, goToVenue: @escaping (TelegramMediaMap) -> Void) -> ListViewItem {
let venue = self.location
let queryId = self.queryId
let resultId = self.resultId
@ -71,9 +75,11 @@ private struct LocationSearchEntry: Identifiable, Comparable {
header = ChatListSearchItemHeader(type: .mapAddress, theme: presentationData.theme, strings: presentationData.strings)
subtitle = presentationData.strings.Map_DistanceAway(stringForDistance(strings: presentationData.strings, distance: self.distance)).string
}
return ItemListVenueItem(presentationData: ItemListPresentationData(presentationData), engine: engine, venue: self.location, title: self.title, subtitle: subtitle, style: .plain, action: {
return ItemListVenueItem(presentationData: ItemListPresentationData(presentationData), engine: engine, venue: self.location, title: self.title, subtitle: subtitle, icon: .goTo, style: .plain, action: {
sendVenue(venue, queryId, resultId)
}, header: header)
}, infoAction: self.story && venue.venue == nil ? {
goToVenue(venue)
} : nil, header: header)
}
}
@ -86,12 +92,12 @@ struct LocationSearchContainerTransition {
let isEmpty: Bool
}
private func locationSearchContainerPreparedTransition(from fromEntries: [LocationSearchEntry], to toEntries: [LocationSearchEntry], query: String, isSearching: Bool, isEmpty: Bool, engine: TelegramEngine, presentationData: PresentationData, sendVenue: @escaping (TelegramMediaMap, Int64?, String?) -> Void) -> LocationSearchContainerTransition {
private func locationSearchContainerPreparedTransition(from fromEntries: [LocationSearchEntry], to toEntries: [LocationSearchEntry], query: String, isSearching: Bool, isEmpty: Bool, engine: TelegramEngine, presentationData: PresentationData, sendVenue: @escaping (TelegramMediaMap, Int64?, String?) -> Void, goToVenue: @escaping (TelegramMediaMap) -> Void) -> LocationSearchContainerTransition {
let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries)
let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) }
let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(engine: engine, presentationData: presentationData, sendVenue: sendVenue), directionHint: nil) }
let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(engine: engine, presentationData: presentationData, sendVenue: sendVenue), directionHint: nil) }
let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(engine: engine, presentationData: presentationData, sendVenue: sendVenue, goToVenue: goToVenue), directionHint: nil) }
let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(engine: engine, presentationData: presentationData, sendVenue: sendVenue, goToVenue: goToVenue), directionHint: nil) }
return LocationSearchContainerTransition(deletions: deletions, insertions: insertions, updates: updates, query: query, isSearching: isSearching, isEmpty: isEmpty)
}
@ -99,6 +105,7 @@ private func locationSearchContainerPreparedTransition(from fromEntries: [Locati
final class LocationSearchContainerNode: ASDisplayNode {
private let context: AccountContext
private let interaction: LocationPickerInteraction
private let story: Bool
private let dimNode: ASDisplayNode
public let listNode: ListView
@ -122,6 +129,7 @@ final class LocationSearchContainerNode: ASDisplayNode {
public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, coordinate: CLLocationCoordinate2D, interaction: LocationPickerInteraction, story: Bool) {
self.context = context
self.interaction = interaction
self.story = story
let presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 }
self.presentationData = presentationData
@ -162,6 +170,8 @@ final class LocationSearchContainerNode: ASDisplayNode {
let currentLocation = CLLocation(latitude: coordinate.latitude, longitude: coordinate.longitude)
let themeAndStringsPromise = self.themeAndStringsPromise
let locale = localeWithStrings(presentationData.strings)
let isSearching = self._isSearching
let searchItems = self.searchQuery.get()
|> mapToSignal { query -> Signal<String?, NoError> in
@ -178,7 +188,6 @@ final class LocationSearchContainerNode: ASDisplayNode {
|> afterCompleted {
isSearching.set(false)
}
let locale = localeWithStrings(presentationData.strings)
let foundPlacemarks = geocodeLocation(address: query, locale: locale)
return combineLatest(foundVenues, foundPlacemarks, themeAndStringsPromise.get())
|> delay(0.1, queue: Queue.concurrentDefaultQueue())
@ -194,9 +203,13 @@ final class LocationSearchContainerNode: ASDisplayNode {
guard let placemarkLocation = placemark.location else {
continue
}
let location = TelegramMediaMap(latitude: placemarkLocation.coordinate.latitude, longitude: placemarkLocation.coordinate.longitude, heading: nil, accuracyRadius: nil, venue: nil, liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil)
var address: MapGeoAddress?
if let countryCode = placemark.isoCountryCode, placemark.thoroughfare == nil {
address = MapGeoAddress(country: countryCode, state: placemark.administrativeArea, city: placemark.locality, street: nil)
}
let location = TelegramMediaMap(latitude: placemarkLocation.coordinate.latitude, longitude: placemarkLocation.coordinate.longitude, heading: nil, accuracyRadius: nil, venue: nil, address: address, liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil)
entries.append(LocationSearchEntry(index: index, theme: themeAndStrings.0, location: location, queryId: nil, resultId: nil, title: placemark.name ?? "Name", distance: placemarkLocation.distance(from: currentLocation)))
entries.append(LocationSearchEntry(index: index, theme: themeAndStrings.0, location: location, queryId: nil, resultId: nil, title: placemark.name ?? "Name", distance: placemarkLocation.distance(from: currentLocation), story: story))
index += 1
}
@ -207,7 +220,7 @@ final class LocationSearchContainerNode: ASDisplayNode {
switch result.message {
case let .mapLocation(mapMedia, _):
if let _ = mapMedia.venue {
entries.append(LocationSearchEntry(index: index, theme: themeAndStrings.0, location: mapMedia, queryId: contextResult.queryId, resultId: result.id, title: nil, distance: 0.0))
entries.append(LocationSearchEntry(index: index, theme: themeAndStrings.0, location: mapMedia, queryId: contextResult.queryId, resultId: result.id, title: nil, distance: 0.0, story: story))
index += 1
}
default:
@ -235,10 +248,21 @@ final class LocationSearchContainerNode: ASDisplayNode {
self?.listNode.clearHighlightAnimated(true)
if let _ = venue.venue {
self?.interaction.sendVenue(venue, queryId, resultId)
} else if story, let address = venue.address {
let name: String
if let city = address.city {
name = city
} else {
name = displayCountryName(address.country, locale: locale)
}
self?.interaction.sendLocation(venue.coordinate, name, address)
} else {
self?.interaction.goToCoordinate(venue.coordinate)
self?.interaction.goToCoordinate(venue.coordinate, false)
self?.interaction.dismissSearch()
}
}, goToVenue: { venue in
self?.interaction.goToCoordinate(venue.coordinate, true)
self?.interaction.dismissSearch()
})
strongSelf.enqueueTransition(transition)
}

View File

@ -50,6 +50,7 @@ swift_library(
"//submodules/ChatSendMessageActionUI",
"//submodules/ComponentFlow",
"//submodules/Components/ComponentDisplayAdapters",
"//submodules/AnimatedCountLabelNode",
],
visibility = [
"//visibility:public",

View File

@ -26,6 +26,7 @@ import CameraScreen
import MediaEditor
import ImageObjectSeparation
import ChatSendMessageActionUI
import AnimatedCountLabelNode
final class MediaPickerInteraction {
let downloadManager: AssetDownloadManager
@ -193,7 +194,9 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
private let saveEditedPhotos: Bool
private let titleView: MediaPickerTitleView
private let cancelButtonNode: WebAppCancelButtonNode
private let moreButtonNode: MoreButtonNode
private let selectedButtonNode: SelectedButtonNode
public weak var webSearchController: WebSearchController?
@ -227,6 +230,11 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
public var getCurrentSendMessageContextMediaPreview: (() -> ChatSendMessageContextScreenMediaPreview?)? = nil
private let selectedCollection = Promise<PHAssetCollection?>(nil)
private var selectedCollectionValue: PHAssetCollection? {
didSet {
self.selectedCollection.set(.single(self.selectedCollectionValue))
}
}
var dismissAll: () -> Void = { }
@ -935,8 +943,6 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
}
}
var previousEntries = self.currentEntries
if self.resetOnUpdate {
@ -992,7 +998,13 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
self.backgroundNode.updateColor(color: self.presentationData.theme.rootController.tabBar.backgroundColor, transition: .immediate)
}
private var currentDisplayMode: DisplayMode = .all
private(set) var currentDisplayMode: DisplayMode = .all {
didSet {
self.displayModeUpdated(self.currentDisplayMode)
}
}
var displayModeUpdated: (DisplayMode) -> Void = { _ in }
func updateDisplayMode(_ displayMode: DisplayMode, animated: Bool = true) {
let updated = self.currentDisplayMode != displayMode
self.currentDisplayMode = displayMode
@ -1803,9 +1815,13 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
self.titleView.title = presentationData.strings.Attachment_Gallery
}
self.cancelButtonNode = WebAppCancelButtonNode(theme: self.presentationData.theme, strings: self.presentationData.strings)
self.moreButtonNode = MoreButtonNode(theme: self.presentationData.theme)
self.moreButtonNode.iconNode.enqueueState(.more, animated: false)
self.selectedButtonNode = SelectedButtonNode(theme: self.presentationData.theme)
super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationData: presentationData))
self.statusBar.statusBarStyle = .Ignore
@ -1906,7 +1922,11 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
if case let .assets(collection, _) = self.subject, collection != nil {
self.navigationItem.leftBarButtonItem = UIBarButtonItem(backButtonAppearanceWithTitle: self.presentationData.strings.Common_Back, target: self, action: #selector(self.backPressed))
} else {
self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(self.cancelPressed))
self.navigationItem.leftBarButtonItem = UIBarButtonItem(customDisplayNode: self.cancelButtonNode)
self.navigationItem.leftBarButtonItem?.action = #selector(self.cancelPressed)
self.navigationItem.leftBarButtonItem?.target = self
// self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(self.cancelPressed))
}
if self.bannedSendPhotos != nil && self.bannedSendVideos != nil {
@ -1923,6 +1943,8 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
}
}
self.selectedButtonNode.addTarget(self, action: #selector(self.selectedPressed), forControlEvents: .touchUpInside)
self.scrollToTop = { [weak self] in
if let strongSelf = self {
if let webSearchController = strongSelf.webSearchController {
@ -2050,6 +2072,13 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
self._ready.set(self.controllerNode.ready.get())
self.controllerNode.displayModeUpdated = { [weak self] _ in
guard let self else {
return
}
let count = Int32(self.interaction?.selectionState?.count() ?? 0)
self.updateSelectionState(count: count)
}
if case .media = self.subject {
self.controllerNode.updateDisplayMode(.selected, animated: false)
}
@ -2086,10 +2115,10 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
}
self.controllerNode.resetOnUpdate = true
if collection.assetCollectionSubtype == .smartAlbumUserLibrary {
self.selectedCollection.set(.single(nil))
self.selectedCollectionValue = nil
self.titleView.title = self.presentationData.strings.MediaPicker_Recents
} else {
self.selectedCollection.set(.single(collection))
self.selectedCollectionValue = collection
self.titleView.title = collection.localizedTitle ?? ""
}
self.scrollToTop?()
@ -2211,6 +2240,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
fileprivate func updateSelectionState(count: Int32) {
self.selectionCount = count
let transition = ContainedViewLayoutTransition.animated(duration: 0.25, curve: .easeInOut)
var moreIsVisible = false
if case let .assets(_, mode) = self.subject, [.story, .createSticker].contains(mode) {
moreIsVisible = true
@ -2220,25 +2250,32 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
moreIsVisible = true
// self.moreButtonNode.iconNode.enqueueState(.more, animated: false)
} else {
if count > 0 {
self.titleView.segments = [self.presentationData.strings.Attachment_AllMedia, self.presentationData.strings.Attachment_SelectedMedia(count)]
self.titleView.segmentsHidden = false
moreIsVisible = true
// self.moreButtonNode.iconNode.enqueueState(.more, animated: true)
let title: String
let isEnabled: Bool
if self.controllerNode.currentDisplayMode == .selected {
title = self.presentationData.strings.Attachment_SelectedMedia(count)
isEnabled = false
} else {
self.titleView.segmentsHidden = true
moreIsVisible = false
// self.moreButtonNode.iconNode.enqueueState(.search, animated: true)
if self.titleView.index != 0 {
Queue.mainQueue().after(0.3) {
self.titleView.index = 0
}
}
title = self.selectedCollectionValue?.localizedTitle ?? self.presentationData.strings.MediaPicker_Recents
isEnabled = true
}
self.titleView.updateTitle(title: title, isEnabled: isEnabled, animated: true)
self.cancelButtonNode.setState(isEnabled ? .cancel : .back, animated: true)
let isSelectionButtonVisible = count > 0 && self.controllerNode.currentDisplayMode == .all
transition.updateAlpha(node: self.selectedButtonNode, alpha: isSelectionButtonVisible ? 1.0 : 0.0)
transition.updateTransformScale(node: self.selectedButtonNode, scale: isSelectionButtonVisible ? 1.0 : 0.01)
let selectedSize = self.selectedButtonNode.update(count: count)
if self.selectedButtonNode.supernode == nil {
self.navigationBar?.addSubnode(self.selectedButtonNode)
}
self.selectedButtonNode.frame = CGRect(origin: CGPoint(x: self.view.bounds.width - 54.0 - selectedSize.width, y: 18.0 + UIScreenPixel), size: selectedSize)
self.titleView.segmentsHidden = true
moreIsVisible = count > 0
}
let transition = ContainedViewLayoutTransition.animated(duration: 0.25, curve: .easeInOut)
transition.updateAlpha(node: self.moreButtonNode.iconNode, alpha: moreIsVisible ? 1.0 : 0.0)
transition.updateTransformScale(node: self.moreButtonNode.iconNode, scale: moreIsVisible ? 1.0 : 0.1)
}
@ -2246,7 +2283,9 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
private func updateThemeAndStrings() {
self.navigationBar?.updatePresentationData(NavigationBarPresentationData(presentationData: self.presentationData))
self.titleView.theme = self.presentationData.theme
self.cancelButtonNode.theme = self.presentationData.theme
self.moreButtonNode.theme = self.presentationData.theme
self.selectedButtonNode.theme = self.presentationData.theme
self.controllerNode.updatePresentationData(self.presentationData)
}
@ -2304,13 +2343,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
return true
}
}
@objc private func cancelPressed() {
self.dismissAllTooltips()
self.dismiss()
}
public override func dismiss(completion: (() -> Void)? = nil) {
self.controllerNode.cancelAssetDownloads()
@ -2408,6 +2441,19 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
self.groupsController = groupsController
}
@objc private func cancelPressed() {
self.dismissAllTooltips()
if case .back = self.cancelButtonNode.state {
self.controllerNode.updateDisplayMode(.all)
} else {
self.dismiss()
}
}
@objc private func selectedPressed() {
self.controllerNode.updateDisplayMode(.selected, animated: true)
}
@objc private func searchOrMorePressed(node: ContextReferenceContentNode, gesture: ContextGesture?) {
guard self.moreButtonNode.iconNode.alpha > 0.0 else {
return
@ -3132,3 +3178,65 @@ public func stickerMediaPickerController(
controller.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait)
return controller
}
private class SelectedButtonNode: HighlightableButtonNode {
private let background = ASImageNode()
private let icon = ASImageNode()
private let label = ImmediateAnimatedCountLabelNode()
var theme: PresentationTheme {
didSet {
self.background.image = generateStretchableFilledCircleImage(radius: 21.0 / 2.0, color: self.theme.list.itemCheckColors.fillColor)
let _ = self.update(count: self.count)
}
}
private var count: Int32 = 0
init(theme: PresentationTheme) {
self.theme = theme
super.init()
self.background.displaysAsynchronously = false
self.icon.displaysAsynchronously = false
self.label.displaysAsynchronously = false
self.icon.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: .white)
self.background.image = generateStretchableFilledCircleImage(radius: 21.0 / 2.0, color: self.theme.list.itemCheckColors.fillColor)
self.addSubnode(self.background)
self.addSubnode(self.icon)
self.addSubnode(self.label)
}
func update(count: Int32) -> CGSize {
self.count = count
let diameter: CGFloat = 21.0
let font = Font.with(size: 15.0, design: .round, weight: .semibold, traits: [.monospacedNumbers])
let stringValue = "\(max(1, count))"
var segments: [AnimatedCountLabelNode.Segment] = []
for char in stringValue {
if let intValue = Int(String(char)) {
segments.append(.number(intValue, NSAttributedString(string: String(char), font: font, textColor: self.theme.list.itemCheckColors.foregroundColor)))
}
}
self.label.segments = segments
let textSize = self.label.updateLayout(size: CGSize(width: 100.0, height: diameter), animated: true)
let size = CGSize(width: textSize.width + 28.0, height: diameter)
if let _ = self.icon.image {
let iconSize = CGSize(width: 22.0, height: 22.0)
let iconFrame = CGRect(origin: CGPoint(x: 0.0, y: floor((size.height - iconSize.height) / 2.0)), size: iconSize)
self.icon.frame = iconFrame
}
self.label.frame = CGRect(origin: CGPoint(x: 21.0, y: floor((size.height - textSize.height) / 2.0) - UIScreenPixel), size: textSize)
self.background.frame = CGRect(origin: .zero, size: size)
return size
}
}

View File

@ -36,6 +36,35 @@ final class MediaPickerTitleView: UIView {
}
}
public func updateTitle(title: String, isEnabled: Bool, animated: Bool) {
if animated {
if self.title != title {
if let snapshotView = self.titleNode.view.snapshotContentTree() {
snapshotView.frame = self.titleNode.frame
self.addSubview(snapshotView)
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in
snapshotView.removeFromSuperview()
})
self.titleNode.view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
}
if self.isEnabled != isEnabled {
if let snapshotView = self.arrowNode.view.snapshotContentTree() {
snapshotView.frame = self.arrowNode.frame
self.addSubview(snapshotView)
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in
snapshotView.removeFromSuperview()
})
self.arrowNode.view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
}
}
self.title = title
self.isEnabled = isEnabled
}
public var isHighlighted: Bool = false {
didSet {
self.alpha = self.isHighlighted ? 0.5 : 1.0
@ -45,7 +74,7 @@ final class MediaPickerTitleView: UIView {
public var segmentsHidden = true {
didSet {
if self.segmentsHidden != oldValue {
let transition = ContainedViewLayoutTransition.animated(duration: 0.21, curve: .easeInOut)
let transition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut)
transition.updateAlpha(node: self.titleNode, alpha: self.segmentsHidden ? 1.0 : 0.0)
transition.updateAlpha(node: self.arrowNode, alpha: self.segmentsHidden ? 1.0 : 0.0)
transition.updateAlpha(node: self.segmentedControlNode, alpha: self.segmentsHidden ? 0.0 : 1.0)

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -32,6 +32,8 @@ extension PremiumGiftSource {
return "attach"
case .settings:
return "settings"
case .stars:
return ""
case .chatList:
return "chats"
case .channelBoost:
@ -241,7 +243,6 @@ private final class PremiumGiftScreenContentComponent: CombinedComponent {
}
names.append("**\(context.component.peers[i].compactDisplayTitle)**")
}
descriptionString = strings.Premium_Gift_MultipleDescription(names, "").string
} else {
for i in 0 ..< min(3, context.component.peers.count) {
if i == 0 {

View File

@ -2038,9 +2038,10 @@ public func channelStatsController(context: AccountContext, updatedPresentationD
peer.set(context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)))
let peerData = context.engine.data.get(
TelegramEngine.EngineData.Item.Peer.AdsRestricted(id: peerId),
TelegramEngine.EngineData.Item.Peer.CanViewRevenue(id: peerId),
TelegramEngine.EngineData.Item.Peer.CanViewStarsRevenue(id: peerId)
TelegramEngine.EngineData.Item.Peer.CanViewStats(id: peerId),
TelegramEngine.EngineData.Item.Peer.AdsRestricted(id: peerId),
TelegramEngine.EngineData.Item.Peer.CanViewRevenue(id: peerId),
TelegramEngine.EngineData.Item.Peer.CanViewStarsRevenue(id: peerId)
)
let longLoadingSignal: Signal<Bool, NoError> = .single(false) |> then(.single(true) |> delay(2.0, queue: Queue.mainQueue()))
@ -2066,7 +2067,7 @@ public func channelStatsController(context: AccountContext, updatedPresentationD
)
|> deliverOnMainQueue
|> map { presentationData, state, peer, data, messageView, stories, boostData, boostersState, giftsState, revenueState, revenueTransactions, starsState, starsTransactions, peerData, longLoading -> (ItemListControllerState, (ItemListNodeState, Any)) in
let (adsRestricted, canViewRevenue, canViewStarsRevenue) = peerData
let (canViewStats, adsRestricted, canViewRevenue, canViewStarsRevenue) = peerData
var isGroup = false
if let peer, case let .channel(channel) = peer, case .group = channel.info {
@ -2149,7 +2150,9 @@ public func channelStatsController(context: AccountContext, updatedPresentationD
index = 2
}
var tabs: [String] = []
tabs.append(presentationData.strings.Stats_Statistics)
if canViewStats {
tabs.append(presentationData.strings.Stats_Statistics)
}
tabs.append(presentationData.strings.Stats_Boosts)
if canViewRevenue || canViewStarsRevenue {
tabs.append(presentationData.strings.Stats_Monetization)

View File

@ -4,7 +4,7 @@
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
NSData * _Nullable prepareSvgImage(NSData * _Nonnull data);
NSData * _Nullable prepareSvgImage(NSData * _Nonnull data, bool pattern);
UIImage * _Nullable renderPreparedImage(NSData * _Nonnull data, CGSize size, UIColor * _Nonnull backgroundColor, CGFloat scale, bool fit);
UIImage * _Nullable drawSvgImage(NSData * _Nonnull data, CGSize size, UIColor * _Nullable backgroundColor, UIColor * _Nullable foregroundColor, bool opaque);

View File

@ -361,14 +361,26 @@ UIImage * _Nullable drawSvgImage(NSData * _Nonnull data, CGSize size, UIColor *b
[_data appendBytes:&command length:sizeof(command)];
}
- (void)setFillColor:(uint32_t)color opacity:(CGFloat)opacity {
uint8_t command = 11;
[_data appendBytes:&command length:sizeof(command)];
color = ((uint32_t)(opacity * 255.0) << 24) | color;
[_data appendBytes:&color length:sizeof(color)];
}
@end
UIColor *colorWithBGRA(uint32_t bgra)
{
return [[UIColor alloc] initWithRed:(((bgra) & 0xff) / 255.0f) green:(((bgra >> 8) & 0xff) / 255.0f) blue:(((bgra >> 16) & 0xff) / 255.0f) alpha:(((bgra >> 24) & 0xff) / 255.0f)];
}
UIImage * _Nullable renderPreparedImage(NSData * _Nonnull data, CGSize size, UIColor *backgroundColor, CGFloat scale, bool fit) {
NSDate *startTime = [NSDate date];
UIColor *foregroundColor = [UIColor whiteColor];
int32_t ptr = 0;
int32_t width;
int32_t height;
@ -544,7 +556,15 @@ UIImage * _Nullable renderPreparedImage(NSData * _Nonnull data, CGSize size, UIC
CGContextStrokePath(context);
}
break;
case 11:
{
uint32_t bgra;
[data getBytes:&bgra range:NSMakeRange(ptr, sizeof(bgra))];
ptr += sizeof(bgra);
CGContextSetFillColorWithColor(context, colorWithBGRA(bgra).CGColor);
CGContextStrokePath(context);
}
default:
break;
}
@ -559,7 +579,7 @@ UIImage * _Nullable renderPreparedImage(NSData * _Nonnull data, CGSize size, UIC
return resultImage;
}
NSData * _Nullable prepareSvgImage(NSData * _Nonnull data) {
NSData * _Nullable prepareSvgImage(NSData * _Nonnull data, bool template) {
NSDate *startTime = [NSDate date];
NSXMLParser *parser = [[NSXMLParser alloc] initWithData:data];
@ -600,8 +620,12 @@ NSData * _Nullable prepareSvgImage(NSData * _Nonnull data) {
}
if (shape->fill.type != NSVG_PAINT_NONE) {
[context setFillColorWithOpacity:shape->opacity];
if (template) {
[context setFillColorWithOpacity:shape->opacity];
} else {
[context setFillColor:shape->fill.color opacity:shape->opacity];
}
bool isFirst = true;
bool hasStartPoint = false;
CGPoint startPoint;

View File

@ -4,11 +4,54 @@ import Postbox
import TelegramApi
import MtProtoKit
public struct RevenueStats: Equatable {
public struct Balances: Equatable {
public struct RevenueStats: Equatable, Codable {
private enum CodingKeys: String, CodingKey {
case topHoursGraph
case revenueGraph
case balances
case usdRate
}
static func key(peerId: PeerId) -> ValueBoxKey {
let key = ValueBoxKey(length: 8 + 4)
key.setInt64(0, value: peerId.toInt64())
return key
}
public struct Balances: Equatable, Codable {
private enum CodingKeys: String, CodingKey {
case currentBalance
case availableBalance
case overallRevenue
}
public let currentBalance: Int64
public let availableBalance: Int64
public let overallRevenue: Int64
init(
currentBalance: Int64,
availableBalance: Int64,
overallRevenue: Int64
) {
self.currentBalance = currentBalance
self.availableBalance = availableBalance
self.overallRevenue = overallRevenue
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.currentBalance = try container.decode(Int64.self, forKey: .currentBalance)
self.availableBalance = try container.decode(Int64.self, forKey: .availableBalance)
self.overallRevenue = try container.decode(Int64.self, forKey: .overallRevenue)
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.currentBalance, forKey: .currentBalance)
try container.encode(self.availableBalance, forKey: .availableBalance)
try container.encode(self.overallRevenue, forKey: .overallRevenue)
}
}
public let topHoursGraph: StatsGraph
@ -23,6 +66,22 @@ public struct RevenueStats: Equatable {
self.usdRate = usdRate
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.topHoursGraph = try container.decode(StatsGraph.self, forKey: .topHoursGraph)
self.revenueGraph = try container.decode(StatsGraph.self, forKey: .revenueGraph)
self.balances = try container.decode(Balances.self, forKey: .balances)
self.usdRate = try container.decode(Double.self, forKey: .usdRate)
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.topHoursGraph, forKey: .topHoursGraph)
try container.encode(self.revenueGraph, forKey: .revenueGraph)
try container.encode(self.balances, forKey: .balances)
try container.encode(self.usdRate, forKey: .usdRate)
}
public static func == (lhs: RevenueStats, rhs: RevenueStats) -> Bool {
if lhs.topHoursGraph != rhs.topHoursGraph {
return false
@ -124,6 +183,17 @@ private final class RevenueStatsContextImpl {
self._statePromise.set(.single(self._state))
self.load()
let _ = (account.postbox.transaction { transaction -> RevenueStats? in
return transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedRevenueStats, key: StarsRevenueStats.key(peerId: peerId)))?.get(RevenueStats.self)
}
|> deliverOnMainQueue).start(next: { [weak self] cachedResult in
guard let self, let cachedResult else {
return
}
self._state = RevenueStatsContextState(stats: cachedResult)
self._statePromise.set(.single(self._state))
})
}
deinit {
@ -155,9 +225,17 @@ private final class RevenueStatsContextImpl {
self.disposable.set((signal
|> deliverOnMainQueue).start(next: { [weak self] stats in
if let strongSelf = self {
strongSelf._state = RevenueStatsContextState(stats: stats)
strongSelf._statePromise.set(.single(strongSelf._state))
if let self {
self._state = RevenueStatsContextState(stats: stats)
self._statePromise.set(.single(self._state))
if let stats {
let _ = (self.account.postbox.transaction { transaction in
if let entry = CodableEntry(stats) {
transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedRevenueStats, key: StarsRevenueStats.key(peerId: peerId)), entry: entry)
}
}).start()
}
}
}))
}

View File

@ -127,6 +127,7 @@ public struct Namespaces {
public static let applicationIcons: Int8 = 36
public static let availableMessageEffects: Int8 = 37
public static let cachedStarsRevenueStats: Int8 = 38
public static let cachedRevenueStats: Int8 = 39
}
public struct UnorderedItemList {

View File

@ -82,6 +82,7 @@ public struct PresentationResourcesSettings {
public static let business = renderIcon(name: "Settings/Menu/Business", backgroundColors: [UIColor(rgb: 0xA95CE3), UIColor(rgb: 0xF16B80)])
public static let myProfile = renderIcon(name: "Settings/Menu/Profile")
public static let reactions = renderIcon(name: "Settings/Menu/Reactions")
public static let balance = renderIcon(name: "Settings/Menu/Balance", scaleFactor: 0.97, backgroundColors: [UIColor(rgb: 0x34c759)])
public static let premium = generateImage(CGSize(width: 29.0, height: 29.0), contextGenerator: { size, context in
let bounds = CGRect(origin: CGPoint(), size: size)

View File

@ -745,6 +745,23 @@ public func universalServiceMessageString(presentationData: (PresentationTheme,
attributes[1] = boldAttributes
attributedString = addAttributesToStringWithRanges(strings.Notification_PremiumGift_Sent(compactAuthorName, price)._tuple, body: bodyAttributes, argumentAttributes: attributes)
}
case let .giftStars(currency, amount, count, _, _, _):
let _ = count
let price = formatCurrencyAmount(amount, currency: currency)
if message.author?.id == accountPeerId {
attributedString = addAttributesToStringWithRanges(strings.Notification_StarsGift_SentYou(price)._tuple, body: bodyAttributes, argumentAttributes: [0: boldAttributes])
} else {
//TODO:localize
var authorName = compactAuthorName
var peerIds: [(Int, EnginePeer.Id?)] = [(0, message.author?.id)]
if message.id.peerId.namespace == Namespaces.Peer.CloudUser && message.id.peerId.id._internalGetInt64Value() == 777000 {
authorName = "Unknown user"
peerIds = []
}
var attributes = peerMentionsAttributes(primaryTextColor: primaryTextColor, peerIds: peerIds)
attributes[1] = boldAttributes
attributedString = addAttributesToStringWithRanges(strings.Notification_StarsGift_Sent(authorName, price)._tuple, body: bodyAttributes, argumentAttributes: attributes)
}
case let .topicCreated(title, iconColor, iconFileId):
if forForumOverview {
let maybeFileId = iconFileId ?? 0
@ -992,6 +1009,39 @@ public func universalServiceMessageString(presentationData: (PresentationTheme,
attributedString = addAttributesToStringWithRanges(resultTitleString._tuple, body: bodyAttributes, argumentAttributes: [0: boldAttributes])
}
}
case let .paymentRefunded(peerId, currency, totalAmount, _, _):
//TODO:localize
let patternString: String
if peerId == message.id.peerId {
patternString = "You received a refund of {amount}"
} else {
patternString = "You received a refund of {amount} from {name}"
}
let mutableString = NSMutableAttributedString()
mutableString.append(NSAttributedString(string: patternString, font: titleFont, textColor: primaryTextColor))
var range = NSRange(location: NSNotFound, length: 0)
range = (mutableString.string as NSString).range(of: "{amount}")
if range.location != NSNotFound {
if currency == "XTR" {
let amountAttributedString = NSMutableAttributedString(string: "#\(totalAmount)", font: titleBoldFont, textColor: primaryTextColor)
if let range = amountAttributedString.string.range(of: "#") {
amountAttributedString.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .stars(tinted: true)), range: NSRange(range, in: amountAttributedString.string))
amountAttributedString.addAttribute(.baselineOffset, value: 1.5, range: NSRange(range, in: amountAttributedString.string))
}
mutableString.replaceCharacters(in: range, with: amountAttributedString)
} else {
mutableString.replaceCharacters(in: range, with: NSAttributedString(string: formatCurrencyAmount(totalAmount, currency: currency), font: titleBoldFont, textColor: primaryTextColor))
}
}
range = (mutableString.string as NSString).range(of: "{name}")
if range.location != NSNotFound {
let peerName = message.peers[peerId].flatMap { EnginePeer($0) }?.compactDisplayTitle ?? ""
mutableString.replaceCharacters(in: range, with: NSAttributedString(string: peerName, font: titleBoldFont, textColor: primaryTextColor))
mutableString.addAttribute(NSAttributedString.Key(TelegramTextAttributes.PeerMention), value: TelegramPeerMention(peerId: peerId, mention: ""), range: NSMakeRange(range.location, (peerName as NSString).length))
}
attributedString = mutableString
case .unknown:
attributedString = nil
}

View File

@ -30,6 +30,7 @@ swift_library(
"//submodules/UndoUI",
"//submodules/TelegramUI/Components/ListSectionComponent",
"//submodules/TelegramUI/Components/ListActionItemComponent",
"//submodules/TelegramUI/Components/NavigationStackComponent",
],
visibility = [
"//visibility:public",

View File

@ -13,6 +13,7 @@ import BalancedTextComponent
import MultilineTextComponent
import ListSectionComponent
import ListActionItemComponent
import NavigationStackComponent
import ItemListUI
import UndoUI
import AccountContext
@ -655,297 +656,3 @@ public final class AdsReportScreen: ViewControllerComponentContainer {
}
}
}
private final class NavigationContainer: UIView, UIGestureRecognizerDelegate {
var requestUpdate: ((ComponentTransition) -> Void)?
var requestPop: (() -> Void)?
var transitionFraction: CGFloat = 0.0
private var panRecognizer: InteractiveTransitionGestureRecognizer?
var isNavigationEnabled: Bool = false {
didSet {
self.panRecognizer?.isEnabled = self.isNavigationEnabled
}
}
init() {
super.init(frame: .zero)
let panRecognizer = InteractiveTransitionGestureRecognizer(target: self, action: #selector(self.panGesture(_:)), allowedDirections: { [weak self] point in
guard let strongSelf = self else {
return []
}
let _ = strongSelf
return [.right]
})
panRecognizer.delegate = self
self.addGestureRecognizer(panRecognizer)
self.panRecognizer = panRecognizer
}
required public init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return false
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer) -> Bool {
if let _ = otherGestureRecognizer as? InteractiveTransitionGestureRecognizer {
return false
}
if let _ = otherGestureRecognizer as? UIPanGestureRecognizer {
return true
}
return false
}
@objc private func panGesture(_ recognizer: UIPanGestureRecognizer) {
switch recognizer.state {
case .began:
self.transitionFraction = 0.0
case .changed:
let distanceFactor: CGFloat = recognizer.translation(in: self).x / self.bounds.width
let transitionFraction = max(0.0, min(1.0, distanceFactor))
if self.transitionFraction != transitionFraction {
self.transitionFraction = transitionFraction
self.requestUpdate?(.immediate)
}
case .ended, .cancelled:
let distanceFactor: CGFloat = recognizer.translation(in: self).x / self.bounds.width
let transitionFraction = max(0.0, min(1.0, distanceFactor))
if transitionFraction > 0.2 {
self.transitionFraction = 0.0
self.requestPop?()
} else {
self.transitionFraction = 0.0
self.requestUpdate?(.spring(duration: 0.45))
}
default:
break
}
}
}
final class NavigationStackComponent<ChildEnvironment: Equatable>: Component {
public let items: [AnyComponentWithIdentity<ChildEnvironment>]
public let requestPop: () -> Void
public init(
items: [AnyComponentWithIdentity<ChildEnvironment>],
requestPop: @escaping () -> Void
) {
self.items = items
self.requestPop = requestPop
}
public static func ==(lhs: NavigationStackComponent, rhs: NavigationStackComponent) -> Bool {
if lhs.items != rhs.items {
return false
}
return true
}
private final class ItemView: UIView {
let contents = ComponentView<ChildEnvironment>()
let dimView = UIView()
override init(frame: CGRect) {
super.init(frame: frame)
self.dimView.alpha = 0.0
self.dimView.backgroundColor = UIColor.black.withAlphaComponent(0.2)
self.dimView.isUserInteractionEnabled = false
self.addSubview(self.dimView)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
private struct ReadyItem {
var index: Int
var itemId: AnyHashable
var itemView: ItemView
var itemTransition: ComponentTransition
var itemSize: CGSize
init(index: Int, itemId: AnyHashable, itemView: ItemView, itemTransition: ComponentTransition, itemSize: CGSize) {
self.index = index
self.itemId = itemId
self.itemView = itemView
self.itemTransition = itemTransition
self.itemSize = itemSize
}
}
public final class View: UIView {
private var itemViews: [AnyHashable: ItemView] = [:]
private let navigationContainer = NavigationContainer()
private var component: NavigationStackComponent?
private var state: EmptyComponentState?
public override init(frame: CGRect) {
super.init(frame: CGRect())
self.addSubview(self.navigationContainer)
self.navigationContainer.requestUpdate = { [weak self] transition in
guard let self else {
return
}
self.state?.updated(transition: transition)
}
self.navigationContainer.requestPop = { [weak self] in
guard let self else {
return
}
self.component?.requestPop()
}
}
required public init?(coder: NSCoder) {
preconditionFailure()
}
func update(component: NavigationStackComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<ChildEnvironment>, transition: ComponentTransition) -> CGSize {
self.component = component
self.state = state
let navigationTransitionFraction = self.navigationContainer.transitionFraction
self.navigationContainer.isNavigationEnabled = component.items.count > 1
var validItemIds: [AnyHashable] = []
var readyItems: [ReadyItem] = []
for i in 0 ..< component.items.count {
let item = component.items[i]
let itemId = item.id
validItemIds.append(itemId)
let itemView: ItemView
var itemTransition = transition
if let current = self.itemViews[itemId] {
itemView = current
} else {
itemTransition = itemTransition.withAnimation(.none)
itemView = ItemView()
self.itemViews[itemId] = itemView
itemView.contents.parentState = state
}
let itemSize = itemView.contents.update(
transition: itemTransition,
component: item.component,
environment: { environment[ChildEnvironment.self] },
containerSize: CGSize(width: availableSize.width, height: availableSize.height)
)
readyItems.append(ReadyItem(
index: i,
itemId: itemId,
itemView: itemView,
itemTransition: itemTransition,
itemSize: itemSize
))
}
let sortedItems = readyItems.sorted(by: { $0.index < $1.index })
for readyItem in sortedItems {
let transitionFraction: CGFloat
let alphaTransitionFraction: CGFloat
if readyItem.index == readyItems.count - 1 {
transitionFraction = navigationTransitionFraction
alphaTransitionFraction = 1.0
} else if readyItem.index == readyItems.count - 2 {
transitionFraction = navigationTransitionFraction - 1.0
alphaTransitionFraction = navigationTransitionFraction
} else {
transitionFraction = 0.0
alphaTransitionFraction = 0.0
}
let transitionOffset: CGFloat
if readyItem.index == readyItems.count - 1 {
transitionOffset = readyItem.itemSize.width * transitionFraction
} else {
transitionOffset = readyItem.itemSize.width / 3.0 * transitionFraction
}
let itemFrame = CGRect(origin: CGPoint(x: transitionOffset, y: 0.0), size: readyItem.itemSize)
let itemBounds = CGRect(origin: .zero, size: itemFrame.size)
if let itemComponentView = readyItem.itemView.contents.view {
var isAdded = false
if itemComponentView.superview == nil {
isAdded = true
readyItem.itemView.insertSubview(itemComponentView, at: 0)
self.navigationContainer.addSubview(readyItem.itemView)
}
readyItem.itemTransition.setFrame(view: readyItem.itemView, frame: itemFrame)
readyItem.itemTransition.setFrame(view: itemComponentView, frame: itemBounds)
readyItem.itemTransition.setFrame(view: readyItem.itemView.dimView, frame: CGRect(origin: .zero, size: availableSize))
readyItem.itemTransition.setAlpha(view: readyItem.itemView.dimView, alpha: 1.0 - alphaTransitionFraction)
if readyItem.index > 0 && isAdded {
transition.animatePosition(view: itemComponentView, from: CGPoint(x: itemFrame.width, y: 0.0), to: .zero, additive: true, completion: nil)
}
}
}
let lastHeight = sortedItems.last?.itemSize.height ?? 0.0
let previousHeight: CGFloat
if sortedItems.count > 1 {
previousHeight = sortedItems[sortedItems.count - 2].itemSize.height
} else {
previousHeight = lastHeight
}
let contentHeight = lastHeight * (1.0 - navigationTransitionFraction) + previousHeight * navigationTransitionFraction
var removedItemIds: [AnyHashable] = []
for (id, _) in self.itemViews {
if !validItemIds.contains(id) {
removedItemIds.append(id)
}
}
for id in removedItemIds {
guard let itemView = self.itemViews[id] else {
continue
}
if let itemComponeentView = itemView.contents.view {
var position = itemComponeentView.center
position.x += itemComponeentView.bounds.width
transition.setPosition(view: itemComponeentView, position: position, completion: { _ in
itemView.removeFromSuperview()
self.itemViews.removeValue(forKey: id)
})
} else {
itemView.removeFromSuperview()
self.itemViews.removeValue(forKey: id)
}
}
let contentSize = CGSize(width: availableSize.width, height: contentHeight)
self.navigationContainer.frame = CGRect(origin: .zero, size: contentSize)
return contentSize
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<ChildEnvironment>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}

View File

@ -203,6 +203,8 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([
result.append((message, ChatMessageCallBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default)))
} else if case .giftPremium = action.action {
result.append((message, ChatMessageGiftBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default)))
} else if case .giftStars = action.action {
result.append((message, ChatMessageGiftBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default)))
} else if case .suggestedProfilePhoto = action.action {
result.append((message, ChatMessageProfilePhotoSuggestionContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default)))
} else if case .setChatWallpaper = action.action {

View File

@ -235,6 +235,8 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
return (contentProperties, nil, CGFloat.greatestFiniteMagnitude, { constrainedSize, position in
var giftSize = CGSize(width: 220.0, height: 240.0)
let incoming = item.message.effectivelyIncoming(item.context.account.peerId)
let attributedString = attributedServiceMessageString(theme: item.presentationData.theme, strings: item.presentationData.strings, nameDisplayOrder: item.presentationData.nameDisplayOrder, dateTimeFormat: item.presentationData.dateTimeFormat, message: EngineMessage(item.message), accountPeerId: item.context.account.peerId)
let primaryTextColor = serviceMessageColorComponents(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper).primaryText
@ -252,6 +254,14 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
case let .giftPremium(_, _, monthsValue, _, _):
months = monthsValue
text = item.presentationData.strings.Notification_PremiumGift_Subtitle(item.presentationData.strings.Notification_PremiumGift_Months(months)).string
case let .giftStars(_, _, count, _, _, _):
months = 6
var peerName = ""
if let peer = item.message.peers[item.message.id.peerId] {
peerName = EnginePeer(peer).compactDisplayTitle
}
title = item.presentationData.strings.Notification_StarsGift_Title(Int32(count))
text = incoming ? item.presentationData.strings.Notification_StarsGift_Subtitle : item.presentationData.strings.Notification_StarsGift_SubtitleYou(peerName).string
case let .giftCode(_, fromGiveaway, unclaimed, channelId, monthsValue, _, _, _, _):
if channelId == nil {
months = monthsValue

View File

@ -137,15 +137,22 @@ private final class BalanceComponent: CombinedComponent {
}
private final class BadgeComponent: Component {
enum Direction {
case left
case right
}
let theme: PresentationTheme
let title: String
let inertiaDirection: Direction?
init(
theme: PresentationTheme,
title: String
title: String,
inertiaDirection: Direction?
) {
self.theme = theme
self.title = title
self.inertiaDirection = inertiaDirection
}
static func ==(lhs: BadgeComponent, rhs: BadgeComponent) -> Bool {
@ -155,6 +162,9 @@ private final class BadgeComponent: Component {
if lhs.title != rhs.title {
return false
}
if lhs.inertiaDirection != rhs.inertiaDirection {
return false
}
return true
}
@ -174,6 +184,7 @@ private final class BadgeComponent: Component {
private var component: BadgeComponent?
private var previousAvailableSize: CGSize?
private var previousInertiaDirection: BadgeComponent.Direction?
override init(frame: CGRect) {
self.badgeView = UIView()
@ -225,9 +236,8 @@ private final class BadgeComponent: Component {
required init(coder: NSCoder) {
preconditionFailure()
}
func update(component: BadgeComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
if self.component == nil {
self.badgeIcon.image = UIImage(bundleImageName: "Premium/SendStarsStarSliderIcon")?.withRenderingMode(.alwaysTemplate)
}
@ -237,23 +247,8 @@ private final class BadgeComponent: Component {
self.badgeLabel.color = .white
let countWidth: CGFloat
switch component.title.count {
case 1:
countWidth = 20.0
case 2:
countWidth = 35.0
case 3:
countWidth = 51.0
case 4:
countWidth = 60.0
case 5:
countWidth = 74.0
case 6:
countWidth = 88.0
default:
countWidth = 51.0
}
let badgeLabelSize = self.badgeLabel.update(value: component.title, transition: .easeInOut(duration: 0.12))
let countWidth: CGFloat = badgeLabelSize.width + 3.0
let badgeWidth: CGFloat = countWidth + 54.0
let badgeSize = CGSize(width: badgeWidth, height: 48.0)
@ -265,6 +260,25 @@ private final class BadgeComponent: Component {
transition.setAnchorPoint(layer: self.badgeView.layer, anchorPoint: CGPoint(x: 0.5, y: 1.0))
if component.inertiaDirection != self.previousInertiaDirection {
self.previousInertiaDirection = component.inertiaDirection
var angle: CGFloat = 0.0
let transition: ContainedViewLayoutTransition
if let inertiaDirection = component.inertiaDirection {
switch inertiaDirection {
case .left:
angle = 0.22
case .right:
angle = -0.22
}
transition = .animated(duration: 0.45, curve: .spring)
} else {
transition = .animated(duration: 0.45, curve: .customSpring(damping: 65.0, initialVelocity: 0.0))
}
transition.updateTransformRotation(view: self.badgeView, angle: angle)
}
self.badgeForeground.bounds = CGRect(origin: CGPoint(), size: CGSize(width: badgeFullSize.width * 3.0, height: badgeFullSize.height))
if self.badgeForeground.animation(forKey: "movement") == nil {
self.badgeForeground.position = CGPoint(x: badgeSize.width * 3.0 / 2.0 - self.badgeForeground.frame.width * 0.35, y: badgeFullSize.height / 2.0)
@ -276,8 +290,6 @@ private final class BadgeComponent: Component {
self.badgeView.alpha = 1.0
let size = badgeSize
let badgeLabelSize = self.badgeLabel.update(value: component.title, transition: .easeInOut(duration: 0.12))
transition.setFrame(view: self.badgeLabel, frame: CGRect(origin: CGPoint(x: 14.0 + floorToScreenPixels((badgeFullSize.width - badgeLabelSize.width) / 2.0), y: 5.0), size: badgeLabelSize))
if self.previousAvailableSize != availableSize {
@ -651,9 +663,11 @@ private final class ChatSendStarsScreenComponent: Component {
private let title = ComponentView<Empty>()
private let descriptionText = ComponentView<Empty>()
private let badgeStars = BadgeStarsView()
private let slider = ComponentView<Empty>()
private let sliderBackground = UIView()
private let sliderForeground = UIView()
private let sliderStars = SliderStarsView()
private let badge = ComponentView<Empty>()
private var topPeersLeftSeparator: SimpleLayer?
@ -703,9 +717,7 @@ private final class ChatSendStarsScreenComponent: Component {
self.addSubview(self.dimView)
self.layer.addSublayer(self.backgroundLayer)
self.addSubview(self.navigationBarContainer)
self.scrollView.delaysContentTouches = true
self.scrollView.canCancelContentTouches = true
self.scrollView.clipsToBounds = false
@ -728,6 +740,11 @@ private final class ChatSendStarsScreenComponent: Component {
self.scrollView.addSubview(self.scrollContentView)
self.sliderForeground.clipsToBounds = true
self.sliderForeground.addSubview(self.sliderStars)
self.addSubview(self.navigationBarContainer)
self.dimView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:))))
}
@ -830,6 +847,10 @@ private final class ChatSendStarsScreenComponent: Component {
}
}
private var previousSliderValue: Float = 0.0
private var previousTimestamp: Double?
private var inertiaDirection: BadgeComponent.Direction?
func update(component: ChatSendStarsScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<ViewControllerComponentContainer.Environment>, transition: ComponentTransition) -> CGSize {
let environment = environment[ViewControllerComponentContainer.Environment.self].value
let themeUpdated = self.environment?.theme !== environment.theme
@ -881,6 +902,53 @@ private final class ChatSendStarsScreenComponent: Component {
}
self.amount = 1 + Int64(value)
self.state?.updated(transition: .immediate)
let sliderValue = Float(value) / 1000.0
let currentTimestamp = CACurrentMediaTime()
if let previousTimestamp {
let deltaTime = currentTimestamp - previousTimestamp
let delta = sliderValue - self.previousSliderValue
let deltaValue = abs(sliderValue - self.previousSliderValue)
let speed = deltaValue / Float(deltaTime)
let newSpeed = max(0, min(65.0, speed * 70.0))
var inertiaDirection: BadgeComponent.Direction?
if newSpeed >= 1.0 {
if delta > 0.0 {
inertiaDirection = .right
} else {
inertiaDirection = .left
}
}
if inertiaDirection != self.inertiaDirection {
self.inertiaDirection = inertiaDirection
self.state?.updated(transition: .immediate)
}
if newSpeed < 0.01 && deltaValue < 0.001 {
} else {
self.badgeStars.update(speed: newSpeed, delta: delta)
}
}
self.previousSliderValue = sliderValue
self.previousTimestamp = currentTimestamp
},
isTrackingUpdated: { [weak self] isTracking in
guard let self else {
return
}
if !isTracking {
self.previousTimestamp = nil
self.badgeStars.update(speed: 0.0)
}
if self.inertiaDirection != nil {
self.inertiaDirection = nil
self.state?.updated(transition: .immediate)
}
}
)),
environment: {},
@ -889,6 +957,7 @@ private final class ChatSendStarsScreenComponent: Component {
let sliderFrame = CGRect(origin: CGPoint(x: sliderInset, y: contentHeight + 127.0), size: sliderSize)
if let sliderView = self.slider.view {
if sliderView.superview == nil {
self.scrollContentView.addSubview(self.badgeStars)
self.scrollContentView.addSubview(self.sliderBackground)
self.scrollContentView.addSubview(self.sliderForeground)
self.scrollContentView.addSubview(sliderView)
@ -910,20 +979,30 @@ private final class ChatSendStarsScreenComponent: Component {
self.sliderBackground.layer.cornerRadius = sliderBackgroundFrame.height * 0.5
self.sliderForeground.layer.cornerRadius = sliderBackgroundFrame.height * 0.5
self.sliderStars.frame = CGRect(origin: .zero, size: sliderBackgroundFrame.size)
self.sliderStars.update(size: sliderBackgroundFrame.size, value: progressFraction)
self.sliderForeground.isHidden = sliderForegroundFrame.width <= sliderMinWidth
var effectiveInertiaDirection = self.inertiaDirection
if progressFraction <= 0.03 || progressFraction >= 0.97 {
effectiveInertiaDirection = nil
}
let badgeSize = self.badge.update(
transition: transition,
component: AnyComponent(BadgeComponent(
theme: environment.theme, title: "\(self.amount)")
),
theme: environment.theme,
title: "\(self.amount)",
inertiaDirection: effectiveInertiaDirection
)),
environment: {},
containerSize: CGSize(width: 200.0, height: 200.0)
)
var badgeFrame = CGRect(origin: CGPoint(x: sliderForegroundFrame.minX + sliderForegroundFrame.width - floorToScreenPixels(sliderMinWidth * 0.5), y: sliderForegroundFrame.minY - 8.0), size: badgeSize)
if let badgeView = self.badge.view as? BadgeComponent.View {
if badgeView.superview == nil {
self.scrollContentView.addSubview(badgeView)
self.scrollContentView.insertSubview(badgeView, belowSubview: self.badgeStars)
}
let badgeSideInset = sideInset + 15.0
@ -943,6 +1022,10 @@ private final class ChatSendStarsScreenComponent: Component {
badgeView.adjustTail(size: badgeSize, overflowWidth: -badgeOverflowWidth)
}
let starsRect = CGRect(origin: .zero, size: CGSize(width: availableSize.width, height: sliderForegroundFrame.midY))
self.badgeStars.frame = starsRect
self.badgeStars.update(size: starsRect.size, emitterPosition: CGPoint(x: badgeFrame.minX, y: badgeFrame.midY - 64.0))
}
contentHeight += 123.0
@ -1437,3 +1520,198 @@ private func generateCloseButtonImage(backgroundColor: UIColor, foregroundColor:
context.strokePath()
})
}
private final class BadgeStarsView: UIView {
private let staticEmitterLayer = CAEmitterLayer()
private let dynamicEmitterLayer = CAEmitterLayer()
override init(frame: CGRect) {
super.init(frame: frame)
self.layer.addSublayer(self.staticEmitterLayer)
self.layer.addSublayer(self.dynamicEmitterLayer)
}
required init(coder: NSCoder) {
preconditionFailure()
}
private func setupEmitter() {
let color = UIColor(rgb: 0xffbe27)
self.staticEmitterLayer.emitterShape = .circle
self.staticEmitterLayer.emitterSize = CGSize(width: 10.0, height: 5.0)
self.staticEmitterLayer.emitterMode = .outline
self.layer.addSublayer(self.staticEmitterLayer)
self.dynamicEmitterLayer.birthRate = 0.0
self.dynamicEmitterLayer.emitterShape = .circle
self.dynamicEmitterLayer.emitterSize = CGSize(width: 10.0, height: 55.0)
self.dynamicEmitterLayer.emitterMode = .surface
self.layer.addSublayer(self.dynamicEmitterLayer)
let staticEmitter = CAEmitterCell()
staticEmitter.name = "emitter"
staticEmitter.contents = UIImage(bundleImageName: "Premium/Stars/Particle")?.cgImage
staticEmitter.birthRate = 20.0
staticEmitter.lifetime = 2.7
staticEmitter.velocity = 30.0
staticEmitter.velocityRange = 3
staticEmitter.scale = 0.15
staticEmitter.scaleRange = 0.08
staticEmitter.emissionRange = .pi * 2.0
staticEmitter.setValue(3.0, forKey: "mass")
staticEmitter.setValue(2.0, forKey: "massRange")
let dynamicEmitter = CAEmitterCell()
dynamicEmitter.name = "emitter"
dynamicEmitter.contents = UIImage(bundleImageName: "Premium/Stars/Particle")?.cgImage
dynamicEmitter.birthRate = 0.0
dynamicEmitter.lifetime = 2.7
dynamicEmitter.velocity = 30.0
dynamicEmitter.velocityRange = 3
dynamicEmitter.scale = 0.15
dynamicEmitter.scaleRange = 0.08
dynamicEmitter.emissionRange = .pi / 3.0
dynamicEmitter.setValue(3.0, forKey: "mass")
dynamicEmitter.setValue(2.0, forKey: "massRange")
let staticColors: [Any] = [
UIColor.white.withAlphaComponent(0.0).cgColor,
UIColor.white.withAlphaComponent(0.35).cgColor,
color.cgColor,
color.cgColor,
color.withAlphaComponent(0.0).cgColor
]
let staticColorBehavior = CAEmitterCell.createEmitterBehavior(type: "colorOverLife")
staticColorBehavior.setValue(staticColors, forKey: "colors")
staticEmitter.setValue([staticColorBehavior], forKey: "emitterBehaviors")
let dynamicColors: [Any] = [
UIColor.white.withAlphaComponent(0.35).cgColor,
color.withAlphaComponent(0.85).cgColor,
color.cgColor,
color.cgColor,
color.withAlphaComponent(0.0).cgColor
]
let dynamicColorBehavior = CAEmitterCell.createEmitterBehavior(type: "colorOverLife")
dynamicColorBehavior.setValue(dynamicColors, forKey: "colors")
dynamicEmitter.setValue([dynamicColorBehavior], forKey: "emitterBehaviors")
let attractor = CAEmitterCell.createEmitterBehavior(type: "simpleAttractor")
attractor.setValue("attractor", forKey: "name")
attractor.setValue(20, forKey: "falloff")
attractor.setValue(35, forKey: "radius")
self.staticEmitterLayer.setValue([attractor], forKey: "emitterBehaviors")
self.staticEmitterLayer.setValue(4.0, forKeyPath: "emitterBehaviors.attractor.stiffness")
self.staticEmitterLayer.setValue(false, forKeyPath: "emitterBehaviors.attractor.enabled")
self.staticEmitterLayer.emitterCells = [staticEmitter]
self.dynamicEmitterLayer.emitterCells = [dynamicEmitter]
}
func update(speed: Float, delta: Float? = nil) {
if speed > 0.0 {
if self.dynamicEmitterLayer.birthRate.isZero {
self.dynamicEmitterLayer.beginTime = CACurrentMediaTime()
}
self.dynamicEmitterLayer.setValue(Float(20.0 + speed * 1.4), forKeyPath: "emitterCells.emitter.birthRate")
self.dynamicEmitterLayer.setValue(2.7 - min(1.1, 1.5 * speed / 120.0), forKeyPath: "emitterCells.emitter.lifetime")
self.dynamicEmitterLayer.setValue(30.0 + CGFloat(speed / 80.0), forKeyPath: "emitterCells.emitter.velocity")
if let delta, speed > 15.0 {
self.dynamicEmitterLayer.setValue(delta > 0 ? .pi : 0, forKeyPath: "emitterCells.emitter.emissionLongitude")
self.dynamicEmitterLayer.setValue(.pi / 2.0, forKeyPath: "emitterCells.emitter.emissionRange")
} else {
self.dynamicEmitterLayer.setValue(0.0, forKeyPath: "emitterCells.emitter.emissionLongitude")
self.dynamicEmitterLayer.setValue(.pi * 2.0, forKeyPath: "emitterCells.emitter.emissionRange")
}
self.staticEmitterLayer.setValue(true, forKeyPath: "emitterBehaviors.attractor.enabled")
self.dynamicEmitterLayer.birthRate = 1.0
self.staticEmitterLayer.birthRate = 0.0
} else {
self.dynamicEmitterLayer.birthRate = 0.0
if let staticEmitter = self.staticEmitterLayer.emitterCells?.first {
staticEmitter.beginTime = CACurrentMediaTime()
}
self.staticEmitterLayer.birthRate = 1.0
self.staticEmitterLayer.setValue(false, forKeyPath: "emitterBehaviors.attractor.enabled")
}
}
func update(size: CGSize, emitterPosition: CGPoint) {
if self.staticEmitterLayer.emitterCells == nil {
self.setupEmitter()
}
self.staticEmitterLayer.frame = CGRect(origin: .zero, size: size)
self.staticEmitterLayer.emitterPosition = emitterPosition
self.dynamicEmitterLayer.frame = CGRect(origin: .zero, size: size)
self.dynamicEmitterLayer.emitterPosition = emitterPosition
self.staticEmitterLayer.setValue(emitterPosition, forKeyPath: "emitterBehaviors.attractor.position")
}
}
private final class SliderStarsView: UIView {
private let emitterLayer = CAEmitterLayer()
override init(frame: CGRect) {
super.init(frame: frame)
self.layer.addSublayer(self.emitterLayer)
}
required init(coder: NSCoder) {
preconditionFailure()
}
private func setupEmitter() {
self.emitterLayer.emitterShape = .rectangle
self.emitterLayer.emitterMode = .surface
self.layer.addSublayer(self.emitterLayer)
let emitter = CAEmitterCell()
emitter.name = "emitter"
emitter.contents = UIImage(bundleImageName: "Premium/Stars/Particle")?.cgImage
emitter.birthRate = 20.0
emitter.lifetime = 2.0
emitter.velocity = 15.0
emitter.velocityRange = 10
emitter.scale = 0.15
emitter.scaleRange = 0.08
emitter.emissionRange = .pi / 4.0
emitter.setValue(3.0, forKey: "mass")
emitter.setValue(2.0, forKey: "massRange")
self.emitterLayer.emitterCells = [emitter]
let colors: [Any] = [
UIColor.white.withAlphaComponent(0.0).cgColor,
UIColor.white.withAlphaComponent(0.38).cgColor,
UIColor.white.withAlphaComponent(0.38).cgColor,
UIColor.white.withAlphaComponent(0.0).cgColor,
UIColor.white.withAlphaComponent(0.38).cgColor,
UIColor.white.withAlphaComponent(0.38).cgColor,
UIColor.white.withAlphaComponent(0.0).cgColor
]
let colorBehavior = CAEmitterCell.createEmitterBehavior(type: "colorOverLife")
colorBehavior.setValue(colors, forKey: "colors")
emitter.setValue([colorBehavior], forKey: "emitterBehaviors")
}
func update(size: CGSize, value: CGFloat) {
if self.emitterLayer.emitterCells == nil {
self.setupEmitter()
}
self.emitterLayer.setValue(20.0 + Float(value * 40.0), forKeyPath: "emitterCells.emitter.birthRate")
self.emitterLayer.setValue(15.0 + value * 75.0, forKeyPath: "emitterCells.emitter.velocity")
self.emitterLayer.frame = CGRect(origin: .zero, size: size)
self.emitterLayer.emitterPosition = CGPoint(x: size.width / 2.0, y: size.height / 2.0)
self.emitterLayer.emitterSize = size
}
}

View File

@ -438,6 +438,8 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget {
if tinted {
self.updateTintColor()
}
case .ton:
self.updateTon()
}
} else if let file = file {
self.updateFile(file: file, attemptSynchronousLoad: attemptSynchronousLoad)
@ -623,6 +625,10 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget {
self.contents = tinted ? tintedStarImage?.cgImage : starImage?.cgImage
}
private func updateTon() {
self.contents = tonImage?.cgImage
}
private func updateFile(file: TelegramMediaFile, attemptSynchronousLoad: Bool) {
guard let arguments = self.arguments else {
return
@ -899,7 +905,17 @@ private let starImage: UIImage? = {
context.clear(CGRect(origin: .zero, size: size))
if let image = UIImage(bundleImageName: "Premium/Stars/StarLarge"), let cgImage = image.cgImage {
context.draw(cgImage, in: CGRect(origin: .zero, size: size).insetBy(dx: 2.0, dy: 2.0), byTiling: false)
context.draw(cgImage, in: CGRect(origin: .zero, size: size).insetBy(dx: 4.0, dy: 4.0), byTiling: false)
}
})?.withRenderingMode(.alwaysTemplate)
}()
private let tonImage: UIImage? = {
generateImage(CGSize(width: 32.0, height: 32.0), contextGenerator: { size, context in
context.clear(CGRect(origin: .zero, size: size))
if let image = generateTintedImage(image: UIImage(bundleImageName: "Ads/TonBig"), color: UIColor(rgb: 0x007aff)), let cgImage = image.cgImage {
context.draw(cgImage, in: CGRect(origin: .zero, size: size).insetBy(dx: 4.0, dy: 4.0), byTiling: false)
}
})?.withRenderingMode(.alwaysTemplate)
}()

View File

@ -692,7 +692,7 @@ public final class EntityKeyboardComponent: Component {
deleteBackwards?()
AudioServicesPlaySystemSound(1155)
}
).withHoldAction({
).withHoldAction({ _ in
deleteBackwards?()
AudioServicesPlaySystemSound(1155)
}).minSize(CGSize(width: 38.0, height: 38.0)))))

View File

@ -24,6 +24,7 @@ public enum CodableDrawingEntity: Equatable {
case vector(DrawingVectorEntity)
case location(DrawingLocationEntity)
case link(DrawingLinkEntity)
case weather(DrawingWeatherEntity)
public init?(entity: DrawingEntity) {
if let entity = entity as? DrawingStickerEntity {
@ -40,6 +41,8 @@ public enum CodableDrawingEntity: Equatable {
self = .location(entity)
} else if let entity = entity as? DrawingLinkEntity {
self = .link(entity)
} else if let entity = entity as? DrawingWeatherEntity {
self = .weather(entity)
} else {
return nil
}
@ -61,6 +64,8 @@ public enum CodableDrawingEntity: Equatable {
return entity
case let .link(entity):
return entity
case let .weather(entity):
return entity
}
}
@ -109,6 +114,14 @@ public enum CodableDrawingEntity: Equatable {
size = entitySize
}
}
case let .weather(entity):
position = entity.position
size = entity.renderImage?.size
rotation = entity.rotation
scale = entity.scale
if let size {
cornerRadius = 10.0 / (size.width * entity.scale)
}
default:
return nil
}
@ -198,6 +211,7 @@ extension CodableDrawingEntity: Codable {
case vector
case location
case link
case weather
}
public init(from decoder: Decoder) throws {
@ -218,6 +232,8 @@ extension CodableDrawingEntity: Codable {
self = .location(try container.decode(DrawingLocationEntity.self, forKey: .entity))
case .link:
self = .link(try container.decode(DrawingLinkEntity.self, forKey: .entity))
case .weather:
self = .weather(try container.decode(DrawingWeatherEntity.self, forKey: .entity))
}
}
@ -245,6 +261,9 @@ extension CodableDrawingEntity: Codable {
case let .link(payload):
try container.encode(EntityType.link, forKey: .type)
try container.encode(payload, forKey: .entity)
case let .weather(payload):
try container.encode(EntityType.weather, forKey: .type)
try container.encode(payload, forKey: .entity)
}
}
}

View File

@ -0,0 +1,182 @@
import Foundation
import UIKit
import Display
import AccountContext
import TextFormat
import Postbox
import TelegramCore
public final class DrawingWeatherEntity: DrawingEntity, Codable {
private enum CodingKeys: String, CodingKey {
case uuid
case style
case color
case hasCustomColor
case temperature
case icon
case referenceDrawingSize
case position
case width
case scale
case rotation
case renderImage
}
public enum Style: Codable, Equatable {
case white
case black
case transparent
case custom
case blur
}
public var uuid: UUID
public var isAnimated: Bool {
return false
}
public var style: Style
public var temperature: String
public var icon: TelegramMediaFile?
public var color: DrawingColor = DrawingColor(color: .white) {
didSet {
if self.color.toUIColor().argb == UIColor.white.argb {
self.style = .white
self.hasCustomColor = false
} else {
self.style = .custom
self.hasCustomColor = true
}
}
}
public var hasCustomColor = false
public var lineWidth: CGFloat = 0.0
public var referenceDrawingSize: CGSize
public var position: CGPoint
public var width: CGFloat
public var scale: CGFloat {
didSet {
self.scale = min(2.5, self.scale)
}
}
public var rotation: CGFloat
public var center: CGPoint {
return self.position
}
public var renderImage: UIImage?
public var renderSubEntities: [DrawingEntity]?
public var isMedia: Bool {
return false
}
public init(temperature: String, style: Style, icon: TelegramMediaFile?) {
self.uuid = UUID()
self.temperature = temperature
self.style = style
self.icon = icon
self.referenceDrawingSize = .zero
self.position = .zero
self.width = 100.0
self.scale = 1.0
self.rotation = 0.0
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.uuid = try container.decode(UUID.self, forKey: .uuid)
self.temperature = try container.decode(String.self, forKey: .temperature)
self.style = try container.decode(Style.self, forKey: .style)
self.color = try container.decodeIfPresent(DrawingColor.self, forKey: .color) ?? DrawingColor(color: .white)
self.hasCustomColor = try container.decodeIfPresent(Bool.self, forKey: .hasCustomColor) ?? false
if let iconData = try container.decodeIfPresent(Data.self, forKey: .icon) {
self.icon = PostboxDecoder(buffer: MemoryBuffer(data: iconData)).decodeRootObject() as? TelegramMediaFile
}
self.referenceDrawingSize = try container.decode(CGSize.self, forKey: .referenceDrawingSize)
self.position = try container.decode(CGPoint.self, forKey: .position)
self.width = try container.decode(CGFloat.self, forKey: .width)
self.scale = try container.decode(CGFloat.self, forKey: .scale)
self.rotation = try container.decode(CGFloat.self, forKey: .rotation)
if let renderImageData = try? container.decodeIfPresent(Data.self, forKey: .renderImage) {
self.renderImage = UIImage(data: renderImageData)
}
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.uuid, forKey: .uuid)
try container.encode(self.temperature, forKey: .temperature)
try container.encode(self.style, forKey: .style)
try container.encode(self.color, forKey: .color)
try container.encode(self.hasCustomColor, forKey: .hasCustomColor)
var encoder = PostboxEncoder()
if let icon = self.icon {
encoder = PostboxEncoder()
encoder.encodeRootObject(icon)
let iconData = encoder.makeData()
try container.encode(iconData, forKey: .icon)
}
try container.encode(self.referenceDrawingSize, forKey: .referenceDrawingSize)
try container.encode(self.position, forKey: .position)
try container.encode(self.width, forKey: .width)
try container.encode(self.scale, forKey: .scale)
try container.encode(self.rotation, forKey: .rotation)
if let renderImage, let data = renderImage.pngData() {
try container.encode(data, forKey: .renderImage)
}
}
public func duplicate(copy: Bool) -> DrawingEntity {
let newEntity = DrawingWeatherEntity(temperature: self.temperature, style: self.style, icon: self.icon)
if copy {
newEntity.uuid = self.uuid
}
newEntity.referenceDrawingSize = self.referenceDrawingSize
newEntity.position = self.position
newEntity.width = self.width
newEntity.scale = self.scale
newEntity.rotation = self.rotation
return newEntity
}
public func isEqual(to other: DrawingEntity) -> Bool {
guard let other = other as? DrawingWeatherEntity else {
return false
}
if self.uuid != other.uuid {
return false
}
if self.temperature != other.temperature {
return false
}
if self.style != other.style {
return false
}
if self.referenceDrawingSize != other.referenceDrawingSize {
return false
}
if self.position != other.position {
return false
}
if self.width != other.width {
return false
}
if self.scale != other.scale {
return false
}
if self.rotation != other.rotation {
return false
}
return true
}
}

View File

@ -4155,6 +4155,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
}
if !self.didSetupStaticEmojiPack {
self.didSetupStaticEmojiPack = true
self.staticEmojiPack.set(self.context.engine.stickers.loadedStickerPack(reference: .name("staticemoji"), forceActualized: false))
}
@ -4212,7 +4213,8 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
emojiFile = .single(nil)
}
let _ = emojiFile.start(next: { [weak self] emojiFile in
let _ = (emojiFile
|> deliverOnMainQueue).start(next: { [weak self] emojiFile in
guard let self else {
return
}
@ -4570,6 +4572,63 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
self.mediaEditor?.play()
}
func addWeather() {
if !self.didSetupStaticEmojiPack {
self.didSetupStaticEmojiPack = true
self.staticEmojiPack.set(self.context.engine.stickers.loadedStickerPack(reference: .name("staticemoji"), forceActualized: false))
}
let emojiFile: Signal<TelegramMediaFile?, NoError>
let emoji = "☀️".strippedEmoji
emojiFile = self.context.animatedEmojiStickers
|> take(1)
|> map { result -> TelegramMediaFile? in
if let file = result[emoji]?.first {
return file.file
} else {
return nil
}
// if case let .result(_, items, _) = result, let match = items.first(where: { item in
// var displayText: String?
// for attribute in item.file.attributes {
// if case let .Sticker(alt, _, _) = attribute {
// displayText = alt
// break
// }
// }
// if let displayText, displayText.hasPrefix(emoji) {
// return true
// } else {
// return false
// }
// }) {
// return match.file
// } else {
// return nil
// }
}
let _ = (emojiFile
|> deliverOnMainQueue).start(next: { [weak self] emojiFile in
guard let self else {
return
}
let scale = 1.0
self.interaction?.insertEntity(
DrawingWeatherEntity(
temperature: "35°C",
style: .white,
icon: emojiFile
),
scale: scale,
position: nil
)
})
}
func updateModalTransitionFactor(_ value: CGFloat, transition: ContainedViewLayoutTransition) {
guard let layout = self.validLayout, case .compact = layout.metrics.widthClass else {
return
@ -4824,6 +4883,14 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
controller?.dismiss(animated: true)
}
}
controller.addWeather = { [weak self, weak controller] in
if let self {
self.addWeather()
self.stickerScreen = nil
controller?.dismiss(animated: true)
}
}
controller.pushController = { [weak self] c in
self?.controller?.push(c)
}

View File

@ -4,14 +4,6 @@ import Display
import CoreImage
import MediaEditor
func createEmitterBehavior(type: String) -> NSObject {
let selector = ["behaviorWith", "Type:"].joined(separator: "")
let behaviorClass = NSClassFromString(["CA", "Emitter", "Behavior"].joined(separator: "")) as! NSObject.Type
let behaviorWithType = behaviorClass.method(for: NSSelectorFromString(selector))!
let castedBehaviorWithType = unsafeBitCast(behaviorWithType, to:(@convention(c)(Any?, Selector, Any?) -> NSObject).self)
return castedBehaviorWithType(behaviorClass, NSSelectorFromString(selector), type)
}
private var previousBeginTime: Int = 3
final class StickerCutoutOutlineView: UIView {
@ -81,7 +73,7 @@ final class StickerCutoutOutlineView: UIView {
let lineEmitterCell = CAEmitterCell()
lineEmitterCell.beginTime = CACurrentMediaTime()
let lineAlphaBehavior = createEmitterBehavior(type: "valueOverLife")
let lineAlphaBehavior = CAEmitterCell.createEmitterBehavior(type: "valueOverLife")
lineAlphaBehavior.setValue("color.alpha", forKey: "keyPath")
lineAlphaBehavior.setValue([0.0, 0.5, 0.8, 0.5, 0.0], forKey: "values")
lineEmitterCell.setValue([lineAlphaBehavior], forKey: "emitterBehaviors")
@ -107,7 +99,7 @@ final class StickerCutoutOutlineView: UIView {
let glowEmitterCell = CAEmitterCell()
glowEmitterCell.beginTime = CACurrentMediaTime()
let glowAlphaBehavior = createEmitterBehavior(type: "valueOverLife")
let glowAlphaBehavior = CAEmitterCell.createEmitterBehavior(type: "valueOverLife")
glowAlphaBehavior.setValue("color.alpha", forKey: "keyPath")
glowAlphaBehavior.setValue([0.0, 0.32, 0.4, 0.2, 0.0], forKey: "values")
glowEmitterCell.setValue([glowAlphaBehavior], forKey: "emitterBehaviors")

View File

@ -241,7 +241,10 @@ public class MinimizedContainerImpl: ASDisplayNode, MinimizedContainer, ASScroll
if let snapshotView = self.snapshotView {
var snapshotFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - snapshotView.bounds.size.width) / 2.0), y: 0.0), size: snapshotView.bounds.size)
if self.item.controller.minimizedTopEdgeOffset == nil && isExpanded {
snapshotFrame = snapshotFrame.offsetBy(dx: 0.0, dy: -12.0)
}
var requiresBlur = false
var blurFrame = snapshotFrame
if snapshotView.frame.width * 1.1 < size.width {
@ -1018,6 +1021,14 @@ public class MinimizedContainerImpl: ASDisplayNode, MinimizedContainer, ASScroll
transition.updateBounds(node: itemNode, bounds: CGRect(origin: .zero, size: layout.size))
}
transition.updateTransform(node: itemNode, transform: CATransform3DIdentity)
if let _ = itemNode.snapshotView {
if itemNode.item.controller.minimizedTopEdgeOffset == nil, let snapshotView = itemNode.snapshotView, snapshotView.frame.origin.y == -12.0 {
let snapshotFrame = snapshotView.frame.offsetBy(dx: 0.0, dy: 12.0)
transition.updateFrame(view: snapshotView, frame: snapshotFrame)
}
}
transition.updatePosition(node: itemNode, position: CGPoint(x: layout.size.width / 2.0, y: layout.size.height / 2.0 + topInset + self.scrollView.contentOffset.y), completion: { _ in
self.isApplyingTransition = false
if self.currentTransition == currentTransition {

View File

@ -25,11 +25,14 @@ final class MinimizedHeaderNode: ASDisplayNode {
var theme: NavigationControllerTheme {
didSet {
self.backgroundView.backgroundColor = self.theme.navigationBar.opaqueBackgroundColor
self.progressView.backgroundColor = self.theme.navigationBar.primaryTextColor.withAlphaComponent(0.06)
self.iconView.tintColor = self.theme.navigationBar.primaryTextColor
}
}
let strings: PresentationStrings
private let backgroundView = UIView()
private let progressView = UIView()
private var iconView = UIImageView()
private let titleLabel = ComponentView<Empty>()
private let closeButton = ComponentView<Empty>()
@ -48,6 +51,12 @@ final class MinimizedHeaderNode: ASDisplayNode {
self.icon = nil
}
if self.controllers.count == 1, let progress = self.controllers.first?.minimizedProgress {
self.progress = progress
} else {
self.progress = nil
}
if newValue.count != self.controllers.count {
self._controllers = newValue.map { WeakController($0) }
@ -93,6 +102,14 @@ final class MinimizedHeaderNode: ASDisplayNode {
}
}
var progress: Float? {
didSet {
if let (size, insets, isExpanded) = self.validLayout {
self.update(size: size, insets: insets, isExpanded: isExpanded, transition: .immediate)
}
}
}
var title: String? {
didSet {
if let (size, insets, isExpanded) = self.validLayout {
@ -111,20 +128,25 @@ final class MinimizedHeaderNode: ASDisplayNode {
self.strings = strings
self.backgroundView.clipsToBounds = true
self.backgroundView.backgroundColor = theme.navigationBar.opaqueBackgroundColor
self.backgroundView.backgroundColor = self.theme.navigationBar.opaqueBackgroundColor
self.backgroundView.layer.cornerRadius = 10.0
if #available(iOS 11.0, *) {
self.backgroundView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
}
self.progressView.backgroundColor = self.theme.navigationBar.primaryTextColor.withAlphaComponent(0.06)
self.iconView.contentMode = .scaleAspectFit
self.iconView.clipsToBounds = true
self.iconView.layer.cornerRadius = 2.5
self.iconView.tintColor = self.theme.navigationBar.primaryTextColor
super.init()
self.clipsToBounds = true
self.view.addSubview(self.backgroundView)
self.backgroundView.addSubview(self.progressView)
self.backgroundView.addSubview(self.iconView)
applySmoothRoundedCorners(self.backgroundView.layer)
@ -149,9 +171,9 @@ final class MinimizedHeaderNode: ASDisplayNode {
func update(size: CGSize, insets: UIEdgeInsets, isExpanded: Bool, transition: ContainedViewLayoutTransition) {
self.validLayout = (size, insets, isExpanded)
let headerHeight: CGFloat = 44.0
let titleSpacing: CGFloat = 4.0
let titleSpacing: CGFloat = 6.0
var titleSideInset: CGFloat = 56.0
if !isExpanded {
titleSideInset += insets.left
@ -177,7 +199,7 @@ final class MinimizedHeaderNode: ASDisplayNode {
}
let iconFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - totalWidth) / 2.0), y: floorToScreenPixels((headerHeight - iconSize.height) / 2.0)), size: iconSize)
transition.updateFrame(view: self.iconView, frame: iconFrame)
self.iconView.frame = iconFrame
let titleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - totalWidth) / 2.0) + totalWidth - titleSize.width, y: floorToScreenPixels((headerHeight - titleSize.height) / 2.0)), size: titleSize)
if let view = self.titleLabel.view {
@ -220,5 +242,10 @@ final class MinimizedHeaderNode: ASDisplayNode {
}
transition.updateFrame(view: self.backgroundView, frame: CGRect(origin: .zero, size: CGSize(width: size.width, height: 243.0)))
transition.updateAlpha(layer: self.progressView.layer, alpha: isExpanded && self.progress != nil ? 1.0 : 0.0)
if let progress = self.progress {
self.progressView.frame = CGRect(origin: .zero, size: CGSize(width: size.width * CGFloat(progress), height: 243.0))
}
}
}

View File

@ -0,0 +1,21 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "NavigationStackComponent",
module_name = "NavigationStackComponent",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/Display",
"//submodules/ComponentFlow",
"//submodules/AppBundle",
"//submodules/Components/BundleIconComponent",
],
visibility = [
"//visibility:public",
],
)

View File

@ -0,0 +1,297 @@
import Foundation
import UIKit
import Display
import ComponentFlow
import AppBundle
import BundleIconComponent
private final class NavigationContainer: UIView, UIGestureRecognizerDelegate {
var requestUpdate: ((ComponentTransition) -> Void)?
var requestPop: (() -> Void)?
var transitionFraction: CGFloat = 0.0
private var panRecognizer: InteractiveTransitionGestureRecognizer?
var isNavigationEnabled: Bool = false {
didSet {
self.panRecognizer?.isEnabled = self.isNavigationEnabled
}
}
init() {
super.init(frame: .zero)
let panRecognizer = InteractiveTransitionGestureRecognizer(target: self, action: #selector(self.panGesture(_:)), allowedDirections: { [weak self] point in
guard let strongSelf = self else {
return []
}
let _ = strongSelf
return [.right]
})
panRecognizer.delegate = self
self.addGestureRecognizer(panRecognizer)
self.panRecognizer = panRecognizer
}
required public init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return false
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer) -> Bool {
if let _ = otherGestureRecognizer as? InteractiveTransitionGestureRecognizer {
return false
}
if let _ = otherGestureRecognizer as? UIPanGestureRecognizer {
return true
}
return false
}
@objc private func panGesture(_ recognizer: UIPanGestureRecognizer) {
switch recognizer.state {
case .began:
self.transitionFraction = 0.0
case .changed:
let distanceFactor: CGFloat = recognizer.translation(in: self).x / self.bounds.width
let transitionFraction = max(0.0, min(1.0, distanceFactor))
if self.transitionFraction != transitionFraction {
self.transitionFraction = transitionFraction
self.requestUpdate?(.immediate)
}
case .ended, .cancelled:
let distanceFactor: CGFloat = recognizer.translation(in: self).x / self.bounds.width
let transitionFraction = max(0.0, min(1.0, distanceFactor))
if transitionFraction > 0.2 {
self.transitionFraction = 0.0
self.requestPop?()
} else {
self.transitionFraction = 0.0
self.requestUpdate?(.spring(duration: 0.45))
}
default:
break
}
}
}
public final class NavigationStackComponent<ChildEnvironment: Equatable>: Component {
public let items: [AnyComponentWithIdentity<ChildEnvironment>]
public let requestPop: () -> Void
public init(
items: [AnyComponentWithIdentity<ChildEnvironment>],
requestPop: @escaping () -> Void
) {
self.items = items
self.requestPop = requestPop
}
public static func ==(lhs: NavigationStackComponent, rhs: NavigationStackComponent) -> Bool {
if lhs.items != rhs.items {
return false
}
return true
}
private final class ItemView: UIView {
let contents = ComponentView<ChildEnvironment>()
let dimView = UIView()
override init(frame: CGRect) {
super.init(frame: frame)
self.dimView.alpha = 0.0
self.dimView.backgroundColor = UIColor.black.withAlphaComponent(0.2)
self.dimView.isUserInteractionEnabled = false
self.addSubview(self.dimView)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
private struct ReadyItem {
var index: Int
var itemId: AnyHashable
var itemView: ItemView
var itemTransition: ComponentTransition
var itemSize: CGSize
init(index: Int, itemId: AnyHashable, itemView: ItemView, itemTransition: ComponentTransition, itemSize: CGSize) {
self.index = index
self.itemId = itemId
self.itemView = itemView
self.itemTransition = itemTransition
self.itemSize = itemSize
}
}
public final class View: UIView {
private var itemViews: [AnyHashable: ItemView] = [:]
private let navigationContainer = NavigationContainer()
private var component: NavigationStackComponent?
private var state: EmptyComponentState?
public override init(frame: CGRect) {
super.init(frame: CGRect())
self.addSubview(self.navigationContainer)
self.navigationContainer.requestUpdate = { [weak self] transition in
guard let self else {
return
}
self.state?.updated(transition: transition)
}
self.navigationContainer.requestPop = { [weak self] in
guard let self else {
return
}
self.component?.requestPop()
}
}
required public init?(coder: NSCoder) {
preconditionFailure()
}
func update(component: NavigationStackComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<ChildEnvironment>, transition: ComponentTransition) -> CGSize {
self.component = component
self.state = state
let navigationTransitionFraction = self.navigationContainer.transitionFraction
self.navigationContainer.isNavigationEnabled = component.items.count > 1
var validItemIds: [AnyHashable] = []
var readyItems: [ReadyItem] = []
for i in 0 ..< component.items.count {
let item = component.items[i]
let itemId = item.id
validItemIds.append(itemId)
let itemView: ItemView
var itemTransition = transition
if let current = self.itemViews[itemId] {
itemView = current
} else {
itemTransition = itemTransition.withAnimation(.none)
itemView = ItemView()
self.itemViews[itemId] = itemView
itemView.contents.parentState = state
}
let itemSize = itemView.contents.update(
transition: itemTransition,
component: item.component,
environment: { environment[ChildEnvironment.self] },
containerSize: CGSize(width: availableSize.width, height: availableSize.height)
)
readyItems.append(ReadyItem(
index: i,
itemId: itemId,
itemView: itemView,
itemTransition: itemTransition,
itemSize: itemSize
))
}
let sortedItems = readyItems.sorted(by: { $0.index < $1.index })
for readyItem in sortedItems {
let transitionFraction: CGFloat
let alphaTransitionFraction: CGFloat
if readyItem.index == readyItems.count - 1 {
transitionFraction = navigationTransitionFraction
alphaTransitionFraction = 1.0
} else if readyItem.index == readyItems.count - 2 {
transitionFraction = navigationTransitionFraction - 1.0
alphaTransitionFraction = navigationTransitionFraction
} else {
transitionFraction = 0.0
alphaTransitionFraction = 0.0
}
let transitionOffset: CGFloat
if readyItem.index == readyItems.count - 1 {
transitionOffset = readyItem.itemSize.width * transitionFraction
} else {
transitionOffset = readyItem.itemSize.width / 3.0 * transitionFraction
}
let itemFrame = CGRect(origin: CGPoint(x: transitionOffset, y: 0.0), size: readyItem.itemSize)
let itemBounds = CGRect(origin: .zero, size: itemFrame.size)
if let itemComponentView = readyItem.itemView.contents.view {
var isAdded = false
if itemComponentView.superview == nil {
isAdded = true
readyItem.itemView.insertSubview(itemComponentView, at: 0)
self.navigationContainer.addSubview(readyItem.itemView)
}
readyItem.itemTransition.setFrame(view: readyItem.itemView, frame: itemFrame)
readyItem.itemTransition.setFrame(view: itemComponentView, frame: itemBounds)
readyItem.itemTransition.setFrame(view: readyItem.itemView.dimView, frame: CGRect(origin: .zero, size: availableSize))
readyItem.itemTransition.setAlpha(view: readyItem.itemView.dimView, alpha: 1.0 - alphaTransitionFraction)
if readyItem.index > 0 && isAdded {
transition.animatePosition(view: itemComponentView, from: CGPoint(x: itemFrame.width, y: 0.0), to: .zero, additive: true, completion: nil)
}
}
}
let lastHeight = sortedItems.last?.itemSize.height ?? 0.0
let previousHeight: CGFloat
if sortedItems.count > 1 {
previousHeight = sortedItems[sortedItems.count - 2].itemSize.height
} else {
previousHeight = lastHeight
}
let contentHeight = lastHeight * (1.0 - navigationTransitionFraction) + previousHeight * navigationTransitionFraction
var removedItemIds: [AnyHashable] = []
for (id, _) in self.itemViews {
if !validItemIds.contains(id) {
removedItemIds.append(id)
}
}
for id in removedItemIds {
guard let itemView = self.itemViews[id] else {
continue
}
if let itemComponeentView = itemView.contents.view {
var position = itemComponeentView.center
position.x += itemComponeentView.bounds.width
transition.setPosition(view: itemComponeentView, position: position, completion: { _ in
itemView.removeFromSuperview()
self.itemViews.removeValue(forKey: id)
})
} else {
itemView.removeFromSuperview()
self.itemViews.removeValue(forKey: id)
}
}
let contentSize = CGSize(width: availableSize.width, height: contentHeight)
self.navigationContainer.frame = CGRect(origin: .zero, size: contentSize)
return contentSize
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<ChildEnvironment>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}

View File

@ -194,7 +194,7 @@ public final class EmojiSelectionComponent: Component {
component.backspace?()
AudioServicesPlaySystemSound(1155)
}
).withHoldAction({ [weak self] in
).withHoldAction({ [weak self] _ in
guard let self, let component = self.component else {
return
}

View File

@ -146,6 +146,7 @@ swift_library(
"//submodules/ConfettiEffect",
"//submodules/ContactsPeerItem",
"//submodules/TelegramUI/Components/PeerManagement/OldChannelsController",
"//submodules/TelegramUI/Components/TextNodeWithEntities",
],
visibility = [
"//visibility:public",

View File

@ -3,6 +3,7 @@ import Display
import SwiftSignalKit
import TelegramPresentationData
import AvatarNode
import AccountContext
enum PeerInfoScreenActionColor {
case accent
@ -89,7 +90,7 @@ private final class PeerInfoScreenActionItemNode: PeerInfoScreenItemNode {
self.iconDisposable.dispose()
}
override func update(width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, hasCorners: Bool, transition: ContainedViewLayoutTransition) -> CGFloat {
override func update(context: AccountContext, width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, hasCorners: Bool, transition: ContainedViewLayoutTransition) -> CGFloat {
guard let item = item as? PeerInfoScreenActionItem else {
return 10.0
}

View File

@ -170,7 +170,7 @@ private final class PeerInfoScreenAddressItemNode: PeerInfoScreenItemNode {
}
}
override func update(width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, hasCorners: Bool, transition: ContainedViewLayoutTransition) -> CGFloat {
override func update(context: AccountContext, width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, hasCorners: Bool, transition: ContainedViewLayoutTransition) -> CGFloat {
guard let item = item as? PeerInfoScreenAddressItem else {
return 10.0
}

View File

@ -52,7 +52,7 @@ private final class PeerInfoScreenBirthdatePickerItemNode: PeerInfoScreenItemNod
self.addSubnode(self.maskNode)
}
override func update(width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, hasCorners: Bool, transition: ContainedViewLayoutTransition) -> CGFloat {
override func update(context: AccountContext, width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, hasCorners: Bool, transition: ContainedViewLayoutTransition) -> CGFloat {
guard let item = item as? PeerInfoScreenBirthdatePickerItem else {
return 10.0
}

View File

@ -12,6 +12,7 @@ import ComponentFlow
import MultilineTextComponent
import BundleIconComponent
import PlainButtonComponent
import AccountContext
func businessHoursTextToCopy(businessHours: TelegramBusinessHours, presentationData: PresentationData, displayLocalTimezone: Bool) -> String {
var text = ""
@ -279,7 +280,7 @@ private final class PeerInfoScreenBusinessHoursItemNode: PeerInfoScreenItemNode
}
}
override func update(width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, hasCorners: Bool, transition: ContainedViewLayoutTransition) -> CGFloat {
override func update(context: AccountContext, width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, hasCorners: Bool, transition: ContainedViewLayoutTransition) -> CGFloat {
guard let item = item as? PeerInfoScreenBusinessHoursItem else {
return 10.0
}

View File

@ -57,7 +57,7 @@ private final class PeerInfoScreenCallListItemNode: PeerInfoScreenItemNode {
self.addSubnode(self.maskNode)
}
override func update(width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, hasCorners: Bool, transition: ContainedViewLayoutTransition) -> CGFloat {
override func update(context: AccountContext, width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, hasCorners: Bool, transition: ContainedViewLayoutTransition) -> CGFloat {
guard let item = item as? PeerInfoScreenCallListItem else {
return 10.0
}

View File

@ -3,6 +3,7 @@ import Display
import TelegramPresentationData
import TextFormat
import Markdown
import AccountContext
final class PeerInfoScreenCommentItem: PeerInfoScreenItem {
enum LinkAction {
@ -63,7 +64,7 @@ private final class PeerInfoScreenCommentItemNode: PeerInfoScreenItemNode {
self.view.addGestureRecognizer(recognizer)
}
override func update(width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, hasCorners: Bool, transition: ContainedViewLayoutTransition) -> CGFloat {
override func update(context: AccountContext, width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, hasCorners: Bool, transition: ContainedViewLayoutTransition) -> CGFloat {
guard let item = item as? PeerInfoScreenCommentItem else {
return 10.0
}

View File

@ -237,7 +237,7 @@ private final class PeerInfoScreenContactInfoItemNode: PeerInfoScreenItemNode {
return nil
}
override func update(width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, hasCorners: Bool, transition: ContainedViewLayoutTransition) -> CGFloat {
override func update(context: AccountContext, width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, hasCorners: Bool, transition: ContainedViewLayoutTransition) -> CGFloat {
guard let item = item as? PeerInfoScreenContactInfoItem else {
return 10.0
}

View File

@ -3,6 +3,7 @@ import Display
import TelegramPresentationData
import EncryptionKeyVisualization
import TelegramCore
import AccountContext
final class PeerInfoScreenDisclosureEncryptionKeyItem: PeerInfoScreenItem {
let id: AnyHashable
@ -71,7 +72,7 @@ private final class PeerInfoScreenDisclosureEncryptionKeyItemNode: PeerInfoScree
self.addSubnode(self.maskNode)
}
override func update(width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, hasCorners: Bool, transition: ContainedViewLayoutTransition) -> CGFloat {
override func update(context: AccountContext, width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, hasCorners: Bool, transition: ContainedViewLayoutTransition) -> CGFloat {
guard let item = item as? PeerInfoScreenDisclosureEncryptionKeyItem else {
return 10.0
}

View File

@ -2,6 +2,8 @@ import AsyncDisplayKit
import Display
import SwiftSignalKit
import TelegramPresentationData
import TextNodeWithEntities
import AccountContext
final class PeerInfoScreenDisclosureItem: PeerInfoScreenItem {
enum Label {
@ -12,6 +14,7 @@ final class PeerInfoScreenDisclosureItem: PeerInfoScreenItem {
case none
case text(String)
case attributedText(NSAttributedString)
case coloredText(String, LabelColor)
case badge(String, UIColor)
case semitransparentBadge(String, UIColor)
@ -22,6 +25,8 @@ final class PeerInfoScreenDisclosureItem: PeerInfoScreenItem {
switch self {
case .none, .image:
return ""
case let .attributedText(text):
return text.string
case let .text(text), let .coloredText(text, _), let .badge(text, _), let .semitransparentBadge(text, _), let .titleBadge(text, _):
return text
}
@ -29,7 +34,7 @@ final class PeerInfoScreenDisclosureItem: PeerInfoScreenItem {
var badgeColor: UIColor? {
switch self {
case .none, .text, .coloredText, .image:
case .none, .text, .coloredText, .image, .attributedText:
return nil
case let .badge(_, color), let .semitransparentBadge(_, color), let .titleBadge(_, color):
return color
@ -69,7 +74,7 @@ private final class PeerInfoScreenDisclosureItemNode: PeerInfoScreenItemNode {
private let maskNode: ASImageNode
private let iconNode: ASImageNode
private let labelBadgeNode: ASImageNode
private let labelNode: ImmediateTextNode
private let labelNode: ImmediateTextNodeWithEntities
private var additionalLabelNode: ImmediateTextNode?
private var additionalLabelBadgeNode: ASImageNode?
private let textNode: ImmediateTextNode
@ -97,7 +102,7 @@ private final class PeerInfoScreenDisclosureItemNode: PeerInfoScreenItemNode {
self.labelBadgeNode.displaysAsynchronously = false
self.labelBadgeNode.isLayerBacked = true
self.labelNode = ImmediateTextNode()
self.labelNode = ImmediateTextNodeWithEntities()
self.labelNode.displaysAsynchronously = false
self.labelNode.isUserInteractionEnabled = false
@ -135,7 +140,7 @@ private final class PeerInfoScreenDisclosureItemNode: PeerInfoScreenItemNode {
self.iconDisposable.dispose()
}
override func update(width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, hasCorners: Bool, transition: ContainedViewLayoutTransition) -> CGFloat {
override func update(context: AccountContext, width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, hasCorners: Bool, transition: ContainedViewLayoutTransition) -> CGFloat {
guard let item = item as? PeerInfoScreenDisclosureItem else {
return 10.0
}
@ -177,8 +182,20 @@ private final class PeerInfoScreenDisclosureItemNode: PeerInfoScreenItemNode {
labelColorValue = presentationData.theme.list.itemSecondaryTextColor
labelFont = titleFont
}
self.labelNode.attributedText = NSAttributedString(string: item.label.text, font: labelFont, textColor: labelColorValue)
self.labelNode.arguments = TextNodeWithEntities.Arguments(
context: context,
cache: context.animationCache,
renderer: context.animationRenderer,
placeholderColor: .clear,
attemptSynchronous: true
)
if case let .attributedText(text) = item.label {
self.labelNode.attributedText = text
} else {
self.labelNode.attributedText = NSAttributedString(string: item.label.text, font: labelFont, textColor: labelColorValue)
}
self.textNode.maximumNumberOfLines = 1
self.textNode.attributedText = NSAttributedString(string: item.text, font: titleFont, textColor: textColorValue)

View File

@ -1,6 +1,7 @@
import AsyncDisplayKit
import Display
import TelegramPresentationData
import AccountContext
final class PeerInfoScreenHeaderItem: PeerInfoScreenItem {
let id: AnyHashable
@ -44,7 +45,7 @@ private final class PeerInfoScreenHeaderItemNode: PeerInfoScreenItemNode {
self.addSubnode(self.activateArea)
}
override func update(width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, hasCorners: Bool, transition: ContainedViewLayoutTransition) -> CGFloat {
override func update(context: AccountContext, width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, hasCorners: Bool, transition: ContainedViewLayoutTransition) -> CGFloat {
guard let item = item as? PeerInfoScreenHeaderItem else {
return 10.0
}

View File

@ -55,7 +55,7 @@ private final class PeerInfoScreenInfoItemNode: PeerInfoScreenItemNode {
self.addSubnode(self.bottomSeparatorNode)
}
override func update(width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, hasCorners: Bool, transition: ContainedViewLayoutTransition) -> CGFloat {
override func update(context: AccountContext, width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, hasCorners: Bool, transition: ContainedViewLayoutTransition) -> CGFloat {
guard let item = item as? PeerInfoScreenInfoItem else {
return 10.0
}

View File

@ -121,6 +121,8 @@ private func generateExpandBackground(size: CGSize, color: UIColor) -> UIImage?
}
private final class PeerInfoScreenLabeledValueItemNode: PeerInfoScreenItemNode {
private weak var context: AccountContext?
private let containerNode: ContextControllerSourceNode
private let contextSourceNode: ContextExtractedContentContainingNode
@ -383,8 +385,8 @@ private final class PeerInfoScreenLabeledValueItemNode: PeerInfoScreenItemNode {
if self.linkItemWithProgress != currentLinkItem {
self.linkItemWithProgress = currentLinkItem
if let validLayout = self.validLayout {
let _ = self.update(width: validLayout.width, safeInsets: validLayout.safeInsets, presentationData: validLayout.presentationData, item: validLayout.item, topItem: validLayout.topItem, bottomItem: validLayout.bottomItem, hasCorners: validLayout.hasCorners, transition: .immediate)
if let validLayout = self.validLayout, let context = self.context {
let _ = self.update(context: context, width: validLayout.width, safeInsets: validLayout.safeInsets, presentationData: validLayout.presentationData, item: validLayout.item, topItem: validLayout.topItem, bottomItem: validLayout.bottomItem, hasCorners: validLayout.hasCorners, transition: .immediate)
}
}
})
@ -412,8 +414,8 @@ private final class PeerInfoScreenLabeledValueItemNode: PeerInfoScreenItemNode {
if self.linkItemWithProgress != currentLinkItem {
self.linkItemWithProgress = currentLinkItem
if let validLayout = self.validLayout {
let _ = self.update(width: validLayout.width, safeInsets: validLayout.safeInsets, presentationData: validLayout.presentationData, item: validLayout.item, topItem: validLayout.topItem, bottomItem: validLayout.bottomItem, hasCorners: validLayout.hasCorners, transition: .immediate)
if let validLayout = self.validLayout, let context = self.context {
let _ = self.update(context: context, width: validLayout.width, safeInsets: validLayout.safeInsets, presentationData: validLayout.presentationData, item: validLayout.item, topItem: validLayout.topItem, bottomItem: validLayout.bottomItem, hasCorners: validLayout.hasCorners, transition: .immediate)
}
}
})
@ -430,11 +432,12 @@ private final class PeerInfoScreenLabeledValueItemNode: PeerInfoScreenItemNode {
}
}
override func update(width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, hasCorners: Bool, transition: ContainedViewLayoutTransition) -> CGFloat {
override func update(context: AccountContext, width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, hasCorners: Bool, transition: ContainedViewLayoutTransition) -> CGFloat {
guard let item = item as? PeerInfoScreenLabeledValueItem else {
return 10.0
}
self.context = context
self.validLayout = (width, safeInsets, presentationData, item, topItem, bottomItem, hasCorners)
self.item = item

View File

@ -118,7 +118,7 @@ private final class PeerInfoScreenMemberItemNode: PeerInfoScreenItemNode {
}
}
override func update(width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, hasCorners: Bool, transition: ContainedViewLayoutTransition) -> CGFloat {
override func update(context: AccountContext, width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, hasCorners: Bool, transition: ContainedViewLayoutTransition) -> CGFloat {
guard let item = item as? PeerInfoScreenMemberItem else {
return 10.0
}

View File

@ -416,7 +416,7 @@ private final class PeerInfoScreenPersonalChannelItemNode: PeerInfoScreenItemNod
}
}
override func update(width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, hasCorners: Bool, transition: ContainedViewLayoutTransition) -> CGFloat {
override func update(context: AccountContext, width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, hasCorners: Bool, transition: ContainedViewLayoutTransition) -> CGFloat {
guard let item = item as? PeerInfoScreenPersonalChannelItem else {
return 50.0
}

View File

@ -2,6 +2,7 @@ import AsyncDisplayKit
import Display
import TelegramPresentationData
import AppBundle
import AccountContext
final class PeerInfoScreenSwitchItem: PeerInfoScreenItem {
let id: AnyHashable
@ -89,7 +90,7 @@ private final class PeerInfoScreenSwitchItemNode: PeerInfoScreenItemNode {
}
}
override func update(width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, hasCorners: Bool, transition: ContainedViewLayoutTransition) -> CGFloat {
override func update(context: AccountContext, width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, hasCorners: Bool, transition: ContainedViewLayoutTransition) -> CGFloat {
guard let item = item as? PeerInfoScreenSwitchItem else {
return 10.0
}

View File

@ -351,6 +351,7 @@ final class PeerInfoScreenData {
let starsState: StarsContext.State?
let starsRevenueStatsState: StarsRevenueStats?
let starsRevenueStatsContext: StarsRevenueStatsContext?
let revenueStatsState: RevenueStats?
let _isContact: Bool
var forceIsContact: Bool = false
@ -393,7 +394,8 @@ final class PeerInfoScreenData {
personalChannel: PeerInfoPersonalChannelData?,
starsState: StarsContext.State?,
starsRevenueStatsState: StarsRevenueStats?,
starsRevenueStatsContext: StarsRevenueStatsContext?
starsRevenueStatsContext: StarsRevenueStatsContext?,
revenueStatsState: RevenueStats?
) {
self.peer = peer
self.chatPeer = chatPeer
@ -425,6 +427,7 @@ final class PeerInfoScreenData {
self.starsState = starsState
self.starsRevenueStatsState = starsRevenueStatsState
self.starsRevenueStatsContext = starsRevenueStatsContext
self.revenueStatsState = revenueStatsState
}
}
@ -920,7 +923,8 @@ func peerInfoScreenSettingsData(context: AccountContext, peerId: EnginePeer.Id,
personalChannel: personalChannel,
starsState: starsState,
starsRevenueStatsState: nil,
starsRevenueStatsContext: nil
starsRevenueStatsContext: nil,
revenueStatsState: nil
)
}
}
@ -962,7 +966,8 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
personalChannel: nil,
starsState: nil,
starsRevenueStatsState: nil,
starsRevenueStatsContext: nil
starsRevenueStatsContext: nil,
revenueStatsState: nil
))
case let .user(userPeerId, secretChatId, kind):
let groupsInCommon: GroupsInCommonContext?
@ -1304,7 +1309,8 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
personalChannel: personalChannel,
starsState: nil,
starsRevenueStatsState: starsRevenueContextAndState.1,
starsRevenueStatsContext: starsRevenueContextAndState.0
starsRevenueStatsContext: starsRevenueContextAndState.0,
revenueStatsState: nil
)
}
case .channel:
@ -1380,6 +1386,36 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
let isPremiumRequiredForStoryPosting: Signal<Bool, NoError> = isPremiumRequiredForStoryPosting(context: context)
let starsRevenueContextAndState = context.engine.data.subscribe(
TelegramEngine.EngineData.Item.Peer.CanViewStarsRevenue(id: peerId)
)
|> distinctUntilChanged
|> mapToSignal { canViewStarsRevenue -> Signal<(StarsRevenueStatsContext?, StarsRevenueStats?), NoError> in
guard canViewStarsRevenue else {
return .single((nil, nil))
}
let starsRevenueStatsContext = StarsRevenueStatsContext(account: context.account, peerId: peerId)
return starsRevenueStatsContext.state
|> map { state -> (StarsRevenueStatsContext?, StarsRevenueStats?) in
return (starsRevenueStatsContext, state.stats)
}
}
let revenueContextAndState = context.engine.data.subscribe(
TelegramEngine.EngineData.Item.Peer.CanViewRevenue(id: peerId)
)
|> distinctUntilChanged
|> mapToSignal { canViewRevenue -> Signal<(RevenueStatsContext?, RevenueStats?), NoError> in
guard canViewRevenue else {
return .single((nil, nil))
}
let revenueStatsContext = RevenueStatsContext(account: context.account, peerId: peerId)
return revenueStatsContext.state
|> map { state -> (RevenueStatsContext?, RevenueStats?) in
return (revenueStatsContext, state.stats)
}
}
return combineLatest(
context.account.viewTracker.peerView(peerId, updateData: true),
peerInfoAvailableMediaPanes(context: context, peerId: peerId, chatLocation: chatLocation, isMyProfile: false, chatLocationContextHolder: chatLocationContextHolder),
@ -1395,9 +1431,11 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
hasSavedMessages,
hasSavedMessagesChats,
hasSavedMessageTags,
isPremiumRequiredForStoryPosting
isPremiumRequiredForStoryPosting,
starsRevenueContextAndState,
revenueContextAndState
)
|> map { peerView, availablePanes, globalNotificationSettings, status, currentInvitationsContext, invitations, currentRequestsContext, requests, hasStories, accountIsPremium, recommendedChannels, hasSavedMessages, hasSavedMessagesChats, hasSavedMessageTags, isPremiumRequiredForStoryPosting -> PeerInfoScreenData in
|> map { peerView, availablePanes, globalNotificationSettings, status, currentInvitationsContext, invitations, currentRequestsContext, requests, hasStories, accountIsPremium, recommendedChannels, hasSavedMessages, hasSavedMessagesChats, hasSavedMessageTags, isPremiumRequiredForStoryPosting, starsRevenueContextAndState, revenueContextAndState -> PeerInfoScreenData in
var availablePanes = availablePanes
if let hasStories {
if hasStories {
@ -1447,7 +1485,7 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
requestsStatePromise.set(requestsContext.state |> map(Optional.init))
}
}
return PeerInfoScreenData(
peer: peerView.peers[peerId],
chatPeer: peerView.peers[peerId],
@ -1477,8 +1515,9 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
isPremiumRequiredForStoryPosting: isPremiumRequiredForStoryPosting,
personalChannel: nil,
starsState: nil,
starsRevenueStatsState: nil,
starsRevenueStatsContext: nil
starsRevenueStatsState: starsRevenueContextAndState.1,
starsRevenueStatsContext: starsRevenueContextAndState.0,
revenueStatsState: revenueContextAndState.1
)
}
case let .group(groupId):
@ -1775,7 +1814,8 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
personalChannel: nil,
starsState: nil,
starsRevenueStatsState: nil,
starsRevenueStatsContext: nil
starsRevenueStatsContext: nil,
revenueStatsState: nil
))
}
}

View File

@ -126,7 +126,7 @@ protocol PeerInfoScreenItem: AnyObject {
class PeerInfoScreenItemNode: ASDisplayNode, AccessibilityFocusableNode {
var bringToFrontForHighlight: (() -> Void)?
func update(width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, hasCorners: Bool, transition: ContainedViewLayoutTransition) -> CGFloat {
func update(context: AccountContext, width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, hasCorners: Bool, transition: ContainedViewLayoutTransition) -> CGFloat {
preconditionFailure()
}
@ -165,7 +165,7 @@ private final class PeerInfoScreenItemSectionContainerNode: ASDisplayNode {
self.addSubnode(self.bottomSeparatorNode)
}
func update(width: CGFloat, safeInsets: UIEdgeInsets, hasCorners: Bool, presentationData: PresentationData, items: [PeerInfoScreenItem], transition: ContainedViewLayoutTransition) -> CGFloat {
func update(context: AccountContext, width: CGFloat, safeInsets: UIEdgeInsets, hasCorners: Bool, presentationData: PresentationData, items: [PeerInfoScreenItem], transition: ContainedViewLayoutTransition) -> CGFloat {
self.backgroundNode.backgroundColor = presentationData.theme.list.itemBlocksBackgroundColor
self.topSeparatorNode.backgroundColor = presentationData.theme.list.itemBlocksSeparatorColor
self.bottomSeparatorNode.backgroundColor = presentationData.theme.list.itemBlocksSeparatorColor
@ -217,7 +217,7 @@ private final class PeerInfoScreenItemSectionContainerNode: ASDisplayNode {
bottomItem = items[i + 1]
}
let itemHeight = itemNode.update(width: width, safeInsets: safeInsets, presentationData: presentationData, item: item, topItem: topItem, bottomItem: bottomItem, hasCorners: hasCorners, transition: itemTransition)
let itemHeight = itemNode.update(context: context, width: width, safeInsets: safeInsets, presentationData: presentationData, item: item, topItem: topItem, bottomItem: bottomItem, hasCorners: hasCorners, transition: itemTransition)
let itemFrame = CGRect(origin: CGPoint(x: 0.0, y: contentHeight), size: CGSize(width: width, height: itemHeight))
itemTransition.updateFrame(node: itemNode, frame: itemFrame)
if wasAdded {
@ -561,7 +561,7 @@ private final class PeerInfoInteraction {
let editingToggleMessageSignatures: (Bool) -> Void
let openParticipantsSection: (PeerInfoParticipantsSection) -> Void
let openRecentActions: () -> Void
let openStats: (Bool) -> Void
let openStats: (ChannelStatsSection) -> Void
let editingOpenPreHistorySetup: () -> Void
let editingOpenAutoremoveMesages: () -> Void
let openPermissions: () -> Void
@ -629,7 +629,7 @@ private final class PeerInfoInteraction {
editingToggleMessageSignatures: @escaping (Bool) -> Void,
openParticipantsSection: @escaping (PeerInfoParticipantsSection) -> Void,
openRecentActions: @escaping () -> Void,
openStats: @escaping (Bool) -> Void,
openStats: @escaping (ChannelStatsSection) -> Void,
editingOpenPreHistorySetup: @escaping () -> Void,
editingOpenAutoremoveMesages: @escaping () -> Void,
openPermissions: @escaping () -> Void,
@ -1444,6 +1444,31 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese
}))
items[.peerInfo]!.append(PeerInfoScreenCommentItem(id: 8, text: presentationData.strings.Bot_AddToChatInfo))
}
if let botInfo = user.botInfo, botInfo.flags.contains(.canEdit) {
let starsBalance = data.starsRevenueStatsState?.balances.availableBalance ?? 0
let overallStarsBalance = data.starsRevenueStatsState?.balances.overallRevenue ?? 0
if overallStarsBalance > 0 {
var string = ""
if overallStarsBalance > 0 {
string.append("*\(starsBalance)")
}
let attributedString = NSMutableAttributedString(string: string, font: Font.regular(presentationData.listsFontSize.itemListBaseFontSize), textColor: presentationData.theme.list.itemSecondaryTextColor)
if let range = attributedString.string.range(of: "*") {
attributedString.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .stars(tinted: false)), range: NSRange(range, in: attributedString.string))
attributedString.addAttribute(.baselineOffset, value: 1.5, range: NSRange(range, in: attributedString.string))
}
items[.peerMembers]!.append(PeerInfoScreenDisclosureItem(id: 9, label: .attributedText(attributedString), text: presentationData.strings.PeerInfo_Bot_Balance, icon: PresentationResourcesSettings.balance, action: {
interaction.editingOpenStars()
}))
}
items[.peerMembers]!.append(PeerInfoScreenDisclosureItem(id: 10, label: .none, text: presentationData.strings.Bot_Settings, icon: UIImage(bundleImageName: "Chat/Info/SettingsIcon"), action: {
interaction.openEditing()
}))
}
}
}
} else if let channel = data.peer as? TelegramChannel {
@ -1455,7 +1480,8 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese
let ItemAdmins = 6
let ItemMembers = 7
let ItemMemberRequests = 8
let ItemEdit = 9
let ItemBalance = 9
let ItemEdit = 10
if let _ = data.threadData {
let mainUsername: String
@ -1609,6 +1635,40 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese
}))
}
if cachedData.flags.contains(.canViewRevenue) || cachedData.flags.contains(.canViewStarsRevenue) {
let revenueBalance = data.revenueStatsState?.balances.availableBalance ?? 0
let starsBalance = data.starsRevenueStatsState?.balances.availableBalance ?? 0
let overallRevenueBalance = data.revenueStatsState?.balances.overallRevenue ?? 0
let overallStarsBalance = data.starsRevenueStatsState?.balances.overallRevenue ?? 0
if overallRevenueBalance > 0 || overallStarsBalance > 0 {
var string = ""
if overallRevenueBalance > 0 {
string.append("#\(revenueBalance)")
}
if overallStarsBalance > 0 {
if !string.isEmpty {
string.append(" ")
}
string.append("*\(starsBalance)")
}
let attributedString = NSMutableAttributedString(string: string, font: Font.regular(presentationData.listsFontSize.itemListBaseFontSize), textColor: presentationData.theme.list.itemSecondaryTextColor)
if let range = attributedString.string.range(of: "#") {
attributedString.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .ton), range: NSRange(range, in: attributedString.string))
attributedString.addAttribute(.baselineOffset, value: 1.5, range: NSRange(range, in: attributedString.string))
}
if let range = attributedString.string.range(of: "*") {
attributedString.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .stars(tinted: false)), range: NSRange(range, in: attributedString.string))
attributedString.addAttribute(.baselineOffset, value: 1.5, range: NSRange(range, in: attributedString.string))
}
items[.peerMembers]!.append(PeerInfoScreenDisclosureItem(id: ItemBalance, label: .attributedText(attributedString), text: presentationData.strings.PeerInfo_Bot_Balance, icon: PresentationResourcesSettings.balance, action: {
interaction.openStats(.monetization)
}))
}
}
items[.peerMembers]!.append(PeerInfoScreenDisclosureItem(id: ItemEdit, label: .none, text: presentationData.strings.Channel_Info_Settings, icon: UIImage(bundleImageName: "Chat/Info/SettingsIcon"), action: {
interaction.openEditing()
}))
@ -1721,7 +1781,6 @@ private func editingItems(data: PeerInfoScreenData?, state: PeerInfoState, chatL
let ItemInfo = 3
let ItemDelete = 4
let ItemUsername = 5
let ItemStars = 6
let ItemIntro = 7
let ItemCommands = 8
@ -1732,13 +1791,7 @@ private func editingItems(data: PeerInfoScreenData?, state: PeerInfoState, chatL
items[.peerDataSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemUsername, label: .text("@\(user.addressName ?? "")"), text: presentationData.strings.PeerInfo_Bot_Username, icon: PresentationResourcesSettings.bot, action: {
interaction.editingOpenPublicLinkSetup()
}))
if let starsRevenueStats = data.starsRevenueStatsState, starsRevenueStats.balances.overallRevenue > 0 {
items[.peerDataSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemStars, label: .text(presentationData.strings.PeerInfo_Bot_Balance_Stars(Int32(starsRevenueStats.balances.currentBalance))), text: presentationData.strings.PeerInfo_Bot_Balance, icon: PresentationResourcesSettings.stars, action: {
interaction.editingOpenStars()
}))
}
items[.peerSettings]!.append(PeerInfoScreenActionItem(id: ItemIntro, text: presentationData.strings.PeerInfo_Bot_EditIntro, icon: UIImage(bundleImageName: "Peer Info/BotIntro"), action: {
interaction.openPeerMention("botfather", .withBotStartPayload(ChatControllerInitialBotStart(payload: "\(user.addressName ?? "")-intro", behavior: .interactive)))
}))
@ -1959,7 +2012,7 @@ private func editingItems(data: PeerInfoScreenData?, state: PeerInfoState, chatL
if let cachedData = data.cachedData as? CachedChannelData, cachedData.flags.contains(.canViewStats) {
items[.peerAdditionalSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemStats, label: .none, text: presentationData.strings.Channel_Info_Stats, icon: UIImage(bundleImageName: "Chat/Info/StatsIcon"), action: {
interaction.openStats(false)
interaction.openStats(.stats)
}))
}
@ -2649,8 +2702,8 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
openRecentActions: { [weak self] in
self?.openRecentActions()
},
openStats: { [weak self] boosts in
self?.openStats(boosts: boosts)
openStats: { [weak self] section in
self?.openStats(section: section)
},
editingOpenPreHistorySetup: { [weak self] in
self?.editingOpenPreHistorySetup()
@ -6132,7 +6185,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
}, action: { [weak self] _, f in
f(.dismissWithoutContent)
self?.openStats()
self?.openStats(section: .stats)
})))
}
if cachedData.flags.contains(.translationHidden) {
@ -7820,7 +7873,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
self.controller?.push(PeerInfoStoryGridScreen(context: self.context, peerId: self.peerId, scope: .archive))
}
private func openStats(boosts: Bool = false, boostStatus: ChannelBoostStatus? = nil) {
private func openStats(section: ChannelStatsSection, boostStatus: ChannelBoostStatus? = nil) {
guard let controller = self.controller, let data = self.data, let peer = data.peer else {
return
}
@ -7830,7 +7883,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
if let channel = peer as? TelegramChannel, case .group = channel.info {
statsController = groupStatsController(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, peerId: peer.id)
} else {
statsController = channelStatsController(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, peerId: peer.id, section: boosts ? .boosts : .stats, boostStatus: boostStatus)
statsController = channelStatsController(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, peerId: peer.id, section: section, boostStatus: boostStatus)
}
controller.push(statsController)
}
@ -9732,7 +9785,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
}
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)
self.openStats(section: .boosts, boostStatus: boostStatus)
}
})
navigationController.pushViewController(controller)
@ -11132,7 +11185,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
contentHeight -= 16.0
}
}
let sectionHeight = sectionNode.update(width: sectionWidth, safeInsets: UIEdgeInsets(), hasCorners: !insets.left.isZero, presentationData: self.presentationData, items: sectionItems, transition: transition)
let sectionHeight = sectionNode.update(context: self.context, width: sectionWidth, safeInsets: UIEdgeInsets(), hasCorners: !insets.left.isZero, presentationData: self.presentationData, items: sectionItems, transition: transition)
let sectionFrame = CGRect(origin: CGPoint(x: insets.left, y: contentHeight), size: CGSize(width: sectionWidth, height: sectionHeight))
if additive {
transition.updateFrameAdditive(node: sectionNode, frame: sectionFrame)
@ -11191,9 +11244,9 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
self.editingSections[sectionId] = sectionNode
self.scrollNode.addSubnode(sectionNode)
}
let sectionWidth = layout.size.width - insets.left - insets.right
let sectionHeight = sectionNode.update(width: sectionWidth, safeInsets: UIEdgeInsets(), hasCorners: !insets.left.isZero, presentationData: self.presentationData, items: sectionItems, transition: transition)
let sectionHeight = sectionNode.update(context: self.context, width: sectionWidth, safeInsets: UIEdgeInsets(), hasCorners: !insets.left.isZero, presentationData: self.presentationData, items: sectionItems, transition: transition)
let sectionFrame = CGRect(origin: CGPoint(x: insets.left, y: contentHeight), size: CGSize(width: sectionWidth, height: sectionHeight))
if wasAdded {

View File

@ -2,6 +2,7 @@ import AsyncDisplayKit
import Display
import TelegramPresentationData
import ItemListUI
import AccountContext
final class PeerInfoScreenMultilineInputItem: PeerInfoScreenItem {
let id: AnyHashable
@ -53,7 +54,7 @@ final class PeerInfoScreenMultilineInputItemNode: PeerInfoScreenItemNode {
self.addSubnode(self.bottomSeparatorNode)
}
override func update(width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, hasCorners: Bool, transition: ContainedViewLayoutTransition) -> CGFloat {
override func update(context: AccountContext, width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, hasCorners: Bool, transition: ContainedViewLayoutTransition) -> CGFloat {
guard let item = item as? PeerInfoScreenMultilineInputItem else {
return 10.0
}

View File

@ -22,42 +22,42 @@ public final class GiftAvatarComponent: Component {
let theme: PresentationTheme
let peers: [EnginePeer]
let photo: TelegramMediaWebFile?
let starsPeer: StarsContext.State.Transaction.Peer?
let isVisible: Bool
let hasIdleAnimations: Bool
let hasScaleAnimation: Bool
let avatarSize: CGFloat
let color: UIColor?
let offset: CGFloat?
var hasLargeParticles: Bool
public init(
context: AccountContext,
theme: PresentationTheme,
peers: [EnginePeer],
photo: TelegramMediaWebFile? = nil,
starsPeer: StarsContext.State.Transaction.Peer? = nil,
isVisible: Bool,
hasIdleAnimations: Bool,
hasScaleAnimation: Bool = true,
avatarSize: CGFloat = 100.0,
color: UIColor? = nil,
offset: CGFloat? = nil
offset: CGFloat? = nil,
hasLargeParticles: Bool = false
) {
self.context = context
self.theme = theme
self.peers = peers
self.photo = photo
self.starsPeer = starsPeer
self.isVisible = isVisible
self.hasIdleAnimations = hasIdleAnimations
self.hasScaleAnimation = hasScaleAnimation
self.avatarSize = avatarSize
self.color = color
self.offset = offset
self.hasLargeParticles = hasLargeParticles
}
public static func ==(lhs: GiftAvatarComponent, rhs: GiftAvatarComponent) -> Bool {
return lhs.peers == rhs.peers && lhs.photo == rhs.photo && lhs.theme === rhs.theme && lhs.isVisible == rhs.isVisible && lhs.hasIdleAnimations == rhs.hasIdleAnimations && lhs.hasScaleAnimation == rhs.hasScaleAnimation && lhs.avatarSize == rhs.avatarSize && lhs.offset == rhs.offset
return lhs.peers == rhs.peers && lhs.photo == rhs.photo && lhs.theme === rhs.theme && lhs.isVisible == rhs.isVisible && lhs.hasIdleAnimations == rhs.hasIdleAnimations && lhs.hasScaleAnimation == rhs.hasScaleAnimation && lhs.avatarSize == rhs.avatarSize && lhs.offset == rhs.offset && lhs.hasLargeParticles == rhs.hasLargeParticles
}
public final class View: UIView, SCNSceneRendererDelegate, ComponentTaggedView {
@ -142,7 +142,7 @@ public final class GiftAvatarComponent: Component {
private var didSetup = false
private func setup() {
guard let scene = loadCompressedScene(name: "gift", version: sceneVersion), !self.didSetup else {
guard let scene = loadCompressedScene(name: "gift2", version: sceneVersion), !self.didSetup else {
return
}
@ -152,6 +152,21 @@ public final class GiftAvatarComponent: Component {
self.sceneView.delegate = self
if let color = self.component?.color {
// let names: [String] = [
// "particles_left",
// "particles_right",
// "particles_left_bottom",
// "particles_right_bottom",
// "particles_center"
// ]
//
// for name in names {
// if let node = scene.rootNode.childNode(withName: name, recursively: false), let particleSystem = node.particleSystems?.first {
// particleSystem.particleColor = color
// particleSystem.particleColorVariation = SCNVector4Make(0, 0, 0, 0)
// }
// }
let names: [String] = [
"particles_left",
"particles_right",
@ -160,10 +175,59 @@ public final class GiftAvatarComponent: Component {
"particles_center"
]
let starNames: [String] = [
"coins_left",
"coins_right"
]
let particleColor = color
for name in starNames {
if let node = scene.rootNode.childNode(withName: name, recursively: false), let particleSystem = node.particleSystems?.first {
particleSystem.particleIntensity = 1.0
particleSystem.particleIntensityVariation = 0.05
particleSystem.particleColor = particleColor
particleSystem.particleColorVariation = SCNVector4Make(0.07, 0.0, 0.1, 0.0)
node.isHidden = false
if let propertyControllers = particleSystem.propertyControllers, let sizeController = propertyControllers[.size], let colorController = propertyControllers[.color] {
let animation = CAKeyframeAnimation()
if let existing = colorController.animation as? CAKeyframeAnimation {
animation.keyTimes = existing.keyTimes
animation.values = existing.values?.compactMap { ($0 as? UIColor)?.alpha } ?? []
} else {
animation.values = [ 0.0, 1.0, 1.0, 0.0 ]
}
let opacityController = SCNParticlePropertyController(animation: animation)
particleSystem.propertyControllers = [
.size: sizeController,
.opacity: opacityController
]
}
}
}
for name in names {
if let node = scene.rootNode.childNode(withName: name, recursively: false), let particleSystem = node.particleSystems?.first {
particleSystem.particleColor = color
particleSystem.particleColorVariation = SCNVector4Make(0, 0, 0, 0)
particleSystem.particleIntensity = min(1.0, 2.0 * particleSystem.particleIntensity)
particleSystem.particleIntensityVariation = 0.05
particleSystem.particleColor = particleColor
particleSystem.particleColorVariation = SCNVector4Make(0.1, 0.0, 0.12, 0.0)
if let propertyControllers = particleSystem.propertyControllers, let sizeController = propertyControllers[.size], let colorController = propertyControllers[.color] {
let animation = CAKeyframeAnimation()
if let existing = colorController.animation as? CAKeyframeAnimation {
animation.keyTimes = existing.keyTimes
animation.values = existing.values?.compactMap { ($0 as? UIColor)?.alpha } ?? []
} else {
animation.values = [ 0.0, 1.0, 1.0, 0.0 ]
}
let opacityController = SCNParticlePropertyController(animation: animation)
particleSystem.propertyControllers = [
.size: sizeController,
.opacity: opacityController
]
}
}
}
@ -187,9 +251,7 @@ public final class GiftAvatarComponent: Component {
}
}
private func onReady() {
self.setupScaleAnimation()
private func onReady() {
self.playAppearanceAnimation(explode: true)
self.previousInteractionTimestamp = CACurrentMediaTime()
@ -203,23 +265,7 @@ public final class GiftAvatarComponent: Component {
}, queue: Queue.mainQueue())
self.timer?.start()
}
private func setupScaleAnimation() {
guard self.component?.hasScaleAnimation == true else {
return
}
let animation = CABasicAnimation(keyPath: "transform.scale")
animation.duration = 2.0
animation.fromValue = 1.0
animation.toValue = 1.15
animation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
animation.autoreverses = true
animation.repeatCount = .infinity
self.avatarNode.view.layer.add(animation, forKey: "scale")
}
private func playAppearanceAnimation(velocity: CGFloat? = nil, smallAngle: Bool = false, mirror: Bool = false, explode: Bool = false) {
guard let scene = self.sceneView.scene else {
return
@ -319,6 +365,10 @@ public final class GiftAvatarComponent: Component {
self.hasIdleAnimations = component.hasIdleAnimations
if let _ = component.color {
self.sceneView.backgroundColor = component.theme.list.blocksBackgroundColor
}
if let photo = component.photo {
let imageNode: TransformImageNode
if let current = self.imageNode {
@ -339,86 +389,6 @@ public final class GiftAvatarComponent: Component {
imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(radius: imageSize.width / 2.0), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets(), emptyColor: component.theme.list.mediaPlaceholderColor))()
self.avatarNode.isHidden = true
} else if let starsPeer = component.starsPeer {
let iconBackgroundView: UIImageView
let iconView: UIImageView
if let currentBackground = self.iconBackgroundView, let current = self.iconView {
iconBackgroundView = currentBackground
iconView = current
} else {
iconBackgroundView = UIImageView()
iconView = UIImageView()
self.addSubview(iconBackgroundView)
self.addSubview(iconView)
self.iconBackgroundView = iconBackgroundView
self.iconView = iconView
let size = CGSize(width: component.avatarSize, height: component.avatarSize)
var iconInset: CGFloat = 9.0
var iconOffset: CGFloat = 0.0
switch starsPeer {
case .appStore:
iconBackgroundView.image = generateGradientFilledCircleImage(
diameter: size.width,
colors: [
UIColor(rgb: 0x2a9ef1).cgColor,
UIColor(rgb: 0x72d5fd).cgColor
],
direction: .mirroredDiagonal
)
iconView.image = UIImage(bundleImageName: "Premium/Stars/Apple")
case .playMarket:
iconBackgroundView.image = generateGradientFilledCircleImage(
diameter: size.width,
colors: [
UIColor(rgb: 0x54cb68).cgColor,
UIColor(rgb: 0xa0de7e).cgColor
],
direction: .mirroredDiagonal
)
iconView.image = UIImage(bundleImageName: "Premium/Stars/Google")
case .fragment:
iconBackgroundView.image = generateFilledCircleImage(diameter: size.width, color: UIColor(rgb: 0x1b1f24))
iconView.image = UIImage(bundleImageName: "Premium/Stars/Fragment")
iconOffset = 5.0
case .ads:
iconBackgroundView.image = generateFilledCircleImage(diameter: size.width, color: UIColor(rgb: 0x1b1f24))
iconView.image = UIImage(bundleImageName: "Premium/Stars/Fragment")
iconOffset = 5.0
case .premiumBot:
iconInset = 15.0
iconBackgroundView.image = generateGradientFilledCircleImage(
diameter: size.width,
colors: [
UIColor(rgb: 0x6b93ff).cgColor,
UIColor(rgb: 0x6b93ff).cgColor,
UIColor(rgb: 0x8d77ff).cgColor,
UIColor(rgb: 0xb56eec).cgColor,
UIColor(rgb: 0xb56eec).cgColor
],
direction: .mirroredDiagonal
)
iconView.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Media/EntityInputPremiumIcon"), color: .white)
case .peer, .unsupported:
iconInset = 15.0
iconBackgroundView.image = generateGradientFilledCircleImage(
diameter: size.width,
colors: [
UIColor(rgb: 0xb1b1b1).cgColor,
UIColor(rgb: 0xcdcdcd).cgColor
],
direction: .mirroredDiagonal
)
iconView.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Media/EntityInputPremiumIcon"), color: .white)
}
let imageFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - size.width) / 2.0), y: 113.0 - size.height / 2.0), size: size)
iconBackgroundView.frame = imageFrame
iconView.frame = imageFrame.insetBy(dx: iconInset, dy: iconInset).offsetBy(dx: 0.0, dy: iconOffset)
}
} else if component.peers.count > 1 {
let avatarSize = CGSize(width: 60.0, height: 60.0)

View File

@ -88,15 +88,17 @@ public final class SliderComponent: Component {
if let isTrackingUpdated = component.isTrackingUpdated {
internalIsTrackingUpdated = { [weak self] isTracking in
if let self {
if isTracking {
self.sliderView?.bordered = true
} else {
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.1, execute: { [weak self] in
self?.sliderView?.bordered = false
})
if !"".isEmpty {
if isTracking {
self.sliderView?.bordered = true
} else {
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.1, execute: { [weak self] in
self?.sliderView?.bordered = false
})
}
}
isTrackingUpdated(isTracking)
}
isTrackingUpdated(isTracking)
}
}

View File

@ -65,7 +65,7 @@ public final class StarsAvatarComponent: Component {
private weak var state: EmptyComponentState?
override init(frame: CGRect) {
self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 16.0))
self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 20.0))
super.init(frame: frame)

View File

@ -22,6 +22,8 @@ swift_library(
"//submodules/AvatarNode",
"//submodules/AccountContext",
"//submodules/InvisibleInkDustNode",
"//submodules/AnimatedStickerNode",
"//submodules/TelegramAnimatedStickerNode",
],
visibility = [
"//visibility:public",

View File

@ -11,6 +11,8 @@ import PhotoResources
import AvatarNode
import AccountContext
import InvisibleInkDustNode
import AnimatedStickerNode
import TelegramAnimatedStickerNode
final class StarsParticlesView: UIView {
private struct Particle {
@ -251,6 +253,7 @@ public final class StarsImageComponent: Component {
case media([AnyMediaReference])
case extendedMedia([TelegramExtendedMedia])
case transactionPeer(StarsContext.State.Transaction.Peer)
case gift(Int64)
public static func == (lhs: StarsImageComponent.Subject, rhs: StarsImageComponent.Subject) -> Bool {
switch lhs {
@ -284,6 +287,12 @@ public final class StarsImageComponent: Component {
} else {
return false
}
case let .gift(lhsCount):
if case let .gift = rhs(rhsCount) {
return true
} else {
return false
}
}
}
}
@ -347,6 +356,8 @@ public final class StarsImageComponent: Component {
private var dustNode: MediaDustNode?
private var button: UIControl?
private var animationNode: AnimatedStickerNode?
private var lockView: UIImageView?
private var countView = ComponentView<Empty>()
@ -776,6 +787,31 @@ public final class StarsImageComponent: Component {
iconBackgroundView.frame = imageFrame
iconView.frame = imageFrame.insetBy(dx: iconInset, dy: iconInset).offsetBy(dx: 0.0, dy: iconOffset)
}
case let .gift(count):
let animationNode: AnimatedStickerNode
if let current = self.animationNode {
animationNode = current
} else {
let stickerName: String
if count <= 1000 {
stickerName = "Gift3"
} else if count < 2500 {
stickerName = "Gift6"
} else {
stickerName = "Gift12"
}
animationNode = DefaultAnimatedStickerNodeImpl()
animationNode.autoplay = true
animationNode.setup(source: AnimatedStickerNodeLocalFileSource(name: stickerName), width: 384, height: 384, playbackMode: .still(.end), mode: .direct(cachePathPrefix: nil))
animationNode.visibility = true
containerNode.view.addSubview(animationNode.view)
self.animationNode = animationNode
animationNode.playOnce()
}
let animationFrame = imageFrame.insetBy(dx: -imageFrame.width * 0.19, dy: -imageFrame.height * 0.19).offsetBy(dx: 0.0, dy: -14.0)
animationNode.frame = animationFrame
animationNode.updateLayout(size: animationFrame.size)
}
if let _ = component.action {

View File

@ -27,9 +27,32 @@ import BundleIconComponent
import ConfettiEffect
private struct StarsProduct: Equatable {
let option: StarsTopUpOption
enum Option: Equatable {
case topUp(StarsTopUpOption)
case gift(StarsGiftOption)
}
let option: Option
let storeProduct: InAppPurchaseManager.Product
var count: Int64 {
switch self.option {
case let .topUp(option):
return option.count
case let .gift(option):
return option.count
}
}
var isExtended: Bool {
switch self.option {
case let .topUp(option):
return option.isExtended
case let .gift(option):
return option.isExtended
}
}
var id: String {
return self.storeProduct.id
}
@ -54,13 +77,13 @@ private final class StarsPurchaseScreenContentComponent: CombinedComponent {
let externalState: ExternalState
let containerSize: CGSize
let balance: Int64?
let options: [StarsTopUpOption]
let peerId: EnginePeer.Id?
let requiredStars: Int64?
let options: [Any]
let purpose: StarsPurchasePurpose
let selectedProductId: String?
let forceDark: Bool
let products: [StarsProduct]?
let expanded: Bool
let peers: [EnginePeer.Id: EnginePeer]
let stateUpdated: (ComponentTransition) -> Void
let buy: (StarsProduct) -> Void
@ -69,13 +92,13 @@ private final class StarsPurchaseScreenContentComponent: CombinedComponent {
externalState: ExternalState,
containerSize: CGSize,
balance: Int64?,
options: [StarsTopUpOption],
peerId: EnginePeer.Id?,
requiredStars: Int64?,
options: [Any],
purpose: StarsPurchasePurpose,
selectedProductId: String?,
forceDark: Bool,
products: [StarsProduct]?,
expanded: Bool,
peers: [EnginePeer.Id: EnginePeer],
stateUpdated: @escaping (ComponentTransition) -> Void,
buy: @escaping (StarsProduct) -> Void
) {
@ -84,12 +107,12 @@ private final class StarsPurchaseScreenContentComponent: CombinedComponent {
self.containerSize = containerSize
self.balance = balance
self.options = options
self.peerId = peerId
self.requiredStars = requiredStars
self.purpose = purpose
self.selectedProductId = selectedProductId
self.forceDark = forceDark
self.products = products
self.expanded = expanded
self.peers = peers
self.stateUpdated = stateUpdated
self.buy = buy
}
@ -101,13 +124,7 @@ private final class StarsPurchaseScreenContentComponent: CombinedComponent {
if lhs.containerSize != rhs.containerSize {
return false
}
if lhs.options != rhs.options {
return false
}
if lhs.peerId != rhs.peerId {
return false
}
if lhs.requiredStars != rhs.requiredStars {
if lhs.purpose != rhs.purpose {
return false
}
if lhs.selectedProductId != rhs.selectedProductId {
@ -122,6 +139,9 @@ private final class StarsPurchaseScreenContentComponent: CombinedComponent {
if lhs.expanded != rhs.expanded {
return false
}
if lhs.peers != rhs.peers {
return false
}
return true
}
@ -129,31 +149,18 @@ private final class StarsPurchaseScreenContentComponent: CombinedComponent {
private let context: AccountContext
var products: [StarsProduct]?
var peer: EnginePeer?
private var disposable: Disposable?
var cachedChevronImage: (UIImage, PresentationTheme)?
init(
context: AccountContext,
peerId: EnginePeer.Id?
purpose: StarsPurchasePurpose
) {
self.context = context
super.init()
if let peerId {
self.disposable = (context.engine.data.subscribe(
TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)
)
|> deliverOnMainQueue).start(next: { [weak self] peer in
if let self, let peer {
self.peer = peer
self.updated(transition: .immediate)
}
})
}
let _ = updatePremiumPromoConfigurationOnce(account: context.account).start()
}
deinit {
@ -162,63 +169,32 @@ private final class StarsPurchaseScreenContentComponent: CombinedComponent {
}
func makeState() -> State {
return State(context: self.context, peerId: self.peerId)
return State(context: self.context, purpose: self.purpose)
}
static var body: Body {
// let overscroll = Child(Rectangle.self)
// let fade = Child(RoundedRectangle.self)
let text = Child(BalancedTextComponent.self)
let list = Child(VStack<Empty>.self)
let termsText = Child(BalancedTextComponent.self)
return { context in
let sideInset: CGFloat = 16.0
let component = context.component
let scrollEnvironment = context.environment[ScrollChildEnvironment.self].value
let environment = context.environment[ViewControllerComponentContainer.Environment.self].value
let state = context.state
state.products = context.component.products
state.products = component.products
let theme = environment.theme
let strings = environment.strings
let presentationData = context.component.context.sharedContext.currentPresentationData.with { $0 }
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
let availableWidth = context.availableSize.width
let sideInsets = sideInset * 2.0 + environment.safeInsets.left + environment.safeInsets.right
var size = CGSize(width: context.availableSize.width, height: 0.0)
// var topBackgroundColor = theme.list.plainBackgroundColor
// let bottomBackgroundColor = theme.list.blocksBackgroundColor
// if theme.overallDarkAppearance {
// topBackgroundColor = bottomBackgroundColor
// }
//
// let overscroll = overscroll.update(
// component: Rectangle(color: topBackgroundColor),
// availableSize: CGSize(width: context.availableSize.width, height: 1000),
// transition: context.transition
// )
// context.add(overscroll
// .position(CGPoint(x: overscroll.size.width / 2.0, y: -overscroll.size.height / 2.0))
// )
//
// let fade = fade.update(
// component: RoundedRectangle(
// colors: [
// topBackgroundColor,
// bottomBackgroundColor
// ],
// cornerRadius: 0.0,
// gradientDirection: .vertical
// ),
// availableSize: CGSize(width: availableWidth, height: 300),
// transition: context.transition
// )
// context.add(fade
// .position(CGPoint(x: fade.size.width / 2.0, y: fade.size.height / 2.0))
// )
size.height += 183.0 + 10.0 + environment.navigationHeight - 56.0
let textColor = theme.list.itemPrimaryTextColor
@ -228,22 +204,36 @@ private final class StarsPurchaseScreenContentComponent: CombinedComponent {
let boldTextFont = Font.semibold(15.0)
let textString: String
if let _ = context.component.requiredStars {
textString = state.peer == nil ? strings.Stars_Purchase_StarsNeededUnlockInfo : strings.Stars_Purchase_StarsNeededInfo(state.peer?.compactDisplayTitle ?? "").string
} else {
switch context.component.purpose {
case .generic:
textString = strings.Stars_Purchase_GetStarsInfo
case .gift:
textString = strings.Stars_Purchase_GiftInfo(component.peers.first?.value.compactDisplayTitle ?? "").string
case .transfer:
textString = strings.Stars_Purchase_StarsNeededInfo(component.peers.first?.value.compactDisplayTitle ?? "").string
case let .subscription(_, _, renew):
textString = renew ? strings.Stars_Purchase_SubscriptionRenewInfo(component.peers.first?.value.compactDisplayTitle ?? "").string : strings.Stars_Purchase_SubscriptionInfo(component.peers.first?.value.compactDisplayTitle ?? "").string
case .unlockMedia:
textString = strings.Stars_Purchase_StarsNeededUnlockInfo
}
let markdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor), link: MarkdownAttributeSet(font: textFont, textColor: accentColor), linkAttribute: { contents in
return (TelegramTextAttributes.URL, contents)
})
if state.cachedChevronImage == nil || state.cachedChevronImage?.1 !== theme {
state.cachedChevronImage = (generateTintedImage(image: UIImage(bundleImageName: "Settings/TextArrowRight"), color: accentColor)!, theme)
}
let titleAttributedString = parseMarkdownIntoAttributedString(textString, attributes: markdownAttributes).mutableCopy() as! NSMutableAttributedString
if let range = titleAttributedString.string.range(of: ">"), let chevronImage = state.cachedChevronImage?.0 {
titleAttributedString.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: titleAttributedString.string))
}
let text = text.update(
component: BalancedTextComponent(
text: .markdown(
text: textString,
attributes: markdownAttributes
),
text: .plain(titleAttributedString),
horizontalAlignment: .center,
maximumNumberOfLines: 0,
lineSpacing: 0.2,
@ -271,16 +261,7 @@ private final class StarsPurchaseScreenContentComponent: CombinedComponent {
size.height += 21.0
context.component.externalState.descriptionHeight = text.size.height
let initialValues: [Int64] = [
15,
75,
250,
500,
1000,
2500
]
let stars: [Int64: Int] = [
15: 1,
75: 2,
@ -312,21 +293,21 @@ private final class StarsPurchaseScreenContentComponent: CombinedComponent {
if let products = state.products, let balance = context.component.balance {
var minimumCount: Int64?
if let requiredStars = context.component.requiredStars {
if let requiredStars = context.component.purpose.requiredStars {
minimumCount = requiredStars - balance
}
for product in products {
if let minimumCount, minimumCount > product.option.count && !(items.isEmpty && product.id == products.last?.id) {
if let minimumCount, minimumCount > product.count && !(items.isEmpty && product.id == products.last?.id) {
continue
}
if let _ = minimumCount, items.isEmpty {
} else if !context.component.expanded && !initialValues.contains(product.option.count) {
} else if !context.component.expanded && product.isExtended {
continue
}
let title = strings.Stars_Purchase_Stars(Int32(product.option.count))
let title = strings.Stars_Purchase_Stars(Int32(product.count))
let price = product.price
let titleComponent = AnyComponent(MultilineTextComponent(
@ -360,7 +341,7 @@ private final class StarsPurchaseScreenContentComponent: CombinedComponent {
title: titleComponent,
contentInsets: UIEdgeInsets(top: 12.0, left: -6.0, bottom: 12.0, right: 0.0),
leftIcon: .custom(AnyComponentWithIdentity(id: 0, component: AnyComponent(StarsIconComponent(
count: stars[product.option.count] ?? 1
count: stars[product.count] ?? 1
))), true),
accessory: .custom(ListActionItemComponent.CustomAccessory(component: AnyComponentWithIdentity(id: 0, component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
@ -445,7 +426,6 @@ private final class StarsPurchaseScreenContentComponent: CombinedComponent {
})
let textSideInset: CGFloat = 16.0
let component = context.component
let termsText = termsText.update(
component: BalancedTextComponent(
text: .markdown(text: strings.Stars_Purchase_Info, attributes: termsMarkdownAttributes),
@ -490,9 +470,8 @@ private final class StarsPurchaseScreenComponent: CombinedComponent {
let context: AccountContext
let starsContext: StarsContext
let options: [StarsTopUpOption]
let peerId: EnginePeer.Id?
let requiredStars: Int64?
let options: [Any]
let purpose: StarsPurchasePurpose
let forceDark: Bool
let updateInProgress: (Bool) -> Void
let present: (ViewController) -> Void
@ -501,9 +480,8 @@ private final class StarsPurchaseScreenComponent: CombinedComponent {
init(
context: AccountContext,
starsContext: StarsContext,
options: [StarsTopUpOption],
peerId: EnginePeer.Id?,
requiredStars: Int64?,
options: [Any],
purpose: StarsPurchasePurpose,
forceDark: Bool,
updateInProgress: @escaping (Bool) -> Void,
present: @escaping (ViewController) -> Void,
@ -512,8 +490,7 @@ private final class StarsPurchaseScreenComponent: CombinedComponent {
self.context = context
self.starsContext = starsContext
self.options = options
self.peerId = peerId
self.requiredStars = requiredStars
self.purpose = purpose
self.forceDark = forceDark
self.updateInProgress = updateInProgress
self.present = present
@ -527,13 +504,7 @@ private final class StarsPurchaseScreenComponent: CombinedComponent {
if lhs.starsContext !== rhs.starsContext {
return false
}
if lhs.options != rhs.options {
return false
}
if lhs.peerId != rhs.peerId {
return false
}
if lhs.requiredStars != rhs.requiredStars {
if lhs.purpose != rhs.purpose {
return false
}
if lhs.forceDark != rhs.forceDark {
@ -544,6 +515,8 @@ private final class StarsPurchaseScreenComponent: CombinedComponent {
final class State: ComponentState {
private let context: AccountContext
private let purpose: StarsPurchasePurpose
private let updateInProgress: (Bool) -> Void
private let present: (ViewController) -> Void
private let completion: (Int64) -> Void
@ -554,11 +527,11 @@ private final class StarsPurchaseScreenComponent: CombinedComponent {
var hasIdleAnimations = true
var progressProduct: StarsProduct?
private(set) var promoConfiguration: PremiumPromoConfiguration?
private(set) var products: [StarsProduct]?
private(set) var starsState: StarsContext.State?
var peers: [EnginePeer.Id: EnginePeer] = [:]
let animationCache: AnimationCache
let animationRenderer: MultiAnimationRenderer
@ -569,12 +542,14 @@ private final class StarsPurchaseScreenComponent: CombinedComponent {
init(
context: AccountContext,
starsContext: StarsContext,
initialOptions: [StarsTopUpOption],
purpose: StarsPurchasePurpose,
initialOptions: [Any],
updateInProgress: @escaping (Bool) -> Void,
present: @escaping (ViewController) -> Void,
completion: @escaping (Int64) -> Void
) {
self.context = context
self.purpose = purpose
self.updateInProgress = updateInProgress
self.present = present
self.completion = completion
@ -590,32 +565,65 @@ private final class StarsPurchaseScreenComponent: CombinedComponent {
} else {
availableProducts = .single([])
}
let options: Signal<[StarsTopUpOption], NoError>
if !initialOptions.isEmpty {
options = .single(initialOptions)
} else {
options = .single([]) |> then(context.engine.payments.starsTopUpOptions())
let products: Signal<[StarsProduct], NoError>
switch purpose {
case .gift:
let options: Signal<[StarsGiftOption], NoError>
if !initialOptions.isEmpty, let initialGiftOptions = initialOptions as? [StarsGiftOption] {
options = .single(initialGiftOptions)
} else {
options = .single([]) |> then(context.engine.payments.starsGiftOptions(peerId: nil))
}
products = combineLatest(availableProducts, options)
|> map { availableProducts, options in
var products: [StarsProduct] = []
for option in options {
if let product = availableProducts.first(where: { $0.id == option.storeProductId }) {
products.append(StarsProduct(option: .gift(option), storeProduct: product))
}
}
return products
}
default:
let options: Signal<[StarsTopUpOption], NoError>
if !initialOptions.isEmpty, let initialTopUpOptions = initialOptions as? [StarsTopUpOption] {
options = .single(initialTopUpOptions)
} else {
options = .single([]) |> then(context.engine.payments.starsTopUpOptions())
}
products = combineLatest(availableProducts, options)
|> map { availableProducts, options in
var products: [StarsProduct] = []
for option in options {
if let product = availableProducts.first(where: { $0.id == option.storeProductId }) {
products.append(StarsProduct(option: .topUp(option), storeProduct: product))
}
}
return products
}
}
let peerIds = purpose.peerIds
self.disposable = combineLatest(
queue: Queue.mainQueue(),
availableProducts,
options,
starsContext.state
).start(next: { [weak self] availableProducts, options, starsState in
products,
starsContext.state,
context.engine.data.get(EngineDataMap(peerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init(id:))))
).start(next: { [weak self] products, starsState, result in
guard let self else {
return
}
var products: [StarsProduct] = []
for option in options {
if let product = availableProducts.first(where: { $0.id == option.storeProductId }) {
products.append(StarsProduct(option: option, storeProduct: product))
self.products = products.sorted(by: { $0.count < $1.count })
self.starsState = starsState
var peers: [EnginePeer.Id: EnginePeer] = [:]
for peerId in peerIds {
if let maybePeer = result[peerId], let peer = maybePeer {
peers[peerId] = peer
}
}
self.products = products.sorted(by: { $0.option.count < $1.option.count })
self.starsState = starsState
self.peers = peers
self.updated(transition: .immediate)
})
@ -636,7 +644,13 @@ private final class StarsPurchaseScreenComponent: CombinedComponent {
self.updated(transition: .easeInOut(duration: 0.2))
let (currency, amount) = product.storeProduct.priceCurrencyAndAmount
let purpose: AppStoreTransactionPurpose = .stars(count: product.option.count, currency: currency, amount: amount)
let purpose: AppStoreTransactionPurpose
switch self.purpose {
case let .gift(peerId):
purpose = .starsGift(peerId: peerId, count: product.count, currency: currency, amount: amount)
default:
purpose = .stars(count: product.count, currency: currency, amount: amount)
}
let _ = (self.context.engine.payments.canPurchasePremium(purpose: purpose)
|> deliverOnMainQueue).start(next: { [weak self] available in
@ -649,7 +663,7 @@ private final class StarsPurchaseScreenComponent: CombinedComponent {
self.updateInProgress(false)
self.updated(transition: .easeInOut(duration: 0.2))
self.completion(product.option.count)
self.completion(product.count)
}
}, error: { [weak self] error in
if let strongSelf = self {
@ -699,13 +713,14 @@ private final class StarsPurchaseScreenComponent: CombinedComponent {
}
func makeState() -> State {
return State(context: self.context, starsContext: self.starsContext, initialOptions: self.options, updateInProgress: self.updateInProgress, present: self.present, completion: self.completion)
return State(context: self.context, starsContext: self.starsContext, purpose: self.purpose, initialOptions: self.options, updateInProgress: self.updateInProgress, present: self.present, completion: self.completion)
}
static var body: Body {
let background = Child(Rectangle.self)
let scrollContent = Child(ScrollComponent<EnvironmentType>.self)
let star = Child(PremiumStarComponent.self)
let avatar = Child(GiftAvatarComponent.self)
let topPanel = Child(BlurredBackgroundComponent.self)
let topSeparator = Child(Rectangle.self)
let title = Child(MultilineTextComponent.self)
@ -730,23 +745,44 @@ private final class StarsPurchaseScreenComponent: CombinedComponent {
starIsVisible = false
}
let header = star.update(
component: PremiumStarComponent(
theme: environment.theme,
isIntro: true,
isVisible: starIsVisible,
hasIdleAnimations: state.hasIdleAnimations,
colors: [
UIColor(rgb: 0xe57d02),
UIColor(rgb: 0xf09903),
UIColor(rgb: 0xf9b004),
UIColor(rgb: 0xfdd219)
],
particleColor: UIColor(rgb: 0xf9b004)
),
availableSize: CGSize(width: min(414.0, context.availableSize.width), height: 220.0),
transition: context.transition
)
let header: _UpdatedChildComponent
if case let .gift(peerId) = context.component.purpose {
var peers: [EnginePeer] = []
if let peer = state.peers[peerId] {
peers.append(peer)
}
header = avatar.update(
component: GiftAvatarComponent(
context: context.component.context,
theme: environment.theme,
peers: peers,
isVisible: starIsVisible,
hasIdleAnimations: state.hasIdleAnimations,
color: UIColor(rgb: 0xf9b004),
hasLargeParticles: true
),
availableSize: CGSize(width: min(414.0, context.availableSize.width), height: 220.0),
transition: context.transition
)
} else {
header = star.update(
component: PremiumStarComponent(
theme: environment.theme,
isIntro: true,
isVisible: starIsVisible,
hasIdleAnimations: state.hasIdleAnimations,
colors: [
UIColor(rgb: 0xe57d02),
UIColor(rgb: 0xf09903),
UIColor(rgb: 0xf9b004),
UIColor(rgb: 0xfdd219)
],
particleColor: UIColor(rgb: 0xf9b004)
),
availableSize: CGSize(width: min(414.0, context.availableSize.width), height: 220.0),
transition: context.transition
)
}
let topPanel = topPanel.update(
component: BlurredBackgroundComponent(
@ -765,10 +801,13 @@ private final class StarsPurchaseScreenComponent: CombinedComponent {
)
let titleText: String
if let requiredStars = context.component.requiredStars {
titleText = strings.Stars_Purchase_StarsNeeded(Int32(requiredStars))
} else {
switch context.component.purpose {
case .generic:
titleText = strings.Stars_Purchase_GetStars
case .gift:
titleText = strings.Stars_Purchase_GiftStars
case let .transfer(_, requiredStars), let .subscription(_, requiredStars, _), let .unlockMedia(requiredStars):
titleText = strings.Stars_Purchase_StarsNeeded(Int32(requiredStars))
}
let title = title.update(
@ -820,12 +859,12 @@ private final class StarsPurchaseScreenComponent: CombinedComponent {
containerSize: context.availableSize,
balance: state.starsState?.balance,
options: context.component.options,
peerId: context.component.peerId,
requiredStars: context.component.requiredStars,
purpose: context.component.purpose,
selectedProductId: state.progressProduct?.storeProduct.id,
forceDark: context.component.forceDark,
products: state.products,
expanded: state.isExpanded,
peers: state.peers,
stateUpdated: { [weak state] transition in
scrollAction.invoke(CGPoint(x: 0.0, y: 150.0 + contentExternalState.descriptionHeight))
state?.isExpanded = true
@ -929,7 +968,6 @@ private final class StarsPurchaseScreenComponent: CombinedComponent {
public final class StarsPurchaseScreen: ViewControllerComponentContainer {
fileprivate let context: AccountContext
fileprivate let starsContext: StarsContext
fileprivate let options: [StarsTopUpOption]
private var didSetReady = false
private let _ready = Promise<Bool>()
@ -940,16 +978,12 @@ public final class StarsPurchaseScreen: ViewControllerComponentContainer {
public init(
context: AccountContext,
starsContext: StarsContext,
options: [StarsTopUpOption],
peerId: EnginePeer.Id?,
requiredStars: Int64?,
modal: Bool = true,
forceDark: Bool = false,
options: [Any] = [],
purpose: StarsPurchasePurpose,
completion: @escaping (Int64) -> Void = { _ in }
) {
self.context = context
self.starsContext = starsContext
self.options = options
var updateInProgressImpl: ((Bool) -> Void)?
var presentImpl: ((ViewController) -> Void)?
@ -958,9 +992,8 @@ public final class StarsPurchaseScreen: ViewControllerComponentContainer {
context: context,
starsContext: starsContext,
options: options,
peerId: peerId,
requiredStars: requiredStars,
forceDark: forceDark,
purpose: purpose,
forceDark: false,
updateInProgress: { inProgress in
updateInProgressImpl?(inProgress)
},
@ -970,17 +1003,13 @@ public final class StarsPurchaseScreen: ViewControllerComponentContainer {
completion: { stars in
completionImpl?(stars)
}
), navigationBarAppearance: .transparent, presentationMode: modal ? .modal : .default, theme: forceDark ? .dark : .default)
), navigationBarAppearance: .transparent, presentationMode: .modal, theme: .default)
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
if modal {
let cancelItem = UIBarButtonItem(title: presentationData.strings.Common_Close, style: .plain, target: self, action: #selector(self.cancelPressed))
self.navigationItem.setLeftBarButton(cancelItem, animated: false)
self.navigationPresentation = .modal
} else {
self.navigationPresentation = .modalInLargeLayout
}
let cancelItem = UIBarButtonItem(title: presentationData.strings.Common_Close, style: .plain, target: self, action: #selector(self.cancelPressed))
self.navigationItem.setLeftBarButton(cancelItem, animated: false)
self.navigationPresentation = .modal
updateInProgressImpl = { [weak self] inProgress in
if let strongSelf = self {
@ -1043,6 +1072,9 @@ public final class StarsPurchaseScreen: ViewControllerComponentContainer {
if let view = self.node.hostView.findTaggedView(tag: PremiumStarComponent.View.Tag()) as? PremiumStarComponent.View {
self.didSetReady = true
self._ready.set(view.ready)
} else if let view = self.node.hostView.findTaggedView(tag: GiftAvatarComponent.View.Tag()) as? GiftAvatarComponent.View {
self.didSetReady = true
self._ready.set(view.ready)
}
}
}
@ -1141,3 +1173,31 @@ final class StarsIconComponent: CombinedComponent {
}
}
}
private extension StarsPurchasePurpose {
var peerIds: [EnginePeer.Id] {
switch self {
case let .gift(peerId):
return [peerId]
case let .transfer(peerId, _):
return [peerId]
case let .subscription(peerId, _, _):
return [peerId]
default:
return []
}
}
var requiredStars: Int64? {
switch self {
case let .transfer(_, requiredStars):
return requiredStars
case let .subscription(_, requiredStars, _):
return requiredStars
case let .unlockMedia(requiredStars):
return requiredStars
default:
return nil
}
}
}

View File

@ -32,6 +32,7 @@ swift_library(
"//submodules/Components/SolidRoundedButtonComponent",
"//submodules/AvatarNode",
"//submodules/TelegramUI/Components/Stars/StarsImageComponent",
"//submodules/TelegramUI/Components/Stars/StarsAvatarComponent",
"//submodules/GalleryUI",
],
visibility = [

View File

@ -22,6 +22,7 @@ import TelegramStringFormatting
import UndoUI
import StarsImageComponent
import GalleryUI
import StarsAvatarComponent
private final class StarsTransactionSheetContent: CombinedComponent {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
@ -73,6 +74,7 @@ private final class StarsTransactionSheetContent: CombinedComponent {
var peerMap: [EnginePeer.Id: EnginePeer] = [:]
var cachedCloseImage: (UIImage, PresentationTheme)?
var cachedChevronImage: (UIImage, PresentationTheme)?
var inProgress = false
@ -89,6 +91,8 @@ private final class StarsTransactionSheetContent: CombinedComponent {
}
case let .receipt(receipt):
peerIds.append(receipt.botPaymentId)
case let .gift(message):
peerIds.append(message.id.peerId)
}
self.disposable = (context.engine.data.get(
@ -186,87 +190,110 @@ private final class StarsTransactionSheetContent: CombinedComponent {
let media: [AnyMediaReference]
let photo: TelegramMediaWebFile?
let isRefund: Bool
let isGift: Bool
var delayedCloseOnOpenPeer = true
switch subject {
case let .transaction(transaction, parentPeer):
switch transaction.peer {
case let .peer(peer):
if transaction.flags.contains(.isGift) {
titleText = "Received Gift"
descriptionText = "Use Stars to unlock content and services on Telegram. [See Examples >]()"
count = transaction.count
transactionId = transaction.id
via = nil
messageId = nil
date = transaction.date
if case let .peer(peer) = transaction.peer {
toPeer = peer
} else {
toPeer = nil
}
transactionPeer = transaction.peer
media = []
photo = nil
isRefund = false
isGift = true
delayedCloseOnOpenPeer = false
} else {
switch transaction.peer {
case let .peer(peer):
if !transaction.media.isEmpty {
titleText = strings.Stars_Transaction_MediaPurchase
} else {
titleText = transaction.title ?? peer.compactDisplayTitle
}
via = nil
case .appStore:
titleText = strings.Stars_Transaction_AppleTopUp_Title
via = strings.Stars_Transaction_AppleTopUp_Subtitle
case .playMarket:
titleText = strings.Stars_Transaction_GoogleTopUp_Title
via = strings.Stars_Transaction_GoogleTopUp_Subtitle
case .premiumBot:
titleText = strings.Stars_Transaction_PremiumBotTopUp_Title
via = strings.Stars_Transaction_PremiumBotTopUp_Subtitle
case .fragment:
if parentPeer.id == component.context.account.peerId {
titleText = strings.Stars_Transaction_FragmentTopUp_Title
via = strings.Stars_Transaction_FragmentTopUp_Subtitle
} else {
titleText = strings.Stars_Transaction_FragmentWithdrawal_Title
via = strings.Stars_Transaction_FragmentWithdrawal_Subtitle
}
case .ads:
titleText = strings.Stars_Transaction_TelegramAds_Title
via = strings.Stars_Transaction_TelegramAds_Subtitle
case .unsupported:
titleText = strings.Stars_Transaction_Unsupported_Title
via = nil
}
if !transaction.media.isEmpty {
titleText = strings.Stars_Transaction_MediaPurchase
var description: String = ""
var photoCount: Int32 = 0
var videoCount: Int32 = 0
for media in transaction.media {
if let _ = media as? TelegramMediaFile {
videoCount += 1
} else {
photoCount += 1
}
}
if photoCount > 0 && videoCount > 0 {
description += strings.Stars_Transaction_MediaAnd(strings.Stars_Transaction_Photos(photoCount), strings.Stars_Transaction_Videos(videoCount)).string
} else if photoCount > 0 {
if photoCount > 1 {
description += strings.Stars_Transaction_Photos(photoCount)
} else {
description += strings.Stars_Transaction_SinglePhoto
}
} else if videoCount > 0 {
if videoCount > 1 {
description += strings.Stars_Transaction_Videos(videoCount)
} else {
description += strings.Stars_Transaction_SingleVideo
}
}
descriptionText = description
} else {
titleText = transaction.title ?? peer.compactDisplayTitle
descriptionText = transaction.description ?? ""
}
via = nil
case .appStore:
titleText = strings.Stars_Transaction_AppleTopUp_Title
via = strings.Stars_Transaction_AppleTopUp_Subtitle
case .playMarket:
titleText = strings.Stars_Transaction_GoogleTopUp_Title
via = strings.Stars_Transaction_GoogleTopUp_Subtitle
case .premiumBot:
titleText = strings.Stars_Transaction_PremiumBotTopUp_Title
via = strings.Stars_Transaction_PremiumBotTopUp_Subtitle
case .fragment:
if parentPeer.id == component.context.account.peerId {
titleText = strings.Stars_Transaction_FragmentTopUp_Title
via = strings.Stars_Transaction_FragmentTopUp_Subtitle
messageId = transaction.paidMessageId
count = transaction.count
transactionId = transaction.id
date = transaction.date
if case let .peer(peer) = transaction.peer {
toPeer = peer
} else {
titleText = strings.Stars_Transaction_FragmentWithdrawal_Title
via = strings.Stars_Transaction_FragmentWithdrawal_Subtitle
toPeer = nil
}
case .ads:
titleText = strings.Stars_Transaction_TelegramAds_Title
via = strings.Stars_Transaction_TelegramAds_Subtitle
case .unsupported:
titleText = strings.Stars_Transaction_Unsupported_Title
via = nil
transactionPeer = transaction.peer
media = transaction.media.map { AnyMediaReference.starsTransaction(transaction: StarsTransactionReference(peerId: parentPeer.id, id: transaction.id, isRefund: transaction.flags.contains(.isRefund)), media: $0) }
photo = transaction.photo
isGift = false
isRefund = transaction.flags.contains(.isRefund)
}
if !transaction.media.isEmpty {
var description: String = ""
var photoCount: Int32 = 0
var videoCount: Int32 = 0
for media in transaction.media {
if let _ = media as? TelegramMediaFile {
videoCount += 1
} else {
photoCount += 1
}
}
if photoCount > 0 && videoCount > 0 {
description += strings.Stars_Transaction_MediaAnd(strings.Stars_Transaction_Photos(photoCount), strings.Stars_Transaction_Videos(videoCount)).string
} else if photoCount > 0 {
if photoCount > 1 {
description += strings.Stars_Transaction_Photos(photoCount)
} else {
description += strings.Stars_Transaction_SinglePhoto
}
} else if videoCount > 0 {
if videoCount > 1 {
description += strings.Stars_Transaction_Videos(videoCount)
} else {
description += strings.Stars_Transaction_SingleVideo
}
}
descriptionText = description
} else {
descriptionText = transaction.description ?? ""
}
messageId = transaction.paidMessageId
count = transaction.count
transactionId = transaction.id
date = transaction.date
if case let .peer(peer) = transaction.peer {
toPeer = peer
} else {
toPeer = nil
}
transactionPeer = transaction.peer
media = transaction.media.map { AnyMediaReference.starsTransaction(transaction: StarsTransactionReference(peerId: parentPeer.id, id: transaction.id, isRefund: transaction.flags.contains(.isRefund)), media: $0) }
photo = transaction.photo
isRefund = transaction.flags.contains(.isRefund)
case let .receipt(receipt):
titleText = receipt.invoiceMedia.title
descriptionText = receipt.invoiceMedia.description
@ -284,6 +311,28 @@ private final class StarsTransactionSheetContent: CombinedComponent {
media = []
photo = receipt.invoiceMedia.photo
isRefund = false
isGift = false
delayedCloseOnOpenPeer = false
case let .gift(message):
let incoming = message.flags.contains(.Incoming)
titleText = incoming ? "Received Gift" : "Sent Gift"
let peerName = state.peerMap[message.id.peerId]?.compactDisplayTitle ?? ""
descriptionText = incoming ? "Use Stars to unlock content and services on Telegram. [See Examples >]()" : "With Stars, \(peerName) will be able to unlock content and services on Telegram.\n[See Examples >]()"
if let action = message.media.first(where: { $0 is TelegramMediaAction }) as? TelegramMediaAction, case let .giftStars(_, _, countValue, _, _, _) = action.action {
count = !incoming ? -countValue : countValue
transactionId = nil
} else {
fatalError()
}
via = nil
messageId = nil
date = message.timestamp
toPeer = state.peerMap[message.id.peerId]
transactionPeer = nil
media = []
photo = nil
isRefund = false
isGift = true
delayedCloseOnOpenPeer = false
}
@ -312,7 +361,9 @@ private final class StarsTransactionSheetContent: CombinedComponent {
)
let imageSubject: StarsImageComponent.Subject
if !media.isEmpty {
if isGift {
imageSubject = .gift
} else if !media.isEmpty {
imageSubject = .media(media)
} else if let photo {
imageSubject = .photo(photo)
@ -373,12 +424,14 @@ private final class StarsTransactionSheetContent: CombinedComponent {
content: AnyComponent(
PeerCellComponent(
context: component.context,
textColor: tableLinkColor,
theme: theme,
peer: toPeer
)
),
action: {
if delayedCloseOnOpenPeer {
if toPeer.id.namespace == Namespaces.Peer.CloudUser && toPeer.id.id._internalGetInt64Value() == 777000 {
} else if delayedCloseOnOpenPeer {
component.openPeer(toPeer)
Queue.mainQueue().after(1.0, {
component.cancel(false)
@ -539,14 +592,21 @@ private final class StarsTransactionSheetContent: CombinedComponent {
originY += star.size.height - 23.0
if !descriptionText.isEmpty {
if state.cachedChevronImage == nil || state.cachedChevronImage?.1 !== environment.theme {
state.cachedChevronImage = (generateTintedImage(image: UIImage(bundleImageName: "Settings/TextArrowRight"), color: linkColor)!, theme)
}
let textFont = Font.regular(15.0)
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)
})
let attributedString = parseMarkdownIntoAttributedString(descriptionText, attributes: markdownAttributes, textAlignment: .center).mutableCopy() as! NSMutableAttributedString
if let range = attributedString.string.range(of: ">"), let chevronImage = state.cachedChevronImage?.0 {
attributedString.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: attributedString.string))
}
let description = description.update(
component: MultilineTextComponent(
text: .plain(NSAttributedString(
string: descriptionText,
font: Font.regular(15.0),
textColor: theme.actionSheet.primaryTextColor,
paragraphAlignment: .center
)),
text: .plain(attributedString),
horizontalAlignment: .center,
maximumNumberOfLines: 3
),
@ -768,6 +828,7 @@ public class StarsTransactionScreen: ViewControllerComponentContainer {
public enum Subject: Equatable {
case transaction(StarsContext.State.Transaction, EnginePeer)
case receipt(BotPaymentReceipt)
case gift(EngineMessage)
}
private let context: AccountContext
@ -1166,12 +1227,12 @@ private final class TableComponent: CombinedComponent {
private final class PeerCellComponent: Component {
let context: AccountContext
let textColor: UIColor
let peer: EnginePeer?
let theme: PresentationTheme
let peer: EnginePeer
init(context: AccountContext, textColor: UIColor, peer: EnginePeer?) {
init(context: AccountContext, theme: PresentationTheme, peer: EnginePeer) {
self.context = context
self.textColor = textColor
self.theme = theme
self.peer = peer
}
@ -1179,7 +1240,7 @@ private final class PeerCellComponent: Component {
if lhs.context !== rhs.context {
return false
}
if lhs.textColor !== rhs.textColor {
if lhs.theme !== rhs.theme {
return false
}
if lhs.peer != rhs.peer {
@ -1189,18 +1250,14 @@ private final class PeerCellComponent: Component {
}
final class View: UIView {
private let avatarNode: AvatarNode
private let avatar = ComponentView<Empty>()
private let text = ComponentView<Empty>()
private var component: PeerCellComponent?
private weak var state: EmptyComponentState?
override init(frame: CGRect) {
self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 13.0))
super.init(frame: frame)
self.addSubnode(self.avatarNode)
}
required init?(coder: NSCoder) {
@ -1211,21 +1268,33 @@ private final class PeerCellComponent: Component {
self.component = component
self.state = state
self.avatarNode.setPeer(
context: component.context,
theme: component.context.sharedContext.currentPresentationData.with({ $0 }).theme,
peer: component.peer,
synchronousLoad: true
)
let avatarSize = CGSize(width: 22.0, height: 22.0)
let spacing: CGFloat = 6.0
let peerName: String
let peer: StarsContext.State.Transaction.Peer
if component.peer.id.namespace == Namespaces.Peer.CloudUser && component.peer.id.id._internalGetInt64Value() == 777000 {
peerName = "Unknown User"
peer = .fragment
} else {
peerName = component.peer.compactDisplayTitle
peer = .peer(component.peer)
}
let avatarNaturalSize = self.avatar.update(
transition: .immediate,
component: AnyComponent(
StarsAvatarComponent(context: component.context, theme: component.theme, peer: peer, photo: nil, media: [], backgroundColor: .clear)
),
environment: {},
containerSize: CGSize(width: 40.0, height: 40.0)
)
let textSize = self.text.update(
transition: .immediate,
component: AnyComponent(
MultilineTextComponent(
text: .plain(NSAttributedString(string: component.peer?.compactDisplayTitle ?? "", font: Font.regular(15.0), textColor: component.textColor, paragraphAlignment: .left))
text: .plain(NSAttributedString(string: peerName, font: Font.regular(15.0), textColor: component.theme.list.itemAccentColor, paragraphAlignment: .left))
)
),
environment: {},
@ -1235,7 +1304,15 @@ private final class PeerCellComponent: Component {
let size = CGSize(width: avatarSize.width + textSize.width + spacing, height: textSize.height)
let avatarFrame = CGRect(origin: CGPoint(x: 0.0, y: floorToScreenPixels((size.height - avatarSize.height) / 2.0)), size: avatarSize)
self.avatarNode.frame = avatarFrame
if let view = self.avatar.view {
if view.superview == nil {
self.addSubview(view)
}
let scale = avatarSize.width / avatarNaturalSize.width
view.transform = CGAffineTransform(scaleX: scale, y: scale)
view.frame = avatarFrame
}
if let view = self.text.view {
if view.superview == nil {

View File

@ -23,6 +23,7 @@ final class StarsBalanceComponent: Component {
let actionCooldownUntilTimestamp: Int32?
let action: () -> Void
let buyAds: (() -> Void)?
let additionalAction: AnyComponent<Empty>?
init(
theme: PresentationTheme,
@ -35,7 +36,8 @@ final class StarsBalanceComponent: Component {
actionIsEnabled: Bool,
actionCooldownUntilTimestamp: Int32? = nil,
action: @escaping () -> Void,
buyAds: (() -> Void)?
buyAds: (() -> Void)?,
additionalAction: AnyComponent<Empty>? = nil
) {
self.theme = theme
self.strings = strings
@ -48,6 +50,7 @@ final class StarsBalanceComponent: Component {
self.actionCooldownUntilTimestamp = actionCooldownUntilTimestamp
self.action = action
self.buyAds = buyAds
self.additionalAction = additionalAction
}
static func ==(lhs: StarsBalanceComponent, rhs: StarsBalanceComponent) -> Bool {
@ -88,6 +91,8 @@ final class StarsBalanceComponent: Component {
private var button = ComponentView<Empty>()
private var buyAdsButton = ComponentView<Empty>()
private var additionalButton = ComponentView<Empty>()
private var component: StarsBalanceComponent?
private weak var state: EmptyComponentState?
@ -275,9 +280,29 @@ final class StarsBalanceComponent: Component {
}
}
contentHeight += buttonSize.height
}
if let additionalAction = component.additionalAction {
contentHeight += 18.0
let buttonSize = self.additionalButton.update(
transition: transition,
component: additionalAction,
environment: {},
containerSize: CGSize(width: availableSize.width, height: 50.0)
)
if let buttonView = self.additionalButton.view {
if buttonView.superview == nil {
self.addSubview(buttonView)
}
let buttonFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - buttonSize.width) / 2.0), y: contentHeight), size: buttonSize)
buttonView.frame = buttonFrame
}
contentHeight += buttonSize.height
contentHeight += 2.0
}
contentHeight += sideInset
return CGSize(width: availableSize.width, height: contentHeight)

View File

@ -203,12 +203,13 @@ final class StarsTransactionsListPanelComponent: Component {
let fontBaseDisplaySize = 17.0
let itemTitle: String
var itemTitle: String
let itemSubtitle: String?
var itemDate: String
var itemPeer = item.peer
switch item.peer {
case let .peer(peer):
if !item.media.isEmpty {
if !item.media.isEmpty {
itemTitle = environment.strings.Stars_Intro_Transaction_MediaPurchase
itemSubtitle = peer.displayTitle(strings: environment.strings, displayOrder: .firstLast)
} else if let title = item.title {
@ -216,7 +217,16 @@ final class StarsTransactionsListPanelComponent: Component {
itemSubtitle = peer.displayTitle(strings: environment.strings, displayOrder: .firstLast)
} else {
itemTitle = peer.displayTitle(strings: environment.strings, displayOrder: .firstLast)
itemSubtitle = nil
if item.flags.contains(.isGift) {
//TODO:localize
itemSubtitle = "Received Gift"
if peer.id.namespace == Namespaces.Peer.CloudUser && peer.id.id._internalGetInt64Value() == 777000 {
itemTitle = "Unknown User"
itemPeer = .fragment
}
} else {
itemSubtitle = nil
}
}
case .appStore:
itemTitle = environment.strings.Stars_Intro_Transaction_AppleTopUp_Title
@ -298,7 +308,7 @@ final class StarsTransactionsListPanelComponent: Component {
theme: environment.theme,
title: AnyComponent(VStack(titleComponents, alignment: .left, spacing: 2.0)),
contentInsets: UIEdgeInsets(top: 9.0, left: environment.containerInsets.left, bottom: 8.0, right: environment.containerInsets.right),
leftIcon: .custom(AnyComponentWithIdentity(id: "avatar", component: AnyComponent(StarsAvatarComponent(context: component.context, theme: environment.theme, peer: item.peer, photo: item.photo, media: item.media, backgroundColor: environment.theme.list.plainBackgroundColor))), false),
leftIcon: .custom(AnyComponentWithIdentity(id: "avatar", component: AnyComponent(StarsAvatarComponent(context: component.context, theme: environment.theme, peer: itemPeer, photo: item.photo, media: item.media, backgroundColor: environment.theme.list.plainBackgroundColor))), false),
icon: nil,
accessory: .custom(ListActionItemComponent.CustomAccessory(component: AnyComponentWithIdentity(id: "label", component: AnyComponent(StarsLabelComponent(text: itemLabel))), insets: UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 16.0))),
action: { [weak self] _ in

View File

@ -26,17 +26,20 @@ final class StarsTransactionsScreenComponent: Component {
let starsContext: StarsContext
let openTransaction: (StarsContext.State.Transaction) -> Void
let buy: () -> Void
let gift: () -> Void
init(
context: AccountContext,
starsContext: StarsContext,
openTransaction: @escaping (StarsContext.State.Transaction) -> Void,
buy: @escaping () -> Void
buy: @escaping () -> Void,
gift: @escaping () -> Void
) {
self.context = context
self.starsContext = starsContext
self.openTransaction = openTransaction
self.buy = buy
self.gift = gift
}
static func ==(lhs: StarsTransactionsScreenComponent, rhs: StarsTransactionsScreenComponent) -> Bool {
@ -89,6 +92,8 @@ final class StarsTransactionsScreenComponent: Component {
private let balanceView = ComponentView<Empty>()
private let subscriptionsView = ComponentView<Empty>()
private let topBalanceTitleView = ComponentView<Empty>()
private let topBalanceValueView = ComponentView<Empty>()
private let topBalanceIconView = ComponentView<Empty>()
@ -282,6 +287,7 @@ final class StarsTransactionsScreenComponent: Component {
}
let environment = environment[ViewControllerComponentContainer.Environment.self].value
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
if self.stateDisposable == nil {
self.stateDisposable = (component.starsContext.state
@ -531,7 +537,27 @@ final class StarsTransactionsScreenComponent: Component {
}
component.buy()
},
buyAds: nil
buyAds: nil,
additionalAction: AnyComponent(
Button(
content: AnyComponent(
HStack([
AnyComponentWithIdentity(
id: "icon",
component: AnyComponent(BundleIconComponent(name: "Premium/Stars/Gift", tintColor: environment.theme.list.itemAccentColor))
),
AnyComponentWithIdentity(
id: "label",
component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: "Gift Stars to Friends", font: Font.regular(17.0), textColor: environment.theme.list.itemAccentColor))))
)
],
spacing: 6.0)
),
action: {
component.gift()
}
)
)
)
))]
)),
@ -545,10 +571,42 @@ final class StarsTransactionsScreenComponent: Component {
}
starTransition.setFrame(view: balanceView, frame: balanceFrame)
}
contentHeight += balanceSize.height
contentHeight += 44.0
let subscriptionsItems: [AnyComponentWithIdentity<Empty>] = []
if !subscriptionsItems.isEmpty {
//TODO:localize
let subscriptionsSize = self.subscriptionsView.update(
transition: .immediate,
component: AnyComponent(ListSectionComponent(
theme: environment.theme,
header: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: "My Subscriptions".uppercased(),
font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize),
textColor: environment.theme.list.freeTextColor
)),
maximumNumberOfLines: 0
)),
footer: nil,
items: subscriptionsItems
)),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInsets, height: availableSize.height)
)
let subscriptionsFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - subscriptionsSize.width) / 2.0), y: contentHeight), size: subscriptionsSize)
if let subscriptionsView = self.subscriptionsView.view {
if subscriptionsView.superview == nil {
self.scrollView.addSubview(subscriptionsView)
}
starTransition.setFrame(view: subscriptionsView, frame: subscriptionsFrame)
}
contentHeight += subscriptionsSize.height
contentHeight += 44.0
}
let initialTransactions = self.starsState?.transactions ?? []
var panelItems: [StarsTransactionsPanelContainerComponent.Item] = []
if !initialTransactions.isEmpty {
@ -704,6 +762,7 @@ public final class StarsTransactionsScreen: ViewControllerComponentContainer {
self.starsContext = starsContext
var buyImpl: (() -> Void)?
var giftImpl: (() -> Void)?
var openTransactionImpl: ((StarsContext.State.Transaction) -> Void)?
super.init(context: context, component: StarsTransactionsScreenComponent(
context: context,
@ -713,6 +772,9 @@ public final class StarsTransactionsScreen: ViewControllerComponentContainer {
},
buy: {
buyImpl?()
},
gift: {
giftImpl?()
}
), navigationBarAppearance: .transparent)
@ -744,7 +806,7 @@ public final class StarsTransactionsScreen: ViewControllerComponentContainer {
guard let self else {
return
}
let controller = context.sharedContext.makeStarsPurchaseScreen(context: context, starsContext: starsContext, options: options, peerId: nil, requiredStars: nil, completion: { [weak self] stars in
let controller = context.sharedContext.makeStarsPurchaseScreen(context: context, starsContext: starsContext, options: options, purpose: .generic, completion: { [weak self] stars in
guard let self else {
return
}
@ -768,6 +830,36 @@ public final class StarsTransactionsScreen: ViewControllerComponentContainer {
})
}
giftImpl = { [weak self] in
guard let self else {
return
}
let _ = combineLatest(queue: Queue.mainQueue(),
self.options.get() |> take(1),
self.context.account.stateManager.contactBirthdays |> take(1)
).start(next: { [weak self] options, birthdays in
guard let self else {
return
}
let controller = self.context.sharedContext.makePremiumGiftController(context: self.context, source: .stars(birthdays), completion: { [weak self] peerIds in
guard let self, let peerId = peerIds.first else {
return
}
let purchaseController = self.context.sharedContext.makeStarsPurchaseScreen(
context: self.context,
starsContext: starsContext,
options: options,
purpose: .gift(peerId: peerId),
completion: { stars in
}
)
self.push(purchaseController)
})
self.push(controller)
})
}
self.starsContext.load(force: false)
}

View File

@ -478,12 +478,19 @@ private final class SheetContent: CombinedComponent {
state?.buy(requestTopUp: { [weak controller] completion in
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: accountContext.currentAppConfiguration.with { $0 })
if !premiumConfiguration.isPremiumDisabled {
let purpose: StarsPurchasePurpose
if isMedia {
purpose = .unlockMedia(requiredStars: invoice.totalAmount)
} else if let peerId = state?.botPeer?.id {
purpose = .transfer(peerId: peerId, requiredStars: invoice.totalAmount)
} else {
purpose = .generic
}
let purchaseController = accountContext.sharedContext.makeStarsPurchaseScreen(
context: accountContext,
starsContext: starsContext,
options: state?.options ?? [],
peerId: isMedia ? nil : state?.botPeer?.id,
requiredStars: invoice.totalAmount,
purpose: purpose,
completion: { [weak starsContext] stars in
starsContext?.add(balance: stars)
Queue.mainQueue().after(0.1) {

View File

@ -552,6 +552,9 @@ public class StickerPickerScreen: ViewController {
self.presentLinkPremiumSuggestion()
}
}
self.storyStickersContentView?.weatherAction = { [weak self] in
self?.controller?.addWeather()
}
}
let gifItems: Signal<EntityKeyboardGifContent?, NoError>
@ -2063,6 +2066,7 @@ public class StickerPickerScreen: ViewController {
public var presentAudioPicker: () -> Void = { }
public var addReaction: () -> Void = { }
public var addLink: () -> Void = { }
public var addWeather: () -> Void = { }
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
@ -2204,16 +2208,30 @@ private final class InteractiveStickerButtonContent: Component {
func update(component: InteractiveStickerButtonContent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
self.backgroundLayer.backgroundColor = UIColor(rgb: 0xffffff, alpha: 0.11).cgColor
let iconSize = self.icon.update(
transition: .immediate,
component: AnyComponent(BundleIconComponent(
name: component.iconName,
tintColor: .white,
maxSize: CGSize(width: 20.0, height: 20.0)
)),
environment: {},
containerSize: availableSize
)
let iconSize: CGSize
if component.iconName == "Sun" {
iconSize = self.icon.update(
transition: .immediate,
component: AnyComponent(Text(
text: "☀️",
font: Font.with(size: 23.0, design: .camera),
color: .white
)),
environment: {},
containerSize: availableSize
)
} else {
iconSize = self.icon.update(
transition: .immediate,
component: AnyComponent(BundleIconComponent(
name: component.iconName,
tintColor: .white,
maxSize: CGSize(width: 20.0, height: 20.0)
)),
environment: {},
containerSize: availableSize
)
}
let titleSize = self.title.update(
transition: .immediate,
component: AnyComponent(Text(
@ -2473,7 +2491,7 @@ final class ItemStack<ChildEnvironment: Equatable>: CombinedComponent {
let remainingWidth = context.availableSize.width - itemsWidth - context.component.padding * 2.0
let spacing = remainingWidth / CGFloat(rowItemsCount - 1)
if spacing < context.component.minSpacing || currentGroup.count == 2 {
if spacing < context.component.minSpacing || currentGroup.count == 3 {
groups.append(currentGroup)
currentGroup = []
}
@ -2537,6 +2555,7 @@ final class StoryStickersContentView: UIView, EmojiCustomContentView {
var audioAction: () -> Void = {}
var reactionAction: () -> Void = {}
var linkAction: () -> Void = {}
var weatherAction: () -> Void = {}
init(isPremium: Bool) {
self.isPremium = isPremium
@ -2601,6 +2620,29 @@ final class StoryStickersContentView: UIView, EmojiCustomContentView {
})
)
),
AnyComponentWithIdentity(
id: "weather",
component: AnyComponent(
CameraButton(
content: AnyComponentWithIdentity(
id: "weather",
component: AnyComponent(
InteractiveStickerButtonContent(
theme: theme,
title: "35°C",
iconName: "Sun",
useOpaqueTheme: useOpaqueTheme,
tintContainerView: self.tintContainerView
)
)
),
action: { [weak self] in
if let self {
self.weatherAction()
}
})
)
),
AnyComponentWithIdentity(
id: "audio",
component: AnyComponent(

View File

@ -36,27 +36,59 @@ struct CameraState: Equatable {
case holding
case handsFree
}
enum FlashTint: Equatable {
case white
case yellow
case blue
var color: UIColor {
switch self {
case .white:
return .white
case .yellow:
return UIColor(rgb: 0xffed8c)
case .blue:
return UIColor(rgb: 0x8cdfff)
}
}
}
let position: Camera.Position
let flashMode: Camera.FlashMode
let flashModeDidChange: Bool
let flashTint: FlashTint
let flashTintSize: CGFloat
let recording: Recording
let duration: Double
let isDualCameraEnabled: Bool
let isViewOnceEnabled: Bool
func updatedPosition(_ position: Camera.Position) -> CameraState {
return CameraState(position: position, recording: self.recording, duration: self.duration, isDualCameraEnabled: self.isDualCameraEnabled, isViewOnceEnabled: self.isViewOnceEnabled)
return CameraState(position: position, flashMode: self.flashMode, flashModeDidChange: self.flashModeDidChange, flashTint: self.flashTint, flashTintSize: self.flashTintSize, recording: self.recording, duration: self.duration, isDualCameraEnabled: self.isDualCameraEnabled, isViewOnceEnabled: self.isViewOnceEnabled)
}
func updatedFlashMode(_ flashMode: Camera.FlashMode) -> CameraState {
return CameraState(position: self.position, flashMode: flashMode, flashModeDidChange: self.flashModeDidChange, flashTint: self.flashTint, flashTintSize: self.flashTintSize, recording: self.recording, duration: self.duration, isDualCameraEnabled: self.isDualCameraEnabled, isViewOnceEnabled: self.isViewOnceEnabled)
}
func updatedFlashTint(_ flashTint: FlashTint) -> CameraState {
return CameraState(position: self.position, flashMode: self.flashMode, flashModeDidChange: self.flashModeDidChange, flashTint: flashTint, flashTintSize: self.flashTintSize, recording: self.recording, duration: self.duration, isDualCameraEnabled: self.isDualCameraEnabled, isViewOnceEnabled: self.isViewOnceEnabled)
}
func updatedFlashTintSize(_ flashTintSize: CGFloat) -> CameraState {
return CameraState(position: self.position, flashMode: self.flashMode, flashModeDidChange: self.flashModeDidChange, flashTint: self.flashTint, flashTintSize: flashTintSize, recording: self.recording, duration: self.duration, isDualCameraEnabled: self.isDualCameraEnabled, isViewOnceEnabled: self.isViewOnceEnabled)
}
func updatedRecording(_ recording: Recording) -> CameraState {
return CameraState(position: self.position, recording: recording, duration: self.duration, isDualCameraEnabled: self.isDualCameraEnabled, isViewOnceEnabled: self.isViewOnceEnabled)
return CameraState(position: self.position, flashMode: self.flashMode, flashModeDidChange: self.flashModeDidChange, flashTint: self.flashTint, flashTintSize: self.flashTintSize, recording: recording, duration: self.duration, isDualCameraEnabled: self.isDualCameraEnabled, isViewOnceEnabled: self.isViewOnceEnabled)
}
func updatedDuration(_ duration: Double) -> CameraState {
return CameraState(position: self.position, recording: self.recording, duration: duration, isDualCameraEnabled: self.isDualCameraEnabled, isViewOnceEnabled: self.isViewOnceEnabled)
return CameraState(position: self.position, flashMode: self.flashMode, flashModeDidChange: self.flashModeDidChange, flashTint: self.flashTint, flashTintSize: self.flashTintSize, recording: self.recording, duration: duration, isDualCameraEnabled: self.isDualCameraEnabled, isViewOnceEnabled: self.isViewOnceEnabled)
}
func updatedIsViewOnceEnabled(_ isViewOnceEnabled: Bool) -> CameraState {
return CameraState(position: self.position, recording: self.recording, duration: self.duration, isDualCameraEnabled: self.isDualCameraEnabled, isViewOnceEnabled: isViewOnceEnabled)
return CameraState(position: self.position, flashMode: self.flashMode, flashModeDidChange: self.flashModeDidChange, flashTint: self.flashTint, flashTintSize: self.flashTintSize, recording: self.recording, duration: self.duration, isDualCameraEnabled: self.isDualCameraEnabled, isViewOnceEnabled: isViewOnceEnabled)
}
}
@ -143,7 +175,9 @@ private final class VideoMessageCameraScreenComponent: CombinedComponent {
final class State: ComponentState {
enum ImageKey: Hashable {
case flip
case flash
case buttonBackground
case flashImage
}
private var cachedImages: [ImageKey: UIImage] = [:]
func image(_ key: ImageKey, theme: PresentationTheme) -> UIImage {
@ -154,9 +188,23 @@ private final class VideoMessageCameraScreenComponent: CombinedComponent {
switch key {
case .flip:
image = UIImage(bundleImageName: "Camera/VideoMessageFlip")!.withRenderingMode(.alwaysTemplate)
case .flash:
image = UIImage(bundleImageName: "Camera/VideoMessageFlash")!.withRenderingMode(.alwaysTemplate)
case .buttonBackground:
let innerSize = CGSize(width: 40.0, height: 40.0)
image = generateFilledCircleImage(diameter: innerSize.width, color: theme.rootController.navigationBar.opaqueBackgroundColor, strokeColor: theme.chat.inputPanel.panelSeparatorColor, strokeWidth: 0.5, backgroundColor: nil)!
case .flashImage:
image = generateImage(CGSize(width: 393.0, height: 852.0), rotatedContext: { size, context in
context.clear(CGRect(origin: .zero, size: size))
var locations: [CGFloat] = [0.0, 0.2, 0.6, 1.0]
let colors: [CGColor] = [UIColor(rgb: 0xffffff, alpha: 0.25).cgColor, UIColor(rgb: 0xffffff, alpha: 0.25).cgColor, UIColor(rgb: 0xffffff, alpha: 1.0).cgColor, UIColor(rgb: 0xffffff, alpha: 1.0).cgColor]
let colorSpace = CGColorSpaceCreateDeviceRGB()
let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)!
let center = CGPoint(x: size.width / 2.0, y: size.height / 2.0 - 10.0)
context.drawRadialGradient(gradient, startCenter: center, startRadius: 0.0, endCenter: center, endRadius: size.width, options: .drawsAfterEndLocation)
})!.withRenderingMode(.alwaysTemplate)
}
cachedImages[key] = image
return image
@ -175,6 +223,8 @@ private final class VideoMessageCameraScreenComponent: CombinedComponent {
var cameraState: CameraState?
var didDisplayViewOnce = false
var displayingFlashTint = false
private let hapticFeedback = HapticFeedback()
@ -238,6 +288,81 @@ private final class VideoMessageCameraScreenComponent: CombinedComponent {
self.hapticFeedback.impact(.veryLight)
}
func toggleFlashMode() {
guard let controller = self.getController(), let camera = controller.camera else {
return
}
var flashOn = false
switch controller.cameraState.flashMode {
case .off:
flashOn = true
camera.setFlashMode(.on)
case .on:
camera.setFlashMode(.off)
default:
camera.setFlashMode(.off)
}
self.hapticFeedback.impact(.light)
self.updateScreenBrightness(flashOn: flashOn)
}
private var initialBrightness: CGFloat?
private var brightnessArguments: (Double, Double, CGFloat, CGFloat)?
private var brightnessAnimator: ConstantDisplayLinkAnimator?
func updateScreenBrightness(flashOn: Bool?) {
guard let controller = self.getController() else {
return
}
let isFrontCamera = controller.cameraState.position == .front
let isVideo = true
let isFlashOn = flashOn ?? (controller.cameraState.flashMode == .on)
if isFrontCamera && isVideo && isFlashOn {
if self.initialBrightness == nil {
self.initialBrightness = UIScreen.main.brightness
self.brightnessArguments = (CACurrentMediaTime(), 0.2, UIScreen.main.brightness, 1.0)
self.animateBrightnessChange()
}
} else {
if let initialBrightness = self.initialBrightness {
self.initialBrightness = nil
self.brightnessArguments = (CACurrentMediaTime(), 0.2, UIScreen.main.brightness, initialBrightness)
self.animateBrightnessChange()
}
}
}
private func animateBrightnessChange() {
if self.brightnessAnimator == nil {
self.brightnessAnimator = ConstantDisplayLinkAnimator(update: { [weak self] in
self?.animateBrightnessChange()
})
self.brightnessAnimator?.isPaused = true
}
if let (startTime, duration, initial, target) = self.brightnessArguments {
self.brightnessAnimator?.isPaused = false
let t = CGFloat(max(0.0, min(1.0, (CACurrentMediaTime() - startTime) / duration)))
let value = initial + (target - initial) * t
UIScreen.main.brightness = value
if t >= 1.0 {
self.brightnessArguments = nil
self.brightnessAnimator?.isPaused = true
self.brightnessAnimator?.invalidate()
self.brightnessAnimator = nil
}
} else {
self.brightnessAnimator?.isPaused = true
self.brightnessAnimator?.invalidate()
self.brightnessAnimator = nil
}
}
func startVideoRecording(pressing: Bool) {
guard let controller = self.getController(), let camera = controller.camera else {
return
@ -312,6 +437,12 @@ private final class VideoMessageCameraScreenComponent: CombinedComponent {
controller.updateCameraState({ $0.updatedRecording(.none) }, transition: .spring(duration: 0.4))
}
}))
if case .front = controller.cameraState.position, let initialBrightness = self.initialBrightness {
self.initialBrightness = nil
self.brightnessArguments = (CACurrentMediaTime(), 0.2, UIScreen.main.brightness, initialBrightness)
self.animateBrightnessChange()
}
}
func lockVideoRecording() {
@ -334,7 +465,9 @@ private final class VideoMessageCameraScreenComponent: CombinedComponent {
}
static var body: Body {
let frontFlash = Child(Image.self)
let flipButton = Child(CameraButton.self)
let flashButton = Child(CameraButton.self)
let viewOnceButton = Child(PlainButtonComponent.self)
let recordMoreButton = Child(PlainButtonComponent.self)
@ -381,6 +514,20 @@ private final class VideoMessageCameraScreenComponent: CombinedComponent {
}
if !component.isPreviewing {
if case .on = component.cameraState.flashMode {
let frontFlash = frontFlash.update(
component: Image(image: state.image(.flashImage, theme: environment.theme), tintColor: component.cameraState.flashTint.color),
availableSize: availableSize,
transition: .easeInOut(duration: 0.2)
)
context.add(frontFlash
.position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0))
.scale(1.5 - component.cameraState.flashTintSize * 0.5)
.appear(.default(alpha: true))
.disappear(.default(alpha: true))
)
}
let flipButton = flipButton.update(
component: CameraButton(
content: AnyComponentWithIdentity(
@ -409,6 +556,35 @@ private final class VideoMessageCameraScreenComponent: CombinedComponent {
.appear(.default(scale: true, alpha: true))
.disappear(.default(scale: true, alpha: true))
)
let flashButton = flashButton.update(
component: CameraButton(
content: AnyComponentWithIdentity(
id: "flash",
component: AnyComponent(
Image(
image: state.image(.flash, theme: environment.theme),
tintColor: environment.theme.list.itemAccentColor,
size: CGSize(width: 30.0, height: 30.0)
)
)
),
minSize: CGSize(width: 44.0, height: 44.0),
isExclusive: false,
action: { [weak state] in
if let state {
state.toggleFlashMode()
}
}
),
availableSize: availableSize,
transition: context.transition
)
context.add(flashButton
.position(CGPoint(x: flipButton.size.width + 8.0 + flashButton.size.width / 2.0 + 8.0, y: availableSize.height - flashButton.size.height / 2.0 - 8.0))
.appear(.default(scale: true, alpha: true))
.disappear(.default(scale: true, alpha: true))
)
}
if showViewOnce {
@ -655,6 +831,10 @@ public class VideoMessageCameraScreen: ViewController {
self.cameraState = CameraState(
position: isFrontPosition ? .front : .back,
flashMode: .off,
flashModeDidChange: false,
flashTint: .white,
flashTintSize: 1.0,
recording: .none,
duration: 0.0,
isDualCameraEnabled: isDualCameraEnabled,
@ -760,12 +940,15 @@ public class VideoMessageCameraScreen: ViewController {
secondaryPreviewView: self.additionalPreviewView
)
self.cameraStateDisposable = (camera.position
|> deliverOnMainQueue).start(next: { [weak self] position in
self.cameraStateDisposable = combineLatest(
queue: Queue.mainQueue(),
camera.flashMode,
camera.position
).start(next: { [weak self] flashMode, position in
guard let self else {
return
}
self.cameraState = self.cameraState.updatedPosition(position)
self.cameraState = self.cameraState.updatedPosition(position).updatedFlashMode(flashMode)
if !self.cameraState.isDualCameraEnabled {
self.animatePositionChange()

Some files were not shown because too many files have changed in this diff Show More