mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-15 13:35:19 +00:00
Various improvements
This commit is contained in:
parent
3134a4ef1b
commit
4216ee3933
@ -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";
|
||||
|
@ -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?
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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(
|
||||
|
@ -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?
|
||||
|
157
submodules/AttachmentUI/Sources/BackButtonNode.swift
Normal file
157
submodules/AttachmentUI/Sources/BackButtonNode.swift
Normal 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)
|
||||
}
|
||||
}
|
@ -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",
|
||||
|
@ -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)
|
||||
|
@ -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)))
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
)
|
||||
|
@ -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) }
|
||||
}))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
38
submodules/BrowserUI/Sources/Favicon.swift
Normal file
38
submodules/BrowserUI/Sources/Favicon.swift
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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(
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
643
submodules/DrawingUI/Sources/DrawingWeatherEntityView.swift
Normal file
643
submodules/DrawingUI/Sources/DrawingWeatherEntityView.swift
Normal 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
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -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) {
|
||||
}
|
||||
}
|
||||
|
@ -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() {
|
||||
|
@ -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
|
||||
|
@ -327,7 +327,7 @@ extension ActionSheetControllerTheme {
|
||||
}
|
||||
}
|
||||
|
||||
extension ActionSheetController {
|
||||
public extension ActionSheetController {
|
||||
convenience init(instantPageTheme: InstantPageTheme) {
|
||||
self.init(theme: ActionSheetControllerTheme(instantPageTheme: instantPageTheme), allowInputInset: false)
|
||||
}
|
||||
|
@ -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")
|
||||
|
@ -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]
|
||||
|
@ -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
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -50,6 +50,7 @@ swift_library(
|
||||
"//submodules/ChatSendMessageActionUI",
|
||||
"//submodules/ComponentFlow",
|
||||
"//submodules/Components/ComponentDisplayAdapters",
|
||||
"//submodules/AnimatedCountLabelNode",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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.
BIN
submodules/PremiumUI/Resources/gift2.scn
Normal file
BIN
submodules/PremiumUI/Resources/gift2.scn
Normal file
Binary file not shown.
Binary file not shown.
BIN
submodules/PremiumUI/Resources/star2
Normal file
BIN
submodules/PremiumUI/Resources/star2
Normal file
Binary file not shown.
Binary file not shown.
@ -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 {
|
||||
|
@ -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)
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -30,6 +30,7 @@ swift_library(
|
||||
"//submodules/UndoUI",
|
||||
"//submodules/TelegramUI/Components/ListSectionComponent",
|
||||
"//submodules/TelegramUI/Components/ListActionItemComponent",
|
||||
"//submodules/TelegramUI/Components/NavigationStackComponent",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}()
|
||||
|
@ -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)))))
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
@ -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")
|
||||
|
@ -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 {
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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",
|
||||
],
|
||||
)
|
@ -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)
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
|
@ -146,6 +146,7 @@ swift_library(
|
||||
"//submodules/ConfettiEffect",
|
||||
"//submodules/ContactsPeerItem",
|
||||
"//submodules/TelegramUI/Components/PeerManagement/OldChannelsController",
|
||||
"//submodules/TelegramUI/Components/TextNodeWithEntities",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
))
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
|
||||
|
@ -22,6 +22,8 @@ swift_library(
|
||||
"//submodules/AvatarNode",
|
||||
"//submodules/AccountContext",
|
||||
"//submodules/InvisibleInkDustNode",
|
||||
"//submodules/AnimatedStickerNode",
|
||||
"//submodules/TelegramAnimatedStickerNode",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -32,6 +32,7 @@ swift_library(
|
||||
"//submodules/Components/SolidRoundedButtonComponent",
|
||||
"//submodules/AvatarNode",
|
||||
"//submodules/TelegramUI/Components/Stars/StarsImageComponent",
|
||||
"//submodules/TelegramUI/Components/Stars/StarsAvatarComponent",
|
||||
"//submodules/GalleryUI",
|
||||
],
|
||||
visibility = [
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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(
|
||||
|
@ -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
Loading…
x
Reference in New Issue
Block a user