Various improvements

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

View File

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

View File

@ -1040,7 +1040,7 @@ public protocol SharedAccountContext: AnyObject {
func makePremiumIntroController(context: AccountContext, source: PremiumIntroSource, forceDark: Bool, dismissed: (() -> Void)?) -> ViewController func 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 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 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 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 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 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 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 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 makeStarsTransactionScreen(context: AccountContext, transaction: StarsContext.State.Transaction, peer: EnginePeer) -> ViewController
func makeStarsReceiptScreen(context: AccountContext, receipt: BotPaymentReceipt) -> ViewController func makeStarsReceiptScreen(context: AccountContext, receipt: BotPaymentReceipt) -> ViewController
func makeStarsStatisticsScreen(context: AccountContext, peerId: EnginePeer.Id, revenueContext: StarsRevenueStatsContext) -> ViewController func makeStarsStatisticsScreen(context: AccountContext, peerId: EnginePeer.Id, revenueContext: StarsRevenueStatsContext) -> ViewController
func makeStarsAmountScreen(context: AccountContext, initialValue: Int64?, completion: @escaping (Int64) -> Void) -> ViewController func makeStarsAmountScreen(context: AccountContext, initialValue: Int64?, completion: @escaping (Int64) -> Void) -> ViewController
func makeStarsWithdrawalScreen(context: AccountContext, stats: StarsRevenueStats, 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? func makeDebugSettingsController(context: AccountContext?) -> ViewController?

View File

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

View File

@ -49,6 +49,7 @@ public enum PremiumGiftSource: Equatable {
case attachMenu case attachMenu
case settings([EnginePeer.Id: TelegramBirthday]?) case settings([EnginePeer.Id: TelegramBirthday]?)
case chatList([EnginePeer.Id: TelegramBirthday]?) case chatList([EnginePeer.Id: TelegramBirthday]?)
case stars([EnginePeer.Id: TelegramBirthday]?)
case channelBoost case channelBoost
case deeplink(String?) case deeplink(String?)
} }
@ -121,6 +122,14 @@ public enum BoostSubject: Equatable {
case noAds 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 struct PremiumConfiguration {
public static var defaultValue: PremiumConfiguration { public static var defaultValue: PremiumConfiguration {
return PremiumConfiguration( return PremiumConfiguration(

View File

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

View File

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

View File

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

View File

@ -1,7 +1,9 @@
import Foundation import Foundation
import UIKit import UIKit
import Display
import ComponentFlow import ComponentFlow
import SwiftSignalKit import SwiftSignalKit
import WebKit
final class BrowserContentState: Equatable { final class BrowserContentState: Equatable {
enum ContentType: Equatable { enum ContentType: Equatable {
@ -9,28 +11,62 @@ final class BrowserContentState: Equatable {
case instantPage 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 title: String
let url: String let url: String
let estimatedProgress: Double let estimatedProgress: Double
let readingProgress: Double
let contentType: ContentType let contentType: ContentType
let favicon: UIImage?
var canGoBack: Bool let canGoBack: Bool
var canGoForward: Bool let canGoForward: Bool
let backList: [HistoryItem]
let forwardList: [HistoryItem]
init( init(
title: String, title: String,
url: String, url: String,
estimatedProgress: Double, estimatedProgress: Double,
readingProgress: Double,
contentType: ContentType, contentType: ContentType,
favicon: UIImage? = nil,
canGoBack: Bool = false, canGoBack: Bool = false,
canGoForward: Bool = false canGoForward: Bool = false,
backList: [HistoryItem] = [],
forwardList: [HistoryItem] = []
) { ) {
self.title = title self.title = title
self.url = url self.url = url
self.estimatedProgress = estimatedProgress self.estimatedProgress = estimatedProgress
self.readingProgress = readingProgress
self.contentType = contentType self.contentType = contentType
self.favicon = favicon
self.canGoBack = canGoBack self.canGoBack = canGoBack
self.canGoForward = canGoForward self.canGoForward = canGoForward
self.backList = backList
self.forwardList = forwardList
} }
static func == (lhs: BrowserContentState, rhs: BrowserContentState) -> Bool { static func == (lhs: BrowserContentState, rhs: BrowserContentState) -> Bool {
@ -43,42 +79,80 @@ final class BrowserContentState: Equatable {
if lhs.estimatedProgress != rhs.estimatedProgress { if lhs.estimatedProgress != rhs.estimatedProgress {
return false return false
} }
if lhs.readingProgress != rhs.readingProgress {
return false
}
if lhs.contentType != rhs.contentType { if lhs.contentType != rhs.contentType {
return false return false
} }
if (lhs.favicon == nil) != (rhs.favicon == nil) {
return false
}
if lhs.canGoBack != rhs.canGoBack { if lhs.canGoBack != rhs.canGoBack {
return false return false
} }
if lhs.canGoForward != rhs.canGoForward { if lhs.canGoForward != rhs.canGoForward {
return false return false
} }
if lhs.backList != rhs.backList {
return false
}
if lhs.forwardList != rhs.forwardList {
return false
}
return true return true
} }
func withUpdatedTitle(_ title: String) -> BrowserContentState { 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 { 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 { 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 { 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 { 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 { protocol BrowserContent: UIView {
var uuid: UUID { get }
var currentState: BrowserContentState { get }
var state: Signal<BrowserContentState, NoError> { 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 } var onScrollingUpdate: (ContentScrollingUpdate) -> Void { get set }
func reload() func reload()
@ -86,6 +160,7 @@ protocol BrowserContent: UIView {
func navigateBack() func navigateBack()
func navigateForward() func navigateForward()
func navigateTo(historyItem: BrowserContentState.HistoryItem)
func setFontSize(_ fontSize: CGFloat) func setFontSize(_ fontSize: CGFloat)
func setForceSerif(_ force: Bool) func setForceSerif(_ force: Bool)

View File

@ -17,14 +17,30 @@ import ContextUI
import Pasteboard import Pasteboard
import SaveToCameraRoll import SaveToCameraRoll
import ShareController 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 context: AccountContext
private let webPage: TelegramMediaWebpage
private let presentationData: PresentationData private let presentationData: PresentationData
private let theme: InstantPageTheme private let theme: InstantPageTheme
private let sourceLocation: InstantPageSourceLocation 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 initialAnchor: String?
private var pendingAnchor: String? private var pendingAnchor: String?
private var initialState: InstantPageStoredState? private var initialState: InstantPageStoredState?
@ -48,34 +64,66 @@ private final class InstantPageContainerNode: ASDisplayNode, UIScrollViewDelegat
var currentAccessibilityAreas: [AccessibilityAreaNode] = [] var currentAccessibilityAreas: [AccessibilityAreaNode] = []
var pushContent: (BrowserScreen.Subject) -> Void = { _ in }
var onScrollingUpdate: (ContentScrollingUpdate) -> Void = { _ in } var onScrollingUpdate: (ContentScrollingUpdate) -> Void = { _ in }
var openMedia: (InstantPageMedia) -> Void = { _ in } var minimize: () -> Void = { }
var longPressMedia: (InstantPageMedia) -> Void = { _ in }
var openPeer: (EnginePeer) -> Void = { _ in } 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 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 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 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.context = context
self.webPage = webPage self.webPage = webPage
self.presentationData = context.sharedContext.currentPresentationData.with { $0 } self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
self.theme = instantPageThemeForType(.light, settings: .defaultSettings) self.theme = instantPageThemeForType(.light, settings: .defaultSettings)
self.sourceLocation = sourceLocation 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 = ASScrollNode()
self.scrollNode.backgroundColor = self.theme.pageBackgroundColor self.scrollNode.backgroundColor = self.theme.pageBackgroundColor
self.scrollNodeFooter = ASDisplayNode() self.scrollNodeFooter = ASDisplayNode()
self.scrollNodeFooter.backgroundColor = self.theme.panelBackgroundColor 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.addSubnode(self.scrollNode)
self.scrollNode.addSubnode(self.scrollNodeFooter) self.scrollNode.addSubnode(self.scrollNodeFooter)
@ -101,9 +149,21 @@ private final class InstantPageContainerNode: ASDisplayNode, UIScrollViewDelegat
} }
} }
self.scrollNode.view.addGestureRecognizer(recognizer) 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 { deinit {
self.webpageDisposable?.dispose()
self.hiddenMediaDisposable.dispose()
self.loadWebpageDisposable.dispose()
self.resolveUrlDisposable.dispose()
self.updateLayoutDisposable.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) { func updateLayout(size: CGSize, insets: UIEdgeInsets, transition: ContainedViewLayoutTransition) {
self.containerLayout = (size, insets) self.containerLayout = (size, insets)
var updateVisibleItems = false
let resetContentOffset = self.scrollNode.bounds.size.width.isZero || self.setupScrollOffsetOnLayout || !(self.initialAnchor ?? "").isEmpty
var scrollInsets = insets var scrollInsets = insets
scrollInsets.top = 0.0 scrollInsets.top = 0.0
if self.scrollNode.view.contentInset != insets { if self.scrollNode.view.contentInset != insets {
self.scrollNode.view.contentInset = scrollInsets self.scrollNode.view.contentInset = scrollInsets
self.scrollNode.view.scrollIndicatorInsets = 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 { let scrollFrame = CGRect(origin: CGPoint(x: 0.0, y: insets.top), size: CGSize(width: size.width, height: size.height - insets.top))
self.updatePageLayout() 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) self.updateVisibleItems(visibleBounds: self.scrollNode.view.bounds)
} }
} }
private func updatePageLayout() { private func updatePageLayout() {
guard let (size, insets) = self.containerLayout else { guard let (size, insets) = self.containerLayout, let webPage = self.webPage else {
return return
} }
@ -355,32 +550,9 @@ private final class InstantPageContainerNode: ASDisplayNode, UIScrollViewDelegat
}, longPressMedia: { [weak self] media in }, longPressMedia: { [weak self] media in
self?.longPressMedia(media) self?.longPressMedia(media)
}, activatePinchPreview: { [weak self] sourceNode in }, activatePinchPreview: { [weak self] sourceNode in
let _ = self self?.activatePinchPreview(sourceNode: sourceNode)
// 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)
}, pinchPreviewFinished: { [weak self] itemNode in }, pinchPreviewFinished: { [weak self] itemNode in
let _ = self self?.pinchPreviewFinished(itemNode: itemNode)
// 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
// }
// }
// }
}, openPeer: { [weak self] peerId in }, openPeer: { [weak self] peerId in
self?.openPeer(peerId) self?.openPeer(peerId)
}, openUrl: { [weak self] url in }, openUrl: { [weak self] url in
@ -547,6 +719,13 @@ private final class InstantPageContainerNode: ASDisplayNode, UIScrollViewDelegat
)) ))
} }
self.previousScrollingOffset = ScrollingOffsetState(value: scrollView.contentOffset.y, isDraggingOrDecelerating: isInteracting) 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 { private func scrollableContentOffset(item: InstantPageScrollableItem) -> CGPoint {
@ -645,6 +824,230 @@ private final class InstantPageContainerNode: ASDisplayNode, UIScrollViewDelegat
return nil 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) { 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 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 { 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() 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 }), 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 { 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(self.webPage), media: image), resource: $0.resource)) }))), nil) 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) })], catchTapsOutside: true)
self.present(controller, ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak self] in self.present(controller, ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak self] in
if let self { if let _ = self {
for (_, itemNode) in self.visibleItemsWithNodes { // for (_, itemNode) in self.visibleItemsWithNodes {
if let (node, _, _) = itemNode.transitionNode(media: media) { // if let (node, _, _) = itemNode.transitionNode(media: media) {
return (self.scrollNode, node.convert(node.bounds, to: self.scrollNode), self, self.bounds) // return (self.scrollNode, node.convert(node.bounds, to: self.scrollNode), self, self.bounds)
} // }
} // }
} }
return nil 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) { @objc private func tapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) {
switch recognizer.state { switch recognizer.state {
case .ended: case .ended:
if let (gesture, _) = recognizer.lastRecognizedGestureAndLocation { if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation {
switch gesture { switch gesture {
case .tap: case .tap:
break if let url = self.urlForTapLocation(location) {
// if let url = self.urlForTapLocation(location) { self.openUrl(url)
// self.openUrl(url) }
// }
case .longTap: case .longTap:
break if let url = self.urlForTapLocation(location) {
// if let theme = self.theme, let url = self.urlForTapLocation(location) { let canOpenIn = availableOpenInOptions(context: self.context, item: .url(url: url.url)).count > 1
// 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 openText = canOpenIn ? self.strings.Conversation_FileOpenIn : self.strings.Conversation_LinkDialogOpen let actionSheet = ActionSheetController(instantPageTheme: self.theme)
// let actionSheet = ActionSheetController(instantPageTheme: theme) actionSheet.setItemGroups([ActionSheetItemGroup(items: [
// actionSheet.setItemGroups([ActionSheetItemGroup(items: [ ActionSheetTextItem(title: url.url),
// ActionSheetTextItem(title: url.url), ActionSheetButtonItem(title: openText, color: .accent, action: { [weak self, weak actionSheet] in
// ActionSheetButtonItem(title: openText, color: .accent, action: { [weak self, weak actionSheet] in actionSheet?.dismissAnimated()
// actionSheet?.dismissAnimated() if let strongSelf = self {
// if let strongSelf = self { if canOpenIn {
// if canOpenIn { strongSelf.openUrlIn(url)
// strongSelf.openUrlIn(url) } else {
// } else { strongSelf.openUrl(url)
// strongSelf.openUrl(url) }
// } }
// } }),
// }), ActionSheetButtonItem(title: self.presentationData.strings.ShareMenu_CopyShareLink, color: .accent, action: { [weak actionSheet] in
// ActionSheetButtonItem(title: self.strings.ShareMenu_CopyShareLink, color: .accent, action: { [weak actionSheet] in actionSheet?.dismissAnimated()
// actionSheet?.dismissAnimated() UIPasteboard.general.string = url.url
// UIPasteboard.general.string = url.url }),
// }), ActionSheetButtonItem(title: self.presentationData.strings.Conversation_AddToReadingList, color: .accent, action: { [weak actionSheet] in
// ActionSheetButtonItem(title: self.strings.Conversation_AddToReadingList, color: .accent, action: { [weak actionSheet] in actionSheet?.dismissAnimated()
// actionSheet?.dismissAnimated() if let link = URL(string: url.url) {
// if let link = URL(string: url.url) { let _ = try? SSReadingList.default()?.addItem(with: link, title: nil, previewText: nil)
// let _ = try? SSReadingList.default()?.addItem(with: link, title: nil, previewText: nil) }
// } })
// }) ]), ActionSheetItemGroup(items: [
// ]), ActionSheetItemGroup(items: [ ActionSheetButtonItem(title: self.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
// ActionSheetButtonItem(title: self.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in actionSheet?.dismissAnimated()
// actionSheet?.dismissAnimated() })
// }) ])])
// ])]) self.present(actionSheet, nil)
// self.present(actionSheet, nil) } else if let (item, parentOffset) = self.textItemAtLocation(location) {
// } else if let (item, parentOffset) = self.textItemAtLocation(location) { let textFrame = item.frame
// let textFrame = item.frame var itemRects = item.lineRects()
// var itemRects = item.lineRects() for i in 0 ..< itemRects.count {
// 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)
// 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())
// self.updateTextSelectionRects(itemRects, text: item.plainText()) }
// }
default: default:
break break
} }
@ -917,7 +1340,7 @@ private final class InstantPageContainerNode: ASDisplayNode, UIScrollViewDelegat
} }
self.scrollNode.view.setContentOffset(CGPoint(x: 0.0, y: targetY), animated: true) 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.loadProgress.set(0.5)
self.pendingAnchor = anchor self.pendingAnchor = anchor
} }
@ -952,89 +1375,3 @@ private final class InstantPageContainerNode: ASDisplayNode, UIScrollViewDelegat
self.updateVisibleItems(visibleBounds: self.scrollNode.view.bounds, animated: animated) self.updateVisibleItems(visibleBounds: self.scrollNode.view.bounds, animated: animated)
} }
} }
final class BrowserInstantPageContent: UIView, BrowserContent {
var onScrollingUpdate: (ContentScrollingUpdate) -> Void = { _ in }
private var _state: BrowserContentState
private let statePromise: Promise<BrowserContentState>
private let webPage: TelegramMediaWebpage
private var initialized = false
private let instantPageNode: InstantPageContainerNode
var state: Signal<BrowserContentState, NoError> {
return self.statePromise.get()
}
init(context: AccountContext, webPage: TelegramMediaWebpage, url: String, sourceLocation: InstantPageSourceLocation) {
self.webPage = webPage
let title: String
if case let .Loaded(content) = webPage.content {
title = content.title ?? ""
} else {
title = ""
}
self._state = BrowserContentState(title: title, url: url, estimatedProgress: 0.0, contentType: .instantPage)
self.statePromise = Promise<BrowserContentState>(self._state)
self.instantPageNode = InstantPageContainerNode(context: context, webPage: webPage, sourceLocation: sourceLocation)
super.init(frame: .zero)
self.addSubnode(self.instantPageNode)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func reload() {
}
func stop() {
}
func navigateBack() {
}
func navigateForward() {
}
func setFontSize(_ fontSize: CGFloat) {
}
func setForceSerif(_ force: Bool) {
}
func setSearch(_ query: String?, completion: ((Int) -> Void)?) {
}
func scrollToPreviousSearchResult(completion: ((Int, Int) -> Void)?) {
}
func scrollToNextSearchResult(completion: ((Int, Int) -> Void)?) {
}
func scrollToTop() {
let scrollView = self.instantPageNode.scrollNode.view
scrollView.setContentOffset(CGPoint(x: 0.0, y: -scrollView.contentInset.top), animated: true)
}
func updateLayout(size: CGSize, insets: UIEdgeInsets, transition: ComponentTransition) {
// let layout = ContainerViewLayout(size: size, metrics: LayoutMetrics(widthClass: .compact, heightClass: .compact, orientation: .portrait), deviceMetrics: .iPhoneX, intrinsicInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: insets.bottom, right: 0.0), safeInsets: UIEdgeInsets(top: 0.0, left: insets.left, bottom: 0.0, right: insets.right), additionalInsets: .zero, statusBarHeight: nil, inputHeight: nil, inputHeightIsInteractivellyChanging: false, inVoiceOver: false)
self.instantPageNode.updateLayout(size: size, insets: insets, transition: transition.containedViewLayoutTransition)
self.instantPageNode.frame = CGRect(x: 0.0, y: 0.0, width: size.width, height: size.height)
//transition.updateFrame(view: self.webView, frame: CGRect(origin: CGPoint(x: 0.0, y: 56.0), size: CGSize(width: size.width, height: size.height - 56.0)))
}
}

View File

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

View File

@ -16,6 +16,7 @@ import OpenInExternalAppUI
import MultilineTextComponent import MultilineTextComponent
import MinimizedContainer import MinimizedContainer
import InstantPageUI import InstantPageUI
import NavigationStackComponent
private let settingsTag = GenericComponentViewTag() private let settingsTag = GenericComponentViewTag()
@ -26,6 +27,7 @@ private final class BrowserScreenComponent: CombinedComponent {
let contentState: BrowserContentState? let contentState: BrowserContentState?
let presentationState: BrowserPresentationState let presentationState: BrowserPresentationState
let performAction: ActionSlot<BrowserScreen.Action> let performAction: ActionSlot<BrowserScreen.Action>
let performHoldAction: (UIView, ContextGesture?, BrowserScreen.Action) -> Void
let panelCollapseFraction: CGFloat let panelCollapseFraction: CGFloat
init( init(
@ -33,12 +35,14 @@ private final class BrowserScreenComponent: CombinedComponent {
contentState: BrowserContentState?, contentState: BrowserContentState?,
presentationState: BrowserPresentationState, presentationState: BrowserPresentationState,
performAction: ActionSlot<BrowserScreen.Action>, performAction: ActionSlot<BrowserScreen.Action>,
performHoldAction: @escaping (UIView, ContextGesture?, BrowserScreen.Action) -> Void,
panelCollapseFraction: CGFloat panelCollapseFraction: CGFloat
) { ) {
self.context = context self.context = context
self.contentState = contentState self.contentState = contentState
self.presentationState = presentationState self.presentationState = presentationState
self.performAction = performAction self.performAction = performAction
self.performHoldAction = performHoldAction
self.panelCollapseFraction = panelCollapseFraction self.panelCollapseFraction = panelCollapseFraction
} }
@ -72,7 +76,8 @@ private final class BrowserScreenComponent: CombinedComponent {
return { context in return { context in
let environment = context.environment[ViewControllerComponentContainer.Environment.self].value let environment = context.environment[ViewControllerComponentContainer.Environment.self].value
let performAction = context.component.performAction let performAction = context.component.performAction
let performHoldAction = context.component.performHoldAction
let navigationContent: AnyComponentWithIdentity<Empty>? let navigationContent: AnyComponentWithIdentity<Empty>?
var navigationLeftItems: [AnyComponentWithIdentity<Empty>] var navigationLeftItems: [AnyComponentWithIdentity<Empty>]
var navigationRightItems: [AnyComponentWithIdentity<Empty>] var navigationRightItems: [AnyComponentWithIdentity<Empty>]
@ -172,7 +177,7 @@ private final class BrowserScreenComponent: CombinedComponent {
leftItems: navigationLeftItems, leftItems: navigationLeftItems,
rightItems: navigationRightItems, rightItems: navigationRightItems,
centerItem: navigationContent, centerItem: navigationContent,
readingProgress: 0.0, readingProgress: context.component.contentState?.readingProgress ?? 0.0,
loadingProgress: context.component.contentState?.estimatedProgress, loadingProgress: context.component.contentState?.estimatedProgress,
collapseFraction: collapseFraction collapseFraction: collapseFraction
), ),
@ -206,7 +211,8 @@ private final class BrowserScreenComponent: CombinedComponent {
textColor: environment.theme.rootController.navigationBar.primaryTextColor, textColor: environment.theme.rootController.navigationBar.primaryTextColor,
canGoBack: context.component.contentState?.canGoBack ?? false, canGoBack: context.component.contentState?.canGoBack ?? false,
canGoForward: context.component.contentState?.canGoForward ?? 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 weak var controller: BrowserScreen?
private let context: AccountContext private let context: AccountContext
private let contentContainerView: UIView private let contentContainerView = UIView()
fileprivate var content: BrowserContent? fileprivate let contentNavigationContainer = ComponentView<Empty>()
fileprivate var content: [BrowserContent] = []
private var contentState: BrowserContentState? fileprivate var contentState: BrowserContentState?
private var contentStateDisposable: Disposable? private var contentStateDisposable = MetaDisposable()
private var presentationState: BrowserPresentationState 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 presentationData: PresentationData
private var validLayout: (ContainerViewLayout, CGFloat)? private var validLayout: (ContainerViewLayout, CGFloat)?
@ -296,41 +303,13 @@ public class BrowserScreen: ViewController, MinimizableController {
self.presentationData = self.context.sharedContext.currentPresentationData.with { $0 } self.presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
self.presentationState = BrowserPresentationState(fontSize: 100, fontIsSerif: false, isSearching: false, searchResultIndex: 0, searchResultCount: 0, searchQueryIsEmpty: true) 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() super.init()
let content: BrowserContent self.pushContent(controller.subject, transition: .immediate)
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.performAction.connect { [weak self] action in 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 return
} }
switch action { switch action {
@ -341,7 +320,11 @@ public class BrowserScreen: ViewController, MinimizableController {
case .stop: case .stop:
content.stop() content.stop()
case .navigateBack: case .navigateBack:
content.navigateBack() if content.currentState.canGoBack {
content.navigateBack()
} else {
self.popContent(transition: .spring(duration: 0.4))
}
case .navigateForward: case .navigateForward:
content.navigateForward() content.navigateForward()
case .share: case .share:
@ -458,22 +441,139 @@ public class BrowserScreen: ViewController, MinimizableController {
} }
deinit { deinit {
self.contentStateDisposable?.dispose() self.contentStateDisposable.dispose()
} }
override func didLoad() { override func didLoad() {
super.didLoad() super.didLoad()
self.contentContainerView.clipsToBounds = true
self.view.addSubview(self.contentContainerView) self.view.addSubview(self.contentContainerView)
if let content = self.content {
self.contentContainerView.addSubview(content)
}
} }
func updatePresentationState(animated: Bool = false, _ f: (BrowserPresentationState) -> BrowserPresentationState) { func updatePresentationState(animated: Bool = false, _ f: (BrowserPresentationState) -> BrowserPresentationState) {
self.presentationState = f(self.presentationState) self.presentationState = f(self.presentationState)
self.requestLayout(transition: animated ? .easeInOut(duration: 0.2) : .immediate) 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() { func minimize() {
guard let controller = self.controller, let navigationController = controller.navigationController as? NavigationController else { 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? { override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let result = super.hitTest(point, with: event) 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 content.hitTest(self.view.convert(point, to: content), with: event)
} }
return result 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) { func requestLayout(transition: ComponentTransition) {
if let (layout, navigationBarHeight) = self.validLayout { if let (layout, navigationBarHeight) = self.validLayout {
self.containerLayoutUpdated(layout: layout, navigationBarHeight: navigationBarHeight, transition: transition) self.containerLayoutUpdated(layout: layout, navigationBarHeight: navigationBarHeight, transition: transition)
@ -694,6 +839,11 @@ public class BrowserScreen: ViewController, MinimizableController {
contentState: self.contentState, contentState: self.contentState,
presentationState: self.presentationState, presentationState: self.presentationState,
performAction: self.performAction, performAction: self.performAction,
performHoldAction: { [weak self] view, gesture, action in
if let self {
self.performHoldAction(view: view, gesture: gesture, action: action)
}
},
panelCollapseFraction: self.scrollingPanelOffsetFraction 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: componentView, frame: CGRect(origin: .zero, size: componentSize))
} }
transition.setFrame(view: self.contentContainerView, frame: CGRect(origin: .zero, size: layout.size)) transition.setFrame(view: self.contentContainerView, frame: CGRect(origin: .zero, size: layout.size))
if let content = self.content {
let collapsedHeight: CGFloat = 24.0 var items: [AnyComponentWithIdentity<Empty>] = []
let topInset: CGFloat = environment.statusBarHeight + navigationBarHeight * (1.0 - self.scrollingPanelOffsetFraction) + collapsedHeight * self.scrollingPanelOffsetFraction for content in self.content {
let bottomInset = 49.0 + layout.intrinsicInsets.bottom items.append(
content.updateLayout(size: layout.size, insets: UIEdgeInsets(top: topInset, left: layout.safeInsets.left, bottom: bottomInset, right: layout.safeInsets.right), transition: transition) AnyComponentWithIdentity(id: content.uuid, component: AnyComponent(
transition.setFrame(view: content, frame: CGRect(origin: .zero, size: layout.size)) 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 self.navigationBarHeight = environment.navigationHeight
@ -726,7 +912,7 @@ public class BrowserScreen: ViewController, MinimizableController {
public enum Subject { public enum Subject {
case webPage(url: String) case webPage(url: String)
case instantPage(webPage: TelegramMediaWebpage, sourceLocation: InstantPageSourceLocation) case instantPage(webPage: TelegramMediaWebpage, anchor: String?, sourceLocation: InstantPageSourceLocation)
} }
private let context: AccountContext private let context: AccountContext
@ -743,7 +929,7 @@ public class BrowserScreen: ViewController, MinimizableController {
self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .allButUpsideDown) self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .allButUpsideDown)
self.scrollToTop = { [weak self] in 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() preconditionFailure()
} }
private var node: Node {
return self.displayNode as! Node
}
override public func loadDisplayNode() { override public func loadDisplayNode() {
self.displayNode = Node(controller: self) self.displayNode = Node(controller: self)
@ -760,11 +950,30 @@ public class BrowserScreen: ViewController, MinimizableController {
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition) 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 isMinimized = false
public var isMinimizable = true 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 { private final class BrowserReferenceContentSource: ContextReferenceContentSource {
@ -780,3 +989,70 @@ private final class BrowserReferenceContentSource: ContextReferenceContentSource
return ContextControllerReferenceViewInfo(referenceView: self.sourceView, contentAreaInScreenSpace: UIScreen.main.bounds) return ContextControllerReferenceViewInfo(referenceView: self.sourceView, contentAreaInScreenSpace: UIScreen.main.bounds)
} }
} }
private final class BrowserContentComponent: Component {
let content: BrowserContent
let insets: UIEdgeInsets
let navigationBarHeight: CGFloat
let scrollingPanelOffsetFraction: CGFloat
init(
content: BrowserContent,
insets: UIEdgeInsets,
navigationBarHeight: CGFloat,
scrollingPanelOffsetFraction: CGFloat
) {
self.content = content
self.insets = insets
self.navigationBarHeight = navigationBarHeight
self.scrollingPanelOffsetFraction = scrollingPanelOffsetFraction
}
static func ==(lhs: BrowserContentComponent, rhs: BrowserContentComponent) -> Bool {
if lhs.content.uuid != rhs.content.uuid {
return false
}
if lhs.insets != rhs.insets {
return false
}
if lhs.navigationBarHeight != rhs.navigationBarHeight {
return false
}
if lhs.scrollingPanelOffsetFraction != rhs.scrollingPanelOffsetFraction {
return false
}
return true
}
final class View: UIView {
init() {
super.init(frame: CGRect())
}
required init?(coder aDecoder: NSCoder) {
preconditionFailure()
}
func update(component: BrowserContentComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize {
if component.content.superview !== self {
self.addSubview(component.content)
}
let collapsedHeight: CGFloat = 24.0
let topInset: CGFloat = component.insets.top + component.navigationBarHeight * (1.0 - component.scrollingPanelOffsetFraction) + collapsedHeight * component.scrollingPanelOffsetFraction
let bottomInset = 49.0 + component.insets.bottom
component.content.updateLayout(size: availableSize, insets: UIEdgeInsets(top: topInset, left: component.insets.left, bottom: bottomInset, right: component.insets.right), transition: transition)
transition.setFrame(view: component.content, frame: CGRect(origin: .zero, size: availableSize))
return availableSize
}
}
func makeView() -> View {
return View()
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, transition: transition)
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -21,10 +21,12 @@ private let tagImage: UIImage? = {
}() }()
private final class StarsButtonEffectLayer: SimpleLayer { private final class StarsButtonEffectLayer: SimpleLayer {
let emitterLayer = CAEmitterLayer()
override init() { override init() {
super.init() super.init()
self.backgroundColor = UIColor.lightGray.withAlphaComponent(0.2).cgColor self.addSublayer(self.emitterLayer)
} }
override init(layer: Any) { override init(layer: Any) {
@ -35,7 +37,45 @@ private final class StarsButtonEffectLayer: SimpleLayer {
fatalError("init(coder:) has not been implemented") 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) { func update(size: CGSize) {
if self.emitterLayer.emitterCells == nil {
self.setup()
}
self.emitterLayer.emitterShape = .circle
self.emitterLayer.emitterSize = CGSize(width: size.width * 0.7, height: size.height * 0.7)
self.emitterLayer.emitterMode = .surface
self.emitterLayer.frame = CGRect(origin: .zero, size: size)
self.emitterLayer.emitterPosition = CGPoint(x: size.width / 2.0, y: size.height / 2.0)
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -12,14 +12,6 @@ struct ArbitraryRandomNumberGenerator : RandomNumberGenerator {
func next() -> UInt64 { return UInt64(drand48() * Double(UInt64.max)) } func 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? { func generateMaskImage(size originalSize: CGSize, position: CGPoint, inverse: Bool) -> UIImage? {
var size = originalSize var size = originalSize
var position = position var position = position
@ -123,10 +115,10 @@ public class InvisibleInkDustView: UIView {
emitter.setValue(2.0, forKey: "massRange") emitter.setValue(2.0, forKey: "massRange")
self.emitter = emitter self.emitter = emitter
let fingerAttractor = createEmitterBehavior(type: "simpleAttractor") let fingerAttractor = CAEmitterCell.createEmitterBehavior(type: "simpleAttractor")
fingerAttractor.setValue("fingerAttractor", forKey: "name") fingerAttractor.setValue("fingerAttractor", forKey: "name")
let alphaBehavior = createEmitterBehavior(type: "valueOverLife") let alphaBehavior = CAEmitterCell.createEmitterBehavior(type: "valueOverLife")
alphaBehavior.setValue("color.alpha", forKey: "keyPath") alphaBehavior.setValue("color.alpha", forKey: "keyPath")
alphaBehavior.setValue([0.0, 0.0, 1.0, 0.0, -1.0], forKey: "values") alphaBehavior.setValue([0.0, 0.0, 1.0, 0.0, -1.0], forKey: "values")
alphaBehavior.setValue(true, forKey: "additive") alphaBehavior.setValue(true, forKey: "additive")
@ -435,10 +427,10 @@ public class InvisibleInkDustNode: ASDisplayNode {
emitter.setValue(2.0, forKey: "massRange") emitter.setValue(2.0, forKey: "massRange")
self.emitter = emitter self.emitter = emitter
let fingerAttractor = createEmitterBehavior(type: "simpleAttractor") let fingerAttractor = CAEmitterCell.createEmitterBehavior(type: "simpleAttractor")
fingerAttractor.setValue("fingerAttractor", forKey: "name") fingerAttractor.setValue("fingerAttractor", forKey: "name")
let alphaBehavior = createEmitterBehavior(type: "valueOverLife") let alphaBehavior = CAEmitterCell.createEmitterBehavior(type: "valueOverLife")
alphaBehavior.setValue("color.alpha", forKey: "keyPath") alphaBehavior.setValue("color.alpha", forKey: "keyPath")
alphaBehavior.setValue([0.0, 0.0, 1.0, 0.0, -1.0], forKey: "values") alphaBehavior.setValue([0.0, 0.0, 1.0, 0.0, -1.0], forKey: "values")
alphaBehavior.setValue(true, forKey: "additive") alphaBehavior.setValue(true, forKey: "additive")

View File

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

View File

@ -10,11 +10,17 @@ import LocationResources
import ShimmerEffect import ShimmerEffect
public final class ItemListVenueItem: ListViewItem, ItemListItem { public final class ItemListVenueItem: ListViewItem, ItemListItem {
public enum InfoIcon {
case info
case goTo
}
let presentationData: ItemListPresentationData let presentationData: ItemListPresentationData
let engine: TelegramEngine let engine: TelegramEngine
let venue: TelegramMediaMap? let venue: TelegramMediaMap?
let title: String? let title: String?
let subtitle: String? let subtitle: String?
let icon: InfoIcon
let style: ItemListStyle let style: ItemListStyle
let action: (() -> Void)? let action: (() -> Void)?
let infoAction: (() -> Void)? let infoAction: (() -> Void)?
@ -22,12 +28,13 @@ public final class ItemListVenueItem: ListViewItem, ItemListItem {
public let sectionId: ItemListSectionId public let sectionId: ItemListSectionId
let header: ListViewItemHeader? 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.presentationData = presentationData
self.engine = engine self.engine = engine
self.venue = venue self.venue = venue
self.title = title self.title = title
self.subtitle = subtitle self.subtitle = subtitle
self.icon = icon
self.sectionId = sectionId self.sectionId = sectionId
self.style = style self.style = style
self.action = action self.action = action
@ -274,7 +281,15 @@ public class ItemListVenueItemNode: ListViewItemNode, ItemListItemNode {
strongSelf.bottomStripeNode.backgroundColor = itemSeparatorColor strongSelf.bottomStripeNode.backgroundColor = itemSeparatorColor
strongSelf.backgroundNode.backgroundColor = itemBackgroundColor strongSelf.backgroundNode.backgroundColor = itemBackgroundColor
strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.list.itemHighlightedBackgroundColor 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 let transition = ContainedViewLayoutTransition.immediate

View File

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

View File

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

View File

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

View File

@ -23,6 +23,7 @@ private struct LocationSearchEntry: Identifiable, Comparable {
let resultId: String? let resultId: String?
let title: String? let title: String?
let distance: Double let distance: Double
let story: Bool
var stableId: String { var stableId: String {
return self.location.venue?.id ?? "" return self.location.venue?.id ?? ""
@ -50,6 +51,9 @@ private struct LocationSearchEntry: Identifiable, Comparable {
if lhs.distance != rhs.distance { if lhs.distance != rhs.distance {
return false return false
} }
if lhs.story != rhs.story {
return false
}
return true return true
} }
@ -57,7 +61,7 @@ private struct LocationSearchEntry: Identifiable, Comparable {
return lhs.index < rhs.index 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 venue = self.location
let queryId = self.queryId let queryId = self.queryId
let resultId = self.resultId let resultId = self.resultId
@ -71,9 +75,11 @@ private struct LocationSearchEntry: Identifiable, Comparable {
header = ChatListSearchItemHeader(type: .mapAddress, theme: presentationData.theme, strings: presentationData.strings) header = ChatListSearchItemHeader(type: .mapAddress, theme: presentationData.theme, strings: presentationData.strings)
subtitle = presentationData.strings.Map_DistanceAway(stringForDistance(strings: presentationData.strings, distance: self.distance)).string 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) 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 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 (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries)
let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) } 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 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), 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) 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 { final class LocationSearchContainerNode: ASDisplayNode {
private let context: AccountContext private let context: AccountContext
private let interaction: LocationPickerInteraction private let interaction: LocationPickerInteraction
private let story: Bool
private let dimNode: ASDisplayNode private let dimNode: ASDisplayNode
public let listNode: ListView 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) { public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, coordinate: CLLocationCoordinate2D, interaction: LocationPickerInteraction, story: Bool) {
self.context = context self.context = context
self.interaction = interaction self.interaction = interaction
self.story = story
let presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 } let presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 }
self.presentationData = presentationData self.presentationData = presentationData
@ -162,6 +170,8 @@ final class LocationSearchContainerNode: ASDisplayNode {
let currentLocation = CLLocation(latitude: coordinate.latitude, longitude: coordinate.longitude) let currentLocation = CLLocation(latitude: coordinate.latitude, longitude: coordinate.longitude)
let themeAndStringsPromise = self.themeAndStringsPromise let themeAndStringsPromise = self.themeAndStringsPromise
let locale = localeWithStrings(presentationData.strings)
let isSearching = self._isSearching let isSearching = self._isSearching
let searchItems = self.searchQuery.get() let searchItems = self.searchQuery.get()
|> mapToSignal { query -> Signal<String?, NoError> in |> mapToSignal { query -> Signal<String?, NoError> in
@ -178,7 +188,6 @@ final class LocationSearchContainerNode: ASDisplayNode {
|> afterCompleted { |> afterCompleted {
isSearching.set(false) isSearching.set(false)
} }
let locale = localeWithStrings(presentationData.strings)
let foundPlacemarks = geocodeLocation(address: query, locale: locale) let foundPlacemarks = geocodeLocation(address: query, locale: locale)
return combineLatest(foundVenues, foundPlacemarks, themeAndStringsPromise.get()) return combineLatest(foundVenues, foundPlacemarks, themeAndStringsPromise.get())
|> delay(0.1, queue: Queue.concurrentDefaultQueue()) |> delay(0.1, queue: Queue.concurrentDefaultQueue())
@ -194,9 +203,13 @@ final class LocationSearchContainerNode: ASDisplayNode {
guard let placemarkLocation = placemark.location else { guard let placemarkLocation = placemark.location else {
continue 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 index += 1
} }
@ -207,7 +220,7 @@ final class LocationSearchContainerNode: ASDisplayNode {
switch result.message { switch result.message {
case let .mapLocation(mapMedia, _): case let .mapLocation(mapMedia, _):
if let _ = mapMedia.venue { 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 index += 1
} }
default: default:
@ -235,10 +248,21 @@ final class LocationSearchContainerNode: ASDisplayNode {
self?.listNode.clearHighlightAnimated(true) self?.listNode.clearHighlightAnimated(true)
if let _ = venue.venue { if let _ = venue.venue {
self?.interaction.sendVenue(venue, queryId, resultId) 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 { } else {
self?.interaction.goToCoordinate(venue.coordinate) self?.interaction.goToCoordinate(venue.coordinate, false)
self?.interaction.dismissSearch() self?.interaction.dismissSearch()
} }
}, goToVenue: { venue in
self?.interaction.goToCoordinate(venue.coordinate, true)
self?.interaction.dismissSearch()
}) })
strongSelf.enqueueTransition(transition) strongSelf.enqueueTransition(transition)
} }

View File

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

View File

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

View File

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

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

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

View File

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

View File

@ -4,7 +4,7 @@
#import <Foundation/Foundation.h> #import <Foundation/Foundation.h>
#import <UIKit/UIKit.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 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); UIImage * _Nullable drawSvgImage(NSData * _Nonnull data, CGSize size, UIColor * _Nullable backgroundColor, UIColor * _Nullable foregroundColor, bool opaque);

View File

@ -361,14 +361,26 @@ UIImage * _Nullable drawSvgImage(NSData * _Nonnull data, CGSize size, UIColor *b
[_data appendBytes:&command length:sizeof(command)]; [_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 @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) { UIImage * _Nullable renderPreparedImage(NSData * _Nonnull data, CGSize size, UIColor *backgroundColor, CGFloat scale, bool fit) {
NSDate *startTime = [NSDate date]; NSDate *startTime = [NSDate date];
UIColor *foregroundColor = [UIColor whiteColor]; UIColor *foregroundColor = [UIColor whiteColor];
int32_t ptr = 0; int32_t ptr = 0;
int32_t width; int32_t width;
int32_t height; int32_t height;
@ -544,7 +556,15 @@ UIImage * _Nullable renderPreparedImage(NSData * _Nonnull data, CGSize size, UIC
CGContextStrokePath(context); CGContextStrokePath(context);
} }
break; 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: default:
break; break;
} }
@ -559,7 +579,7 @@ UIImage * _Nullable renderPreparedImage(NSData * _Nonnull data, CGSize size, UIC
return resultImage; return resultImage;
} }
NSData * _Nullable prepareSvgImage(NSData * _Nonnull data) { NSData * _Nullable prepareSvgImage(NSData * _Nonnull data, bool template) {
NSDate *startTime = [NSDate date]; NSDate *startTime = [NSDate date];
NSXMLParser *parser = [[NSXMLParser alloc] initWithData:data]; NSXMLParser *parser = [[NSXMLParser alloc] initWithData:data];
@ -600,8 +620,12 @@ NSData * _Nullable prepareSvgImage(NSData * _Nonnull data) {
} }
if (shape->fill.type != NSVG_PAINT_NONE) { 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 isFirst = true;
bool hasStartPoint = false; bool hasStartPoint = false;
CGPoint startPoint; CGPoint startPoint;

View File

@ -4,11 +4,54 @@ import Postbox
import TelegramApi import TelegramApi
import MtProtoKit import MtProtoKit
public struct RevenueStats: Equatable { public struct RevenueStats: Equatable, Codable {
public struct Balances: Equatable { 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 currentBalance: Int64
public let availableBalance: Int64 public let availableBalance: Int64
public let overallRevenue: 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 public let topHoursGraph: StatsGraph
@ -23,6 +66,22 @@ public struct RevenueStats: Equatable {
self.usdRate = usdRate 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 { public static func == (lhs: RevenueStats, rhs: RevenueStats) -> Bool {
if lhs.topHoursGraph != rhs.topHoursGraph { if lhs.topHoursGraph != rhs.topHoursGraph {
return false return false
@ -124,6 +183,17 @@ private final class RevenueStatsContextImpl {
self._statePromise.set(.single(self._state)) self._statePromise.set(.single(self._state))
self.load() 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 { deinit {
@ -155,9 +225,17 @@ private final class RevenueStatsContextImpl {
self.disposable.set((signal self.disposable.set((signal
|> deliverOnMainQueue).start(next: { [weak self] stats in |> deliverOnMainQueue).start(next: { [weak self] stats in
if let strongSelf = self { if let self {
strongSelf._state = RevenueStatsContextState(stats: stats) self._state = RevenueStatsContextState(stats: stats)
strongSelf._statePromise.set(.single(strongSelf._state)) self._statePromise.set(.single(self._state))
if let stats {
let _ = (self.account.postbox.transaction { transaction in
if let entry = CodableEntry(stats) {
transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedRevenueStats, key: StarsRevenueStats.key(peerId: peerId)), entry: entry)
}
}).start()
}
} }
})) }))
} }

View File

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

View File

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

View File

@ -745,6 +745,23 @@ public func universalServiceMessageString(presentationData: (PresentationTheme,
attributes[1] = boldAttributes attributes[1] = boldAttributes
attributedString = addAttributesToStringWithRanges(strings.Notification_PremiumGift_Sent(compactAuthorName, price)._tuple, body: bodyAttributes, argumentAttributes: attributes) 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): case let .topicCreated(title, iconColor, iconFileId):
if forForumOverview { if forForumOverview {
let maybeFileId = iconFileId ?? 0 let maybeFileId = iconFileId ?? 0
@ -992,6 +1009,39 @@ public func universalServiceMessageString(presentationData: (PresentationTheme,
attributedString = addAttributesToStringWithRanges(resultTitleString._tuple, body: bodyAttributes, argumentAttributes: [0: boldAttributes]) 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: case .unknown:
attributedString = nil attributedString = nil
} }

View File

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

View File

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

View File

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

View File

@ -235,6 +235,8 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
return (contentProperties, nil, CGFloat.greatestFiniteMagnitude, { constrainedSize, position in return (contentProperties, nil, CGFloat.greatestFiniteMagnitude, { constrainedSize, position in
var giftSize = CGSize(width: 220.0, height: 240.0) 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 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 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, _, _): case let .giftPremium(_, _, monthsValue, _, _):
months = monthsValue months = monthsValue
text = item.presentationData.strings.Notification_PremiumGift_Subtitle(item.presentationData.strings.Notification_PremiumGift_Months(months)).string 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, _, _, _, _): case let .giftCode(_, fromGiveaway, unclaimed, channelId, monthsValue, _, _, _, _):
if channelId == nil { if channelId == nil {
months = monthsValue months = monthsValue

View File

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

View File

@ -438,6 +438,8 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget {
if tinted { if tinted {
self.updateTintColor() self.updateTintColor()
} }
case .ton:
self.updateTon()
} }
} else if let file = file { } else if let file = file {
self.updateFile(file: file, attemptSynchronousLoad: attemptSynchronousLoad) self.updateFile(file: file, attemptSynchronousLoad: attemptSynchronousLoad)
@ -623,6 +625,10 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget {
self.contents = tinted ? tintedStarImage?.cgImage : starImage?.cgImage self.contents = tinted ? tintedStarImage?.cgImage : starImage?.cgImage
} }
private func updateTon() {
self.contents = tonImage?.cgImage
}
private func updateFile(file: TelegramMediaFile, attemptSynchronousLoad: Bool) { private func updateFile(file: TelegramMediaFile, attemptSynchronousLoad: Bool) {
guard let arguments = self.arguments else { guard let arguments = self.arguments else {
return return
@ -899,7 +905,17 @@ private let starImage: UIImage? = {
context.clear(CGRect(origin: .zero, size: size)) context.clear(CGRect(origin: .zero, size: size))
if let image = UIImage(bundleImageName: "Premium/Stars/StarLarge"), let cgImage = image.cgImage { 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) })?.withRenderingMode(.alwaysTemplate)
}() }()

View File

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

View File

@ -24,6 +24,7 @@ public enum CodableDrawingEntity: Equatable {
case vector(DrawingVectorEntity) case vector(DrawingVectorEntity)
case location(DrawingLocationEntity) case location(DrawingLocationEntity)
case link(DrawingLinkEntity) case link(DrawingLinkEntity)
case weather(DrawingWeatherEntity)
public init?(entity: DrawingEntity) { public init?(entity: DrawingEntity) {
if let entity = entity as? DrawingStickerEntity { if let entity = entity as? DrawingStickerEntity {
@ -40,6 +41,8 @@ public enum CodableDrawingEntity: Equatable {
self = .location(entity) self = .location(entity)
} else if let entity = entity as? DrawingLinkEntity { } else if let entity = entity as? DrawingLinkEntity {
self = .link(entity) self = .link(entity)
} else if let entity = entity as? DrawingWeatherEntity {
self = .weather(entity)
} else { } else {
return nil return nil
} }
@ -61,6 +64,8 @@ public enum CodableDrawingEntity: Equatable {
return entity return entity
case let .link(entity): case let .link(entity):
return entity return entity
case let .weather(entity):
return entity
} }
} }
@ -109,6 +114,14 @@ public enum CodableDrawingEntity: Equatable {
size = entitySize 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: default:
return nil return nil
} }
@ -198,6 +211,7 @@ extension CodableDrawingEntity: Codable {
case vector case vector
case location case location
case link case link
case weather
} }
public init(from decoder: Decoder) throws { public init(from decoder: Decoder) throws {
@ -218,6 +232,8 @@ extension CodableDrawingEntity: Codable {
self = .location(try container.decode(DrawingLocationEntity.self, forKey: .entity)) self = .location(try container.decode(DrawingLocationEntity.self, forKey: .entity))
case .link: case .link:
self = .link(try container.decode(DrawingLinkEntity.self, forKey: .entity)) 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): case let .link(payload):
try container.encode(EntityType.link, forKey: .type) try container.encode(EntityType.link, forKey: .type)
try container.encode(payload, forKey: .entity) try container.encode(payload, forKey: .entity)
case let .weather(payload):
try container.encode(EntityType.weather, forKey: .type)
try container.encode(payload, forKey: .entity)
} }
} }
} }

View File

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

View File

@ -4155,6 +4155,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
} }
if !self.didSetupStaticEmojiPack { if !self.didSetupStaticEmojiPack {
self.didSetupStaticEmojiPack = true
self.staticEmojiPack.set(self.context.engine.stickers.loadedStickerPack(reference: .name("staticemoji"), forceActualized: false)) 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) emojiFile = .single(nil)
} }
let _ = emojiFile.start(next: { [weak self] emojiFile in let _ = (emojiFile
|> deliverOnMainQueue).start(next: { [weak self] emojiFile in
guard let self else { guard let self else {
return return
} }
@ -4570,6 +4572,63 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
self.mediaEditor?.play() 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) { func updateModalTransitionFactor(_ value: CGFloat, transition: ContainedViewLayoutTransition) {
guard let layout = self.validLayout, case .compact = layout.metrics.widthClass else { guard let layout = self.validLayout, case .compact = layout.metrics.widthClass else {
return return
@ -4824,6 +4883,14 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
controller?.dismiss(animated: true) 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 controller.pushController = { [weak self] c in
self?.controller?.push(c) self?.controller?.push(c)
} }

View File

@ -4,14 +4,6 @@ import Display
import CoreImage import CoreImage
import MediaEditor 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 private var previousBeginTime: Int = 3
final class StickerCutoutOutlineView: UIView { final class StickerCutoutOutlineView: UIView {
@ -81,7 +73,7 @@ final class StickerCutoutOutlineView: UIView {
let lineEmitterCell = CAEmitterCell() let lineEmitterCell = CAEmitterCell()
lineEmitterCell.beginTime = CACurrentMediaTime() lineEmitterCell.beginTime = CACurrentMediaTime()
let lineAlphaBehavior = createEmitterBehavior(type: "valueOverLife") let lineAlphaBehavior = CAEmitterCell.createEmitterBehavior(type: "valueOverLife")
lineAlphaBehavior.setValue("color.alpha", forKey: "keyPath") lineAlphaBehavior.setValue("color.alpha", forKey: "keyPath")
lineAlphaBehavior.setValue([0.0, 0.5, 0.8, 0.5, 0.0], forKey: "values") lineAlphaBehavior.setValue([0.0, 0.5, 0.8, 0.5, 0.0], forKey: "values")
lineEmitterCell.setValue([lineAlphaBehavior], forKey: "emitterBehaviors") lineEmitterCell.setValue([lineAlphaBehavior], forKey: "emitterBehaviors")
@ -107,7 +99,7 @@ final class StickerCutoutOutlineView: UIView {
let glowEmitterCell = CAEmitterCell() let glowEmitterCell = CAEmitterCell()
glowEmitterCell.beginTime = CACurrentMediaTime() glowEmitterCell.beginTime = CACurrentMediaTime()
let glowAlphaBehavior = createEmitterBehavior(type: "valueOverLife") let glowAlphaBehavior = CAEmitterCell.createEmitterBehavior(type: "valueOverLife")
glowAlphaBehavior.setValue("color.alpha", forKey: "keyPath") glowAlphaBehavior.setValue("color.alpha", forKey: "keyPath")
glowAlphaBehavior.setValue([0.0, 0.32, 0.4, 0.2, 0.0], forKey: "values") glowAlphaBehavior.setValue([0.0, 0.32, 0.4, 0.2, 0.0], forKey: "values")
glowEmitterCell.setValue([glowAlphaBehavior], forKey: "emitterBehaviors") glowEmitterCell.setValue([glowAlphaBehavior], forKey: "emitterBehaviors")

View File

@ -241,7 +241,10 @@ public class MinimizedContainerImpl: ASDisplayNode, MinimizedContainer, ASScroll
if let snapshotView = self.snapshotView { 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) 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 requiresBlur = false
var blurFrame = snapshotFrame var blurFrame = snapshotFrame
if snapshotView.frame.width * 1.1 < size.width { 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.updateBounds(node: itemNode, bounds: CGRect(origin: .zero, size: layout.size))
} }
transition.updateTransform(node: itemNode, transform: CATransform3DIdentity) 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 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 self.isApplyingTransition = false
if self.currentTransition == currentTransition { if self.currentTransition == currentTransition {

View File

@ -25,11 +25,14 @@ final class MinimizedHeaderNode: ASDisplayNode {
var theme: NavigationControllerTheme { var theme: NavigationControllerTheme {
didSet { didSet {
self.backgroundView.backgroundColor = self.theme.navigationBar.opaqueBackgroundColor 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 let strings: PresentationStrings
private let backgroundView = UIView() private let backgroundView = UIView()
private let progressView = UIView()
private var iconView = UIImageView() private var iconView = UIImageView()
private let titleLabel = ComponentView<Empty>() private let titleLabel = ComponentView<Empty>()
private let closeButton = ComponentView<Empty>() private let closeButton = ComponentView<Empty>()
@ -48,6 +51,12 @@ final class MinimizedHeaderNode: ASDisplayNode {
self.icon = nil 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 { if newValue.count != self.controllers.count {
self._controllers = newValue.map { WeakController($0) } 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? { var title: String? {
didSet { didSet {
if let (size, insets, isExpanded) = self.validLayout { if let (size, insets, isExpanded) = self.validLayout {
@ -111,20 +128,25 @@ final class MinimizedHeaderNode: ASDisplayNode {
self.strings = strings self.strings = strings
self.backgroundView.clipsToBounds = true self.backgroundView.clipsToBounds = true
self.backgroundView.backgroundColor = theme.navigationBar.opaqueBackgroundColor self.backgroundView.backgroundColor = self.theme.navigationBar.opaqueBackgroundColor
self.backgroundView.layer.cornerRadius = 10.0 self.backgroundView.layer.cornerRadius = 10.0
if #available(iOS 11.0, *) { if #available(iOS 11.0, *) {
self.backgroundView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] 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.clipsToBounds = true
self.iconView.layer.cornerRadius = 2.5 self.iconView.layer.cornerRadius = 2.5
self.iconView.tintColor = self.theme.navigationBar.primaryTextColor
super.init() super.init()
self.clipsToBounds = true self.clipsToBounds = true
self.view.addSubview(self.backgroundView) self.view.addSubview(self.backgroundView)
self.backgroundView.addSubview(self.progressView)
self.backgroundView.addSubview(self.iconView) self.backgroundView.addSubview(self.iconView)
applySmoothRoundedCorners(self.backgroundView.layer) applySmoothRoundedCorners(self.backgroundView.layer)
@ -149,9 +171,9 @@ final class MinimizedHeaderNode: ASDisplayNode {
func update(size: CGSize, insets: UIEdgeInsets, isExpanded: Bool, transition: ContainedViewLayoutTransition) { func update(size: CGSize, insets: UIEdgeInsets, isExpanded: Bool, transition: ContainedViewLayoutTransition) {
self.validLayout = (size, insets, isExpanded) self.validLayout = (size, insets, isExpanded)
let headerHeight: CGFloat = 44.0 let headerHeight: CGFloat = 44.0
let titleSpacing: CGFloat = 4.0 let titleSpacing: CGFloat = 6.0
var titleSideInset: CGFloat = 56.0 var titleSideInset: CGFloat = 56.0
if !isExpanded { if !isExpanded {
titleSideInset += insets.left 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) 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) 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 { 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.updateFrame(view: self.backgroundView, frame: CGRect(origin: .zero, size: CGSize(width: size.width, height: 243.0)))
transition.updateAlpha(layer: self.progressView.layer, alpha: isExpanded && self.progress != nil ? 1.0 : 0.0)
if let progress = self.progress {
self.progressView.frame = CGRect(origin: .zero, size: CGSize(width: size.width * CGFloat(progress), height: 243.0))
}
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

@ -3,6 +3,7 @@ import Display
import SwiftSignalKit import SwiftSignalKit
import TelegramPresentationData import TelegramPresentationData
import AvatarNode import AvatarNode
import AccountContext
enum PeerInfoScreenActionColor { enum PeerInfoScreenActionColor {
case accent case accent
@ -89,7 +90,7 @@ private final class PeerInfoScreenActionItemNode: PeerInfoScreenItemNode {
self.iconDisposable.dispose() 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 { guard let item = item as? PeerInfoScreenActionItem else {
return 10.0 return 10.0
} }

View File

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

View File

@ -52,7 +52,7 @@ private final class PeerInfoScreenBirthdatePickerItemNode: PeerInfoScreenItemNod
self.addSubnode(self.maskNode) 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 { guard let item = item as? PeerInfoScreenBirthdatePickerItem else {
return 10.0 return 10.0
} }

View File

@ -12,6 +12,7 @@ import ComponentFlow
import MultilineTextComponent import MultilineTextComponent
import BundleIconComponent import BundleIconComponent
import PlainButtonComponent import PlainButtonComponent
import AccountContext
func businessHoursTextToCopy(businessHours: TelegramBusinessHours, presentationData: PresentationData, displayLocalTimezone: Bool) -> String { func businessHoursTextToCopy(businessHours: TelegramBusinessHours, presentationData: PresentationData, displayLocalTimezone: Bool) -> String {
var text = "" 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 { guard let item = item as? PeerInfoScreenBusinessHoursItem else {
return 10.0 return 10.0
} }

View File

@ -57,7 +57,7 @@ private final class PeerInfoScreenCallListItemNode: PeerInfoScreenItemNode {
self.addSubnode(self.maskNode) 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 { guard let item = item as? PeerInfoScreenCallListItem else {
return 10.0 return 10.0
} }

View File

@ -3,6 +3,7 @@ import Display
import TelegramPresentationData import TelegramPresentationData
import TextFormat import TextFormat
import Markdown import Markdown
import AccountContext
final class PeerInfoScreenCommentItem: PeerInfoScreenItem { final class PeerInfoScreenCommentItem: PeerInfoScreenItem {
enum LinkAction { enum LinkAction {
@ -63,7 +64,7 @@ private final class PeerInfoScreenCommentItemNode: PeerInfoScreenItemNode {
self.view.addGestureRecognizer(recognizer) 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 { guard let item = item as? PeerInfoScreenCommentItem else {
return 10.0 return 10.0
} }

View File

@ -237,7 +237,7 @@ private final class PeerInfoScreenContactInfoItemNode: PeerInfoScreenItemNode {
return nil 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 { guard let item = item as? PeerInfoScreenContactInfoItem else {
return 10.0 return 10.0
} }

View File

@ -3,6 +3,7 @@ import Display
import TelegramPresentationData import TelegramPresentationData
import EncryptionKeyVisualization import EncryptionKeyVisualization
import TelegramCore import TelegramCore
import AccountContext
final class PeerInfoScreenDisclosureEncryptionKeyItem: PeerInfoScreenItem { final class PeerInfoScreenDisclosureEncryptionKeyItem: PeerInfoScreenItem {
let id: AnyHashable let id: AnyHashable
@ -71,7 +72,7 @@ private final class PeerInfoScreenDisclosureEncryptionKeyItemNode: PeerInfoScree
self.addSubnode(self.maskNode) 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 { guard let item = item as? PeerInfoScreenDisclosureEncryptionKeyItem else {
return 10.0 return 10.0
} }

View File

@ -2,6 +2,8 @@ import AsyncDisplayKit
import Display import Display
import SwiftSignalKit import SwiftSignalKit
import TelegramPresentationData import TelegramPresentationData
import TextNodeWithEntities
import AccountContext
final class PeerInfoScreenDisclosureItem: PeerInfoScreenItem { final class PeerInfoScreenDisclosureItem: PeerInfoScreenItem {
enum Label { enum Label {
@ -12,6 +14,7 @@ final class PeerInfoScreenDisclosureItem: PeerInfoScreenItem {
case none case none
case text(String) case text(String)
case attributedText(NSAttributedString)
case coloredText(String, LabelColor) case coloredText(String, LabelColor)
case badge(String, UIColor) case badge(String, UIColor)
case semitransparentBadge(String, UIColor) case semitransparentBadge(String, UIColor)
@ -22,6 +25,8 @@ final class PeerInfoScreenDisclosureItem: PeerInfoScreenItem {
switch self { switch self {
case .none, .image: case .none, .image:
return "" return ""
case let .attributedText(text):
return text.string
case let .text(text), let .coloredText(text, _), let .badge(text, _), let .semitransparentBadge(text, _), let .titleBadge(text, _): case let .text(text), let .coloredText(text, _), let .badge(text, _), let .semitransparentBadge(text, _), let .titleBadge(text, _):
return text return text
} }
@ -29,7 +34,7 @@ final class PeerInfoScreenDisclosureItem: PeerInfoScreenItem {
var badgeColor: UIColor? { var badgeColor: UIColor? {
switch self { switch self {
case .none, .text, .coloredText, .image: case .none, .text, .coloredText, .image, .attributedText:
return nil return nil
case let .badge(_, color), let .semitransparentBadge(_, color), let .titleBadge(_, color): case let .badge(_, color), let .semitransparentBadge(_, color), let .titleBadge(_, color):
return color return color
@ -69,7 +74,7 @@ private final class PeerInfoScreenDisclosureItemNode: PeerInfoScreenItemNode {
private let maskNode: ASImageNode private let maskNode: ASImageNode
private let iconNode: ASImageNode private let iconNode: ASImageNode
private let labelBadgeNode: ASImageNode private let labelBadgeNode: ASImageNode
private let labelNode: ImmediateTextNode private let labelNode: ImmediateTextNodeWithEntities
private var additionalLabelNode: ImmediateTextNode? private var additionalLabelNode: ImmediateTextNode?
private var additionalLabelBadgeNode: ASImageNode? private var additionalLabelBadgeNode: ASImageNode?
private let textNode: ImmediateTextNode private let textNode: ImmediateTextNode
@ -97,7 +102,7 @@ private final class PeerInfoScreenDisclosureItemNode: PeerInfoScreenItemNode {
self.labelBadgeNode.displaysAsynchronously = false self.labelBadgeNode.displaysAsynchronously = false
self.labelBadgeNode.isLayerBacked = true self.labelBadgeNode.isLayerBacked = true
self.labelNode = ImmediateTextNode() self.labelNode = ImmediateTextNodeWithEntities()
self.labelNode.displaysAsynchronously = false self.labelNode.displaysAsynchronously = false
self.labelNode.isUserInteractionEnabled = false self.labelNode.isUserInteractionEnabled = false
@ -135,7 +140,7 @@ private final class PeerInfoScreenDisclosureItemNode: PeerInfoScreenItemNode {
self.iconDisposable.dispose() 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 { guard let item = item as? PeerInfoScreenDisclosureItem else {
return 10.0 return 10.0
} }
@ -177,8 +182,20 @@ private final class PeerInfoScreenDisclosureItemNode: PeerInfoScreenItemNode {
labelColorValue = presentationData.theme.list.itemSecondaryTextColor labelColorValue = presentationData.theme.list.itemSecondaryTextColor
labelFont = titleFont 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.maximumNumberOfLines = 1
self.textNode.attributedText = NSAttributedString(string: item.text, font: titleFont, textColor: textColorValue) self.textNode.attributedText = NSAttributedString(string: item.text, font: titleFont, textColor: textColorValue)

View File

@ -1,6 +1,7 @@
import AsyncDisplayKit import AsyncDisplayKit
import Display import Display
import TelegramPresentationData import TelegramPresentationData
import AccountContext
final class PeerInfoScreenHeaderItem: PeerInfoScreenItem { final class PeerInfoScreenHeaderItem: PeerInfoScreenItem {
let id: AnyHashable let id: AnyHashable
@ -44,7 +45,7 @@ private final class PeerInfoScreenHeaderItemNode: PeerInfoScreenItemNode {
self.addSubnode(self.activateArea) 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 { guard let item = item as? PeerInfoScreenHeaderItem else {
return 10.0 return 10.0
} }

View File

@ -55,7 +55,7 @@ private final class PeerInfoScreenInfoItemNode: PeerInfoScreenItemNode {
self.addSubnode(self.bottomSeparatorNode) 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 { guard let item = item as? PeerInfoScreenInfoItem else {
return 10.0 return 10.0
} }

View File

@ -121,6 +121,8 @@ private func generateExpandBackground(size: CGSize, color: UIColor) -> UIImage?
} }
private final class PeerInfoScreenLabeledValueItemNode: PeerInfoScreenItemNode { private final class PeerInfoScreenLabeledValueItemNode: PeerInfoScreenItemNode {
private weak var context: AccountContext?
private let containerNode: ContextControllerSourceNode private let containerNode: ContextControllerSourceNode
private let contextSourceNode: ContextExtractedContentContainingNode private let contextSourceNode: ContextExtractedContentContainingNode
@ -383,8 +385,8 @@ private final class PeerInfoScreenLabeledValueItemNode: PeerInfoScreenItemNode {
if self.linkItemWithProgress != currentLinkItem { if self.linkItemWithProgress != currentLinkItem {
self.linkItemWithProgress = currentLinkItem self.linkItemWithProgress = currentLinkItem
if let validLayout = self.validLayout { if let validLayout = self.validLayout, let context = self.context {
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) 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 { if self.linkItemWithProgress != currentLinkItem {
self.linkItemWithProgress = currentLinkItem self.linkItemWithProgress = currentLinkItem
if let validLayout = self.validLayout { if let validLayout = self.validLayout, let context = self.context {
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) 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 { guard let item = item as? PeerInfoScreenLabeledValueItem else {
return 10.0 return 10.0
} }
self.context = context
self.validLayout = (width, safeInsets, presentationData, item, topItem, bottomItem, hasCorners) self.validLayout = (width, safeInsets, presentationData, item, topItem, bottomItem, hasCorners)
self.item = item self.item = item

View File

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

View File

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

View File

@ -2,6 +2,7 @@ import AsyncDisplayKit
import Display import Display
import TelegramPresentationData import TelegramPresentationData
import AppBundle import AppBundle
import AccountContext
final class PeerInfoScreenSwitchItem: PeerInfoScreenItem { final class PeerInfoScreenSwitchItem: PeerInfoScreenItem {
let id: AnyHashable 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 { guard let item = item as? PeerInfoScreenSwitchItem else {
return 10.0 return 10.0
} }

View File

@ -351,6 +351,7 @@ final class PeerInfoScreenData {
let starsState: StarsContext.State? let starsState: StarsContext.State?
let starsRevenueStatsState: StarsRevenueStats? let starsRevenueStatsState: StarsRevenueStats?
let starsRevenueStatsContext: StarsRevenueStatsContext? let starsRevenueStatsContext: StarsRevenueStatsContext?
let revenueStatsState: RevenueStats?
let _isContact: Bool let _isContact: Bool
var forceIsContact: Bool = false var forceIsContact: Bool = false
@ -393,7 +394,8 @@ final class PeerInfoScreenData {
personalChannel: PeerInfoPersonalChannelData?, personalChannel: PeerInfoPersonalChannelData?,
starsState: StarsContext.State?, starsState: StarsContext.State?,
starsRevenueStatsState: StarsRevenueStats?, starsRevenueStatsState: StarsRevenueStats?,
starsRevenueStatsContext: StarsRevenueStatsContext? starsRevenueStatsContext: StarsRevenueStatsContext?,
revenueStatsState: RevenueStats?
) { ) {
self.peer = peer self.peer = peer
self.chatPeer = chatPeer self.chatPeer = chatPeer
@ -425,6 +427,7 @@ final class PeerInfoScreenData {
self.starsState = starsState self.starsState = starsState
self.starsRevenueStatsState = starsRevenueStatsState self.starsRevenueStatsState = starsRevenueStatsState
self.starsRevenueStatsContext = starsRevenueStatsContext self.starsRevenueStatsContext = starsRevenueStatsContext
self.revenueStatsState = revenueStatsState
} }
} }
@ -920,7 +923,8 @@ func peerInfoScreenSettingsData(context: AccountContext, peerId: EnginePeer.Id,
personalChannel: personalChannel, personalChannel: personalChannel,
starsState: starsState, starsState: starsState,
starsRevenueStatsState: nil, starsRevenueStatsState: nil,
starsRevenueStatsContext: nil starsRevenueStatsContext: nil,
revenueStatsState: nil
) )
} }
} }
@ -962,7 +966,8 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
personalChannel: nil, personalChannel: nil,
starsState: nil, starsState: nil,
starsRevenueStatsState: nil, starsRevenueStatsState: nil,
starsRevenueStatsContext: nil starsRevenueStatsContext: nil,
revenueStatsState: nil
)) ))
case let .user(userPeerId, secretChatId, kind): case let .user(userPeerId, secretChatId, kind):
let groupsInCommon: GroupsInCommonContext? let groupsInCommon: GroupsInCommonContext?
@ -1304,7 +1309,8 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
personalChannel: personalChannel, personalChannel: personalChannel,
starsState: nil, starsState: nil,
starsRevenueStatsState: starsRevenueContextAndState.1, starsRevenueStatsState: starsRevenueContextAndState.1,
starsRevenueStatsContext: starsRevenueContextAndState.0 starsRevenueStatsContext: starsRevenueContextAndState.0,
revenueStatsState: nil
) )
} }
case .channel: case .channel:
@ -1380,6 +1386,36 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
let isPremiumRequiredForStoryPosting: Signal<Bool, NoError> = isPremiumRequiredForStoryPosting(context: context) 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( return combineLatest(
context.account.viewTracker.peerView(peerId, updateData: true), context.account.viewTracker.peerView(peerId, updateData: true),
peerInfoAvailableMediaPanes(context: context, peerId: peerId, chatLocation: chatLocation, isMyProfile: false, chatLocationContextHolder: chatLocationContextHolder), peerInfoAvailableMediaPanes(context: context, peerId: peerId, chatLocation: chatLocation, isMyProfile: false, chatLocationContextHolder: chatLocationContextHolder),
@ -1395,9 +1431,11 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
hasSavedMessages, hasSavedMessages,
hasSavedMessagesChats, hasSavedMessagesChats,
hasSavedMessageTags, 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 var availablePanes = availablePanes
if let hasStories { if let hasStories {
if hasStories { if hasStories {
@ -1447,7 +1485,7 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
requestsStatePromise.set(requestsContext.state |> map(Optional.init)) requestsStatePromise.set(requestsContext.state |> map(Optional.init))
} }
} }
return PeerInfoScreenData( return PeerInfoScreenData(
peer: peerView.peers[peerId], peer: peerView.peers[peerId],
chatPeer: peerView.peers[peerId], chatPeer: peerView.peers[peerId],
@ -1477,8 +1515,9 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
isPremiumRequiredForStoryPosting: isPremiumRequiredForStoryPosting, isPremiumRequiredForStoryPosting: isPremiumRequiredForStoryPosting,
personalChannel: nil, personalChannel: nil,
starsState: nil, starsState: nil,
starsRevenueStatsState: nil, starsRevenueStatsState: starsRevenueContextAndState.1,
starsRevenueStatsContext: nil starsRevenueStatsContext: starsRevenueContextAndState.0,
revenueStatsState: revenueContextAndState.1
) )
} }
case let .group(groupId): case let .group(groupId):
@ -1775,7 +1814,8 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
personalChannel: nil, personalChannel: nil,
starsState: nil, starsState: nil,
starsRevenueStatsState: nil, starsRevenueStatsState: nil,
starsRevenueStatsContext: nil starsRevenueStatsContext: nil,
revenueStatsState: nil
)) ))
} }
} }

View File

@ -126,7 +126,7 @@ protocol PeerInfoScreenItem: AnyObject {
class PeerInfoScreenItemNode: ASDisplayNode, AccessibilityFocusableNode { class PeerInfoScreenItemNode: ASDisplayNode, AccessibilityFocusableNode {
var bringToFrontForHighlight: (() -> Void)? 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() preconditionFailure()
} }
@ -165,7 +165,7 @@ private final class PeerInfoScreenItemSectionContainerNode: ASDisplayNode {
self.addSubnode(self.bottomSeparatorNode) 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.backgroundNode.backgroundColor = presentationData.theme.list.itemBlocksBackgroundColor
self.topSeparatorNode.backgroundColor = presentationData.theme.list.itemBlocksSeparatorColor self.topSeparatorNode.backgroundColor = presentationData.theme.list.itemBlocksSeparatorColor
self.bottomSeparatorNode.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] 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)) let itemFrame = CGRect(origin: CGPoint(x: 0.0, y: contentHeight), size: CGSize(width: width, height: itemHeight))
itemTransition.updateFrame(node: itemNode, frame: itemFrame) itemTransition.updateFrame(node: itemNode, frame: itemFrame)
if wasAdded { if wasAdded {
@ -561,7 +561,7 @@ private final class PeerInfoInteraction {
let editingToggleMessageSignatures: (Bool) -> Void let editingToggleMessageSignatures: (Bool) -> Void
let openParticipantsSection: (PeerInfoParticipantsSection) -> Void let openParticipantsSection: (PeerInfoParticipantsSection) -> Void
let openRecentActions: () -> Void let openRecentActions: () -> Void
let openStats: (Bool) -> Void let openStats: (ChannelStatsSection) -> Void
let editingOpenPreHistorySetup: () -> Void let editingOpenPreHistorySetup: () -> Void
let editingOpenAutoremoveMesages: () -> Void let editingOpenAutoremoveMesages: () -> Void
let openPermissions: () -> Void let openPermissions: () -> Void
@ -629,7 +629,7 @@ private final class PeerInfoInteraction {
editingToggleMessageSignatures: @escaping (Bool) -> Void, editingToggleMessageSignatures: @escaping (Bool) -> Void,
openParticipantsSection: @escaping (PeerInfoParticipantsSection) -> Void, openParticipantsSection: @escaping (PeerInfoParticipantsSection) -> Void,
openRecentActions: @escaping () -> Void, openRecentActions: @escaping () -> Void,
openStats: @escaping (Bool) -> Void, openStats: @escaping (ChannelStatsSection) -> Void,
editingOpenPreHistorySetup: @escaping () -> Void, editingOpenPreHistorySetup: @escaping () -> Void,
editingOpenAutoremoveMesages: @escaping () -> Void, editingOpenAutoremoveMesages: @escaping () -> Void,
openPermissions: @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)) 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 { } else if let channel = data.peer as? TelegramChannel {
@ -1455,7 +1480,8 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese
let ItemAdmins = 6 let ItemAdmins = 6
let ItemMembers = 7 let ItemMembers = 7
let ItemMemberRequests = 8 let ItemMemberRequests = 8
let ItemEdit = 9 let ItemBalance = 9
let ItemEdit = 10
if let _ = data.threadData { if let _ = data.threadData {
let mainUsername: String 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: { items[.peerMembers]!.append(PeerInfoScreenDisclosureItem(id: ItemEdit, label: .none, text: presentationData.strings.Channel_Info_Settings, icon: UIImage(bundleImageName: "Chat/Info/SettingsIcon"), action: {
interaction.openEditing() interaction.openEditing()
})) }))
@ -1721,7 +1781,6 @@ private func editingItems(data: PeerInfoScreenData?, state: PeerInfoState, chatL
let ItemInfo = 3 let ItemInfo = 3
let ItemDelete = 4 let ItemDelete = 4
let ItemUsername = 5 let ItemUsername = 5
let ItemStars = 6
let ItemIntro = 7 let ItemIntro = 7
let ItemCommands = 8 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: { items[.peerDataSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemUsername, label: .text("@\(user.addressName ?? "")"), text: presentationData.strings.PeerInfo_Bot_Username, icon: PresentationResourcesSettings.bot, action: {
interaction.editingOpenPublicLinkSetup() 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: { 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))) 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) { 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: { 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 openRecentActions: { [weak self] in
self?.openRecentActions() self?.openRecentActions()
}, },
openStats: { [weak self] boosts in openStats: { [weak self] section in
self?.openStats(boosts: boosts) self?.openStats(section: section)
}, },
editingOpenPreHistorySetup: { [weak self] in editingOpenPreHistorySetup: { [weak self] in
self?.editingOpenPreHistorySetup() self?.editingOpenPreHistorySetup()
@ -6132,7 +6185,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
}, action: { [weak self] _, f in }, action: { [weak self] _, f in
f(.dismissWithoutContent) f(.dismissWithoutContent)
self?.openStats() self?.openStats(section: .stats)
}))) })))
} }
if cachedData.flags.contains(.translationHidden) { 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)) 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 { guard let controller = self.controller, let data = self.data, let peer = data.peer else {
return return
} }
@ -7830,7 +7883,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
if let channel = peer as? TelegramChannel, case .group = channel.info { if let channel = peer as? TelegramChannel, case .group = channel.info {
statsController = groupStatsController(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, peerId: peer.id) statsController = groupStatsController(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, peerId: peer.id)
} else { } 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) 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 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 { if let self {
self.openStats(boosts: true, boostStatus: boostStatus) self.openStats(section: .boosts, boostStatus: boostStatus)
} }
}) })
navigationController.pushViewController(controller) navigationController.pushViewController(controller)
@ -11132,7 +11185,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
contentHeight -= 16.0 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)) let sectionFrame = CGRect(origin: CGPoint(x: insets.left, y: contentHeight), size: CGSize(width: sectionWidth, height: sectionHeight))
if additive { if additive {
transition.updateFrameAdditive(node: sectionNode, frame: sectionFrame) transition.updateFrameAdditive(node: sectionNode, frame: sectionFrame)
@ -11191,9 +11244,9 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
self.editingSections[sectionId] = sectionNode self.editingSections[sectionId] = sectionNode
self.scrollNode.addSubnode(sectionNode) self.scrollNode.addSubnode(sectionNode)
} }
let sectionWidth = layout.size.width - insets.left - insets.right 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)) let sectionFrame = CGRect(origin: CGPoint(x: insets.left, y: contentHeight), size: CGSize(width: sectionWidth, height: sectionHeight))
if wasAdded { if wasAdded {

View File

@ -2,6 +2,7 @@ import AsyncDisplayKit
import Display import Display
import TelegramPresentationData import TelegramPresentationData
import ItemListUI import ItemListUI
import AccountContext
final class PeerInfoScreenMultilineInputItem: PeerInfoScreenItem { final class PeerInfoScreenMultilineInputItem: PeerInfoScreenItem {
let id: AnyHashable let id: AnyHashable
@ -53,7 +54,7 @@ final class PeerInfoScreenMultilineInputItemNode: PeerInfoScreenItemNode {
self.addSubnode(self.bottomSeparatorNode) 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 { guard let item = item as? PeerInfoScreenMultilineInputItem else {
return 10.0 return 10.0
} }

View File

@ -22,42 +22,42 @@ public final class GiftAvatarComponent: Component {
let theme: PresentationTheme let theme: PresentationTheme
let peers: [EnginePeer] let peers: [EnginePeer]
let photo: TelegramMediaWebFile? let photo: TelegramMediaWebFile?
let starsPeer: StarsContext.State.Transaction.Peer?
let isVisible: Bool let isVisible: Bool
let hasIdleAnimations: Bool let hasIdleAnimations: Bool
let hasScaleAnimation: Bool let hasScaleAnimation: Bool
let avatarSize: CGFloat let avatarSize: CGFloat
let color: UIColor? let color: UIColor?
let offset: CGFloat? let offset: CGFloat?
var hasLargeParticles: Bool
public init( public init(
context: AccountContext, context: AccountContext,
theme: PresentationTheme, theme: PresentationTheme,
peers: [EnginePeer], peers: [EnginePeer],
photo: TelegramMediaWebFile? = nil, photo: TelegramMediaWebFile? = nil,
starsPeer: StarsContext.State.Transaction.Peer? = nil,
isVisible: Bool, isVisible: Bool,
hasIdleAnimations: Bool, hasIdleAnimations: Bool,
hasScaleAnimation: Bool = true, hasScaleAnimation: Bool = true,
avatarSize: CGFloat = 100.0, avatarSize: CGFloat = 100.0,
color: UIColor? = nil, color: UIColor? = nil,
offset: CGFloat? = nil offset: CGFloat? = nil,
hasLargeParticles: Bool = false
) { ) {
self.context = context self.context = context
self.theme = theme self.theme = theme
self.peers = peers self.peers = peers
self.photo = photo self.photo = photo
self.starsPeer = starsPeer
self.isVisible = isVisible self.isVisible = isVisible
self.hasIdleAnimations = hasIdleAnimations self.hasIdleAnimations = hasIdleAnimations
self.hasScaleAnimation = hasScaleAnimation self.hasScaleAnimation = hasScaleAnimation
self.avatarSize = avatarSize self.avatarSize = avatarSize
self.color = color self.color = color
self.offset = offset self.offset = offset
self.hasLargeParticles = hasLargeParticles
} }
public static func ==(lhs: GiftAvatarComponent, rhs: GiftAvatarComponent) -> Bool { 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 { public final class View: UIView, SCNSceneRendererDelegate, ComponentTaggedView {
@ -142,7 +142,7 @@ public final class GiftAvatarComponent: Component {
private var didSetup = false private var didSetup = false
private func setup() { 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 return
} }
@ -152,6 +152,21 @@ public final class GiftAvatarComponent: Component {
self.sceneView.delegate = self self.sceneView.delegate = self
if let color = self.component?.color { 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] = [ let names: [String] = [
"particles_left", "particles_left",
"particles_right", "particles_right",
@ -160,10 +175,59 @@ public final class GiftAvatarComponent: Component {
"particles_center" "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 { for name in names {
if let node = scene.rootNode.childNode(withName: name, recursively: false), let particleSystem = node.particleSystems?.first { if let node = scene.rootNode.childNode(withName: name, recursively: false), let particleSystem = node.particleSystems?.first {
particleSystem.particleColor = color particleSystem.particleIntensity = min(1.0, 2.0 * particleSystem.particleIntensity)
particleSystem.particleColorVariation = SCNVector4Make(0, 0, 0, 0) 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() { private func onReady() {
self.setupScaleAnimation()
self.playAppearanceAnimation(explode: true) self.playAppearanceAnimation(explode: true)
self.previousInteractionTimestamp = CACurrentMediaTime() self.previousInteractionTimestamp = CACurrentMediaTime()
@ -203,23 +265,7 @@ public final class GiftAvatarComponent: Component {
}, queue: Queue.mainQueue()) }, queue: Queue.mainQueue())
self.timer?.start() 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) { private func playAppearanceAnimation(velocity: CGFloat? = nil, smallAngle: Bool = false, mirror: Bool = false, explode: Bool = false) {
guard let scene = self.sceneView.scene else { guard let scene = self.sceneView.scene else {
return return
@ -319,6 +365,10 @@ public final class GiftAvatarComponent: Component {
self.hasIdleAnimations = component.hasIdleAnimations self.hasIdleAnimations = component.hasIdleAnimations
if let _ = component.color {
self.sceneView.backgroundColor = component.theme.list.blocksBackgroundColor
}
if let photo = component.photo { if let photo = component.photo {
let imageNode: TransformImageNode let imageNode: TransformImageNode
if let current = self.imageNode { 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))() 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 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 { } else if component.peers.count > 1 {
let avatarSize = CGSize(width: 60.0, height: 60.0) let avatarSize = CGSize(width: 60.0, height: 60.0)

View File

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

View File

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

View File

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

View File

@ -11,6 +11,8 @@ import PhotoResources
import AvatarNode import AvatarNode
import AccountContext import AccountContext
import InvisibleInkDustNode import InvisibleInkDustNode
import AnimatedStickerNode
import TelegramAnimatedStickerNode
final class StarsParticlesView: UIView { final class StarsParticlesView: UIView {
private struct Particle { private struct Particle {
@ -251,6 +253,7 @@ public final class StarsImageComponent: Component {
case media([AnyMediaReference]) case media([AnyMediaReference])
case extendedMedia([TelegramExtendedMedia]) case extendedMedia([TelegramExtendedMedia])
case transactionPeer(StarsContext.State.Transaction.Peer) case transactionPeer(StarsContext.State.Transaction.Peer)
case gift(Int64)
public static func == (lhs: StarsImageComponent.Subject, rhs: StarsImageComponent.Subject) -> Bool { public static func == (lhs: StarsImageComponent.Subject, rhs: StarsImageComponent.Subject) -> Bool {
switch lhs { switch lhs {
@ -284,6 +287,12 @@ public final class StarsImageComponent: Component {
} else { } else {
return false 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 dustNode: MediaDustNode?
private var button: UIControl? private var button: UIControl?
private var animationNode: AnimatedStickerNode?
private var lockView: UIImageView? private var lockView: UIImageView?
private var countView = ComponentView<Empty>() private var countView = ComponentView<Empty>()
@ -776,6 +787,31 @@ public final class StarsImageComponent: Component {
iconBackgroundView.frame = imageFrame iconBackgroundView.frame = imageFrame
iconView.frame = imageFrame.insetBy(dx: iconInset, dy: iconInset).offsetBy(dx: 0.0, dy: iconOffset) 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 { if let _ = component.action {

View File

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

View File

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

View File

@ -22,6 +22,7 @@ import TelegramStringFormatting
import UndoUI import UndoUI
import StarsImageComponent import StarsImageComponent
import GalleryUI import GalleryUI
import StarsAvatarComponent
private final class StarsTransactionSheetContent: CombinedComponent { private final class StarsTransactionSheetContent: CombinedComponent {
typealias EnvironmentType = ViewControllerComponentContainer.Environment typealias EnvironmentType = ViewControllerComponentContainer.Environment
@ -73,6 +74,7 @@ private final class StarsTransactionSheetContent: CombinedComponent {
var peerMap: [EnginePeer.Id: EnginePeer] = [:] var peerMap: [EnginePeer.Id: EnginePeer] = [:]
var cachedCloseImage: (UIImage, PresentationTheme)? var cachedCloseImage: (UIImage, PresentationTheme)?
var cachedChevronImage: (UIImage, PresentationTheme)?
var inProgress = false var inProgress = false
@ -89,6 +91,8 @@ private final class StarsTransactionSheetContent: CombinedComponent {
} }
case let .receipt(receipt): case let .receipt(receipt):
peerIds.append(receipt.botPaymentId) peerIds.append(receipt.botPaymentId)
case let .gift(message):
peerIds.append(message.id.peerId)
} }
self.disposable = (context.engine.data.get( self.disposable = (context.engine.data.get(
@ -186,87 +190,110 @@ private final class StarsTransactionSheetContent: CombinedComponent {
let media: [AnyMediaReference] let media: [AnyMediaReference]
let photo: TelegramMediaWebFile? let photo: TelegramMediaWebFile?
let isRefund: Bool let isRefund: Bool
let isGift: Bool
var delayedCloseOnOpenPeer = true var delayedCloseOnOpenPeer = true
switch subject { switch subject {
case let .transaction(transaction, parentPeer): case let .transaction(transaction, parentPeer):
switch transaction.peer { if transaction.flags.contains(.isGift) {
case let .peer(peer): 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 { 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 { } else {
titleText = transaction.title ?? peer.compactDisplayTitle descriptionText = transaction.description ?? ""
} }
via = nil
case .appStore: messageId = transaction.paidMessageId
titleText = strings.Stars_Transaction_AppleTopUp_Title
via = strings.Stars_Transaction_AppleTopUp_Subtitle count = transaction.count
case .playMarket: transactionId = transaction.id
titleText = strings.Stars_Transaction_GoogleTopUp_Title date = transaction.date
via = strings.Stars_Transaction_GoogleTopUp_Subtitle if case let .peer(peer) = transaction.peer {
case .premiumBot: toPeer = peer
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 { } else {
titleText = strings.Stars_Transaction_FragmentWithdrawal_Title toPeer = nil
via = strings.Stars_Transaction_FragmentWithdrawal_Subtitle
} }
case .ads: transactionPeer = transaction.peer
titleText = strings.Stars_Transaction_TelegramAds_Title media = transaction.media.map { AnyMediaReference.starsTransaction(transaction: StarsTransactionReference(peerId: parentPeer.id, id: transaction.id, isRefund: transaction.flags.contains(.isRefund)), media: $0) }
via = strings.Stars_Transaction_TelegramAds_Subtitle photo = transaction.photo
case .unsupported: isGift = false
titleText = strings.Stars_Transaction_Unsupported_Title isRefund = transaction.flags.contains(.isRefund)
via = nil
} }
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): case let .receipt(receipt):
titleText = receipt.invoiceMedia.title titleText = receipt.invoiceMedia.title
descriptionText = receipt.invoiceMedia.description descriptionText = receipt.invoiceMedia.description
@ -284,6 +311,28 @@ private final class StarsTransactionSheetContent: CombinedComponent {
media = [] media = []
photo = receipt.invoiceMedia.photo photo = receipt.invoiceMedia.photo
isRefund = false 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 delayedCloseOnOpenPeer = false
} }
@ -312,7 +361,9 @@ private final class StarsTransactionSheetContent: CombinedComponent {
) )
let imageSubject: StarsImageComponent.Subject let imageSubject: StarsImageComponent.Subject
if !media.isEmpty { if isGift {
imageSubject = .gift
} else if !media.isEmpty {
imageSubject = .media(media) imageSubject = .media(media)
} else if let photo { } else if let photo {
imageSubject = .photo(photo) imageSubject = .photo(photo)
@ -373,12 +424,14 @@ private final class StarsTransactionSheetContent: CombinedComponent {
content: AnyComponent( content: AnyComponent(
PeerCellComponent( PeerCellComponent(
context: component.context, context: component.context,
textColor: tableLinkColor, theme: theme,
peer: toPeer peer: toPeer
) )
), ),
action: { action: {
if delayedCloseOnOpenPeer { if toPeer.id.namespace == Namespaces.Peer.CloudUser && toPeer.id.id._internalGetInt64Value() == 777000 {
} else if delayedCloseOnOpenPeer {
component.openPeer(toPeer) component.openPeer(toPeer)
Queue.mainQueue().after(1.0, { Queue.mainQueue().after(1.0, {
component.cancel(false) component.cancel(false)
@ -539,14 +592,21 @@ private final class StarsTransactionSheetContent: CombinedComponent {
originY += star.size.height - 23.0 originY += star.size.height - 23.0
if !descriptionText.isEmpty { 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( let description = description.update(
component: MultilineTextComponent( component: MultilineTextComponent(
text: .plain(NSAttributedString( text: .plain(attributedString),
string: descriptionText,
font: Font.regular(15.0),
textColor: theme.actionSheet.primaryTextColor,
paragraphAlignment: .center
)),
horizontalAlignment: .center, horizontalAlignment: .center,
maximumNumberOfLines: 3 maximumNumberOfLines: 3
), ),
@ -768,6 +828,7 @@ public class StarsTransactionScreen: ViewControllerComponentContainer {
public enum Subject: Equatable { public enum Subject: Equatable {
case transaction(StarsContext.State.Transaction, EnginePeer) case transaction(StarsContext.State.Transaction, EnginePeer)
case receipt(BotPaymentReceipt) case receipt(BotPaymentReceipt)
case gift(EngineMessage)
} }
private let context: AccountContext private let context: AccountContext
@ -1166,12 +1227,12 @@ private final class TableComponent: CombinedComponent {
private final class PeerCellComponent: Component { private final class PeerCellComponent: Component {
let context: AccountContext let context: AccountContext
let textColor: UIColor let theme: PresentationTheme
let peer: EnginePeer? let peer: EnginePeer
init(context: AccountContext, textColor: UIColor, peer: EnginePeer?) { init(context: AccountContext, theme: PresentationTheme, peer: EnginePeer) {
self.context = context self.context = context
self.textColor = textColor self.theme = theme
self.peer = peer self.peer = peer
} }
@ -1179,7 +1240,7 @@ private final class PeerCellComponent: Component {
if lhs.context !== rhs.context { if lhs.context !== rhs.context {
return false return false
} }
if lhs.textColor !== rhs.textColor { if lhs.theme !== rhs.theme {
return false return false
} }
if lhs.peer != rhs.peer { if lhs.peer != rhs.peer {
@ -1189,18 +1250,14 @@ private final class PeerCellComponent: Component {
} }
final class View: UIView { final class View: UIView {
private let avatarNode: AvatarNode private let avatar = ComponentView<Empty>()
private let text = ComponentView<Empty>() private let text = ComponentView<Empty>()
private var component: PeerCellComponent? private var component: PeerCellComponent?
private weak var state: EmptyComponentState? private weak var state: EmptyComponentState?
override init(frame: CGRect) { override init(frame: CGRect) {
self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 13.0))
super.init(frame: frame) super.init(frame: frame)
self.addSubnode(self.avatarNode)
} }
required init?(coder: NSCoder) { required init?(coder: NSCoder) {
@ -1211,21 +1268,33 @@ private final class PeerCellComponent: Component {
self.component = component self.component = component
self.state = state 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 avatarSize = CGSize(width: 22.0, height: 22.0)
let spacing: CGFloat = 6.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( let textSize = self.text.update(
transition: .immediate, transition: .immediate,
component: AnyComponent( component: AnyComponent(
MultilineTextComponent( 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: {}, environment: {},
@ -1235,7 +1304,15 @@ private final class PeerCellComponent: Component {
let size = CGSize(width: avatarSize.width + textSize.width + spacing, height: textSize.height) 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) 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 let view = self.text.view {
if view.superview == nil { if view.superview == nil {

View File

@ -23,6 +23,7 @@ final class StarsBalanceComponent: Component {
let actionCooldownUntilTimestamp: Int32? let actionCooldownUntilTimestamp: Int32?
let action: () -> Void let action: () -> Void
let buyAds: (() -> Void)? let buyAds: (() -> Void)?
let additionalAction: AnyComponent<Empty>?
init( init(
theme: PresentationTheme, theme: PresentationTheme,
@ -35,7 +36,8 @@ final class StarsBalanceComponent: Component {
actionIsEnabled: Bool, actionIsEnabled: Bool,
actionCooldownUntilTimestamp: Int32? = nil, actionCooldownUntilTimestamp: Int32? = nil,
action: @escaping () -> Void, action: @escaping () -> Void,
buyAds: (() -> Void)? buyAds: (() -> Void)?,
additionalAction: AnyComponent<Empty>? = nil
) { ) {
self.theme = theme self.theme = theme
self.strings = strings self.strings = strings
@ -48,6 +50,7 @@ final class StarsBalanceComponent: Component {
self.actionCooldownUntilTimestamp = actionCooldownUntilTimestamp self.actionCooldownUntilTimestamp = actionCooldownUntilTimestamp
self.action = action self.action = action
self.buyAds = buyAds self.buyAds = buyAds
self.additionalAction = additionalAction
} }
static func ==(lhs: StarsBalanceComponent, rhs: StarsBalanceComponent) -> Bool { static func ==(lhs: StarsBalanceComponent, rhs: StarsBalanceComponent) -> Bool {
@ -88,6 +91,8 @@ final class StarsBalanceComponent: Component {
private var button = ComponentView<Empty>() private var button = ComponentView<Empty>()
private var buyAdsButton = ComponentView<Empty>() private var buyAdsButton = ComponentView<Empty>()
private var additionalButton = ComponentView<Empty>()
private var component: StarsBalanceComponent? private var component: StarsBalanceComponent?
private weak var state: EmptyComponentState? private weak var state: EmptyComponentState?
@ -275,9 +280,29 @@ final class StarsBalanceComponent: Component {
} }
} }
contentHeight += buttonSize.height 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 contentHeight += sideInset
return CGSize(width: availableSize.width, height: contentHeight) return CGSize(width: availableSize.width, height: contentHeight)

View File

@ -203,12 +203,13 @@ final class StarsTransactionsListPanelComponent: Component {
let fontBaseDisplaySize = 17.0 let fontBaseDisplaySize = 17.0
let itemTitle: String var itemTitle: String
let itemSubtitle: String? let itemSubtitle: String?
var itemDate: String var itemDate: String
var itemPeer = item.peer
switch item.peer { switch item.peer {
case let .peer(peer): case let .peer(peer):
if !item.media.isEmpty { if !item.media.isEmpty {
itemTitle = environment.strings.Stars_Intro_Transaction_MediaPurchase itemTitle = environment.strings.Stars_Intro_Transaction_MediaPurchase
itemSubtitle = peer.displayTitle(strings: environment.strings, displayOrder: .firstLast) itemSubtitle = peer.displayTitle(strings: environment.strings, displayOrder: .firstLast)
} else if let title = item.title { } else if let title = item.title {
@ -216,7 +217,16 @@ final class StarsTransactionsListPanelComponent: Component {
itemSubtitle = peer.displayTitle(strings: environment.strings, displayOrder: .firstLast) itemSubtitle = peer.displayTitle(strings: environment.strings, displayOrder: .firstLast)
} else { } else {
itemTitle = peer.displayTitle(strings: environment.strings, displayOrder: .firstLast) 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: case .appStore:
itemTitle = environment.strings.Stars_Intro_Transaction_AppleTopUp_Title itemTitle = environment.strings.Stars_Intro_Transaction_AppleTopUp_Title
@ -298,7 +308,7 @@ final class StarsTransactionsListPanelComponent: Component {
theme: environment.theme, theme: environment.theme,
title: AnyComponent(VStack(titleComponents, alignment: .left, spacing: 2.0)), 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), 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, 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))), 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 action: { [weak self] _ in

View File

@ -26,17 +26,20 @@ final class StarsTransactionsScreenComponent: Component {
let starsContext: StarsContext let starsContext: StarsContext
let openTransaction: (StarsContext.State.Transaction) -> Void let openTransaction: (StarsContext.State.Transaction) -> Void
let buy: () -> Void let buy: () -> Void
let gift: () -> Void
init( init(
context: AccountContext, context: AccountContext,
starsContext: StarsContext, starsContext: StarsContext,
openTransaction: @escaping (StarsContext.State.Transaction) -> Void, openTransaction: @escaping (StarsContext.State.Transaction) -> Void,
buy: @escaping () -> Void buy: @escaping () -> Void,
gift: @escaping () -> Void
) { ) {
self.context = context self.context = context
self.starsContext = starsContext self.starsContext = starsContext
self.openTransaction = openTransaction self.openTransaction = openTransaction
self.buy = buy self.buy = buy
self.gift = gift
} }
static func ==(lhs: StarsTransactionsScreenComponent, rhs: StarsTransactionsScreenComponent) -> Bool { static func ==(lhs: StarsTransactionsScreenComponent, rhs: StarsTransactionsScreenComponent) -> Bool {
@ -89,6 +92,8 @@ final class StarsTransactionsScreenComponent: Component {
private let balanceView = ComponentView<Empty>() private let balanceView = ComponentView<Empty>()
private let subscriptionsView = ComponentView<Empty>()
private let topBalanceTitleView = ComponentView<Empty>() private let topBalanceTitleView = ComponentView<Empty>()
private let topBalanceValueView = ComponentView<Empty>() private let topBalanceValueView = ComponentView<Empty>()
private let topBalanceIconView = ComponentView<Empty>() private let topBalanceIconView = ComponentView<Empty>()
@ -282,6 +287,7 @@ final class StarsTransactionsScreenComponent: Component {
} }
let environment = environment[ViewControllerComponentContainer.Environment.self].value let environment = environment[ViewControllerComponentContainer.Environment.self].value
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
if self.stateDisposable == nil { if self.stateDisposable == nil {
self.stateDisposable = (component.starsContext.state self.stateDisposable = (component.starsContext.state
@ -531,7 +537,27 @@ final class StarsTransactionsScreenComponent: Component {
} }
component.buy() 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) starTransition.setFrame(view: balanceView, frame: balanceFrame)
} }
contentHeight += balanceSize.height contentHeight += balanceSize.height
contentHeight += 44.0 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 ?? [] let initialTransactions = self.starsState?.transactions ?? []
var panelItems: [StarsTransactionsPanelContainerComponent.Item] = [] var panelItems: [StarsTransactionsPanelContainerComponent.Item] = []
if !initialTransactions.isEmpty { if !initialTransactions.isEmpty {
@ -704,6 +762,7 @@ public final class StarsTransactionsScreen: ViewControllerComponentContainer {
self.starsContext = starsContext self.starsContext = starsContext
var buyImpl: (() -> Void)? var buyImpl: (() -> Void)?
var giftImpl: (() -> Void)?
var openTransactionImpl: ((StarsContext.State.Transaction) -> Void)? var openTransactionImpl: ((StarsContext.State.Transaction) -> Void)?
super.init(context: context, component: StarsTransactionsScreenComponent( super.init(context: context, component: StarsTransactionsScreenComponent(
context: context, context: context,
@ -713,6 +772,9 @@ public final class StarsTransactionsScreen: ViewControllerComponentContainer {
}, },
buy: { buy: {
buyImpl?() buyImpl?()
},
gift: {
giftImpl?()
} }
), navigationBarAppearance: .transparent) ), navigationBarAppearance: .transparent)
@ -744,7 +806,7 @@ public final class StarsTransactionsScreen: ViewControllerComponentContainer {
guard let self else { guard let self else {
return 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 { guard let self else {
return 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) self.starsContext.load(force: false)
} }

View File

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

View File

@ -552,6 +552,9 @@ public class StickerPickerScreen: ViewController {
self.presentLinkPremiumSuggestion() self.presentLinkPremiumSuggestion()
} }
} }
self.storyStickersContentView?.weatherAction = { [weak self] in
self?.controller?.addWeather()
}
} }
let gifItems: Signal<EntityKeyboardGifContent?, NoError> let gifItems: Signal<EntityKeyboardGifContent?, NoError>
@ -2063,6 +2066,7 @@ public class StickerPickerScreen: ViewController {
public var presentAudioPicker: () -> Void = { } public var presentAudioPicker: () -> Void = { }
public var addReaction: () -> Void = { } public var addReaction: () -> Void = { }
public var addLink: () -> 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) { public init(context: AccountContext, inputData: Signal<StickerPickerInput, NoError>, forceDark: Bool = false, expanded: Bool = false, defaultToEmoji: Bool = false, hasEmoji: Bool = true, hasGifs: Bool = false, hasInteractiveStickers: Bool = true) {
self.context = context self.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 { func update(component: InteractiveStickerButtonContent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
self.backgroundLayer.backgroundColor = UIColor(rgb: 0xffffff, alpha: 0.11).cgColor self.backgroundLayer.backgroundColor = UIColor(rgb: 0xffffff, alpha: 0.11).cgColor
let iconSize = self.icon.update( let iconSize: CGSize
transition: .immediate, if component.iconName == "Sun" {
component: AnyComponent(BundleIconComponent( iconSize = self.icon.update(
name: component.iconName, transition: .immediate,
tintColor: .white, component: AnyComponent(Text(
maxSize: CGSize(width: 20.0, height: 20.0) text: "☀️",
)), font: Font.with(size: 23.0, design: .camera),
environment: {}, color: .white
containerSize: availableSize )),
) 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( let titleSize = self.title.update(
transition: .immediate, transition: .immediate,
component: AnyComponent(Text( 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 remainingWidth = context.availableSize.width - itemsWidth - context.component.padding * 2.0
let spacing = remainingWidth / CGFloat(rowItemsCount - 1) 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) groups.append(currentGroup)
currentGroup = [] currentGroup = []
} }
@ -2537,6 +2555,7 @@ final class StoryStickersContentView: UIView, EmojiCustomContentView {
var audioAction: () -> Void = {} var audioAction: () -> Void = {}
var reactionAction: () -> Void = {} var reactionAction: () -> Void = {}
var linkAction: () -> Void = {} var linkAction: () -> Void = {}
var weatherAction: () -> Void = {}
init(isPremium: Bool) { init(isPremium: Bool) {
self.isPremium = isPremium 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( AnyComponentWithIdentity(
id: "audio", id: "audio",
component: AnyComponent( component: AnyComponent(

View File

@ -36,27 +36,59 @@ struct CameraState: Equatable {
case holding case holding
case handsFree 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 position: Camera.Position
let flashMode: Camera.FlashMode
let flashModeDidChange: Bool
let flashTint: FlashTint
let flashTintSize: CGFloat
let recording: Recording let recording: Recording
let duration: Double let duration: Double
let isDualCameraEnabled: Bool let isDualCameraEnabled: Bool
let isViewOnceEnabled: Bool let isViewOnceEnabled: Bool
func updatedPosition(_ position: Camera.Position) -> CameraState { 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 { 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 { 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 { 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 { final class State: ComponentState {
enum ImageKey: Hashable { enum ImageKey: Hashable {
case flip case flip
case flash
case buttonBackground case buttonBackground
case flashImage
} }
private var cachedImages: [ImageKey: UIImage] = [:] private var cachedImages: [ImageKey: UIImage] = [:]
func image(_ key: ImageKey, theme: PresentationTheme) -> UIImage { func image(_ key: ImageKey, theme: PresentationTheme) -> UIImage {
@ -154,9 +188,23 @@ private final class VideoMessageCameraScreenComponent: CombinedComponent {
switch key { switch key {
case .flip: case .flip:
image = UIImage(bundleImageName: "Camera/VideoMessageFlip")!.withRenderingMode(.alwaysTemplate) image = UIImage(bundleImageName: "Camera/VideoMessageFlip")!.withRenderingMode(.alwaysTemplate)
case .flash:
image = UIImage(bundleImageName: "Camera/VideoMessageFlash")!.withRenderingMode(.alwaysTemplate)
case .buttonBackground: case .buttonBackground:
let innerSize = CGSize(width: 40.0, height: 40.0) 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)! 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 cachedImages[key] = image
return image return image
@ -175,6 +223,8 @@ private final class VideoMessageCameraScreenComponent: CombinedComponent {
var cameraState: CameraState? var cameraState: CameraState?
var didDisplayViewOnce = false var didDisplayViewOnce = false
var displayingFlashTint = false
private let hapticFeedback = HapticFeedback() private let hapticFeedback = HapticFeedback()
@ -238,6 +288,81 @@ private final class VideoMessageCameraScreenComponent: CombinedComponent {
self.hapticFeedback.impact(.veryLight) 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) { func startVideoRecording(pressing: Bool) {
guard let controller = self.getController(), let camera = controller.camera else { guard let controller = self.getController(), let camera = controller.camera else {
return return
@ -312,6 +437,12 @@ private final class VideoMessageCameraScreenComponent: CombinedComponent {
controller.updateCameraState({ $0.updatedRecording(.none) }, transition: .spring(duration: 0.4)) 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() { func lockVideoRecording() {
@ -334,7 +465,9 @@ private final class VideoMessageCameraScreenComponent: CombinedComponent {
} }
static var body: Body { static var body: Body {
let frontFlash = Child(Image.self)
let flipButton = Child(CameraButton.self) let flipButton = Child(CameraButton.self)
let flashButton = Child(CameraButton.self)
let viewOnceButton = Child(PlainButtonComponent.self) let viewOnceButton = Child(PlainButtonComponent.self)
let recordMoreButton = Child(PlainButtonComponent.self) let recordMoreButton = Child(PlainButtonComponent.self)
@ -381,6 +514,20 @@ private final class VideoMessageCameraScreenComponent: CombinedComponent {
} }
if !component.isPreviewing { 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( let flipButton = flipButton.update(
component: CameraButton( component: CameraButton(
content: AnyComponentWithIdentity( content: AnyComponentWithIdentity(
@ -409,6 +556,35 @@ private final class VideoMessageCameraScreenComponent: CombinedComponent {
.appear(.default(scale: true, alpha: true)) .appear(.default(scale: true, alpha: true))
.disappear(.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 { if showViewOnce {
@ -655,6 +831,10 @@ public class VideoMessageCameraScreen: ViewController {
self.cameraState = CameraState( self.cameraState = CameraState(
position: isFrontPosition ? .front : .back, position: isFrontPosition ? .front : .back,
flashMode: .off,
flashModeDidChange: false,
flashTint: .white,
flashTintSize: 1.0,
recording: .none, recording: .none,
duration: 0.0, duration: 0.0,
isDualCameraEnabled: isDualCameraEnabled, isDualCameraEnabled: isDualCameraEnabled,
@ -760,12 +940,15 @@ public class VideoMessageCameraScreen: ViewController {
secondaryPreviewView: self.additionalPreviewView secondaryPreviewView: self.additionalPreviewView
) )
self.cameraStateDisposable = (camera.position self.cameraStateDisposable = combineLatest(
|> deliverOnMainQueue).start(next: { [weak self] position in queue: Queue.mainQueue(),
camera.flashMode,
camera.position
).start(next: { [weak self] flashMode, position in
guard let self else { guard let self else {
return return
} }
self.cameraState = self.cameraState.updatedPosition(position) self.cameraState = self.cameraState.updatedPosition(position).updatedFlashMode(flashMode)
if !self.cameraState.isDualCameraEnabled { if !self.cameraState.isDualCameraEnabled {
self.animatePositionChange() self.animatePositionChange()

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