Update API

This commit is contained in:
Ilya Laktyushin 2024-09-19 03:24:24 +04:00
parent 588cdeee7a
commit f7e6755a39
66 changed files with 5688 additions and 236 deletions

View File

@ -1017,6 +1017,7 @@ public protocol SharedAccountContext: AnyObject {
func makeStarsWithdrawalScreen(context: AccountContext, stats: StarsRevenueStats, completion: @escaping (Int64) -> Void) -> ViewController
func makeStarsGiftScreen(context: AccountContext, message: EngineMessage) -> ViewController
func makeStarsGiveawayBoostScreen(context: AccountContext, peerId: EnginePeer.Id, boost: ChannelBoostersContext.State.Boost) -> ViewController
func makeGiftViewScreen(context: AccountContext, message: EngineMessage) -> ViewController
func makeMiniAppListScreenInitialData(context: AccountContext) -> Signal<MiniAppListScreenInitialData, NoError>
func makeMiniAppListScreen(context: AccountContext, initialData: MiniAppListScreenInitialData) -> ViewController

View File

@ -63,6 +63,7 @@ public final class ChatMessageItemAssociatedData: Equatable {
public let isStandalone: Bool
public let isInline: Bool
public let showSensitiveContent: Bool
public let starGifts: [Int64: TelegramMediaFile]
public init(
automaticDownloadPeerType: MediaAutoDownloadPeerType,
@ -96,7 +97,8 @@ public final class ChatMessageItemAssociatedData: Equatable {
deviceContactsNumbers: Set<String> = Set(),
isStandalone: Bool = false,
isInline: Bool = false,
showSensitiveContent: Bool = false
showSensitiveContent: Bool = false,
starGifts: [Int64: TelegramMediaFile] = [:]
) {
self.automaticDownloadPeerType = automaticDownloadPeerType
self.automaticDownloadPeerId = automaticDownloadPeerId
@ -130,6 +132,7 @@ public final class ChatMessageItemAssociatedData: Equatable {
self.isStandalone = isStandalone
self.isInline = isInline
self.showSensitiveContent = showSensitiveContent
self.starGifts = starGifts
}
public static func == (lhs: ChatMessageItemAssociatedData, rhs: ChatMessageItemAssociatedData) -> Bool {
@ -223,6 +226,9 @@ public final class ChatMessageItemAssociatedData: Equatable {
if lhs.showSensitiveContent != rhs.showSensitiveContent {
return false
}
if lhs.starGifts != rhs.starGifts {
return false
}
return true
}
}

View File

@ -276,3 +276,7 @@ public struct PremiumConfiguration {
}
}
}
public protocol GiftOptionsScreenProtocol {
}

View File

@ -992,7 +992,7 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate {
sendWhenOnlineAvailable = true
}
}
if peer.id.namespace == Namespaces.Peer.CloudUser && peer.id.id._internalGetInt64Value() == 777000 {
if peer.id.isTelegramNotifications {
sendWhenOnlineAvailable = false
}

View File

@ -29,22 +29,27 @@ public final class BotCheckoutController: ViewController {
public static func fetch(context: AccountContext, source: BotPaymentInvoiceSource) -> Signal<InputData, FetchError> {
let theme = context.sharedContext.currentPresentationData.with { $0 }.theme
let themeParams: [String: Any] = [
"bg_color": Int32(bitPattern: theme.list.plainBackgroundColor.rgb),
"secondary_bg_color": Int32(bitPattern: theme.list.blocksBackgroundColor.rgb),
"text_color": Int32(bitPattern: theme.list.itemPrimaryTextColor.rgb),
"hint_color": Int32(bitPattern: theme.list.itemSecondaryTextColor.rgb),
"link_color": Int32(bitPattern: theme.list.itemAccentColor.rgb),
"button_color": Int32(bitPattern: theme.list.itemCheckColors.fillColor.rgb),
"button_text_color": Int32(bitPattern: theme.list.itemCheckColors.foregroundColor.rgb),
"header_bg_color": Int32(bitPattern: theme.rootController.navigationBar.opaqueBackgroundColor.rgb),
"accent_text_color": Int32(bitPattern: theme.list.itemAccentColor.rgb),
"section_bg_color": Int32(bitPattern: theme.list.itemBlocksBackgroundColor.rgb),
"section_header_text_color": Int32(bitPattern: theme.list.freeTextColor.rgb),
"subtitle_text_color": Int32(bitPattern: theme.list.itemSecondaryTextColor.rgb),
"destructive_text_color": Int32(bitPattern: theme.list.itemDestructiveColor.rgb),
"section_separator_color": Int32(bitPattern: theme.list.itemBlocksSeparatorColor.rgb)
]
let themeParams: [String: Any]?
if case .starGift = source {
themeParams = nil
} else {
themeParams = [
"bg_color": Int32(bitPattern: theme.list.plainBackgroundColor.rgb),
"secondary_bg_color": Int32(bitPattern: theme.list.blocksBackgroundColor.rgb),
"text_color": Int32(bitPattern: theme.list.itemPrimaryTextColor.rgb),
"hint_color": Int32(bitPattern: theme.list.itemSecondaryTextColor.rgb),
"link_color": Int32(bitPattern: theme.list.itemAccentColor.rgb),
"button_color": Int32(bitPattern: theme.list.itemCheckColors.fillColor.rgb),
"button_text_color": Int32(bitPattern: theme.list.itemCheckColors.foregroundColor.rgb),
"header_bg_color": Int32(bitPattern: theme.rootController.navigationBar.opaqueBackgroundColor.rgb),
"accent_text_color": Int32(bitPattern: theme.list.itemAccentColor.rgb),
"section_bg_color": Int32(bitPattern: theme.list.itemBlocksBackgroundColor.rgb),
"section_header_text_color": Int32(bitPattern: theme.list.freeTextColor.rgb),
"subtitle_text_color": Int32(bitPattern: theme.list.itemSecondaryTextColor.rgb),
"destructive_text_color": Int32(bitPattern: theme.list.itemDestructiveColor.rgb),
"section_separator_color": Int32(bitPattern: theme.list.itemBlocksSeparatorColor.rgb)
]
}
return context.engine.payments.fetchBotPaymentForm(source: source, themeParams: themeParams)
|> mapError { _ -> FetchError in

View File

@ -49,6 +49,7 @@ swift_library(
"//submodules/TelegramUI/Components/SaveProgressScreen",
"//submodules/TelegramUI/Components/ListActionItemComponent",
"//submodules/Utils/DeviceModel",
"//submodules/LegacyMediaPickerUI",
],
visibility = [
"//visibility:public",

View File

@ -147,7 +147,7 @@ public final class BrowserBookmarksScreen: ViewController {
}, openLargeEmojiInfo: { _, _, _ in
}, openJoinLink: { _ in
}, openWebView: { _, _, _, _ in
}, activateAdAction: { _, _ in
}, activateAdAction: { _, _, _, _ in
}, openRequestedPeerSelection: { _, _, _, _ in
}, saveMediaToFiles: { _ in
}, openNoAdsDemo: {

View File

@ -21,6 +21,7 @@ import UrlEscaping
import UrlHandling
import SaveProgressScreen
import DeviceModel
import LegacyMediaPickerUI
private final class TonSchemeHandler: NSObject, WKURLSchemeHandler {
private final class PendingTask {
@ -170,7 +171,7 @@ private func computedUserAgent() -> String {
return DeviceModel.current.isIpad ? "Version/\(osVersion) Safari/605.1.15" : "Version/\(osVersion) Mobile/\(firmwareVersion) Safari/604.1"
}
final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKUIDelegate, UIScrollViewDelegate {
final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKUIDelegate, UIScrollViewDelegate, WKDownloadDelegate {
private let context: AccountContext
private var presentationData: PresentationData
@ -733,15 +734,15 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU
@available(iOS 13.0, *)
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, preferences: WKWebpagePreferences, decisionHandler: @escaping (WKNavigationActionPolicy, WKWebpagePreferences) -> Void) {
// if #available(iOS 14.5, *), navigationAction.shouldPerformDownload {
// self.presentDownloadConfirmation(fileName: navigationAction.request.mainDocumentURL?.lastPathComponent ?? "file", proceed: { download in
// if download {
// decisionHandler(.download, preferences)
// } else {
//// decisionHandler(.cancel, preferences)
// }
// })
// } else {
if #available(iOS 14.5, *), navigationAction.shouldPerformDownload {
self.presentDownloadConfirmation(fileName: navigationAction.request.mainDocumentURL?.lastPathComponent ?? "file", proceed: { download in
if download {
decisionHandler(.download, preferences)
} else {
decisionHandler(.cancel, preferences)
}
})
} else {
if let url = navigationAction.request.url?.absoluteString {
if (navigationAction.targetFrame == nil || navigationAction.targetFrame?.isMainFrame == true) && (isTelegramMeLink(url) || isTelegraPhLink(url) || url.hasPrefix("tg://")) && !url.contains("/auth/push?") && !self._state.url.contains("/auth/push?") {
decisionHandler(.cancel, preferences)
@ -758,24 +759,25 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU
} else {
decisionHandler(.allow, preferences)
}
// }
}
}
// func webView(_ webView: WKWebView, decidePolicyFor navigationResponse: WKNavigationResponse, decisionHandler: @escaping (WKNavigationResponsePolicy) -> Void) {
// if navigationResponse.canShowMIMEType {
// decisionHandler(.allow)
// } else if #available(iOS 14.5, *) {
// self.presentDownloadConfirmation(fileName: navigationResponse.response.suggestedFilename ?? "file", proceed: { download in
// if download {
// decisionHandler(.download)
// } else {
// decisionHandler(.cancel)
// }
// })
// } else {
// decisionHandler(.cancel)
// }
// }
func webView(_ webView: WKWebView, decidePolicyFor navigationResponse: WKNavigationResponse, decisionHandler: @escaping (WKNavigationResponsePolicy) -> Void) {
if navigationResponse.canShowMIMEType {
decisionHandler(.allow)
} else if #available(iOS 14.5, *) {
// decisionHandler(.download)
self.presentDownloadConfirmation(fileName: navigationResponse.response.suggestedFilename ?? "file", proceed: { download in
if download {
decisionHandler(.download)
} else {
decisionHandler(.cancel)
}
})
} else {
decisionHandler(.cancel)
}
}
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
if let url = navigationAction.request.url?.absoluteString {
@ -790,6 +792,101 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU
decisionHandler(.allow)
}
}
private var downloadArguments: (String, String)?
private var downloadController: (AlertController, (Int64, Int64) -> Void)?
private var downloadProgressObserver: Any?
@available(iOS 14.5, *)
func webView(_ webView: WKWebView, navigationAction: WKNavigationAction, didBecome download: WKDownload) {
download.delegate = self
}
@available(iOS 14.5, *)
func webView(_ webView: WKWebView, navigationResponse: WKNavigationResponse, didBecome download: WKDownload) {
download.delegate = self
}
@available(iOS 14.5, *)
func download(_ download: WKDownload, decideDestinationUsing response: URLResponse, suggestedFilename: String, completionHandler: @escaping (URL?) -> Void) {
let path = NSTemporaryDirectory() + NSUUID().uuidString
self.downloadArguments = (path, suggestedFilename)
completionHandler(URL(fileURLWithPath: path))
let downloadController = progressAlertController(sharedContext: self.context.sharedContext, title: "", cancel: { [weak download] in
download?.cancel()
})
self.downloadController = downloadController
self.present(downloadController.0, nil)
downloadController.1(download.progress.completedUnitCount, download.progress.totalUnitCount)
self.downloadProgressObserver = download.progress.observe(\.fractionCompleted) { [weak self] progress, _ in
if let (_, update) = self?.downloadController {
update(progress.completedUnitCount, progress.totalUnitCount)
}
}
}
@available(iOS 14.5, *)
func downloadDidFinish(_ download: WKDownload) {
if let (controller, _ ) = self.downloadController {
controller.dismissAnimated()
self.downloadController = nil
}
if let (path, fileName) = self.downloadArguments {
let tempFile = TempBox.shared.file(path: path, fileName: fileName)
let url = URL(fileURLWithPath: tempFile.path)
let controller = legacyICloudFilePicker(theme: self.presentationData.theme, mode: .export, url: url, documentTypes: [], forceDarkTheme: false, dismissed: {}, completion: { _ in
})
self.present(controller, nil)
self.downloadArguments = nil
self.downloadProgressObserver = nil
}
}
@available(iOS 14.5, *)
func download(_ download: WKDownload, didFailWithError error: Error, resumeData: Data?) {
self.downloadArguments = nil
self.downloadProgressObserver = nil
}
func webView(_ webView: WKWebView, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
if let url = webView.url, !url.absoluteString.contains("beatsnvibes") {
completionHandler(.performDefaultHandling, nil)
return
}
var completed = false
let host = webView.url?.host ?? ""
let authController = authController(sharedContext: self.context.sharedContext, updatedPresentationData: nil, title: "Sign in to \(host)", text: "Your login information will be sent securely.", apply: { result in
if !completed {
completed = true
if let (login, password) = result {
let credential = URLCredential(
user: login,
password: password,
persistence: .permanent
)
completionHandler(.useCredential, credential)
} else {
completionHandler(.cancelAuthenticationChallenge, nil)
}
}
})
authController.dismissed = { byOutsideTap in
if byOutsideTap {
if !completed {
completed = true
completionHandler(.cancelAuthenticationChallenge, nil)
}
}
}
self.present(authController, nil)
}
private let isLoaded = ValuePromise<Bool>(false)
private var instantPageDisposable = MetaDisposable()
@ -880,7 +977,7 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU
}
func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) {
if [-1003, -1100, 102].contains((error as NSError).code) {
if [-1003, -1100].contains((error as NSError).code) {
if let url = (error as NSError).userInfo["NSErrorFailingURLKey"] as? URL, url.absoluteString.hasPrefix("itms-appss:") {
} else {
self.currentError = error

View File

@ -17,6 +17,7 @@ swift_library(
"//submodules/TelegramCore:TelegramCore",
"//submodules/AccountContext:AccountContext",
"//submodules/TelegramPresentationData:TelegramPresentationData",
"//submodules/TelegramStringFormatting",
],
visibility = [
"//visibility:public",

View File

@ -7,6 +7,7 @@ import Postbox
import TelegramCore
import TelegramPresentationData
import AccountContext
import TelegramStringFormatting
private final class PromptInputFieldNode: ASDisplayNode, ASEditableTextNodeDelegate {
private var theme: PresentationTheme
@ -39,7 +40,7 @@ private final class PromptInputFieldNode: ASDisplayNode, ASEditableTextNodeDeleg
}
}
init(theme: PresentationTheme, placeholder: String, characterLimit: Int) {
init(theme: PresentationTheme, placeholder: String, characterLimit: Int, isPassword: Bool = false) {
self.theme = theme
self.characterLimit = characterLimit
@ -60,6 +61,7 @@ private final class PromptInputFieldNode: ASDisplayNode, ASEditableTextNodeDeleg
self.textInputNode.returnKeyType = .done
self.textInputNode.autocorrectionType = .default
self.textInputNode.tintColor = theme.actionSheet.controlAccentColor
self.textInputNode.isSecureTextEntry = isPassword
self.placeholderNode = ASTextNode()
self.placeholderNode.isUserInteractionEnabled = false
@ -448,3 +450,538 @@ public func promptController(sharedContext: SharedAccountContext, updatedPresent
}
return controller
}
private final class AuthAlertContentNode: AlertContentNode {
private let strings: PresentationStrings
private let title: String
private let text: String
private let titleNode: ASTextNode
private let textNode: ASTextNode
let inputFieldNode: PromptInputFieldNode
let passwordFieldNode: PromptInputFieldNode
private let actionNodesSeparator: ASDisplayNode
private let actionNodes: [TextAlertContentActionNode]
private let actionVerticalSeparators: [ASDisplayNode]
private let disposable = MetaDisposable()
private var validLayout: CGSize?
private let hapticFeedback = HapticFeedback()
var complete: (() -> Void)? {
didSet {
self.inputFieldNode.complete = self.complete
}
}
override var dismissOnOutsideTap: Bool {
return self.isUserInteractionEnabled
}
init(theme: AlertControllerTheme, ptheme: PresentationTheme, strings: PresentationStrings, actions: [TextAlertAction], title: String, text: String) {
self.strings = strings
self.title = title
self.text = text
self.titleNode = ASTextNode()
self.titleNode.maximumNumberOfLines = 2
self.textNode = ASTextNode()
self.textNode.maximumNumberOfLines = 2
self.inputFieldNode = PromptInputFieldNode(theme: ptheme, placeholder: "User Name", characterLimit: 1024)
self.passwordFieldNode = PromptInputFieldNode(theme: ptheme, placeholder: "Password", characterLimit: 1024, isPassword: true)
self.actionNodesSeparator = ASDisplayNode()
self.actionNodesSeparator.isLayerBacked = true
self.actionNodes = actions.map { action -> TextAlertContentActionNode in
return TextAlertContentActionNode(theme: theme, action: action)
}
var actionVerticalSeparators: [ASDisplayNode] = []
if actions.count > 1 {
for _ in 0 ..< actions.count - 1 {
let separatorNode = ASDisplayNode()
separatorNode.isLayerBacked = true
actionVerticalSeparators.append(separatorNode)
}
}
self.actionVerticalSeparators = actionVerticalSeparators
super.init()
self.addSubnode(self.titleNode)
self.addSubnode(self.textNode)
self.addSubnode(self.inputFieldNode)
self.addSubnode(self.passwordFieldNode)
self.addSubnode(self.actionNodesSeparator)
for actionNode in self.actionNodes {
self.addSubnode(actionNode)
}
self.actionNodes.last?.actionEnabled = true
for separatorNode in self.actionVerticalSeparators {
self.addSubnode(separatorNode)
}
self.inputFieldNode.updateHeight = { [weak self] in
if let strongSelf = self {
if let _ = strongSelf.validLayout {
strongSelf.requestLayout?(.animated(duration: 0.15, curve: .spring))
}
}
}
self.updateTheme(theme)
}
deinit {
self.disposable.dispose()
}
var login: String {
return self.inputFieldNode.text
}
var password: String {
return self.passwordFieldNode.text
}
override func updateTheme(_ theme: AlertControllerTheme) {
self.titleNode.attributedText = NSAttributedString(string: self.title, font: Font.semibold(17.0), textColor: theme.primaryColor, paragraphAlignment: .center)
self.textNode.attributedText = NSAttributedString(string: self.text, font: Font.regular(13.0), textColor: theme.primaryColor, paragraphAlignment: .center)
self.actionNodesSeparator.backgroundColor = theme.separatorColor
for actionNode in self.actionNodes {
actionNode.updateTheme(theme)
}
for separatorNode in self.actionVerticalSeparators {
separatorNode.backgroundColor = theme.separatorColor
}
if let size = self.validLayout {
_ = self.updateLayout(size: size, transition: .immediate)
}
}
override func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize {
var size = size
size.width = min(size.width, 270.0)
let measureSize = CGSize(width: size.width - 16.0 * 2.0, height: CGFloat.greatestFiniteMagnitude)
let hadValidLayout = self.validLayout != nil
self.validLayout = size
var origin: CGPoint = CGPoint(x: 0.0, y: 20.0)
let spacing: CGFloat = 5.0
let titleSize = self.titleNode.measure(measureSize)
transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - titleSize.width) / 2.0), y: origin.y), size: titleSize))
origin.y += titleSize.height + 4.0
let textSize = self.textNode.measure(measureSize)
transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - textSize.width) / 2.0), y: origin.y), size: textSize))
origin.y += textSize.height + 6.0 + spacing
let actionButtonHeight: CGFloat = 44.0
var minActionsWidth: CGFloat = 0.0
let maxActionWidth: CGFloat = floor(size.width / CGFloat(self.actionNodes.count))
let actionTitleInsets: CGFloat = 8.0
var effectiveActionLayout = TextAlertContentActionLayout.horizontal
for actionNode in self.actionNodes {
let actionTitleSize = actionNode.titleNode.updateLayout(CGSize(width: maxActionWidth, height: actionButtonHeight))
if case .horizontal = effectiveActionLayout, actionTitleSize.height > actionButtonHeight * 0.6667 {
effectiveActionLayout = .vertical
}
switch effectiveActionLayout {
case .horizontal:
minActionsWidth += actionTitleSize.width + actionTitleInsets
case .vertical:
minActionsWidth = max(minActionsWidth, actionTitleSize.width + actionTitleInsets)
}
}
let insets = UIEdgeInsets(top: 18.0, left: 18.0, bottom: 9.0, right: 18.0)
var contentWidth = max(titleSize.width, minActionsWidth)
contentWidth = max(contentWidth, 234.0)
var actionsHeight: CGFloat = 0.0
switch effectiveActionLayout {
case .horizontal:
actionsHeight = actionButtonHeight
case .vertical:
actionsHeight = actionButtonHeight * CGFloat(self.actionNodes.count)
}
let resultWidth = contentWidth + insets.left + insets.right
let inputFieldWidth = resultWidth
let inputFieldHeight = self.inputFieldNode.updateLayout(width: inputFieldWidth, transition: transition)
let inputHeight = inputFieldHeight
transition.updateFrame(node: self.inputFieldNode, frame: CGRect(x: 0.0, y: origin.y, width: resultWidth, height: inputFieldHeight))
transition.updateAlpha(node: self.inputFieldNode, alpha: inputHeight > 0.0 ? 1.0 : 0.0)
let fieldSpacing: CGFloat = -11.0
origin.y += inputFieldHeight + fieldSpacing
let passwordFieldHeight = self.passwordFieldNode.updateLayout(width: inputFieldWidth, transition: transition)
transition.updateFrame(node: self.passwordFieldNode, frame: CGRect(x: 0.0, y: origin.y, width: resultWidth, height: passwordFieldHeight))
let resultSize = CGSize(width: resultWidth, height: titleSize.height + textSize.height + spacing + inputHeight + fieldSpacing + passwordFieldHeight + actionsHeight + insets.top + insets.bottom)
transition.updateFrame(node: self.actionNodesSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel)))
var actionOffset: CGFloat = 0.0
let actionWidth: CGFloat = floor(resultSize.width / CGFloat(self.actionNodes.count))
var separatorIndex = -1
var nodeIndex = 0
for actionNode in self.actionNodes {
if separatorIndex >= 0 {
let separatorNode = self.actionVerticalSeparators[separatorIndex]
switch effectiveActionLayout {
case .horizontal:
transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: actionOffset - UIScreenPixel, y: resultSize.height - actionsHeight), size: CGSize(width: UIScreenPixel, height: actionsHeight - UIScreenPixel)))
case .vertical:
transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel)))
}
}
separatorIndex += 1
let currentActionWidth: CGFloat
switch effectiveActionLayout {
case .horizontal:
if nodeIndex == self.actionNodes.count - 1 {
currentActionWidth = resultSize.width - actionOffset
} else {
currentActionWidth = actionWidth
}
case .vertical:
currentActionWidth = resultSize.width
}
let actionNodeFrame: CGRect
switch effectiveActionLayout {
case .horizontal:
actionNodeFrame = CGRect(origin: CGPoint(x: actionOffset, y: resultSize.height - actionsHeight), size: CGSize(width: currentActionWidth, height: actionButtonHeight))
actionOffset += currentActionWidth
case .vertical:
actionNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset), size: CGSize(width: currentActionWidth, height: actionButtonHeight))
actionOffset += actionButtonHeight
}
transition.updateFrame(node: actionNode, frame: actionNodeFrame)
nodeIndex += 1
}
if !hadValidLayout {
self.inputFieldNode.activateInput()
}
return resultSize
}
func animateError() {
self.inputFieldNode.layer.addShakeAnimation()
self.hapticFeedback.error()
}
}
public func authController(sharedContext: SharedAccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, title: String, text: String, apply: @escaping ((String, String)?) -> Void) -> AlertController {
let presentationData = updatedPresentationData?.initial ?? sharedContext.currentPresentationData.with { $0 }
var dismissImpl: ((Bool) -> Void)?
var applyImpl: (() -> Void)?
let actions: [TextAlertAction] = [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {
dismissImpl?(true)
apply(nil)
}), TextAlertAction(type: .defaultAction, title: "Sign In", action: {
dismissImpl?(true)
applyImpl?()
})]
let contentNode = AuthAlertContentNode(theme: AlertControllerTheme(presentationData: presentationData), ptheme: presentationData.theme, strings: presentationData.strings, actions: actions, title: title, text: text)
contentNode.complete = {
applyImpl?()
}
applyImpl = { [weak contentNode] in
guard let contentNode = contentNode else {
return
}
apply((contentNode.login, contentNode.password))
}
let controller = AlertController(theme: AlertControllerTheme(presentationData: presentationData), contentNode: contentNode)
let presentationDataDisposable = (updatedPresentationData?.signal ?? sharedContext.presentationData).start(next: { [weak controller, weak contentNode] presentationData in
controller?.theme = AlertControllerTheme(presentationData: presentationData)
contentNode?.inputFieldNode.updateTheme(presentationData.theme)
})
controller.dismissed = { _ in
presentationDataDisposable.dispose()
}
dismissImpl = { [weak controller] animated in
contentNode.inputFieldNode.deactivateInput()
if animated {
controller?.dismissAnimated()
} else {
controller?.dismiss()
}
}
return controller
}
private final class ProgressAlertContentNode: AlertContentNode {
private let theme: AlertControllerTheme
private let strings: PresentationStrings
private let title: String
var text: String {
didSet {
self.updateTheme(self.theme)
}
}
private let titleNode: ASTextNode
private let textNode: ASTextNode
private let actionNodesSeparator: ASDisplayNode
private let actionNodes: [TextAlertContentActionNode]
private let actionVerticalSeparators: [ASDisplayNode]
private let disposable = MetaDisposable()
private var validLayout: CGSize?
private let hapticFeedback = HapticFeedback()
override var dismissOnOutsideTap: Bool {
return self.isUserInteractionEnabled
}
init(theme: AlertControllerTheme, ptheme: PresentationTheme, strings: PresentationStrings, actions: [TextAlertAction], title: String, text: String) {
self.theme = theme
self.strings = strings
self.title = title
self.text = text
self.titleNode = ASTextNode()
self.titleNode.maximumNumberOfLines = 2
self.textNode = ASTextNode()
self.textNode.maximumNumberOfLines = 2
self.actionNodesSeparator = ASDisplayNode()
self.actionNodesSeparator.isLayerBacked = true
self.actionNodes = actions.map { action -> TextAlertContentActionNode in
return TextAlertContentActionNode(theme: theme, action: action)
}
var actionVerticalSeparators: [ASDisplayNode] = []
if actions.count > 1 {
for _ in 0 ..< actions.count - 1 {
let separatorNode = ASDisplayNode()
separatorNode.isLayerBacked = true
actionVerticalSeparators.append(separatorNode)
}
}
self.actionVerticalSeparators = actionVerticalSeparators
super.init()
self.addSubnode(self.titleNode)
self.addSubnode(self.textNode)
self.addSubnode(self.actionNodesSeparator)
for actionNode in self.actionNodes {
self.addSubnode(actionNode)
}
self.actionNodes.last?.actionEnabled = true
for separatorNode in self.actionVerticalSeparators {
self.addSubnode(separatorNode)
}
self.updateTheme(theme)
}
deinit {
self.disposable.dispose()
}
override func updateTheme(_ theme: AlertControllerTheme) {
self.titleNode.attributedText = NSAttributedString(string: self.title, font: Font.semibold(17.0), textColor: theme.primaryColor, paragraphAlignment: .center)
self.textNode.attributedText = NSAttributedString(string: self.text, font: Font.regular(13.0), textColor: theme.primaryColor, paragraphAlignment: .center)
self.actionNodesSeparator.backgroundColor = theme.separatorColor
for actionNode in self.actionNodes {
actionNode.updateTheme(theme)
}
for separatorNode in self.actionVerticalSeparators {
separatorNode.backgroundColor = theme.separatorColor
}
if let size = self.validLayout {
_ = self.updateLayout(size: size, transition: .immediate)
}
}
override func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize {
var size = size
size.width = min(size.width, 270.0)
let measureSize = CGSize(width: size.width - 16.0 * 2.0, height: CGFloat.greatestFiniteMagnitude)
self.validLayout = size
var origin: CGPoint = CGPoint(x: 0.0, y: 20.0)
let spacing: CGFloat = 5.0
let titleSize = self.titleNode.measure(measureSize)
transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - titleSize.width) / 2.0), y: origin.y), size: titleSize))
origin.y += titleSize.height + 4.0
let textSize = self.textNode.measure(measureSize)
transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - textSize.width) / 2.0), y: origin.y), size: textSize))
origin.y += textSize.height + 6.0 + spacing
let actionButtonHeight: CGFloat = 44.0
var minActionsWidth: CGFloat = 0.0
let maxActionWidth: CGFloat = floor(size.width / CGFloat(self.actionNodes.count))
let actionTitleInsets: CGFloat = 8.0
var effectiveActionLayout = TextAlertContentActionLayout.horizontal
for actionNode in self.actionNodes {
let actionTitleSize = actionNode.titleNode.updateLayout(CGSize(width: maxActionWidth, height: actionButtonHeight))
if case .horizontal = effectiveActionLayout, actionTitleSize.height > actionButtonHeight * 0.6667 {
effectiveActionLayout = .vertical
}
switch effectiveActionLayout {
case .horizontal:
minActionsWidth += actionTitleSize.width + actionTitleInsets
case .vertical:
minActionsWidth = max(minActionsWidth, actionTitleSize.width + actionTitleInsets)
}
}
let insets = UIEdgeInsets(top: 18.0, left: 18.0, bottom: 9.0, right: 18.0)
var contentWidth = max(titleSize.width, minActionsWidth)
contentWidth = max(contentWidth, 234.0)
var actionsHeight: CGFloat = 0.0
switch effectiveActionLayout {
case .horizontal:
actionsHeight = actionButtonHeight
case .vertical:
actionsHeight = actionButtonHeight * CGFloat(self.actionNodes.count)
}
let resultWidth = contentWidth + insets.left + insets.right
let resultSize = CGSize(width: resultWidth, height: titleSize.height + textSize.height + spacing + actionsHeight + insets.top + insets.bottom)
transition.updateFrame(node: self.actionNodesSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel)))
var actionOffset: CGFloat = 0.0
let actionWidth: CGFloat = floor(resultSize.width / CGFloat(self.actionNodes.count))
var separatorIndex = -1
var nodeIndex = 0
for actionNode in self.actionNodes {
if separatorIndex >= 0 {
let separatorNode = self.actionVerticalSeparators[separatorIndex]
switch effectiveActionLayout {
case .horizontal:
transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: actionOffset - UIScreenPixel, y: resultSize.height - actionsHeight), size: CGSize(width: UIScreenPixel, height: actionsHeight - UIScreenPixel)))
case .vertical:
transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel)))
}
}
separatorIndex += 1
let currentActionWidth: CGFloat
switch effectiveActionLayout {
case .horizontal:
if nodeIndex == self.actionNodes.count - 1 {
currentActionWidth = resultSize.width - actionOffset
} else {
currentActionWidth = actionWidth
}
case .vertical:
currentActionWidth = resultSize.width
}
let actionNodeFrame: CGRect
switch effectiveActionLayout {
case .horizontal:
actionNodeFrame = CGRect(origin: CGPoint(x: actionOffset, y: resultSize.height - actionsHeight), size: CGSize(width: currentActionWidth, height: actionButtonHeight))
actionOffset += currentActionWidth
case .vertical:
actionNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset), size: CGSize(width: currentActionWidth, height: actionButtonHeight))
actionOffset += actionButtonHeight
}
transition.updateFrame(node: actionNode, frame: actionNodeFrame)
nodeIndex += 1
}
return resultSize
}
}
public func progressAlertController(sharedContext: SharedAccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, title: String, cancel: @escaping () -> Void) -> (AlertController, (Int64, Int64) -> Void) {
let presentationData = updatedPresentationData?.initial ?? sharedContext.currentPresentationData.with { $0 }
var dismissImpl: ((Bool) -> Void)?
var applyImpl: (() -> Void)?
let actions: [TextAlertAction] = [TextAlertAction(type: .defaultAction, title: "Cancel", action: {
dismissImpl?(true)
applyImpl?()
})]
let contentNode = ProgressAlertContentNode(theme: AlertControllerTheme(presentationData: presentationData), ptheme: presentationData.theme, strings: presentationData.strings, actions: actions, title: "Downloading...", text: " ")
let updateProgress: (Int64, Int64) -> Void = { [weak contentNode] current, total in
if let contentNode {
contentNode.text = "\(dataSizeString(Int(current), formatting: DataSizeStringFormatting(presentationData: presentationData))) of \(dataSizeString(Int(total), formatting: DataSizeStringFormatting(presentationData: presentationData)))"
}
}
applyImpl = {
dismissImpl?(true)
cancel()
}
let controller = AlertController(theme: AlertControllerTheme(presentationData: presentationData), contentNode: contentNode)
let presentationDataDisposable = (updatedPresentationData?.signal ?? sharedContext.presentationData).start(next: { [weak controller] presentationData in
controller?.theme = AlertControllerTheme(presentationData: presentationData)
})
controller.dismissed = { _ in
presentationDataDisposable.dispose()
}
dismissImpl = { [weak controller] animated in
if animated {
controller?.dismissAnimated()
} else {
controller?.dismiss()
}
}
return (controller, updateProgress)
}

View File

@ -238,6 +238,12 @@ public func combineLatest<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13
}, initialValues: [:], queue: queue)
}
public func combineLatest<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20, T21, T22, T23, T24, T25, T26, E>(queue: Queue? = nil, _ s1: Signal<T1, E>, _ s2: Signal<T2, E>, _ s3: Signal<T3, E>, _ s4: Signal<T4, E>, _ s5: Signal<T5, E>, _ s6: Signal<T6, E>, _ s7: Signal<T7, E>, _ s8: Signal<T8, E>, _ s9: Signal<T9, E>, _ s10: Signal<T10, E>, _ s11: Signal<T11, E>, _ s12: Signal<T12, E>, _ s13: Signal<T13, E>, _ s14: Signal<T14, E>, _ s15: Signal<T15, E>, _ s16: Signal<T16, E>, _ s17: Signal<T17, E>, _ s18: Signal<T18, E>, _ s19: Signal<T19, E>, _ s20: Signal<T20, E>, _ s21: Signal<T21, E>, _ s22: Signal<T22, E>, _ s23: Signal<T23, E>, _ s24: Signal<T24, E>, _ s25: Signal<T25, E>, _ s26: Signal<T26, E>) -> Signal<(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20, T21, T22, T23, T24, T25, T26), E> {
return combineLatestAny([signalOfAny(s1), signalOfAny(s2), signalOfAny(s3), signalOfAny(s4), signalOfAny(s5), signalOfAny(s6), signalOfAny(s7), signalOfAny(s8), signalOfAny(s9), signalOfAny(s10), signalOfAny(s11), signalOfAny(s12), signalOfAny(s13), signalOfAny(s14), signalOfAny(s15), signalOfAny(s16), signalOfAny(s17), signalOfAny(s18), signalOfAny(s19), signalOfAny(s20), signalOfAny(s21), signalOfAny(s22), signalOfAny(s23), signalOfAny(s24), signalOfAny(s25), signalOfAny(s26)], combine: { values in
return (values[0] as! T1, values[1] as! T2, values[2] as! T3, values[3] as! T4, values[4] as! T5, values[5] as! T6, values[6] as! T7, values[7] as! T8, values[8] as! T9, values[9] as! T10, values[10] as! T11, values[11] as! T12, values[12] as! T13, values[13] as! T14, values[14] as! T15, values[15] as! T16, values[16] as! T17, values[17] as! T18, values[18] as! T19, values[19] as! T20, values[20] as! T21, values[21] as! T22, values[22] as! T23, values[23] as! T24, values[24] as! T25, values[25] as! T26)
}, initialValues: [:], queue: queue)
}
public func combineLatest<T, E>(queue: Queue? = nil, _ signals: [Signal<T, E>]) -> Signal<[T], E> {
if signals.count == 0 {
return single([T](), E.self)

View File

@ -1103,7 +1103,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
dict[-1831650802] = { return Api.UrlAuthResult.parse_urlAuthResultRequest($0) }
dict[-2093920310] = { return Api.User.parse_user($0) }
dict[-742634630] = { return Api.User.parse_userEmpty($0) }
dict[-862357728] = { return Api.UserFull.parse_userFull($0) }
dict[525919081] = { return Api.UserFull.parse_userFull($0) }
dict[-2100168954] = { return Api.UserProfilePhoto.parse_userProfilePhoto($0) }
dict[1326562017] = { return Api.UserProfilePhoto.parse_userProfilePhotoEmpty($0) }
dict[-291202450] = { return Api.UserStarGift.parse_userStarGift($0) }

View File

@ -606,13 +606,13 @@ public extension Api {
}
public extension Api {
enum UserFull: TypeConstructorDescription {
case userFull(flags: Int32, flags2: Int32, id: Int64, about: String?, settings: Api.PeerSettings, personalPhoto: Api.Photo?, profilePhoto: Api.Photo?, fallbackPhoto: Api.Photo?, notifySettings: Api.PeerNotifySettings, botInfo: Api.BotInfo?, pinnedMsgId: Int32?, commonChatsCount: Int32, folderId: Int32?, ttlPeriod: Int32?, themeEmoticon: String?, privateForwardName: String?, botGroupAdminRights: Api.ChatAdminRights?, botBroadcastAdminRights: Api.ChatAdminRights?, premiumGifts: [Api.PremiumGiftOption]?, wallpaper: Api.WallPaper?, stories: Api.PeerStories?, businessWorkHours: Api.BusinessWorkHours?, businessLocation: Api.BusinessLocation?, businessGreetingMessage: Api.BusinessGreetingMessage?, businessAwayMessage: Api.BusinessAwayMessage?, businessIntro: Api.BusinessIntro?, birthday: Api.Birthday?, personalChannelId: Int64?, personalChannelMessage: Int32?)
case userFull(flags: Int32, flags2: Int32, id: Int64, about: String?, settings: Api.PeerSettings, personalPhoto: Api.Photo?, profilePhoto: Api.Photo?, fallbackPhoto: Api.Photo?, notifySettings: Api.PeerNotifySettings, botInfo: Api.BotInfo?, pinnedMsgId: Int32?, commonChatsCount: Int32, folderId: Int32?, ttlPeriod: Int32?, themeEmoticon: String?, privateForwardName: String?, botGroupAdminRights: Api.ChatAdminRights?, botBroadcastAdminRights: Api.ChatAdminRights?, premiumGifts: [Api.PremiumGiftOption]?, wallpaper: Api.WallPaper?, stories: Api.PeerStories?, businessWorkHours: Api.BusinessWorkHours?, businessLocation: Api.BusinessLocation?, businessGreetingMessage: Api.BusinessGreetingMessage?, businessAwayMessage: Api.BusinessAwayMessage?, businessIntro: Api.BusinessIntro?, birthday: Api.Birthday?, personalChannelId: Int64?, personalChannelMessage: Int32?, stargiftsCount: Int32?)
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self {
case .userFull(let flags, let flags2, let id, let about, let settings, let personalPhoto, let profilePhoto, let fallbackPhoto, let notifySettings, let botInfo, let pinnedMsgId, let commonChatsCount, let folderId, let ttlPeriod, let themeEmoticon, let privateForwardName, let botGroupAdminRights, let botBroadcastAdminRights, let premiumGifts, let wallpaper, let stories, let businessWorkHours, let businessLocation, let businessGreetingMessage, let businessAwayMessage, let businessIntro, let birthday, let personalChannelId, let personalChannelMessage):
case .userFull(let flags, let flags2, let id, let about, let settings, let personalPhoto, let profilePhoto, let fallbackPhoto, let notifySettings, let botInfo, let pinnedMsgId, let commonChatsCount, let folderId, let ttlPeriod, let themeEmoticon, let privateForwardName, let botGroupAdminRights, let botBroadcastAdminRights, let premiumGifts, let wallpaper, let stories, let businessWorkHours, let businessLocation, let businessGreetingMessage, let businessAwayMessage, let businessIntro, let birthday, let personalChannelId, let personalChannelMessage, let stargiftsCount):
if boxed {
buffer.appendInt32(-862357728)
buffer.appendInt32(525919081)
}
serializeInt32(flags, buffer: buffer, boxed: false)
serializeInt32(flags2, buffer: buffer, boxed: false)
@ -647,14 +647,15 @@ public extension Api {
if Int(flags2) & Int(1 << 5) != 0 {birthday!.serialize(buffer, true)}
if Int(flags2) & Int(1 << 6) != 0 {serializeInt64(personalChannelId!, buffer: buffer, boxed: false)}
if Int(flags2) & Int(1 << 6) != 0 {serializeInt32(personalChannelMessage!, buffer: buffer, boxed: false)}
if Int(flags2) & Int(1 << 8) != 0 {serializeInt32(stargiftsCount!, buffer: buffer, boxed: false)}
break
}
}
public func descriptionFields() -> (String, [(String, Any)]) {
switch self {
case .userFull(let flags, let flags2, let id, let about, let settings, let personalPhoto, let profilePhoto, let fallbackPhoto, let notifySettings, let botInfo, let pinnedMsgId, let commonChatsCount, let folderId, let ttlPeriod, let themeEmoticon, let privateForwardName, let botGroupAdminRights, let botBroadcastAdminRights, let premiumGifts, let wallpaper, let stories, let businessWorkHours, let businessLocation, let businessGreetingMessage, let businessAwayMessage, let businessIntro, let birthday, let personalChannelId, let personalChannelMessage):
return ("userFull", [("flags", flags as Any), ("flags2", flags2 as Any), ("id", id as Any), ("about", about as Any), ("settings", settings as Any), ("personalPhoto", personalPhoto as Any), ("profilePhoto", profilePhoto as Any), ("fallbackPhoto", fallbackPhoto as Any), ("notifySettings", notifySettings as Any), ("botInfo", botInfo as Any), ("pinnedMsgId", pinnedMsgId as Any), ("commonChatsCount", commonChatsCount as Any), ("folderId", folderId as Any), ("ttlPeriod", ttlPeriod as Any), ("themeEmoticon", themeEmoticon as Any), ("privateForwardName", privateForwardName as Any), ("botGroupAdminRights", botGroupAdminRights as Any), ("botBroadcastAdminRights", botBroadcastAdminRights as Any), ("premiumGifts", premiumGifts as Any), ("wallpaper", wallpaper as Any), ("stories", stories as Any), ("businessWorkHours", businessWorkHours as Any), ("businessLocation", businessLocation as Any), ("businessGreetingMessage", businessGreetingMessage as Any), ("businessAwayMessage", businessAwayMessage as Any), ("businessIntro", businessIntro as Any), ("birthday", birthday as Any), ("personalChannelId", personalChannelId as Any), ("personalChannelMessage", personalChannelMessage as Any)])
case .userFull(let flags, let flags2, let id, let about, let settings, let personalPhoto, let profilePhoto, let fallbackPhoto, let notifySettings, let botInfo, let pinnedMsgId, let commonChatsCount, let folderId, let ttlPeriod, let themeEmoticon, let privateForwardName, let botGroupAdminRights, let botBroadcastAdminRights, let premiumGifts, let wallpaper, let stories, let businessWorkHours, let businessLocation, let businessGreetingMessage, let businessAwayMessage, let businessIntro, let birthday, let personalChannelId, let personalChannelMessage, let stargiftsCount):
return ("userFull", [("flags", flags as Any), ("flags2", flags2 as Any), ("id", id as Any), ("about", about as Any), ("settings", settings as Any), ("personalPhoto", personalPhoto as Any), ("profilePhoto", profilePhoto as Any), ("fallbackPhoto", fallbackPhoto as Any), ("notifySettings", notifySettings as Any), ("botInfo", botInfo as Any), ("pinnedMsgId", pinnedMsgId as Any), ("commonChatsCount", commonChatsCount as Any), ("folderId", folderId as Any), ("ttlPeriod", ttlPeriod as Any), ("themeEmoticon", themeEmoticon as Any), ("privateForwardName", privateForwardName as Any), ("botGroupAdminRights", botGroupAdminRights as Any), ("botBroadcastAdminRights", botBroadcastAdminRights as Any), ("premiumGifts", premiumGifts as Any), ("wallpaper", wallpaper as Any), ("stories", stories as Any), ("businessWorkHours", businessWorkHours as Any), ("businessLocation", businessLocation as Any), ("businessGreetingMessage", businessGreetingMessage as Any), ("businessAwayMessage", businessAwayMessage as Any), ("businessIntro", businessIntro as Any), ("birthday", birthday as Any), ("personalChannelId", personalChannelId as Any), ("personalChannelMessage", personalChannelMessage as Any), ("stargiftsCount", stargiftsCount as Any)])
}
}
@ -751,6 +752,8 @@ public extension Api {
if Int(_2!) & Int(1 << 6) != 0 {_28 = reader.readInt64() }
var _29: Int32?
if Int(_2!) & Int(1 << 6) != 0 {_29 = reader.readInt32() }
var _30: Int32?
if Int(_2!) & Int(1 << 8) != 0 {_30 = reader.readInt32() }
let _c1 = _1 != nil
let _c2 = _2 != nil
let _c3 = _3 != nil
@ -780,8 +783,9 @@ public extension Api {
let _c27 = (Int(_2!) & Int(1 << 5) == 0) || _27 != nil
let _c28 = (Int(_2!) & Int(1 << 6) == 0) || _28 != nil
let _c29 = (Int(_2!) & Int(1 << 6) == 0) || _29 != nil
if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 && _c15 && _c16 && _c17 && _c18 && _c19 && _c20 && _c21 && _c22 && _c23 && _c24 && _c25 && _c26 && _c27 && _c28 && _c29 {
return Api.UserFull.userFull(flags: _1!, flags2: _2!, id: _3!, about: _4, settings: _5!, personalPhoto: _6, profilePhoto: _7, fallbackPhoto: _8, notifySettings: _9!, botInfo: _10, pinnedMsgId: _11, commonChatsCount: _12!, folderId: _13, ttlPeriod: _14, themeEmoticon: _15, privateForwardName: _16, botGroupAdminRights: _17, botBroadcastAdminRights: _18, premiumGifts: _19, wallpaper: _20, stories: _21, businessWorkHours: _22, businessLocation: _23, businessGreetingMessage: _24, businessAwayMessage: _25, businessIntro: _26, birthday: _27, personalChannelId: _28, personalChannelMessage: _29)
let _c30 = (Int(_2!) & Int(1 << 8) == 0) || _30 != nil
if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 && _c15 && _c16 && _c17 && _c18 && _c19 && _c20 && _c21 && _c22 && _c23 && _c24 && _c25 && _c26 && _c27 && _c28 && _c29 && _c30 {
return Api.UserFull.userFull(flags: _1!, flags2: _2!, id: _3!, about: _4, settings: _5!, personalPhoto: _6, profilePhoto: _7, fallbackPhoto: _8, notifySettings: _9!, botInfo: _10, pinnedMsgId: _11, commonChatsCount: _12!, folderId: _13, ttlPeriod: _14, themeEmoticon: _15, privateForwardName: _16, botGroupAdminRights: _17, botBroadcastAdminRights: _18, premiumGifts: _19, wallpaper: _20, stories: _21, businessWorkHours: _22, businessLocation: _23, businessGreetingMessage: _24, businessAwayMessage: _25, businessIntro: _26, birthday: _27, personalChannelId: _28, personalChannelMessage: _29, stargiftsCount: _30)
}
else {
return nil

View File

@ -9211,12 +9211,13 @@ public extension Api.functions.payments {
}
}
public extension Api.functions.payments {
static func saveStarGift(userId: Api.InputUser, msgId: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Bool>) {
static func saveStarGift(flags: Int32, userId: Api.InputUser, msgId: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Bool>) {
let buffer = Buffer()
buffer.appendInt32(-1394197223)
buffer.appendInt32(-2018709362)
serializeInt32(flags, buffer: buffer, boxed: false)
userId.serialize(buffer, true)
serializeInt32(msgId, buffer: buffer, boxed: false)
return (FunctionDescription(name: "payments.saveStarGift", parameters: [("userId", String(describing: userId)), ("msgId", String(describing: msgId))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in
return (FunctionDescription(name: "payments.saveStarGift", parameters: [("flags", String(describing: flags)), ("userId", String(describing: userId)), ("msgId", String(describing: msgId))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in
let reader = BufferReader(buffer)
var result: Api.Bool?
if let signature = reader.readInt32() {

View File

@ -204,7 +204,7 @@ func _internal_convertStarGift(account: Account, messageId: EngineMessage.Id) ->
}
}
func _internal_saveStarGiftToProfile(account: Account, messageId: EngineMessage.Id) -> Signal<Never, NoError> {
func _internal_updateStarGiftAddedToProfile(account: Account, messageId: EngineMessage.Id, added: Bool) -> Signal<Never, NoError> {
return account.postbox.transaction { transaction -> Api.InputUser? in
return transaction.getPeer(messageId.peerId).flatMap(apiInputUser)
}
@ -212,7 +212,11 @@ func _internal_saveStarGiftToProfile(account: Account, messageId: EngineMessage.
guard let inputUser else {
return .complete()
}
return account.network.request(Api.functions.payments.saveStarGift(userId: inputUser, msgId: messageId.id))
var flags: Int32 = 0
if !added {
flags |= (1 << 0)
}
return account.network.request(Api.functions.payments.saveStarGift(flags: flags, userId: inputUser, msgId: messageId.id))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.Bool?, NoError> in
return .single(nil)

View File

@ -113,14 +113,12 @@ public extension TelegramEngine {
return _internal_keepCachedStarGiftsUpdated(postbox: self.account.postbox, network: self.account.network)
}
public func convertStarGift(messageId: EngineMessage.Id) -> Signal<Never, NoError> {
return _internal_convertStarGift(account: self.account, messageId: messageId)
}
public func saveStarGiftToProfile(messageId: EngineMessage.Id) -> Signal<Never, NoError> {
return _internal_saveStarGiftToProfile(account: self.account, messageId: messageId)
public func updateStarGiftAddedToProfile(messageId: EngineMessage.Id, added: Bool) -> Signal<Never, NoError> {
return _internal_updateStarGiftAddedToProfile(account: self.account, messageId: messageId, added: added)
}
}
}

View File

@ -286,7 +286,6 @@ public func customizeDefaultDayTheme(theme: PresentationTheme, editing: Bool, ti
reactionActiveMediaPlaceholder: UIColor(rgb: 0xffffff, alpha: 0.2)
)
),
linkHighlightColor: accentColor?.withAlphaComponent(0.3),
accentTextColor: accentColor,
accentControlColor: accentColor,
accentControlDisabledColor: accentColor?.withAlphaComponent(0.7),
@ -331,7 +330,7 @@ public func customizeDefaultDayTheme(theme: PresentationTheme, editing: Bool, ti
primaryTextColor: outgoingPrimaryTextColor,
secondaryTextColor: outgoingSecondaryTextColor,
linkTextColor: outgoingLinkTextColor,
linkHighlightColor: day ? nil : accentColor?.withAlphaComponent(0.3),
linkHighlightColor: day ? nil : outgoingLinkTextColor?.withAlphaComponent(0.3),
scamColor: outgoingScamColor,
accentTextColor: outgoingAccentTextColor,
accentControlColor: outgoingControlColor,

View File

@ -1051,14 +1051,27 @@ public func universalServiceMessageString(presentationData: (PresentationTheme,
attributedString = mutableString
case .prizeStars:
attributedString = NSAttributedString(string: strings.Notification_StarsPrize, font: titleFont, textColor: primaryTextColor)
case let .starGift(amount, _, nameHidden, limitNumber, limitTotal, text, entities):
let _ = amount
case let .starGift(gift, _, nameHidden, limitNumber, limitTotal, text, entities):
let _ = nameHidden
let _ = limitNumber
let _ = limitTotal
let _ = text
let _ = entities
attributedString = nil
let starsPrice = "\(gift.price) Stars"
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 = strings.Notification_StarsGift_UnknownUser
peerIds = []
}
if message.author?.id == accountPeerId {
attributedString = addAttributesToStringWithRanges(strings.Notification_StarsGift_SentYou(starsPrice)._tuple, body: bodyAttributes, argumentAttributes: [0: boldAttributes])
} else {
var attributes = peerMentionsAttributes(primaryTextColor: primaryTextColor, peerIds: peerIds)
attributes[1] = boldAttributes
attributedString = addAttributesToStringWithRanges(strings.Notification_StarsGift_Sent(authorName, starsPrice)._tuple, body: bodyAttributes, argumentAttributes: attributes)
}
case .unknown:
attributedString = nil
}

View File

@ -459,6 +459,7 @@ swift_library(
"//submodules/TelegramUI/Components/MinimizedContainer",
"//submodules/TelegramUI/Components/SpaceWarpView",
"//submodules/TelegramUI/Components/MiniAppListScreen",
"//submodules/TelegramUI/Components/Gifts/GiftOptionsScreen",
] + select({
"@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets,
"//build-system:ios_sim_arm64": [],

View File

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

View File

@ -243,6 +243,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
var months: Int32 = 3
var animationName: String = ""
var animationFile: TelegramMediaFile?
var title = item.presentationData.strings.Notification_PremiumGift_Title
var text = ""
var buttonTitle = item.presentationData.strings.Notification_PremiumGift_View
@ -314,6 +315,35 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
buttonTitle = item.presentationData.strings.Notification_PremiumPrize_View
hasServiceMessage = false
}
case let .starGift(gift, convertStars, giftText, entities, nameHidden, savedToProfile, converted)://(amount, giftId, nameHidden, limitNumber, limitTotal, giftText, _):
let _ = nameHidden
let authorName = item.message.author.flatMap { EnginePeer($0) }?.compactDisplayTitle ?? ""
title = nameHidden ? "Anonymous Gift" : "Gift from \(authorName)"
if let giftText, !giftText.isEmpty {
text = giftText
let _ = entities
} else {
if incoming {
if converted {
text = "You converted this gift to \(convertStars) Stars."
} else if savedToProfile {
text = "You are displaying this gift on your page. You can also convert it to \(convertStars) Stars."
} else {
text = "Display this gift on your page or convert it to \(convertStars) Stars."
}
} else {
var peerName = ""
if let peer = item.message.peers[item.message.id.peerId] {
peerName = EnginePeer(peer).compactDisplayTitle
}
if peerName.isEmpty {
text = "Display this gift on your page or convert it to \(convertStars) Stars."
} else {
text = "\(peerName) can keep this gift on their page or convert it to \(convertStars) Stars."
}
}
}
animationFile = gift.file
default:
break
}
@ -396,7 +426,12 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
if let strongSelf = self {
if strongSelf.item == nil {
strongSelf.animationNode.autoplay = true
strongSelf.animationNode.setup(source: AnimatedStickerNodeLocalFileSource(name: animationName), width: 384, height: 384, playbackMode: .still(.end), mode: .direct(cachePathPrefix: nil))
if let file = animationFile {
strongSelf.animationNode.setup(source: AnimatedStickerResourceSource(account: item.context.account, resource: file.resource), width: 384, height: 384, playbackMode: .once, mode: .direct(cachePathPrefix: nil))
} else if animationName.hasPrefix("Gift") {
strongSelf.animationNode.setup(source: AnimatedStickerNodeLocalFileSource(name: animationName), width: 384, height: 384, playbackMode: .still(.end), mode: .direct(cachePathPrefix: nil))
}
}
strongSelf.item = item
@ -412,8 +447,13 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
strongSelf.mediaBackgroundNode.update(size: mediaBackgroundFrame.size, transition: .immediate)
strongSelf.buttonNode.backgroundColor = item.presentationData.theme.theme.overallDarkAppearance ? UIColor(rgb: 0xffffff, alpha: 0.12) : UIColor(rgb: 0x000000, alpha: 0.12)
let iconSize = CGSize(width: 160.0, height: 160.0)
strongSelf.animationNode.frame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - iconSize.width) / 2.0), y: mediaBackgroundFrame.minY - 16.0), size: iconSize)
var iconSize = CGSize(width: 160.0, height: 160.0)
var iconOffset: CGFloat = 0.0
if let _ = animationFile {
iconSize = CGSize(width: 120.0, height: 120.0)
iconOffset = 32.0
}
strongSelf.animationNode.frame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - iconSize.width) / 2.0), y: mediaBackgroundFrame.minY - 16.0 + iconOffset), size: iconSize)
strongSelf.animationNode.updateLayout(size: iconSize)
let _ = labelApply()

View File

@ -755,7 +755,7 @@ public final class ChatMessageAvatarHeaderNodeImpl: ListViewItemHeaderNode, Chat
self.controllerInteraction?.displayMessageTooltip(id, self.presentationData.strings.Conversation_ForwardAuthorHiddenTooltip, false, self, self.avatarNode.frame)
} else if let peer = self.peer {
if let adMessageId = self.adMessageId {
self.controllerInteraction?.activateAdAction(adMessageId, nil)
self.controllerInteraction?.activateAdAction(adMessageId, nil, false, false)
} else {
if let channel = peer as? TelegramChannel, case .broadcast = channel.info {
self.controllerInteraction?.openPeer(EnginePeer(peer), .chat(textInputState: nil, subject: nil, peekData: nil), self.messageReference, .default)

View File

@ -118,7 +118,7 @@ public final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContent
self.contentNode.activateAction = { [weak self] in
if let strongSelf = self, let item = strongSelf.item {
if let _ = item.message.adAttribute {
item.controllerInteraction.activateAdAction(item.message.id, strongSelf.contentNode.makeProgress())
item.controllerInteraction.activateAdAction(item.message.id, strongSelf.contentNode.makeProgress(), false, false)
} else {
var webPageContent: TelegramMediaWebpageLoadedContent?
for media in item.message.media {

View File

@ -616,7 +616,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode {
}, openLargeEmojiInfo: { _, _, _ in
}, openJoinLink: { _ in
}, openWebView: { _, _, _, _ in
}, activateAdAction: { _, _ in
}, activateAdAction: { _, _, _, _ in
}, openRequestedPeerSelection: { _, _, _, _ in
}, saveMediaToFiles: { _ in
}, openNoAdsDemo: {
@ -1208,8 +1208,8 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode {
if let invoice {
let inputData = Promise<BotCheckoutController.InputData?>()
inputData.set(BotCheckoutController.InputData.fetch(context: strongSelf.context, source: .slug(slug))
|> map(Optional.init)
|> `catch` { _ -> Signal<BotCheckoutController.InputData?, NoError> in
|> map(Optional.init)
|> `catch` { _ -> Signal<BotCheckoutController.InputData?, NoError> in
return .single(nil)
})
strongSelf.controllerInteraction.presentController(BotCheckoutController(context: strongSelf.context, invoice: invoice, source: .slug(slug), inputData: inputData, completed: { currencyValue, receiptMessageId in

View File

@ -473,7 +473,7 @@ public final class ChatSendGroupMediaMessageContextPreview: UIView, ChatSendMess
}, openLargeEmojiInfo: { _, _, _ in
}, openJoinLink: { _ in
}, openWebView: { _, _, _, _ in
}, activateAdAction: { _, _ in
}, activateAdAction: { _, _, _, _ in
}, openRequestedPeerSelection: { _, _, _, _ in
}, saveMediaToFiles: { _ in
}, openNoAdsDemo: {

View File

@ -252,7 +252,7 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol
public let openLargeEmojiInfo: (String, String?, TelegramMediaFile) -> Void
public let openJoinLink: (String) -> Void
public let openWebView: (String, String, Bool, ChatOpenWebViewSource) -> Void
public let activateAdAction: (EngineMessage.Id, Promise<Bool>?) -> Void
public let activateAdAction: (EngineMessage.Id, Promise<Bool>?, Bool, Bool) -> Void
public let openRequestedPeerSelection: (EngineMessage.Id, ReplyMarkupButtonRequestPeerType, Int32, Int32) -> Void
public let saveMediaToFiles: (EngineMessage.Id) -> Void
public let openNoAdsDemo: () -> Void
@ -382,7 +382,7 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol
openLargeEmojiInfo: @escaping (String, String?, TelegramMediaFile) -> Void,
openJoinLink: @escaping (String) -> Void,
openWebView: @escaping (String, String, Bool, ChatOpenWebViewSource) -> Void,
activateAdAction: @escaping (EngineMessage.Id, Promise<Bool>?) -> Void,
activateAdAction: @escaping (EngineMessage.Id, Promise<Bool>?, Bool, Bool) -> Void,
openRequestedPeerSelection: @escaping (EngineMessage.Id, ReplyMarkupButtonRequestPeerType, Int32, Int32) -> Void,
saveMediaToFiles: @escaping (EngineMessage.Id) -> Void,
openNoAdsDemo: @escaping () -> Void,

View File

@ -144,6 +144,34 @@ public func animationCacheFetchFile(postbox: Postbox, userLocation: MediaResourc
}
}
public func animationCacheLoadLocalFile(name: String, type: AnimationCacheAnimationType, keyframeOnly: Bool, customColor: UIColor?) -> (AnimationCacheFetchOptions) -> Disposable {
return { options in
let source = AnimatedStickerNodeLocalFileSource(name: name)
let dataDisposable = source.directDataPath(attemptSynchronously: false).start(next: { result in
guard let result = result else {
return
}
switch type {
case .video:
cacheVideoAnimation(path: result, width: Int(options.size.width), height: Int(options.size.height), writer: options.writer, firstFrameOnly: options.firstFrameOnly, customColor: customColor)
case .lottie:
guard let data = try? Data(contentsOf: URL(fileURLWithPath: result)) else {
options.writer.finish()
return
}
cacheLottieAnimation(data: data, width: Int(options.size.width), height: Int(options.size.height), keyframeOnly: keyframeOnly, writer: options.writer, firstFrameOnly: options.firstFrameOnly, customColor: customColor)
case .still:
cacheStillSticker(path: result, width: Int(options.size.width), height: Int(options.size.height), writer: options.writer, customColor: customColor)
}
})
return ActionDisposable {
dataDisposable.dispose()
}
}
}
private func generatePeerNameColorImage(nameColor: PeerNameColors.Colors, isDark: Bool, bounds: CGSize = CGSize(width: 40.0, height: 40.0), size: CGSize = CGSize(width: 40.0, height: 40.0)) -> UIImage? {
return generateImage(bounds, rotatedContext: { contextSize, context in
let bounds = CGRect(origin: CGPoint(), size: contextSize)
@ -310,6 +338,8 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget {
private var didProcessTintColor: Bool = false
public private(set) var file: TelegramMediaFile?
private var localAnimationName: String?
private var infoDisposable: Disposable?
private var disposable: Disposable?
private var fetchDisposable: Disposable?
@ -440,6 +470,8 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget {
}
case .ton:
self.updateTon()
case let .animation(name):
self.updateLocalAnimation(name: name, attemptSynchronousLoad: attemptSynchronousLoad)
}
} else if let file = file {
self.updateFile(file: file, attemptSynchronousLoad: attemptSynchronousLoad)
@ -629,6 +661,42 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget {
self.contents = tonImage?.cgImage
}
private func updateLocalAnimation(name: String, attemptSynchronousLoad: Bool) {
guard let arguments = self.arguments else {
return
}
self.localAnimationName = name
if attemptSynchronousLoad {
if !arguments.renderer.loadFirstFrameSynchronously(target: self, cache: arguments.cache, itemId: name, size: arguments.pixelSize) {
}
self.loadAnimation()
} else {
self.loadDisposable = arguments.renderer.loadFirstFrame(target: self, cache: arguments.cache, itemId: name, size: arguments.pixelSize, fetch: animationCacheLoadLocalFile(name: name, type: .lottie, keyframeOnly: true, customColor: nil), completion: { [weak self] result, isFinal in
guard let strongSelf = self else {
return
}
strongSelf.loadAnimation()
})
}
}
private func loadLocalAnimation() {
guard let arguments = self.arguments else {
return
}
guard let name = self.localAnimationName else {
return
}
let keyframeOnly = arguments.pixelSize.width >= 120.0
self.disposable = arguments.renderer.add(target: self, cache: arguments.cache, itemId: name, unique: arguments.unique, size: arguments.pixelSize, fetch: animationCacheLoadLocalFile(name: name, type: .lottie, keyframeOnly: keyframeOnly, customColor: nil))
}
private func updateFile(file: TelegramMediaFile, attemptSynchronousLoad: Bool) {
guard let arguments = self.arguments else {
return

View File

@ -0,0 +1,36 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "GiftItemComponent",
module_name = "GiftItemComponent",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/AsyncDisplayKit",
"//submodules/Display",
"//submodules/Postbox",
"//submodules/TelegramCore",
"//submodules/SSignalKit/SwiftSignalKit",
"//submodules/ComponentFlow",
"//submodules/Components/ViewControllerComponent",
"//submodules/Components/ComponentDisplayAdapters",
"//submodules/Components/MultilineTextComponent",
"//submodules/Components/MultilineTextWithEntitiesComponent",
"//submodules/TelegramPresentationData",
"//submodules/AccountContext",
"//submodules/AppBundle",
"//submodules/TelegramStringFormatting",
"//submodules/PresentationDataUtils",
"//submodules/TextFormat",
"//submodules/AvatarNode",
"//submodules/TelegramUI/Components/EmojiTextAttachmentView",
"//submodules/TelegramUI/Components/Stars/ItemShimmeringLoadingComponent",
],
visibility = [
"//visibility:public",
],
)

View File

@ -0,0 +1,524 @@
import Foundation
import UIKit
import Display
import ComponentFlow
import TelegramCore
import TelegramPresentationData
import AppBundle
import AccountContext
import MultilineTextComponent
import MultilineTextWithEntitiesComponent
import EmojiTextAttachmentView
import TextFormat
import ItemShimmeringLoadingComponent
import AvatarNode
public final class GiftItemComponent: Component {
public enum Subject: Equatable {
case premium(Int32)
case starGift(Int64, TelegramMediaFile)
}
public struct Ribbon: Equatable {
public let text: String
public let color: UIColor
public init(text: String, color: UIColor) {
self.text = text
self.color = color
}
}
let context: AccountContext
let theme: PresentationTheme
let peer: EnginePeer?
let subject: Subject
let title: String?
let subtitle: String?
let price: String
let ribbon: Ribbon?
let isLoading: Bool
public init(
context: AccountContext,
theme: PresentationTheme,
peer: EnginePeer?,
subject: Subject,
title: String? = nil,
subtitle: String? = nil,
price: String,
ribbon: Ribbon? = nil,
isLoading: Bool = false
) {
self.context = context
self.theme = theme
self.peer = peer
self.subject = subject
self.title = title
self.subtitle = subtitle
self.price = price
self.ribbon = ribbon
self.isLoading = isLoading
}
public static func ==(lhs: GiftItemComponent, rhs: GiftItemComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.theme !== rhs.theme {
return false
}
if lhs.peer != rhs.peer {
return false
}
if lhs.subject != rhs.subject {
return false
}
if lhs.title != rhs.title {
return false
}
if lhs.subtitle != rhs.subtitle {
return false
}
if lhs.price != rhs.price {
return false
}
if lhs.ribbon != rhs.ribbon {
return false
}
if lhs.isLoading != rhs.isLoading {
return false
}
return true
}
public final class View: UIView {
private var component: GiftItemComponent?
private weak var componentState: EmptyComponentState?
private let backgroundLayer = SimpleLayer()
private var loadingBackground: ComponentView<Empty>?
private var avatarNode: AvatarNode?
private let title = ComponentView<Empty>()
private let subtitle = ComponentView<Empty>()
private let button = ComponentView<Empty>()
private let ribbon = UIImageView()
private let ribbonText = ComponentView<Empty>()
private var animationLayer: InlineStickerItemLayer?
override init(frame: CGRect) {
super.init(frame: frame)
self.layer.addSublayer(self.backgroundLayer)
self.backgroundLayer.cornerRadius = 10.0
if #available(iOS 13.0, *) {
self.backgroundLayer.cornerCurve = .circular
}
self.backgroundLayer.masksToBounds = true
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(component: GiftItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
self.component = component
self.componentState = state
let size = CGSize(width: availableSize.width, height: component.title != nil ? 178.0 : 154.0)
if component.isLoading {
let loadingBackground: ComponentView<Empty>
if let current = self.loadingBackground {
loadingBackground = current
} else {
loadingBackground = ComponentView<Empty>()
self.loadingBackground = loadingBackground
}
let _ = loadingBackground.update(
transition: transition,
component: AnyComponent(
ItemShimmeringLoadingComponent(color: component.theme.list.itemAccentColor, cornerRadius: 10.0)
),
environment: {},
containerSize: size
)
if let loadingBackgroundView = loadingBackground.view {
if loadingBackgroundView.layer.superlayer == nil {
self.layer.insertSublayer(loadingBackgroundView.layer, above: self.backgroundLayer)
}
loadingBackgroundView.frame = CGRect(origin: .zero, size: size)
}
} else if let loadingBackground = self.loadingBackground {
loadingBackground.view?.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in
loadingBackground.view?.layer.removeFromSuperlayer()
})
self.loadingBackground = nil
}
let emoji: ChatTextInputTextCustomEmojiAttribute?
var file: TelegramMediaFile?
var animationOffset: CGFloat = 0.0
switch component.subject {
case let .premium(months):
emoji = ChatTextInputTextCustomEmojiAttribute(
interactivelySelectedFromPackId: nil,
fileId: 0,
file: nil,
custom: .animation(name: "Gift\(months)")
)
case let .starGift(_, fileValue):
file = fileValue
emoji = ChatTextInputTextCustomEmojiAttribute(
interactivelySelectedFromPackId: nil,
fileId: fileValue.fileId.id,
file: fileValue
)
animationOffset = 16.0
}
let iconSize = CGSize(width: 88.0, height: 88.0)
if self.animationLayer == nil, let emoji {
let animationLayer = InlineStickerItemLayer(
context: .account(component.context),
userLocation: .other,
attemptSynchronousLoad: false,
emoji: emoji,
file: file,
cache: component.context.animationCache,
renderer: component.context.animationRenderer,
unique: false,
placeholderColor: component.theme.list.mediaPlaceholderColor,
pointSize: CGSize(width: iconSize.width * 2.0, height: iconSize.height * 2.0),
loopCount: 1
)
animationLayer.isVisibleForAnimations = true
self.animationLayer = animationLayer
self.layer.addSublayer(animationLayer)
}
if let animationLayer = self.animationLayer {
transition.setFrame(layer: animationLayer, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - iconSize.width) / 2.0), y: animationOffset), size: iconSize))
}
if let title = component.title {
let titleSize = self.title.update(
transition: transition,
component: AnyComponent(
MultilineTextComponent(
text: .plain(NSAttributedString(string: title, font: Font.semibold(15.0), textColor: component.theme.list.itemPrimaryTextColor)),
horizontalAlignment: .center
)
),
environment: {},
containerSize: availableSize
)
let titleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - titleSize.width) / 2.0), y: 94.0), size: titleSize)
if let titleView = self.title.view {
if titleView.superview == nil {
self.addSubview(titleView)
}
transition.setFrame(view: titleView, frame: titleFrame)
}
}
if let subtitle = component.subtitle {
let subtitleSize = self.subtitle.update(
transition: transition,
component: AnyComponent(
MultilineTextComponent(
text: .plain(NSAttributedString(string: subtitle, font: Font.regular(13.0), textColor: component.theme.list.itemPrimaryTextColor)),
horizontalAlignment: .center
)
),
environment: {},
containerSize: availableSize
)
let subtitleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - subtitleSize.width) / 2.0), y: 112.0), size: subtitleSize)
if let subtitleView = self.subtitle.view {
if subtitleView.superview == nil {
self.addSubview(subtitleView)
}
transition.setFrame(view: subtitleView, frame: subtitleFrame)
}
}
let buttonSize = self.button.update(
transition: transition,
component: AnyComponent(
ButtonContentComponent(
context: component.context,
text: component.price,
color: component.price.containsEmoji ? UIColor(rgb: 0xd3720a) : component.theme.list.itemAccentColor,
isStars: component.price.containsEmoji)
),
environment: {},
containerSize: availableSize
)
let buttonFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - buttonSize.width) / 2.0), y: size.height - buttonSize.height - 10.0), size: buttonSize)
if let buttonView = self.button.view {
if buttonView.superview == nil {
self.addSubview(buttonView)
}
transition.setFrame(view: buttonView, frame: buttonFrame)
}
if let ribbon = component.ribbon {
let ribbonTextSize = self.ribbonText.update(
transition: transition,
component: AnyComponent(
MultilineTextComponent(
text: .plain(NSAttributedString(string: ribbon.text, font: Font.semibold(11.0), textColor: .white)),
horizontalAlignment: .center
)
),
environment: {},
containerSize: availableSize
)
if let ribbonTextView = self.ribbonText.view {
if ribbonTextView.superview == nil {
self.addSubview(self.ribbon)
self.addSubview(ribbonTextView)
}
ribbonTextView.bounds = CGRect(origin: .zero, size: ribbonTextSize)
if self.ribbon.image == nil {
self.ribbon.image = generateGradientTintedImage(image: UIImage(bundleImageName: "Premium/GiftRibbon"), colors: [ribbon.color.withMultipliedBrightnessBy(1.1), ribbon.color.withMultipliedBrightnessBy(0.9)], direction: .diagonal)
}
if let ribbonImage = self.ribbon.image {
self.ribbon.frame = CGRect(origin: CGPoint(x: size.width - ribbonImage.size.width + 2.0, y: -2.0), size: ribbonImage.size)
}
ribbonTextView.transform = CGAffineTransform(rotationAngle: .pi / 4.0)
ribbonTextView.center = CGPoint(x: size.width - 20.0, y: 20.0)
}
} else {
if self.ribbonText.view?.superview != nil {
self.ribbon.removeFromSuperview()
self.ribbonText.view?.removeFromSuperview()
}
}
if let peer = component.peer {
let avatarNode: AvatarNode
if let current = self.avatarNode {
avatarNode = current
} else {
avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 8.0))
self.addSubview(avatarNode.view)
self.avatarNode = avatarNode
}
avatarNode.setPeer(context: component.context, theme: component.theme, peer: peer, displayDimensions: CGSize(width: 20.0, height: 20.0))
avatarNode.frame = CGRect(origin: CGPoint(x: 2.0, y: 2.0), size: CGSize(width: 20.0, height: 20.0))
}
self.backgroundLayer.backgroundColor = component.theme.list.itemBlocksBackgroundColor.cgColor
transition.setFrame(layer: self.backgroundLayer, frame: CGRect(origin: .zero, size: size))
return size
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
private final class ButtonContentComponent: Component {
let context: AccountContext
let text: String
let color: UIColor
let isStars: Bool
public init(
context: AccountContext,
text: String,
color: UIColor,
isStars: Bool = false
) {
self.context = context
self.text = text
self.color = color
self.isStars = isStars
}
public static func ==(lhs: ButtonContentComponent, rhs: ButtonContentComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.text != rhs.text {
return false
}
if lhs.color != rhs.color {
return false
}
if lhs.isStars != rhs.isStars {
return false
}
return true
}
public final class View: UIView {
private var component: ButtonContentComponent?
private weak var componentState: EmptyComponentState?
private let backgroundLayer = SimpleLayer()
private let title = ComponentView<Empty>()
private var starsLayer: StarsButtonEffectLayer?
override init(frame: CGRect) {
super.init(frame: frame)
self.layer.addSublayer(self.backgroundLayer)
self.backgroundLayer.masksToBounds = true
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(component: ButtonContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
self.component = component
self.componentState = state
let attributedText = NSMutableAttributedString(string: component.text, font: Font.semibold(11.0), textColor: component.color)
let range = (attributedText.string as NSString).range(of: "⭐️")
if range.location != NSNotFound {
attributedText.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .stars(tinted: false)), range: range)
attributedText.addAttribute(.font, value: Font.semibold(15.0), range: range)
attributedText.addAttribute(.baselineOffset, value: 2.0, range: NSRange(location: range.upperBound, length: attributedText.length - range.upperBound))
}
let titleSize = self.title.update(
transition: transition,
component: AnyComponent(
MultilineTextWithEntitiesComponent(
context: component.context,
animationCache: component.context.animationCache,
animationRenderer: component.context.animationRenderer,
placeholderColor: .white,
text: .plain(attributedText)
)
),
environment: {},
containerSize: availableSize
)
let padding: CGFloat = 9.0
let size = CGSize(width: titleSize.width + padding * 2.0, height: 30.0)
if component.isStars {
let starsLayer: StarsButtonEffectLayer
if let current = self.starsLayer {
starsLayer = current
} else {
starsLayer = StarsButtonEffectLayer()
self.layer.addSublayer(starsLayer)
self.starsLayer = starsLayer
}
starsLayer.frame = CGRect(origin: .zero, size: size)
starsLayer.update(size: size)
} else {
self.starsLayer?.removeFromSuperlayer()
self.starsLayer = nil
}
let titleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - titleSize.width) / 2.0), y: floorToScreenPixels((size.height - titleSize.height) / 2.0)), size: titleSize)
if let titleView = self.title.view {
if titleView.superview == nil {
self.addSubview(titleView)
}
transition.setFrame(view: titleView, frame: titleFrame)
}
let backgroundColor: UIColor
if component.color.rgb == 0xd3720a {
backgroundColor = UIColor(rgb: 0xffc83d, alpha: 0.2)
} else {
backgroundColor = component.color.withAlphaComponent(0.1)
}
self.backgroundLayer.backgroundColor = backgroundColor.cgColor
transition.setFrame(layer: self.backgroundLayer, frame: CGRect(origin: .zero, size: size))
self.backgroundLayer.cornerRadius = size.height / 2.0
return size
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
private final class StarsButtonEffectLayer: SimpleLayer {
let emitterLayer = CAEmitterLayer()
override init() {
super.init()
self.addSublayer(self.emitterLayer)
}
override init(layer: Any) {
super.init(layer: layer)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setup() {
let color = UIColor(rgb: 0xffbe27)
let emitter = CAEmitterCell()
emitter.name = "emitter"
emitter.contents = UIImage(bundleImageName: "Premium/Stars/Particle")?.cgImage
emitter.birthRate = 25.0
emitter.lifetime = 2.0
emitter.velocity = 12.0
emitter.velocityRange = 3
emitter.scale = 0.1
emitter.scaleRange = 0.08
emitter.alphaRange = 0.1
emitter.emissionRange = .pi * 2.0
emitter.setValue(3.0, forKey: "mass")
emitter.setValue(2.0, forKey: "massRange")
let staticColors: [Any] = [
color.withAlphaComponent(0.0).cgColor,
color.cgColor,
color.cgColor,
color.withAlphaComponent(0.0).cgColor
]
let staticColorBehavior = CAEmitterCell.createEmitterBehavior(type: "colorOverLife")
staticColorBehavior.setValue(staticColors, forKey: "colors")
emitter.setValue([staticColorBehavior], forKey: "emitterBehaviors")
self.emitterLayer.emitterCells = [emitter]
}
func update(size: CGSize) {
if self.emitterLayer.emitterCells == nil {
self.setup()
}
self.emitterLayer.emitterShape = .circle
self.emitterLayer.emitterSize = CGSize(width: size.width * 0.7, height: size.height * 0.7)
self.emitterLayer.emitterMode = .surface
self.emitterLayer.frame = CGRect(origin: .zero, size: size)
self.emitterLayer.emitterPosition = CGPoint(x: size.width / 2.0, y: size.height / 2.0)
}
}

View File

@ -0,0 +1,49 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "GiftOptionsScreen",
module_name = "GiftOptionsScreen",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/AsyncDisplayKit",
"//submodules/Display",
"//submodules/Postbox",
"//submodules/TelegramCore",
"//submodules/SSignalKit/SwiftSignalKit",
"//submodules/ComponentFlow",
"//submodules/Components/ViewControllerComponent",
"//submodules/Components/ComponentDisplayAdapters",
"//submodules/Components/MultilineTextComponent",
"//submodules/Components/BalancedTextComponent",
"//submodules/TelegramPresentationData",
"//submodules/AccountContext",
"//submodules/AppBundle",
"//submodules/ItemListUI",
"//submodules/TelegramStringFormatting",
"//submodules/PresentationDataUtils",
"//submodules/Components/SheetComponent",
"//submodules/UndoUI",
"//submodules/TextFormat",
"//submodules/TelegramUI/Components/ListSectionComponent",
"//submodules/TelegramUI/Components/ListActionItemComponent",
"//submodules/TelegramUI/Components/ScrollComponent",
"//submodules/TelegramUI/Components/PlainButtonComponent",
"//submodules/TelegramUI/Components/Premium/PremiumStarComponent",
"//submodules/Components/BlurredBackgroundComponent",
"//submodules/TelegramUI/Components/ButtonComponent",
"//submodules/Components/BundleIconComponent",
"//submodules/TelegramUI/Components/Gifts/GiftItemComponent",
"//submodules/ConfettiEffect",
"//submodules/InAppPurchaseManager",
"//submodules/TelegramUI/Components/TabSelectorComponent",
"//submodules/TelegramUI/Components/Gifts/GiftSetupScreen",
],
visibility = [
"//visibility:public",
],
)

View File

@ -0,0 +1,909 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import Postbox
import TelegramCore
import TelegramPresentationData
import TelegramUIPreferences
import PresentationDataUtils
import AccountContext
import ComponentFlow
import ViewControllerComponent
import MultilineTextComponent
import BalancedTextComponent
import BundleIconComponent
import Markdown
import TelegramStringFormatting
import PlainButtonComponent
import BlurredBackgroundComponent
import PremiumStarComponent
import ConfettiEffect
import TextFormat
import GiftItemComponent
import InAppPurchaseManager
import TabSelectorComponent
import GiftSetupScreen
final class GiftOptionsScreenComponent: Component {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let context: AccountContext
let peerId: EnginePeer.Id
let premiumOptions: [CachedPremiumGiftOption]
init(
context: AccountContext,
peerId: EnginePeer.Id,
premiumOptions: [CachedPremiumGiftOption]
) {
self.context = context
self.peerId = peerId
self.premiumOptions = premiumOptions
}
static func ==(lhs: GiftOptionsScreenComponent, rhs: GiftOptionsScreenComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.peerId != rhs.peerId {
return false
}
if lhs.premiumOptions != rhs.premiumOptions {
return false
}
return true
}
private final class ScrollView: UIScrollView {
override func touchesShouldCancel(in view: UIView) -> Bool {
return true
}
}
public enum StarsFilter: Int {
case all
case limited
case stars10
case stars25
case stars50
case stars100
}
final class View: UIView, UIScrollViewDelegate {
private let topOverscrollLayer = SimpleLayer()
private let scrollView: ScrollView
private let topPanel = ComponentView<Empty>()
private let topSeparator = ComponentView<Empty>()
private let cancelButton = ComponentView<Empty>()
private let header = ComponentView<Empty>()
private let premiumTitle = ComponentView<Empty>()
private let premiumDescription = ComponentView<Empty>()
private var premiumItems: [AnyHashable: ComponentView<Empty>] = [:]
private var selectedPremiumGift: String?
private let starsTitle = ComponentView<Empty>()
private let starsDescription = ComponentView<Empty>()
private var starsItems: [AnyHashable: ComponentView<Empty>] = [:]
private let tabSelector = ComponentView<Empty>()
private var starsFilter: StarsFilter = .all
private var isUpdating: Bool = false
private var component: GiftOptionsScreenComponent?
private(set) weak var state: State?
private var environment: EnvironmentType?
private var starsItemsOrigin: CGFloat = 0.0
private var chevronImage: (UIImage, PresentationTheme)?
override init(frame: CGRect) {
self.scrollView = ScrollView()
self.scrollView.showsVerticalScrollIndicator = true
self.scrollView.showsHorizontalScrollIndicator = false
self.scrollView.scrollsToTop = false
self.scrollView.delaysContentTouches = false
self.scrollView.canCancelContentTouches = true
self.scrollView.contentInsetAdjustmentBehavior = .never
if #available(iOS 13.0, *) {
self.scrollView.automaticallyAdjustsScrollIndicatorInsets = false
}
self.scrollView.alwaysBounceVertical = true
super.init(frame: frame)
self.scrollView.delegate = self
self.addSubview(self.scrollView)
self.scrollView.layer.addSublayer(self.topOverscrollLayer)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
}
func scrollToTop() {
self.scrollView.setContentOffset(CGPoint(), animated: true)
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
self.updateScrolling(transition: .immediate)
}
private func updateScrolling(transition: ComponentTransition) {
guard let environment = self.environment, let component = self.component else {
return
}
let availableWidth = self.scrollView.bounds.width
let contentOffset = self.scrollView.contentOffset.y
let topPanelAlpha = min(20.0, max(0.0, contentOffset - 95.0)) / 20.0
if let topPanelView = self.topPanel.view, let topSeparator = self.topSeparator.view {
transition.setAlpha(view: topPanelView, alpha: topPanelAlpha)
transition.setAlpha(view: topSeparator, alpha: topPanelAlpha)
}
let topInset: CGFloat = environment.navigationHeight - 56.0
let premiumTitleInitialPosition = (topInset + 160.0)
let premiumTitleOffsetDelta = premiumTitleInitialPosition - (environment.statusBarHeight + (environment.navigationHeight - environment.statusBarHeight) / 2.0)
let premiumTitleOffset = contentOffset + max(0.0, min(1.0, contentOffset / premiumTitleOffsetDelta)) * 10.0
let premiumTitleFraction = max(0.0, min(1.0, premiumTitleOffset / premiumTitleOffsetDelta))
let premiumTitleScale = 1.0 - premiumTitleFraction * 0.36
var premiumTitleAdditionalOffset: CGFloat = 0.0
let starsTitleOffsetDelta = (topInset + 100.0) - (environment.statusBarHeight + (environment.navigationHeight - environment.statusBarHeight) / 2.0)
let starsTitleOffset: CGFloat
let starsTitleFraction: CGFloat
if contentOffset > 350 {
starsTitleOffset = contentOffset + max(0.0, min(1.0, (contentOffset - 350.0) / starsTitleOffsetDelta)) * 10.0
starsTitleFraction = max(0.0, min(1.0, (starsTitleOffset - 350.0) / starsTitleOffsetDelta))
if contentOffset > 380.0 {
premiumTitleAdditionalOffset = contentOffset - 380.0
}
} else {
starsTitleOffset = contentOffset
starsTitleFraction = 0.0
}
let starsTitleScale = 1.0 - starsTitleFraction * 0.36
if let starsTitleView = self.starsTitle.view {
transition.setPosition(view: starsTitleView, position: CGPoint(x: availableWidth / 2.0, y: max(topInset + 455.0 - starsTitleOffset, environment.statusBarHeight + (environment.navigationHeight - environment.statusBarHeight) / 2.0)))
transition.setScale(view: starsTitleView, scale: starsTitleScale)
}
if let premiumTitleView = self.premiumTitle.view {
transition.setPosition(view: premiumTitleView, position: CGPoint(x: availableWidth / 2.0, y: max(premiumTitleInitialPosition - premiumTitleOffset, environment.statusBarHeight + (environment.navigationHeight - environment.statusBarHeight) / 2.0) - premiumTitleAdditionalOffset))
transition.setScale(view: premiumTitleView, scale: premiumTitleScale)
}
if let headerView = self.header.view {
transition.setPosition(view: headerView, position: CGPoint(x: availableWidth / 2.0, y: topInset + headerView.bounds.height / 2.0 - 30.0 - premiumTitleOffset * premiumTitleScale))
transition.setScale(view: headerView, scale: premiumTitleScale)
}
let visibleBounds = self.scrollView.bounds.insetBy(dx: 0.0, dy: -10.0)
if let starGifts = self.state?.starGifts {
let sideInset: CGFloat = 16.0 + environment.safeInsets.left
let optionSpacing: CGFloat = 10.0
let optionWidth = (availableWidth - sideInset * 2.0 - optionSpacing * 2.0) / 3.0
let starsOptionSize = CGSize(width: optionWidth, height: 154.0)
var validIds: [AnyHashable] = []
var itemFrame = CGRect(origin: CGPoint(x: sideInset, y: self.starsItemsOrigin), size: starsOptionSize)
let controller = environment.controller
for gift in starGifts {
var isVisible = false
if visibleBounds.intersects(itemFrame) {
isVisible = true
}
if isVisible {
if self.starsFilter != .all {
switch self.starsFilter {
case .all:
break
case .limited:
if gift.availability == nil {
continue
}
case .stars10:
if gift.price != 10 {
continue
}
case .stars25:
if gift.price != 25 {
continue
}
case .stars50:
if gift.price != 50 {
continue
}
case .stars100:
if gift.price != 100 {
continue
}
}
}
let itemId = AnyHashable(gift.id)
validIds.append(itemId)
var itemTransition = transition
let visibleItem: ComponentView<Empty>
if let current = self.starsItems[itemId] {
visibleItem = current
} else {
visibleItem = ComponentView()
if !transition.animation.isImmediate {
itemTransition = .immediate
}
self.starsItems[itemId] = visibleItem
}
let _ = visibleItem.update(
transition: itemTransition,
component: AnyComponent(
PlainButtonComponent(
content: AnyComponent(
GiftItemComponent(
context: component.context,
theme: environment.theme,
peer: nil,
subject: .starGift(gift.id, gift.file),
price: "⭐️ \(gift.price)",
ribbon: gift.availability != nil ?
GiftItemComponent.Ribbon(
text: "Limited",
color: UIColor(rgb: 0x58c1fe)
)
: nil
)
),
effectAlignment: .center,
action: { [weak self] in
if let self, let component = self.component {
controller()?.push(GiftSetupScreen(context: component.context, peerId: component.peerId, gift: gift))
}
},
animateAlpha: false
)
),
environment: {},
containerSize: starsOptionSize
)
if let itemView = visibleItem.view {
if itemView.superview == nil {
self.scrollView.addSubview(itemView)
if !transition.animation.isImmediate {
transition.animateAlpha(view: itemView, from: 0.0, to: 1.0)
transition.animateScale(view: itemView, from: 0.01, to: 1.0)
}
}
itemTransition.setFrame(view: itemView, frame: itemFrame)
}
}
itemFrame.origin.x += itemFrame.width + optionSpacing
if itemFrame.maxX > availableWidth {
itemFrame.origin.x = sideInset
itemFrame.origin.y += starsOptionSize.height + optionSpacing
}
}
var removeIds: [AnyHashable] = []
for (id, item) in self.starsItems {
if !validIds.contains(id) {
removeIds.append(id)
if let itemView = item.view {
if !transition.animation.isImmediate {
itemView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.25, removeOnCompletion: false)
itemView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in
itemView.removeFromSuperview()
})
} else {
itemView.removeFromSuperview()
}
}
}
}
for id in removeIds {
self.starsItems.removeValue(forKey: id)
}
}
}
func update(component: GiftOptionsScreenComponent, availableSize: CGSize, state: State, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
self.isUpdating = true
defer {
self.isUpdating = false
}
let environment = environment[EnvironmentType.self].value
let controller = environment.controller
let themeUpdated = self.environment?.theme !== environment.theme
self.environment = environment
if self.component == nil {
}
self.component = component
self.state = state
if themeUpdated {
self.backgroundColor = environment.theme.list.blocksBackgroundColor
}
// let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
let theme = environment.theme
let strings = environment.strings
let textColor = theme.list.itemPrimaryTextColor
let accentColor = theme.list.itemAccentColor
let textFont = Font.regular(15.0)
let boldTextFont = Font.semibold(15.0)
let bottomContentInset: CGFloat = 24.0
let sideInset: CGFloat = 16.0 + environment.safeInsets.left
let sectionSpacing: CGFloat = 24.0
let _ = bottomContentInset
let _ = sectionSpacing
var contentHeight: CGFloat = 0.0
contentHeight += environment.navigationHeight - 56.0 + 188.0
let headerSize = self.header.update(
transition: .immediate,
component: AnyComponent(
GiftAvatarComponent(
context: component.context,
theme: theme,
peers: state.peer.flatMap { [$0] } ?? [],
isVisible: true,
hasIdleAnimations: true,
color: UIColor(rgb: 0xf9b004),
hasLargeParticles: true
)
),
environment: {},
containerSize: CGSize(width: min(414.0, availableSize.width), height: 220.0)
)
if let headerView = self.header.view {
if headerView.superview == nil {
self.addSubview(headerView)
}
transition.setBounds(view: headerView, bounds: CGRect(origin: .zero, size: headerSize))
}
let topPanelSize = self.topPanel.update(
transition: transition,
component: AnyComponent(BlurredBackgroundComponent(
color: theme.rootController.navigationBar.blurredBackgroundColor
)),
environment: {},
containerSize: CGSize(width: availableSize.width, height: environment.navigationHeight)
)
let topSeparatorSize = self.topSeparator.update(
transition: transition,
component: AnyComponent(Rectangle(
color: theme.rootController.navigationBar.separatorColor
)),
environment: {},
containerSize: CGSize(width: availableSize.width, height: UIScreenPixel)
)
let topPanelFrame = CGRect(origin: .zero, size: CGSize(width: availableSize.width, height: topPanelSize.height))
let topSeparatorFrame = CGRect(origin: CGPoint(x: 0.0, y: topPanelSize.height), size: CGSize(width: topSeparatorSize.width, height: topSeparatorSize.height))
if let topPanelView = self.topPanel.view, let topSeparatorView = self.topSeparator.view {
if topPanelView.superview == nil {
self.addSubview(topPanelView)
self.addSubview(topSeparatorView)
}
transition.setFrame(view: topPanelView, frame: topPanelFrame)
transition.setFrame(view: topSeparatorView, frame: topSeparatorFrame)
}
let cancelButtonSize = self.cancelButton.update(
transition: transition,
component: AnyComponent(
PlainButtonComponent(
content: AnyComponent(
MultilineTextComponent(
text: .plain(NSAttributedString(string: strings.Common_Cancel, font: Font.regular(17.0), textColor: theme.rootController.navigationBar.accentTextColor)),
horizontalAlignment: .center
)
),
effectAlignment: .center,
action: {
controller()?.dismiss()
},
animateScale: false
)
),
environment: {},
containerSize: CGSize(width: availableSize.width, height: 100.0)
)
let cancelButtonFrame = CGRect(origin: CGPoint(x: environment.safeInsets.left + 16.0, y: environment.statusBarHeight + (environment.navigationHeight - environment.statusBarHeight) / 2.0 - cancelButtonSize.height / 2.0), size: cancelButtonSize)
if let cancelButtonView = self.cancelButton.view {
if cancelButtonView.superview == nil {
self.addSubview(cancelButtonView)
}
transition.setFrame(view: cancelButtonView, frame: cancelButtonFrame)
}
let premiumTitleSize = self.premiumTitle.update(
transition: transition,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: "Gift Premium", font: Font.bold(28.0), textColor: theme.rootController.navigationBar.primaryTextColor)),
horizontalAlignment: .center
)),
environment: {},
containerSize: CGSize(width: availableSize.width, height: 100.0)
)
if let premiumTitleView = self.premiumTitle.view {
if premiumTitleView.superview == nil {
self.addSubview(premiumTitleView)
}
transition.setBounds(view: premiumTitleView, bounds: CGRect(origin: .zero, size: premiumTitleSize))
}
if self.chevronImage == nil || self.chevronImage?.1 !== theme {
self.chevronImage = (generateTintedImage(image: UIImage(bundleImageName: "Settings/TextArrowRight"), color: accentColor)!, theme)
}
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)
})
let peerName = state.peer?.compactDisplayTitle ?? ""
let premiumDescriptionString = parseMarkdownIntoAttributedString("Give **\(peerName)** access to exclusive features with Telegram Premium. [See Features >]()", attributes: markdownAttributes).mutableCopy() as! NSMutableAttributedString
if let range = premiumDescriptionString.string.range(of: ">"), let chevronImage = self.chevronImage?.0 {
premiumDescriptionString.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: premiumDescriptionString.string))
}
let premiumDescriptionSize = self.premiumDescription.update(
transition: transition,
component: AnyComponent(BalancedTextComponent(
text: .plain(premiumDescriptionString),
horizontalAlignment: .center,
maximumNumberOfLines: 0,
lineSpacing: 0.2,
highlightColor: accentColor.withAlphaComponent(0.2),
highlightAction: { attributes in
if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] {
return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)
} else {
return nil
}
},
tapAction: { _, _ in
}
)),
environment: {},
containerSize: CGSize(width: availableSize.width, height: 1000.0)
)
let premiumDescriptionFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - premiumDescriptionSize.width) / 2.0), y: contentHeight), size: premiumDescriptionSize)
if let premiumDescriptionView = self.premiumDescription.view {
if premiumDescriptionView.superview == nil {
self.scrollView.addSubview(premiumDescriptionView)
}
transition.setFrame(view: premiumDescriptionView, frame: premiumDescriptionFrame)
}
contentHeight += premiumDescriptionSize.height
contentHeight += 11.0
let optionSpacing: CGFloat = 10.0
let optionWidth = (availableSize.width - sideInset * 2.0 - optionSpacing * 2.0) / 3.0
if let premiumProducts = state.premiumProducts {
let premiumOptionSize = CGSize(width: optionWidth, height: 178.0)
var validIds: [AnyHashable] = []
var itemFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: premiumOptionSize)
for product in premiumProducts {
let itemId = AnyHashable(product.storeProduct.id)
validIds.append(itemId)
var itemTransition = transition
let visibleItem: ComponentView<Empty>
if let current = self.premiumItems[itemId] {
visibleItem = current
} else {
visibleItem = ComponentView()
if !transition.animation.isImmediate {
itemTransition = .immediate
}
self.premiumItems[itemId] = visibleItem
}
let title: String
switch product.months {
case 6:
title = "6 months"
case 12:
title = "1 year"
default:
title = "3 months"
}
let _ = visibleItem.update(
transition: itemTransition,
component: AnyComponent(
PlainButtonComponent(
content: AnyComponent(
GiftItemComponent(
context: component.context,
theme: theme,
peer: nil,
subject: .premium(product.months),
title: title,
subtitle: "Premium",
price: product.price,
ribbon: product.discount.flatMap {
GiftItemComponent.Ribbon(
text: "-\($0)%",
color: UIColor(rgb: 0xfa4846)
)
},
isLoading: self.selectedPremiumGift == product.id
)
),
effectAlignment: .center,
action: { [weak self] in
self?.selectedPremiumGift = product.id
self?.state?.updated()
Queue.mainQueue().after(4.0, {
self?.selectedPremiumGift = nil
self?.state?.updated()
})
},
animateAlpha: false
)
),
environment: {},
containerSize: premiumOptionSize
)
if let itemView = visibleItem.view {
if itemView.superview == nil {
self.scrollView.addSubview(itemView)
if !transition.animation.isImmediate {
transition.animateAlpha(view: itemView, from: 0.0, to: 1.0)
}
}
itemTransition.setFrame(view: itemView, frame: itemFrame)
}
itemFrame.origin.x += itemFrame.width + optionSpacing
if itemFrame.maxX > availableSize.width {
itemFrame.origin.x = sideInset
itemFrame.origin.y += premiumOptionSize.height + optionSpacing
}
}
var removeIds: [AnyHashable] = []
for (id, item) in self.premiumItems {
if !validIds.contains(id) {
removeIds.append(id)
if let itemView = item.view {
if !transition.animation.isImmediate {
itemView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in
itemView.removeFromSuperview()
})
} else {
itemView.removeFromSuperview()
}
}
}
}
for id in removeIds {
self.premiumItems.removeValue(forKey: id)
}
contentHeight += ceil(CGFloat(premiumProducts.count) / 3.0) * premiumOptionSize.height
contentHeight += 66.0
}
let starsTitleSize = self.starsTitle.update(
transition: transition,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: "Send a Gift", font: Font.bold(28.0), textColor: theme.rootController.navigationBar.primaryTextColor)),
horizontalAlignment: .center
)),
environment: {},
containerSize: CGSize(width: availableSize.width, height: 100.0)
)
if let starsTitleView = self.starsTitle.view {
if starsTitleView.superview == nil {
self.addSubview(starsTitleView)
}
transition.setBounds(view: starsTitleView, bounds: CGRect(origin: .zero, size: starsTitleSize))
}
let starsDescriptionString = parseMarkdownIntoAttributedString("Give **\(peerName)** gifts that can be kept on the profile or converted to Stars. [What are Stars >]()", attributes: markdownAttributes).mutableCopy() as! NSMutableAttributedString
if let range = starsDescriptionString.string.range(of: ">"), let chevronImage = self.chevronImage?.0 {
starsDescriptionString.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: starsDescriptionString.string))
}
let starsDescriptionSize = self.starsDescription.update(
transition: transition,
component: AnyComponent(BalancedTextComponent(
text: .plain(starsDescriptionString),
horizontalAlignment: .center,
maximumNumberOfLines: 0,
lineSpacing: 0.2,
highlightColor: accentColor.withAlphaComponent(0.2),
highlightAction: { attributes in
if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] {
return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)
} else {
return nil
}
},
tapAction: { _, _ in
}
)),
environment: {},
containerSize: CGSize(width: availableSize.width, height: 1000.0)
)
let starsDescriptionFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - starsDescriptionSize.width) / 2.0), y: contentHeight), size: starsDescriptionSize)
if let starsDescriptionView = self.starsDescription.view {
if starsDescriptionView.superview == nil {
self.scrollView.addSubview(starsDescriptionView)
}
transition.setFrame(view: starsDescriptionView, frame: starsDescriptionFrame)
}
contentHeight += starsDescriptionSize.height
contentHeight += 16.0
let tabSelectorSize = self.tabSelector.update(
transition: transition,
component: AnyComponent(TabSelectorComponent(
context: component.context,
colors: TabSelectorComponent.Colors(
foreground: theme.list.itemSecondaryTextColor,
selection: theme.list.itemSecondaryTextColor.withMultipliedAlpha(0.15),
simple: true
),
items: [
TabSelectorComponent.Item(
id: AnyHashable(StarsFilter.all.rawValue),
title: "All Gifts"
),
TabSelectorComponent.Item(
id: AnyHashable(StarsFilter.limited.rawValue),
title: "Limited"
),
TabSelectorComponent.Item(
id: AnyHashable(StarsFilter.stars10.rawValue),
title: "10"
),
TabSelectorComponent.Item(
id: AnyHashable(StarsFilter.stars25.rawValue),
title: "25"
),
TabSelectorComponent.Item(
id: AnyHashable(StarsFilter.stars50.rawValue),
title: "50"
),
TabSelectorComponent.Item(
id: AnyHashable(StarsFilter.stars100.rawValue),
title: "100"
)
],
selectedId: AnyHashable(self.starsFilter.rawValue),
setSelectedId: { [weak self] id in
guard let self, let idValue = id.base as? Int, let starsFilter = StarsFilter(rawValue: idValue) else {
return
}
if self.starsFilter != starsFilter {
self.starsFilter = starsFilter
self.state?.updated(transition: .easeInOut(duration: 0.25))
}
}
)),
environment: {},
containerSize: CGSize(width: availableSize.width - 10.0 * 2.0, height: 50.0)
)
if let tabSelectorView = self.tabSelector.view {
if tabSelectorView.superview == nil {
self.scrollView.addSubview(tabSelectorView)
}
transition.setFrame(view: tabSelectorView, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - tabSelectorSize.width) / 2.0), y: contentHeight), size: tabSelectorSize))
}
contentHeight += tabSelectorSize.height
contentHeight += 19.0
if let starGifts = state.starGifts {
self.starsItemsOrigin = contentHeight
let starsOptionSize = CGSize(width: optionWidth, height: 154.0)
contentHeight += ceil(CGFloat(starGifts.count) / 3.0) * starsOptionSize.height
contentHeight += 66.0
}
contentHeight += bottomContentInset
contentHeight += environment.safeInsets.bottom
let previousBounds = self.scrollView.bounds
let contentSize = CGSize(width: availableSize.width, height: contentHeight)
if self.scrollView.frame != CGRect(origin: CGPoint(), size: availableSize) {
self.scrollView.frame = CGRect(origin: CGPoint(), size: availableSize)
}
if self.scrollView.contentSize != contentSize {
self.scrollView.contentSize = contentSize
}
let scrollInsets = UIEdgeInsets(top: environment.navigationHeight, left: 0.0, bottom: 0.0, right: 0.0)
if self.scrollView.scrollIndicatorInsets != scrollInsets {
self.scrollView.scrollIndicatorInsets = scrollInsets
}
if !previousBounds.isEmpty, !transition.animation.isImmediate {
let bounds = self.scrollView.bounds
if bounds.maxY != previousBounds.maxY {
let offsetY = previousBounds.maxY - bounds.maxY
transition.animateBoundsOrigin(view: self.scrollView, from: CGPoint(x: 0.0, y: offsetY), to: CGPoint(), additive: true)
}
}
self.topOverscrollLayer.frame = CGRect(origin: CGPoint(x: 0.0, y: -3000.0), size: CGSize(width: availableSize.width, height: 3000.0))
self.updateScrolling(transition: transition)
return availableSize
}
}
func makeView() -> View {
return View()
}
final class State: ComponentState {
private let context: AccountContext
private var disposable: Disposable?
private var updateDisposable: Disposable?
fileprivate var peer: EnginePeer?
fileprivate var premiumProducts: [PremiumGiftProduct]?
fileprivate var starGifts: [StarGift]?
init(
context: AccountContext,
peerId: EnginePeer.Id,
premiumOptions: [CachedPremiumGiftOption]
) {
self.context = context
super.init()
let availableProducts: Signal<[InAppPurchaseManager.Product], NoError>
if let inAppPurchaseManager = context.inAppPurchaseManager {
availableProducts = inAppPurchaseManager.availableProducts
} else {
availableProducts = .single([])
}
self.disposable = combineLatest(
queue: Queue.mainQueue(),
context.engine.data.get(
TelegramEngine.EngineData.Item.Peer.Peer.init(id: peerId)
),
availableProducts,
context.engine.payments.cachedStarGifts()
).start(next: { [weak self] peer, availableProducts, starGifts in
guard let self, let peer else {
return
}
self.peer = peer
let shortestOptionPrice: (Int64, NSDecimalNumber)
if let product = availableProducts.first(where: { $0.id.hasSuffix(".monthly") }) {
shortestOptionPrice = (Int64(Float(product.priceCurrencyAndAmount.amount)), product.priceValue)
} else {
shortestOptionPrice = (1, NSDecimalNumber(decimal: 1))
}
var premiumProducts: [PremiumGiftProduct] = []
for option in premiumOptions {
if let product = availableProducts.first(where: { $0.id == option.storeProductId }), !product.isSubscription {
let fraction = Float(product.priceCurrencyAndAmount.amount) / Float(option.months) / Float(shortestOptionPrice.0)
let discountValue = Int(round((1.0 - fraction) * 20.0) * 5.0)
premiumProducts.append(PremiumGiftProduct(giftOption: option, storeProduct: product, discount: discountValue > 0 ? discountValue : nil))
}
}
self.premiumProducts = premiumProducts.sorted(by: { $0.months < $1.months })
self.starGifts = starGifts
self.updated()
})
self.updateDisposable = self.context.engine.payments.keepStarGiftsUpdated().start()
}
deinit {
self.disposable?.dispose()
self.updateDisposable?.dispose()
}
}
func makeState() -> State {
return State(context: self.context, peerId: self.peerId, premiumOptions: self.premiumOptions)
}
func update(view: View, availableSize: CGSize, state: State, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
public final class GiftOptionsScreen: ViewControllerComponentContainer, GiftOptionsScreenProtocol {
private let context: AccountContext
public init(context: AccountContext, peerId: EnginePeer.Id, premiumOptions: [CachedPremiumGiftOption]) {
self.context = context
super.init(context: context, component: GiftOptionsScreenComponent(
context: context,
peerId: peerId,
premiumOptions: premiumOptions
), navigationBarAppearance: .none, theme: .default, updatedPresentationData: nil)
self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.context.sharedContext.currentPresentationData.with { $0 }.strings.Common_Back, style: .plain, target: nil, action: nil)
self.scrollToTop = { [weak self] in
guard let self, let componentView = self.node.hostView.componentView as? GiftOptionsScreenComponent.View else {
return
}
componentView.scrollToTop()
}
}
required public init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
}
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
}
}
private struct PremiumGiftProduct: Equatable {
let giftOption: CachedPremiumGiftOption
let storeProduct: InAppPurchaseManager.Product
let discount: Int?
var id: String {
return self.storeProduct.id
}
var months: Int32 {
return self.giftOption.months
}
var price: String {
return self.storeProduct.price
}
var pricePerMonth: String {
return self.storeProduct.pricePerMonth(Int(self.months))
}
}

View File

@ -0,0 +1,45 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "GiftSetupScreen",
module_name = "GiftSetupScreen",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/AsyncDisplayKit",
"//submodules/Display",
"//submodules/Postbox",
"//submodules/TelegramCore",
"//submodules/SSignalKit/SwiftSignalKit",
"//submodules/TelegramPresentationData",
"//submodules/TelegramUIPreferences",
"//submodules/AccountContext",
"//submodules/PresentationDataUtils",
"//submodules/Markdown",
"//submodules/ComponentFlow",
"//submodules/Components/ComponentDisplayAdapters",
"//submodules/Components/ViewControllerComponent",
"//submodules/Components/BundleIconComponent",
"//submodules/Components/MultilineTextComponent",
"//submodules/Components/BalancedTextComponent",
"//submodules/TelegramUI/Components/ListSectionComponent",
"//submodules/TelegramUI/Components/ListActionItemComponent",
"//submodules/TelegramUI/Components/ListMultilineTextFieldItemComponent",
"//submodules/TelegramUI/Components/LottieComponent",
"//submodules/TelegramUI/Components/PlainButtonComponent",
"//submodules/TelegramUI/Components/ButtonComponent",
"//submodules/AppBundle",
"//submodules/WallpaperBackgroundNode",
"//submodules/ChatPresentationInterfaceState",
"//submodules/TelegramUI/Components/TextFieldComponent",
"//submodules/TelegramUI/Components/ListItemComponentAdaptor",
"//submodules/BotPaymentsUI",
],
visibility = [
"//visibility:public",
],
)

View File

@ -0,0 +1,366 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramCore
import Postbox
import TelegramPresentationData
import TelegramUIPreferences
import ItemListUI
import PresentationDataUtils
import AccountContext
import WallpaperBackgroundNode
import ListItemComponentAdaptor
final class ChatGiftPreviewItem: ListViewItem, ItemListItem, ListItemComponentAdaptor.ItemGenerator {
let context: AccountContext
let theme: PresentationTheme
let componentTheme: PresentationTheme
let strings: PresentationStrings
let sectionId: ItemListSectionId
let fontSize: PresentationFontSize
let chatBubbleCorners: PresentationChatBubbleCorners
let wallpaper: TelegramWallpaper
let dateTimeFormat: PresentationDateTimeFormat
let nameDisplayOrder: PresentationPersonNameOrder
let accountPeer: EnginePeer?
let gift: StarGift
let text: String
init(
context: AccountContext,
theme: PresentationTheme,
componentTheme: PresentationTheme,
strings: PresentationStrings,
sectionId: ItemListSectionId,
fontSize: PresentationFontSize,
chatBubbleCorners: PresentationChatBubbleCorners,
wallpaper: TelegramWallpaper,
dateTimeFormat: PresentationDateTimeFormat,
nameDisplayOrder: PresentationPersonNameOrder,
accountPeer: EnginePeer?,
gift: StarGift,
text: String
) {
self.context = context
self.theme = theme
self.componentTheme = componentTheme
self.strings = strings
self.sectionId = sectionId
self.fontSize = fontSize
self.chatBubbleCorners = chatBubbleCorners
self.wallpaper = wallpaper
self.dateTimeFormat = dateTimeFormat
self.nameDisplayOrder = nameDisplayOrder
self.accountPeer = accountPeer
self.gift = gift
self.text = text
}
func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
async {
let node = ChatGiftPreviewItemNode()
let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
node.contentSize = layout.contentSize
node.insets = layout.insets
Queue.mainQueue().async {
completion(node, {
return (nil, { _ in apply() })
})
}
}
}
func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
Queue.mainQueue().async {
if let nodeValue = node() as? ChatGiftPreviewItemNode {
let makeLayout = nodeValue.asyncLayout()
async {
let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
Queue.mainQueue().async {
completion(layout, { _ in
apply()
})
}
}
}
}
}
public func item() -> ListViewItem {
return self
}
public static func ==(lhs: ChatGiftPreviewItem, rhs: ChatGiftPreviewItem) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.theme !== rhs.theme {
return false
}
if lhs.componentTheme !== rhs.componentTheme {
return false
}
if lhs.strings !== rhs.strings {
return false
}
if lhs.fontSize != rhs.fontSize {
return false
}
if lhs.chatBubbleCorners != rhs.chatBubbleCorners {
return false
}
if lhs.wallpaper != rhs.wallpaper {
return false
}
if lhs.dateTimeFormat != rhs.dateTimeFormat {
return false
}
if lhs.nameDisplayOrder != rhs.nameDisplayOrder {
return false
}
if lhs.accountPeer != rhs.accountPeer {
return false
}
if lhs.text != rhs.text {
return false
}
return true
}
}
final class ChatGiftPreviewItemNode: ListViewItemNode {
private var backgroundNode: WallpaperBackgroundNode?
private let topStripeNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode
private let maskNode: ASImageNode
private let containerNode: ASDisplayNode
private var messageNodes: [ListViewItemNode]?
private var itemHeaderNodes: [ListViewItemNode.HeaderId: ListViewItemHeaderNode] = [:]
private var item: ChatGiftPreviewItem?
private let disposable = MetaDisposable()
init() {
self.topStripeNode = ASDisplayNode()
self.topStripeNode.isLayerBacked = true
self.bottomStripeNode = ASDisplayNode()
self.bottomStripeNode.isLayerBacked = true
self.maskNode = ASImageNode()
self.containerNode = ASDisplayNode()
self.containerNode.subnodeTransform = CATransform3DMakeRotation(CGFloat.pi, 0.0, 0.0, 1.0)
super.init(layerBacked: false, dynamicBounce: false)
self.clipsToBounds = true
self.isUserInteractionEnabled = false
self.addSubnode(self.containerNode)
}
deinit {
self.disposable.dispose()
}
func asyncLayout() -> (_ item: ChatGiftPreviewItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
let currentNodes = self.messageNodes
var currentBackgroundNode = self.backgroundNode
return { item, params, neighbors in
if currentBackgroundNode == nil {
currentBackgroundNode = createWallpaperBackgroundNode(context: item.context, forChatDisplay: false)
currentBackgroundNode?.update(wallpaper: item.wallpaper, animated: false)
currentBackgroundNode?.updateBubbleTheme(bubbleTheme: item.componentTheme, bubbleCorners: item.chatBubbleCorners)
}
var insets: UIEdgeInsets
let separatorHeight = UIScreenPixel
let peerId = PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(1))
var items: [ListViewItem] = []
for _ in 0 ..< 1 {
let authorPeerId = item.context.account.peerId
var peers = SimpleDictionary<PeerId, Peer>()
let messages = SimpleDictionary<MessageId, Message>()
peers[authorPeerId] = item.accountPeer?._asPeer()
let media: [Media] = [
TelegramMediaAction(action: .starGift(gift: item.gift, convertStars: item.gift.price, text: item.text, entities: [], nameHidden: false, savedToProfile: false, converted: false))
//TelegramMediaAction(action: .starGift(amount: item.gift.price, giftId: item.gift.id, nameHidden: false, limitNumber: item.gift.availability != nil ? 1 : nil, limitTotal: item.gift.availability?.total, text: item.text, entities: []))
]
let message = Message(stableId: 1, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66000, flags: [.Incoming], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: peers[authorPeerId], text: "", attributes: [], media: media, peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:])
items.append(item.context.sharedContext.makeChatMessagePreviewItem(context: item.context, messages: [message], theme: item.componentTheme, strings: item.strings, wallpaper: item.wallpaper, fontSize: item.fontSize, chatBubbleCorners: item.chatBubbleCorners, dateTimeFormat: item.dateTimeFormat, nameOrder: item.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: currentBackgroundNode, availableReactions: nil, accountPeer: nil, isCentered: false, isPreview: true, isStandalone: false))
}
var nodes: [ListViewItemNode] = []
if let messageNodes = currentNodes {
nodes = messageNodes
for i in 0 ..< items.count {
let itemNode = messageNodes[i]
items[i].updateNode(async: { $0() }, node: {
return itemNode
}, params: params, previousItem: i == 0 ? nil : items[i - 1], nextItem: i == (items.count - 1) ? nil : items[i + 1], animation: .None, completion: { (layout, apply) in
let nodeFrame = CGRect(origin: itemNode.frame.origin, size: CGSize(width: layout.size.width, height: layout.size.height))
itemNode.contentSize = layout.contentSize
itemNode.insets = layout.insets
itemNode.frame = nodeFrame
itemNode.isUserInteractionEnabled = false
Queue.mainQueue().after(0.01) {
apply(ListViewItemApply(isOnScreen: true))
}
})
}
} else {
var messageNodes: [ListViewItemNode] = []
for i in 0 ..< items.count {
var itemNode: ListViewItemNode?
items[i].nodeConfiguredForParams(async: { $0() }, params: params, synchronousLoads: false, previousItem: i == 0 ? nil : items[i - 1], nextItem: i == (items.count - 1) ? nil : items[i + 1], completion: { node, apply in
itemNode = node
apply().1(ListViewItemApply(isOnScreen: true))
})
itemNode!.isUserInteractionEnabled = false
messageNodes.append(itemNode!)
}
nodes = messageNodes
}
var contentSize = CGSize(width: params.width, height: 4.0 + 4.0)
// for node in nodes {
// contentSize.height += node.frame.size.height
// }
contentSize.height = 346.0
insets = itemListNeighborsGroupedInsets(neighbors, params)
if params.width <= 320.0 {
insets.top = 0.0
}
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: .zero)
let layoutSize = layout.size
return (layout, { [weak self] in
if let strongSelf = self {
strongSelf.item = item
if let currentBackgroundNode {
currentBackgroundNode.update(wallpaper: item.wallpaper, animated: false)
currentBackgroundNode.updateBubbleTheme(bubbleTheme: item.theme, bubbleCorners: item.chatBubbleCorners)
}
strongSelf.containerNode.frame = CGRect(origin: CGPoint(), size: contentSize)
strongSelf.messageNodes = nodes
//var topOffset: CGFloat = 4.0
for node in nodes {
if node.supernode == nil {
strongSelf.containerNode.addSubnode(node)
}
node.updateFrame(CGRect(origin: CGPoint(x: 0.0, y: floor((contentSize.height - node.frame.size.height) / 2.0)), size: node.frame.size), within: layoutSize)
//topOffset += node.frame.size.height
}
if let currentBackgroundNode = currentBackgroundNode, strongSelf.backgroundNode !== currentBackgroundNode {
strongSelf.backgroundNode = currentBackgroundNode
strongSelf.insertSubnode(currentBackgroundNode, at: 0)
}
strongSelf.topStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor
strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor
if strongSelf.topStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1)
}
if strongSelf.bottomStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2)
}
if strongSelf.maskNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.maskNode, at: 3)
}
if params.isStandalone {
strongSelf.topStripeNode.isHidden = true
strongSelf.bottomStripeNode.isHidden = true
strongSelf.maskNode.isHidden = true
} else {
let hasCorners = itemListHasRoundedBlockLayout(params)
var hasTopCorners = false
var hasBottomCorners = false
switch neighbors.top {
case .sameSection(false):
strongSelf.topStripeNode.isHidden = true
default:
hasTopCorners = true
strongSelf.topStripeNode.isHidden = hasCorners
}
let bottomStripeInset: CGFloat
let bottomStripeOffset: CGFloat
switch neighbors.bottom {
case .sameSection(false):
bottomStripeInset = 0.0
bottomStripeOffset = -separatorHeight
strongSelf.bottomStripeNode.isHidden = false
default:
bottomStripeInset = 0.0
bottomStripeOffset = 0.0
hasBottomCorners = true
strongSelf.bottomStripeNode.isHidden = hasCorners
}
strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.componentTheme, top: hasTopCorners, bottom: hasBottomCorners) : nil
strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight))
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight))
}
let backgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
let displayMode: WallpaperDisplayMode
if abs(params.availableHeight - params.width) < 100.0, params.availableHeight > 700.0 {
displayMode = .halfAspectFill
} else {
if backgroundFrame.width > backgroundFrame.height * 4.0 {
if params.availableHeight < 700.0 {
displayMode = .halfAspectFill
} else {
displayMode = .aspectFill
}
} else {
displayMode = .aspectFill
}
}
if let backgroundNode = strongSelf.backgroundNode {
backgroundNode.frame = backgroundFrame
backgroundNode.updateLayout(size: backgroundNode.bounds.size, displayMode: displayMode, transition: .immediate)
}
strongSelf.maskNode.frame = backgroundFrame.insetBy(dx: params.leftInset, dy: 0.0)
}
})
}
}
override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
}
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
}
}

View File

@ -0,0 +1,607 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import Postbox
import TelegramCore
import TelegramPresentationData
import TelegramUIPreferences
import PresentationDataUtils
import AccountContext
import ComponentFlow
import ViewControllerComponent
import MultilineTextComponent
import BalancedTextComponent
import ListSectionComponent
import ListActionItemComponent
import ListMultilineTextFieldItemComponent
import ListItemComponentAdaptor
import BundleIconComponent
import LottieComponent
import TextFieldComponent
import ButtonComponent
import BotPaymentsUI
final class GiftSetupScreenComponent: Component {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let context: AccountContext
let peerId: EnginePeer.Id
let gift: StarGift
init(
context: AccountContext,
peerId: EnginePeer.Id,
gift: StarGift
) {
self.context = context
self.peerId = peerId
self.gift = gift
}
static func ==(lhs: GiftSetupScreenComponent, rhs: GiftSetupScreenComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.peerId != rhs.peerId {
return false
}
if lhs.gift != rhs.gift {
return false
}
return true
}
private final class ScrollView: UIScrollView {
override func touchesShouldCancel(in view: UIView) -> Bool {
return true
}
}
final class View: UIView, UIScrollViewDelegate {
private let topOverscrollLayer = SimpleLayer()
private let scrollView: ScrollView
private let navigationTitle = ComponentView<Empty>()
private let introContent = ComponentView<Empty>()
private let introSection = ComponentView<Empty>()
private let hideSection = ComponentView<Empty>()
private let button = ComponentView<Empty>()
private var ignoreScrolling: Bool = false
private var isUpdating: Bool = false
private var component: GiftSetupScreenComponent?
private(set) weak var state: EmptyComponentState?
private var environment: EnvironmentType?
private let introPlaceholderTag = NSObject()
private let textInputState = ListMultilineTextFieldItemComponent.ExternalState()
private let textInputTag = NSObject()
private var resetText: String?
private var hideName = false
private var previousHadInputHeight: Bool = false
private var recenterOnTag: NSObject?
private var peerMap: [EnginePeer.Id: EnginePeer] = [:]
private var starImage: (UIImage, PresentationTheme)?
override init(frame: CGRect) {
self.scrollView = ScrollView()
self.scrollView.showsVerticalScrollIndicator = true
self.scrollView.showsHorizontalScrollIndicator = false
self.scrollView.scrollsToTop = false
self.scrollView.delaysContentTouches = false
self.scrollView.canCancelContentTouches = true
self.scrollView.contentInsetAdjustmentBehavior = .never
if #available(iOS 13.0, *) {
self.scrollView.automaticallyAdjustsScrollIndicatorInsets = false
}
self.scrollView.alwaysBounceVertical = true
super.init(frame: frame)
self.scrollView.delegate = self
self.addSubview(self.scrollView)
self.scrollView.layer.addSublayer(self.topOverscrollLayer)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
}
func scrollToTop() {
self.scrollView.setContentOffset(CGPoint(), animated: true)
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if !self.ignoreScrolling {
self.updateScrolling(transition: .immediate)
}
}
private var scrolledUp = true
private func updateScrolling(transition: ComponentTransition) {
let navigationRevealOffsetY: CGFloat = 0.0
let navigationAlphaDistance: CGFloat = 16.0
let navigationAlpha: CGFloat = max(0.0, min(1.0, (self.scrollView.contentOffset.y - navigationRevealOffsetY) / navigationAlphaDistance))
if let controller = self.environment?.controller(), let navigationBar = controller.navigationBar {
transition.setAlpha(layer: navigationBar.backgroundNode.layer, alpha: navigationAlpha)
transition.setAlpha(layer: navigationBar.stripeNode.layer, alpha: navigationAlpha)
}
var scrolledUp = false
if navigationAlpha < 0.5 {
scrolledUp = true
} else if navigationAlpha > 0.5 {
scrolledUp = false
}
if self.scrolledUp != scrolledUp {
self.scrolledUp = scrolledUp
if !self.isUpdating {
self.state?.updated()
}
}
if let navigationTitleView = self.navigationTitle.view {
transition.setAlpha(view: navigationTitleView, alpha: 1.0)
}
}
func proceed() {
guard let component = self.component else {
return
}
let source: BotPaymentInvoiceSource = .starGift(hideName: self.hideName, peerId: component.peerId, giftId: component.gift.id, text: self.textInputState.text.string, entities: [])
let inputData = BotCheckoutController.InputData.fetch(context: component.context, source: source)
|> map(Optional.init)
|> `catch` { _ -> Signal<BotCheckoutController.InputData?, NoError> in
return .single(nil)
}
let _ = (inputData
|> deliverOnMainQueue).startStandalone(next: { [weak self] inputData in
guard let inputData else {
return
}
let _ = (component.context.engine.payments.sendStarsPaymentForm(formId: inputData.form.id, source: source)
|> deliverOnMainQueue).start(next: { [weak self] result in
guard let self, let controller = self.environment?.controller(), let navigationController = controller.navigationController as? NavigationController else {
return
}
var controllers = navigationController.viewControllers
controllers = controllers.filter { !($0 is GiftSetupScreen) && !($0 is GiftOptionsScreenProtocol) }
var foundController = false
for controller in controllers.reversed() {
if let chatController = controller as? ChatController, case .peer(id: component.peerId) = chatController.chatLocation {
chatController.hintPlayNextOutgoingGift()
foundController = true
break
}
}
if !foundController {
let chatController = component.context.sharedContext.makeChatController(context: component.context, chatLocation: .peer(id: component.peerId), subject: nil, botStart: nil, mode: .standard(.default), params: nil)
chatController.hintPlayNextOutgoingGift()
controllers.append(chatController)
}
navigationController.setViewControllers(controllers, animated: true)
})
})
}
func update(component: GiftSetupScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
self.isUpdating = true
defer {
self.isUpdating = false
}
if self.component == nil {
let _ = (component.context.engine.data.get(
TelegramEngine.EngineData.Item.Peer.Peer(id: component.peerId),
TelegramEngine.EngineData.Item.Peer.Peer(id: component.context.account.peerId)
)
|> deliverOnMainQueue).start(next: { [weak self] peer, accountPeer in
guard let self else {
return
}
if let peer {
self.peerMap[peer.id] = peer
}
if let accountPeer {
self.peerMap[accountPeer.id] = accountPeer
}
self.state?.updated()
})
}
let environment = environment[EnvironmentType.self].value
let themeUpdated = self.environment?.theme !== environment.theme
self.environment = environment
self.component = component
self.state = state
let alphaTransition: ComponentTransition
if !transition.animation.isImmediate {
alphaTransition = .easeInOut(duration: 0.25)
} else {
alphaTransition = .immediate
}
if themeUpdated {
self.backgroundColor = environment.theme.list.blocksBackgroundColor
}
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
let _ = alphaTransition
let _ = presentationData
let navigationTitleSize = self.navigationTitle.update(
transition: transition,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: "Send a Gift", font: Font.semibold(17.0), textColor: environment.theme.rootController.navigationBar.primaryTextColor)),
horizontalAlignment: .center
)),
environment: {},
containerSize: CGSize(width: availableSize.width, height: 100.0)
)
let navigationTitleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - navigationTitleSize.width) / 2.0), y: environment.statusBarHeight + floor((environment.navigationHeight - environment.statusBarHeight - navigationTitleSize.height) / 2.0)), size: navigationTitleSize)
if let navigationTitleView = self.navigationTitle.view {
if navigationTitleView.superview == nil {
if let controller = self.environment?.controller(), let navigationBar = controller.navigationBar {
navigationBar.view.addSubview(navigationTitleView)
}
}
transition.setFrame(view: navigationTitleView, frame: navigationTitleFrame)
}
let bottomContentInset: CGFloat = 24.0
let sideInset: CGFloat = 16.0 + environment.safeInsets.left
let sectionSpacing: CGFloat = 24.0
var contentHeight: CGFloat = 0.0
contentHeight += environment.navigationHeight
contentHeight += 26.0
self.recenterOnTag = nil
if let hint = transition.userData(TextFieldComponent.AnimationHint.self), let targetView = hint.view {
if let textView = self.introSection.findTaggedView(tag: self.textInputTag) {
if targetView.isDescendant(of: textView) {
self.recenterOnTag = self.textInputTag
}
}
}
var introSectionItems: [AnyComponentWithIdentity<Empty>] = []
introSectionItems.append(AnyComponentWithIdentity(id: introSectionItems.count, component: AnyComponent(Rectangle(color: .clear, height: 346.0, tag: self.introPlaceholderTag))))
introSectionItems.append(AnyComponentWithIdentity(id: introSectionItems.count, component: AnyComponent(ListMultilineTextFieldItemComponent(
externalState: self.textInputState,
context: component.context,
theme: environment.theme,
strings: environment.strings,
initialText: "",
resetText: self.resetText.flatMap {
return ListMultilineTextFieldItemComponent.ResetText(value: $0)
},
placeholder: environment.strings.Business_Intro_IntroTextPlaceholder,
autocapitalizationType: .none,
autocorrectionType: .no,
returnKeyType: .done,
characterLimit: 70,
displayCharacterLimit: true,
emptyLineHandling: .notAllowed,
updated: { _ in
},
returnKeyAction: { [weak self] in
guard let self else {
return
}
if let titleView = self.introSection.findTaggedView(tag: self.textInputTag) as? ListMultilineTextFieldItemComponent.View {
titleView.endEditing(true)
}
},
textUpdateTransition: .spring(duration: 0.4),
tag: self.textInputTag
))))
self.resetText = nil
let introSectionSize = self.introSection.update(
transition: transition,
component: AnyComponent(ListSectionComponent(
theme: environment.theme,
header: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: "CUSTOMIZE YOUR GIFT",
font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize),
textColor: environment.theme.list.freeTextColor
)),
maximumNumberOfLines: 0
)),
footer: nil,
items: introSectionItems
)),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0)
)
let introSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: introSectionSize)
if let introSectionView = self.introSection.view {
if introSectionView.superview == nil {
self.scrollView.addSubview(introSectionView)
self.introSection.parentState = state
}
transition.setFrame(view: introSectionView, frame: introSectionFrame)
}
contentHeight += introSectionSize.height
contentHeight += sectionSpacing
// let titleText: String
// if self.titleInputState.text.string.isEmpty {
// titleText = environment.strings.Conversation_EmptyPlaceholder
// } else {
// let rawTitle = self.titleInputState.text.string
// titleText = rawTitle.count <= maxTitleLength ? rawTitle : String(rawTitle[rawTitle.startIndex ..< rawTitle.index(rawTitle.startIndex, offsetBy: maxTitleLength)])
// }
// let textText: String
// if self.textInputState.text.string.isEmpty {
// textText = environment.strings.Conversation_GreetingText
// } else {
// let rawText = self.textInputState.text.string
// textText = rawText.count <= maxTextLength ? rawText : String(rawText[rawText.startIndex ..< rawText.index(rawText.startIndex, offsetBy: maxTextLength)])
// }
let listItemParams = ListViewItemLayoutParams(width: availableSize.width - sideInset * 2.0, leftInset: 0.0, rightInset: 0.0, availableHeight: 10000.0, isStandalone: true)
let introContentSize = self.introContent.update(
transition: transition,
component: AnyComponent(
ListItemComponentAdaptor(
itemGenerator: ChatGiftPreviewItem(
context: component.context,
theme: environment.theme,
componentTheme: environment.theme,
strings: environment.strings,
sectionId: 0,
fontSize: presentationData.chatFontSize,
chatBubbleCorners: presentationData.chatBubbleCorners,
wallpaper: presentationData.chatWallpaper,
dateTimeFormat: environment.dateTimeFormat,
nameDisplayOrder: presentationData.nameDisplayOrder,
accountPeer: self.peerMap[component.context.account.peerId],
gift: component.gift,
text: self.textInputState.text.string
),
params: listItemParams
)
),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0)
)
if let introContentView = self.introContent.view {
if introContentView.superview == nil {
if let placeholderView = self.introSection.findTaggedView(tag: self.introPlaceholderTag) {
placeholderView.addSubview(introContentView)
}
}
transition.setFrame(view: introContentView, frame: CGRect(origin: CGPoint(), size: introContentSize))
}
if self.recenterOnTag == nil && self.previousHadInputHeight != (environment.inputHeight > 0.0) {
if self.textInputState.isEditing {
self.recenterOnTag = self.textInputTag
}
}
self.previousHadInputHeight = environment.inputHeight > 0.0
let peerName = self.peerMap[component.peerId]?.compactDisplayTitle ?? ""
let hideSectionSize = self.hideSection.update(
transition: transition,
component: AnyComponent(ListSectionComponent(
theme: environment.theme,
header: nil,
footer: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: "Hide my name and message from visitors to \(peerName)'s profile. \(peerName) will still see your name and message.",
font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize),
textColor: environment.theme.list.freeTextColor
)),
maximumNumberOfLines: 0
)),
items: [
AnyComponentWithIdentity(id: 0, component: AnyComponent(ListActionItemComponent(
theme: environment.theme,
title: AnyComponent(VStack([
AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: "Hide My Name",
font: Font.regular(presentationData.listsFontSize.baseDisplaySize),
textColor: environment.theme.list.itemPrimaryTextColor
)),
maximumNumberOfLines: 1
))),
], alignment: .left, spacing: 2.0)),
accessory: .toggle(ListActionItemComponent.Toggle(style: .regular, isOn: self.hideName, action: { [weak self] _ in
guard let self else {
return
}
self.hideName = !self.hideName
self.state?.updated(transition: .spring(duration: 0.4))
})),
action: nil
)))
]
)),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0)
)
let hideSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: hideSectionSize)
if let hideSectionView = self.hideSection.view {
if hideSectionView.superview == nil {
self.scrollView.addSubview(hideSectionView)
}
transition.setFrame(view: hideSectionView, frame: hideSectionFrame)
}
contentHeight += hideSectionSize.height
contentHeight += bottomContentInset
let inputHeight: CGFloat = environment.inputHeight
let combinedBottomInset = max(inputHeight, environment.safeInsets.bottom)
contentHeight += combinedBottomInset
if self.starImage == nil || self.starImage?.1 !== environment.theme {
self.starImage = (generateTintedImage(image: UIImage(bundleImageName: "Item List/PremiumIcon"), color: environment.theme.list.itemCheckColors.foregroundColor)!, environment.theme)
}
let amountString = presentationStringsFormattedNumber(Int32(component.gift.price), presentationData.dateTimeFormat.groupingSeparator)
let buttonAttributedString = NSMutableAttributedString(string: "Send a Gift for # \(amountString)", font: Font.semibold(17.0), textColor: environment.theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center)
if let range = buttonAttributedString.string.range(of: "#"), let starImage = self.starImage?.0 {
buttonAttributedString.addAttribute(.attachment, value: starImage, range: NSRange(range, in: buttonAttributedString.string))
buttonAttributedString.addAttribute(.foregroundColor, value: environment.theme.list.itemCheckColors.foregroundColor, range: NSRange(range, in: buttonAttributedString.string))
buttonAttributedString.addAttribute(.baselineOffset, value: 1.0, range: NSRange(range, in: buttonAttributedString.string))
}
let buttonSize = self.button.update(
transition: .immediate,
component: AnyComponent(ButtonComponent(
background: ButtonComponent.Background(
color: environment.theme.list.itemCheckColors.fillColor,
foreground: environment.theme.list.itemCheckColors.foregroundColor,
pressedColor: environment.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9),
cornerRadius: 10.0
),
content: AnyComponentWithIdentity(
id: AnyHashable(0),
component: AnyComponent(MultilineTextComponent(text: .plain(buttonAttributedString)))
),
isEnabled: true,
displaysProgress: false,
action: { [weak self] in
self?.proceed()
}
)),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 50)
)
if let buttonView = self.button.view {
if buttonView.superview == nil {
self.addSubview(buttonView)
}
buttonView.frame = CGRect(origin: CGPoint(x: floor((availableSize.width - buttonSize.width) / 2.0), y: availableSize.height - environment.safeInsets.bottom - buttonSize.height), size: buttonSize)
}
let previousBounds = self.scrollView.bounds
self.ignoreScrolling = true
let contentSize = CGSize(width: availableSize.width, height: contentHeight)
if self.scrollView.frame != CGRect(origin: CGPoint(), size: availableSize) {
self.scrollView.frame = CGRect(origin: CGPoint(), size: availableSize)
}
if self.scrollView.contentSize != contentSize {
self.scrollView.contentSize = contentSize
}
let scrollInsets = UIEdgeInsets(top: environment.navigationHeight, left: 0.0, bottom: 0.0, right: 0.0)
if self.scrollView.scrollIndicatorInsets != scrollInsets {
self.scrollView.scrollIndicatorInsets = scrollInsets
}
if !previousBounds.isEmpty, !transition.animation.isImmediate {
let bounds = self.scrollView.bounds
if bounds.maxY != previousBounds.maxY {
let offsetY = previousBounds.maxY - bounds.maxY
transition.animateBoundsOrigin(view: self.scrollView, from: CGPoint(x: 0.0, y: offsetY), to: CGPoint(), additive: true)
}
}
if let recenterOnTag = self.recenterOnTag {
self.recenterOnTag = nil
if let targetView = self.introSection.findTaggedView(tag: recenterOnTag) {
let caretRect = targetView.convert(targetView.bounds, to: self.scrollView)
var scrollViewBounds = self.scrollView.bounds
let minButtonDistance: CGFloat = 16.0
if -scrollViewBounds.minY + caretRect.maxY > availableSize.height - combinedBottomInset - minButtonDistance {
scrollViewBounds.origin.y = -(availableSize.height - combinedBottomInset - minButtonDistance - caretRect.maxY)
if scrollViewBounds.origin.y < 0.0 {
scrollViewBounds.origin.y = 0.0
}
}
if self.scrollView.bounds != scrollViewBounds {
transition.setBounds(view: self.scrollView, bounds: scrollViewBounds)
}
}
}
self.topOverscrollLayer.frame = CGRect(origin: CGPoint(x: 0.0, y: -3000.0), size: CGSize(width: availableSize.width, height: 3000.0))
self.ignoreScrolling = false
self.updateScrolling(transition: transition)
return availableSize
}
}
func makeView() -> View {
return View()
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
public final class GiftSetupScreen: ViewControllerComponentContainer {
private let context: AccountContext
public init(
context: AccountContext,
peerId: EnginePeer.Id,
gift: StarGift
) {
self.context = context
super.init(context: context, component: GiftSetupScreenComponent(
context: context,
peerId: peerId,
gift: gift
), navigationBarAppearance: .default, theme: .default, updatedPresentationData: nil)
self.title = ""
//self.navigationItem.backBarButtonItem = UIBarButtonItem(title: presentationData.strings.Common_Back, style: .plain, target: nil, action: nil)
self.scrollToTop = { [weak self] in
guard let self, let componentView = self.node.hostView.componentView as? GiftSetupScreenComponent.View else {
return
}
componentView.scrollToTop()
}
}
required public init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
}
@objc private func cancelPressed() {
self.dismiss()
}
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
}
}

View File

@ -0,0 +1,44 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "GiftViewScreen",
module_name = "GiftViewScreen",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/AsyncDisplayKit",
"//submodules/Display",
"//submodules/Postbox",
"//submodules/TelegramCore",
"//submodules/SSignalKit/SwiftSignalKit",
"//submodules/TelegramPresentationData",
"//submodules/TelegramUIPreferences",
"//submodules/AccountContext",
"//submodules/PresentationDataUtils",
"//submodules/Markdown",
"//submodules/ComponentFlow",
"//submodules/Components/ComponentDisplayAdapters",
"//submodules/Components/ViewControllerComponent",
"//submodules/Components/BundleIconComponent",
"//submodules/Components/MultilineTextComponent",
"//submodules/Components/BalancedTextComponent",
"//submodules/TelegramUI/Components/ListSectionComponent",
"//submodules/TelegramUI/Components/ListActionItemComponent",
"//submodules/TelegramUI/Components/LottieComponent",
"//submodules/TelegramUI/Components/PlainButtonComponent",
"//submodules/TelegramUI/Components/ButtonComponent",
"//submodules/AppBundle",
"//submodules/Components/SheetComponent",
"//submodules/Components/SolidRoundedButtonComponent",
"//submodules/TelegramUI/Components/Stars/StarsAvatarComponent",
"//submodules/TelegramUI/Components/EmojiTextAttachmentView",
"//submodules/UndoUI",
],
visibility = [
"//visibility:public",
],
)

View File

@ -20,6 +20,7 @@ public enum PeerInfoPaneKey: Int32 {
case gifs
case groupsInCommon
case recommended
case gifts
}
public struct PeerInfoStatusData: Equatable {

View File

@ -383,6 +383,7 @@ final class PeerInfoScreenData {
let starsRevenueStatsState: StarsRevenueStats?
let starsRevenueStatsContext: StarsRevenueStatsContext?
let revenueStatsState: RevenueStats?
let profileGiftsContext: ProfileGiftsContext?
let _isContact: Bool
var forceIsContact: Bool = false
@ -428,7 +429,8 @@ final class PeerInfoScreenData {
starsState: StarsContext.State?,
starsRevenueStatsState: StarsRevenueStats?,
starsRevenueStatsContext: StarsRevenueStatsContext?,
revenueStatsState: RevenueStats?
revenueStatsState: RevenueStats?,
profileGiftsContext: ProfileGiftsContext?
) {
self.peer = peer
self.chatPeer = chatPeer
@ -463,6 +465,7 @@ final class PeerInfoScreenData {
self.starsRevenueStatsState = starsRevenueStatsState
self.starsRevenueStatsContext = starsRevenueStatsContext
self.revenueStatsState = revenueStatsState
self.profileGiftsContext = profileGiftsContext
}
}
@ -955,7 +958,8 @@ func peerInfoScreenSettingsData(context: AccountContext, peerId: EnginePeer.Id,
starsState: starsState,
starsRevenueStatsState: nil,
starsRevenueStatsContext: nil,
revenueStatsState: nil
revenueStatsState: nil,
profileGiftsContext: nil
)
}
}
@ -1000,7 +1004,8 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
starsState: nil,
starsRevenueStatsState: nil,
starsRevenueStatsContext: nil,
revenueStatsState: nil
revenueStatsState: nil,
profileGiftsContext: nil
))
case let .user(userPeerId, secretChatId, kind):
let groupsInCommon: GroupsInCommonContext?
@ -1012,6 +1017,13 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
groupsInCommon = nil
}
let profileGiftsContext: ProfileGiftsContext?
if case .user = kind {
profileGiftsContext = ProfileGiftsContext(account: context.account, peerId: userPeerId)
} else {
profileGiftsContext = nil
}
enum StatusInputData: Equatable {
case none
case presence(TelegramUserPresence)
@ -1247,6 +1259,16 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
}
}
let profileGiftsCount: Signal<Int32, NoError>
if let profileGiftsContext {
profileGiftsCount = profileGiftsContext.state
|> map { state in
return state.count ?? 0
}
} else {
profileGiftsCount = .single(0)
}
return combineLatest(
context.account.viewTracker.peerView(peerId, updateData: true),
peerInfoAvailableMediaPanes(context: context, peerId: peerId, chatLocation: chatLocation, isMyProfile: isMyProfile, chatLocationContextHolder: chatLocationContextHolder),
@ -1263,9 +1285,10 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
hasBotPreviewItems,
peerInfoPersonalChannel(context: context, peerId: peerId, isSettings: false),
privacySettings,
starsRevenueContextAndState
starsRevenueContextAndState,
profileGiftsCount
)
|> map { peerView, availablePanes, globalNotificationSettings, encryptionKeyFingerprint, status, hasStories, hasStoryArchive, accountIsPremium, savedMessagesPeer, hasSavedMessagesChats, hasSavedMessages, hasSavedMessageTags, hasBotPreviewItems, personalChannel, privacySettings, starsRevenueContextAndState -> PeerInfoScreenData in
|> map { peerView, availablePanes, globalNotificationSettings, encryptionKeyFingerprint, status, hasStories, hasStoryArchive, accountIsPremium, savedMessagesPeer, hasSavedMessagesChats, hasSavedMessages, hasSavedMessageTags, hasBotPreviewItems, personalChannel, privacySettings, starsRevenueContextAndState, profileGiftsCount -> PeerInfoScreenData in
var availablePanes = availablePanes
if isMyProfile {
availablePanes?.insert(.stories, at: 0)
@ -1277,6 +1300,10 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
availablePanes?.insert(.stories, at: 0)
}
if profileGiftsCount > 0 {
availablePanes?.insert(.gifts, at: hasStories ? 1 : 0)
}
if availablePanes != nil, groupsInCommon != nil, let cachedData = peerView.cachedData as? CachedUserData {
if cachedData.commonGroupCount != 0 {
availablePanes?.append(.groupsInCommon)
@ -1370,7 +1397,8 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
starsState: nil,
starsRevenueStatsState: starsRevenueContextAndState.1,
starsRevenueStatsContext: starsRevenueContextAndState.0,
revenueStatsState: nil
revenueStatsState: nil,
profileGiftsContext: profileGiftsContext
)
}
case .channel:
@ -1579,7 +1607,8 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
starsState: nil,
starsRevenueStatsState: starsRevenueContextAndState.1,
starsRevenueStatsContext: starsRevenueContextAndState.0,
revenueStatsState: revenueContextAndState.1
revenueStatsState: revenueContextAndState.1,
profileGiftsContext: nil
)
}
case let .group(groupId):
@ -1879,7 +1908,8 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
starsState: nil,
starsRevenueStatsState: nil,
starsRevenueStatsContext: nil,
revenueStatsState: nil
revenueStatsState: nil,
profileGiftsContext: nil
))
}
}

View File

@ -407,6 +407,8 @@ private final class PeerInfoPendingPane {
var captureProtected = data.peer?.isCopyProtectionEnabled ?? false
let paneNode: PeerInfoPaneNode
switch key {
case .gifts:
paneNode = PeerInfoGiftsPaneNode(context: context, peerId: peerId, chatControllerInteraction: chatControllerInteraction, openPeerContextAction: openPeerContextAction, profileGifts: data.profileGiftsContext!)
case .stories, .storyArchive, .botPreview:
var canManage = false
if let peer = data.peer {
@ -1149,6 +1151,9 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, ASGestureRecognizerDelegat
title = presentationData.strings.DialogList_TabTitle
case .savedMessages:
title = presentationData.strings.PeerInfo_SavedMessagesTabTitle
case .gifts:
//TODO:localize
title = "Gifts"
}
return PeerInfoPaneSpecifier(key: key, title: title)
}, selectedPane: self.currentPaneKey, disableSwitching: disableTabSwitching, transitionFraction: self.transitionFraction, transition: transition)

View File

@ -3472,7 +3472,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
}, openLargeEmojiInfo: { _, _, _ in
}, openJoinLink: { _ in
}, openWebView: { _, _, _, _ in
}, activateAdAction: { _, _ in
}, activateAdAction: { _, _, _, _ in
}, openRequestedPeerSelection: { _, _, _, _ in
}, saveMediaToFiles: { _ in
}, openNoAdsDemo: {

View File

@ -48,6 +48,9 @@ swift_library(
"//submodules/Components/MultilineTextComponent",
"//submodules/TelegramUI/Components/TabSelectorComponent",
"//submodules/TelegramUI/Components/Settings/LanguageSelectionScreen",
"//submodules/TelegramUI/Components/Gifts/GiftItemComponent",
"//submodules/TelegramUI/Components/Gifts/GiftViewScreen",
"//submodules/TelegramUI/Components/ButtonComponent",
],
visibility = [
"//visibility:public",

View File

@ -0,0 +1,290 @@
import AsyncDisplayKit
import Display
import ComponentFlow
import TelegramCore
import SwiftSignalKit
import Postbox
import TelegramPresentationData
import AccountContext
import ContextUI
import PhotoResources
import TelegramUIPreferences
import ItemListPeerItem
import ItemListPeerActionItem
import MergeLists
import ItemListUI
import ChatControllerInteraction
import MultilineTextComponent
import Markdown
import PeerInfoPaneNode
import GiftItemComponent
import PlainButtonComponent
import GiftViewScreen
import ButtonComponent
public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScrollViewDelegate {
private let context: AccountContext
private let peerId: PeerId
private let profileGifts: ProfileGiftsContext
private var dataDisposable: Disposable?
private let chatControllerInteraction: ChatControllerInteraction
private let openPeerContextAction: (Bool, Peer, ASDisplayNode, ContextGesture?) -> Void
public weak var parentController: ViewController?
private let backgroundNode: ASDisplayNode
private let scrollNode: ASScrollNode
private var currentParams: (size: CGSize, sideInset: CGFloat, bottomInset: CGFloat, isScrollingLockedAtTop: Bool, presentationData: PresentationData)?
private var theme: PresentationTheme?
private let presentationDataPromise = Promise<PresentationData>()
private let ready = Promise<Bool>()
private var didSetReady: Bool = false
public var isReady: Signal<Bool, NoError> {
return self.ready.get()
}
private let statusPromise = Promise<PeerInfoStatusData?>(nil)
public var status: Signal<PeerInfoStatusData?, NoError> {
self.statusPromise.get()
}
public var tabBarOffsetUpdated: ((ContainedViewLayoutTransition) -> Void)?
public var tabBarOffset: CGFloat {
return 0.0
}
private var starsProducts: [ProfileGiftsContext.State.StarGift]?
private var starsItems: [AnyHashable: ComponentView<Empty>] = [:]
public init(context: AccountContext, peerId: PeerId, chatControllerInteraction: ChatControllerInteraction, openPeerContextAction: @escaping (Bool, Peer, ASDisplayNode, ContextGesture?) -> Void, profileGifts: ProfileGiftsContext) {
self.context = context
self.peerId = peerId
self.chatControllerInteraction = chatControllerInteraction
self.openPeerContextAction = openPeerContextAction
self.profileGifts = profileGifts
self.backgroundNode = ASDisplayNode()
self.scrollNode = ASScrollNode()
super.init()
self.addSubnode(self.backgroundNode)
self.addSubnode(self.scrollNode)
self.dataDisposable = (profileGifts.state
|> deliverOnMainQueue).startStrict(next: { [weak self] state in
guard let self else {
return
}
self.statusPromise.set(.single(PeerInfoStatusData(text: "\(state.count ?? 0) gifts", isActivity: true, key: .gifts)))
self.starsProducts = state.gifts
if !self.didSetReady {
self.didSetReady = true
self.ready.set(.single(true))
}
self.updateScrolling()
})
}
deinit {
self.dataDisposable?.dispose()
}
public override func didLoad() {
super.didLoad()
self.scrollNode.view.delegate = self
}
public func ensureMessageIsVisible(id: MessageId) {
}
public func scrollToTop() -> Bool {
self.scrollNode.view.setContentOffset(.zero, animated: true)
return true
}
public func scrollViewDidScroll(_ scrollView: UIScrollView) {
self.updateScrolling()
}
func updateScrolling() {
if let starsProducts = self.starsProducts, let params = self.currentParams {
let optionSpacing: CGFloat = 10.0
let sideInset = params.sideInset + 16.0
let itemsInRow = min(starsProducts.count, 3)
let optionWidth = (params.size.width - sideInset * 2.0 - optionSpacing * CGFloat(itemsInRow - 1)) / CGFloat(itemsInRow)
let starsOptionSize = CGSize(width: optionWidth, height: 154.0)
let visibleBounds = self.scrollNode.bounds.insetBy(dx: 0.0, dy: -10.0)
var validIds: [AnyHashable] = []
var itemFrame = CGRect(origin: CGPoint(x: sideInset, y: 60.0), size: starsOptionSize)
for product in starsProducts {
let itemId = AnyHashable(product.date)
validIds.append(itemId)
let itemTransition = ComponentTransition.immediate
let visibleItem: ComponentView<Empty>
if let current = self.starsItems[itemId] {
visibleItem = current
} else {
visibleItem = ComponentView()
self.starsItems[itemId] = visibleItem
}
var isVisible = false
if visibleBounds.intersects(itemFrame) {
isVisible = true
}
if isVisible {
let _ = visibleItem.update(
transition: itemTransition,
component: AnyComponent(
PlainButtonComponent(
content: AnyComponent(
GiftItemComponent(
context: self.context,
theme: params.presentationData.theme,
peer: product.fromPeer,
subject: .starGift(product.gift.id, product.gift.file),
price: "⭐️ \(product.gift.price)",
ribbon: product.gift.availability != nil ?
GiftItemComponent.Ribbon(
text: "1 of 1K",
color: UIColor(rgb: 0x58c1fe)
)
: nil
)
),
effectAlignment: .center,
action: { [weak self] in
if let self {
let controller = GiftViewScreen(
context: self.context,
subject: .profileGift(self.peerId, product)
)
self.parentController?.push(controller)
}
},
animateAlpha: false
)
),
environment: {},
containerSize: starsOptionSize
)
if let itemView = visibleItem.view {
if itemView.superview == nil {
self.scrollNode.view.addSubview(itemView)
}
itemTransition.setFrame(view: itemView, frame: itemFrame)
}
}
itemFrame.origin.x += itemFrame.width + optionSpacing
if itemFrame.maxX > params.size.width {
itemFrame.origin.x = sideInset
itemFrame.origin.y += starsOptionSize.height + optionSpacing
}
}
let contentHeight = ceil(CGFloat(starsProducts.count) / 3.0) * starsOptionSize.height + 60.0 + params.bottomInset + 16.0
// //TODO:localize
// let buttonSize = self.button.update(
// transition: .immediate,
// component: AnyComponent(ButtonComponent(
// background: ButtonComponent.Background(
// color: params.presentationData.theme.list.itemCheckColors.fillColor,
// foreground: params.presentationData.theme.list.itemCheckColors.foregroundColor,
// pressedColor: params.presentationData.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9),
// cornerRadius: 10.0
// ),
// content: AnyComponentWithIdentity(
// id: AnyHashable(0),
// component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: "Send Gifts to Friends", font: Font.semibold(17.0), textColor: )params.presentationData.theme.list.itemCheckColors.foregroundColor)))
// ),
// isEnabled: true,
// displaysProgress: false,
// action: {
//
// }
// )),
// environment: {},
// containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 50)
// )
// if let buttonView = self.button.view {
// if buttonView.superview == nil {
// self.addSubview(buttonView)
// }
// buttonView.frame = CGRect(origin: CGPoint(x: floor((availableSize.width - buttonSize.width) / 2.0), y: availableSize.height - environment.safeInsets.bottom - buttonSize.height), size: buttonSize)
// }
// contentHeight += 100.0
let contentSize = CGSize(width: params.size.width, height: contentHeight)
if self.scrollNode.view.contentSize != contentSize {
self.scrollNode.view.contentSize = contentSize
}
}
}
public func update(size: CGSize, topInset: CGFloat, sideInset: CGFloat, bottomInset: CGFloat, deviceMetrics: DeviceMetrics, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, navigationHeight: CGFloat, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) {
self.currentParams = (size, sideInset, bottomInset, isScrollingLockedAtTop, presentationData)
self.presentationDataPromise.set(.single(presentationData))
self.backgroundNode.backgroundColor = presentationData.theme.list.blocksBackgroundColor
transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 48.0), size: size))
transition.updateFrame(node: self.scrollNode, frame: CGRect(origin: CGPoint(), size: size))
if isScrollingLockedAtTop {
self.scrollNode.view.contentOffset = .zero
}
self.scrollNode.view.isScrollEnabled = !isScrollingLockedAtTop
self.updateScrolling()
}
public func findLoadedMessage(id: MessageId) -> Message? {
return nil
}
public func updateHiddenMedia() {
}
public func transferVelocity(_ velocity: CGFloat) {
if velocity > 0.0 {
// self.scrollNode.transferVelocity(velocity)
}
}
public func cancelPreviewGestures() {
}
public func transitionNodeForGallery(messageId: MessageId, media: Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? {
return nil
}
public func addToTransitionSurface(view: UIView) {
}
public func updateSelectedMessages(animated: Bool) {
}
}
private struct StarsGiftProduct: Equatable {
let emoji: String
let price: Int64
let isLimited: Bool
}

View File

@ -870,7 +870,10 @@ private final class SparseItemGridBindingImpl: SparseItemGridBinding, ListShimme
}
let message = item.message
let hasSpoiler = message.attributes.contains(where: { $0 is MediaSpoilerMessageAttribute }) && !self.revealedSpoilerMessageIds.contains(message.id)
var hasSpoiler = message.attributes.contains(where: { $0 is MediaSpoilerMessageAttribute }) && !self.revealedSpoilerMessageIds.contains(message.id)
if message.isSensitiveContent(platform: "ios") {
hasSpoiler = true
}
layer.updateHasSpoiler(hasSpoiler: hasSpoiler)
var selectedMedia: Media?
@ -1272,7 +1275,11 @@ public final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode,
}
strongSelf.chatControllerInteraction.toggleMessagesSelection([item.message.id], toggledValue)
} else {
let _ = strongSelf.chatControllerInteraction.openMessage(item.message, OpenMessageParams(mode: .default))
if item.message.isSensitiveContent(platform: "ios") {
// strongSelf.context.currentContentSettings.with { $0 }.ignoreContentRestrictionReasons
} else {
let _ = strongSelf.chatControllerInteraction.openMessage(item.message, OpenMessageParams(mode: .default))
}
}
}

View File

@ -0,0 +1,24 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "ItemShimmeringLoadingComponent",
module_name = "ItemShimmeringLoadingComponent",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/AsyncDisplayKit",
"//submodules/Display",
"//submodules/Postbox",
"//submodules/ComponentFlow",
"//submodules/AppBundle",
"//submodules/Components/HierarchyTrackingLayer",
"//submodules/TelegramUI/Components/TextLoadingEffect",
],
visibility = [
"//visibility:public",
],
)

View File

@ -6,17 +6,25 @@ import HierarchyTrackingLayer
import ComponentFlow
import TextLoadingEffect
final class ItemLoadingComponent: Component {
public final class ItemShimmeringLoadingComponent: Component {
private let color: UIColor
private let cornerRadius: CGFloat
public init(color: UIColor) {
public init(
color: UIColor,
cornerRadius: CGFloat = 10.0
) {
self.color = color
self.cornerRadius = cornerRadius
}
public static func ==(lhs: ItemLoadingComponent, rhs: ItemLoadingComponent) -> Bool {
public static func ==(lhs: ItemShimmeringLoadingComponent, rhs: ItemShimmeringLoadingComponent) -> Bool {
if !lhs.color.isEqual(rhs.color) {
return false
}
if lhs.cornerRadius != rhs.cornerRadius {
return false
}
return true
}
@ -28,16 +36,14 @@ final class ItemLoadingComponent: Component {
private let borderMaskGradientView = UIImageView()
private let borderMaskFillView = UIImageView()
private var component: ItemLoadingComponent?
private var component: ItemShimmeringLoadingComponent?
override public init(frame: CGRect) {
super.init(frame: frame)
self.addSubview(self.loadingView)
self.addSubview(self.borderView)
self.borderView.image = generateFilledRoundedRectImage(size: CGSize(width: 24.0, height: 24.0), cornerRadius: 10.0, color: nil, strokeColor: .white, strokeWidth: 1.0 + UIScreenPixel, backgroundColor: nil)?.stretchableImage(withLeftCapWidth: 10, topCapHeight: 10).withRenderingMode(.alwaysTemplate)
self.borderMaskView.backgroundColor = .clear
self.borderMaskFillView.backgroundColor = .white
@ -63,14 +69,23 @@ final class ItemLoadingComponent: Component {
})
}
func update(component: ItemLoadingComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
func update(component: ItemShimmeringLoadingComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
let isFirstTime = self.component == nil
let previousCornerRadius = self.component?.cornerRadius
self.component = component
if previousCornerRadius != component.cornerRadius {
self.borderView.image = generateFilledRoundedRectImage(size: CGSize(width: 24.0, height: 24.0), cornerRadius: component.cornerRadius, color: nil, strokeColor: .white, strokeWidth: 1.0 + UIScreenPixel, backgroundColor: nil)?.stretchableImage(withLeftCapWidth: Int(component.cornerRadius), topCapHeight: Int(component.cornerRadius)).withRenderingMode(.alwaysTemplate)
}
self.borderView.tintColor = component.color
self.loadingView.update(color: component.color, rect: CGRect(origin: .zero, size: availableSize))
self.loadingView.frame = CGRect(origin: .zero, size: availableSize)
self.loadingView.layer.cornerRadius = component.cornerRadius
self.loadingView.clipsToBounds = true
transition.setFrame(view: self.borderView, frame: CGRect(origin: .zero, size: availableSize))
self.borderMaskView.frame = self.borderView.bounds

View File

@ -37,6 +37,9 @@ swift_library(
"//submodules/Components/BlurredBackgroundComponent",
"//submodules/Components/BundleIconComponent",
"//submodules/ConfettiEffect",
"//submodules/AnimatedStickerNode",
"//submodules/TelegramAnimatedStickerNode",
"//submodules/TelegramUI/Components/Stars/ItemShimmeringLoadingComponent",
],
visibility = [
"//visibility:public",

View File

@ -25,6 +25,7 @@ import TextFormat
import PremiumStarComponent
import BundleIconComponent
import ConfettiEffect
import ItemShimmeringLoadingComponent
private struct StarsProduct: Equatable {
enum Option: Equatable {
@ -328,7 +329,7 @@ private final class StarsPurchaseScreenContentComponent: CombinedComponent {
let backgroundComponent: AnyComponent<Empty>?
if product.storeProduct.id == context.component.selectedProductId {
backgroundComponent = AnyComponent(
ItemLoadingComponent(color: environment.theme.list.itemAccentColor)
ItemShimmeringLoadingComponent(color: environment.theme.list.itemAccentColor)
)
} else {
backgroundComponent = nil

View File

@ -239,7 +239,6 @@ final class StarsTransactionsScreenComponent: Component {
if let starView = self.starView.view {
let starPosition = CGPoint(x: self.scrollView.frame.width / 2.0, y: topInset + starView.bounds.height / 2.0 - 30.0 - titleOffset * titleScale)
headerTransition.setPosition(view: starView, position: starPosition)
headerTransition.setScale(view: starView, scale: titleScale)
}

View File

@ -918,7 +918,7 @@ final class StoryItemContentComponent: Component {
}
}
let shimmeringMediaAreas: [MediaArea] = component.item.mediaAreas.filter { mediaArea in
var shimmeringMediaAreas: [MediaArea] = component.item.mediaAreas.filter { mediaArea in
if case .link = mediaArea {
return true
} else if case .venue = mediaArea {
@ -928,6 +928,10 @@ final class StoryItemContentComponent: Component {
}
}
if component.peer.id.isTelegramNotifications {
shimmeringMediaAreas = []
}
if !shimmeringMediaAreas.isEmpty {
let mediaAreasEffectView: StoryItemLoadingEffectView
if let current = self.mediaAreasEffectView {

View File

@ -13,6 +13,9 @@ swift_library(
"//submodules/Display",
"//submodules/ComponentFlow",
"//submodules/TelegramUI/Components/PlainButtonComponent",
"//submodules/Components/MultilineTextWithEntitiesComponent",
"//submodules/TextFormat",
"//submodules/AccountContext"
],
visibility = [
"//visibility:public",

View File

@ -3,18 +3,24 @@ import UIKit
import Display
import ComponentFlow
import PlainButtonComponent
import MultilineTextWithEntitiesComponent
import TextFormat
import AccountContext
public final class TabSelectorComponent: Component {
public struct Colors: Equatable {
public var foreground: UIColor
public var selection: UIColor
public var simple: Bool
public init(
foreground: UIColor,
selection: UIColor
selection: UIColor,
simple: Bool = false
) {
self.foreground = foreground
self.selection = selection
self.simple = simple
}
}
@ -45,6 +51,7 @@ public final class TabSelectorComponent: Component {
}
}
public let context: AccountContext?
public let colors: Colors
public let customLayout: CustomLayout?
public let items: [Item]
@ -53,6 +60,7 @@ public final class TabSelectorComponent: Component {
public let transitionFraction: CGFloat?
public init(
context: AccountContext? = nil,
colors: Colors,
customLayout: CustomLayout? = nil,
items: [Item],
@ -60,6 +68,7 @@ public final class TabSelectorComponent: Component {
setSelectedId: @escaping (AnyHashable) -> Void,
transitionFraction: CGFloat? = nil
) {
self.context = context
self.colors = colors
self.customLayout = customLayout
self.items = items
@ -69,6 +78,9 @@ public final class TabSelectorComponent: Component {
}
public static func ==(lhs: TabSelectorComponent, rhs: TabSelectorComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.colors != rhs.colors {
return false
}
@ -211,6 +223,7 @@ public final class TabSelectorComponent: Component {
transition: .immediate,
component: AnyComponent(PlainButtonComponent(
content: AnyComponent(ItemComponent(
context: component.context,
text: item.title,
font: itemFont,
color: component.colors.foreground,
@ -254,7 +267,7 @@ public final class TabSelectorComponent: Component {
}
itemTransition.setPosition(view: itemTitleView, position: itemTitleFrame.origin)
itemTransition.setBounds(view: itemTitleView, bounds: CGRect(origin: CGPoint(), size: itemTitleFrame.size))
itemTransition.setAlpha(view: itemTitleView, alpha: item.id == component.selectedId || isLineSelection ? 1.0 : 0.4)
itemTransition.setAlpha(view: itemTitleView, alpha: item.id == component.selectedId || isLineSelection || component.colors.simple ? 1.0 : 0.4)
}
index += 1
}
@ -302,7 +315,7 @@ public final class TabSelectorComponent: Component {
self.contentSize = CGSize(width: contentWidth, height: baseHeight + verticalInset * 2.0)
self.disablesInteractiveTransitionGestureRecognizer = contentWidth > availableSize.width
if let selectedBackgroundRect {
if let selectedBackgroundRect, self.bounds.width > 0.0 {
self.scrollRectToVisible(selectedBackgroundRect.insetBy(dx: -spacing, dy: 0.0), animated: false)
}
@ -331,6 +344,7 @@ extension CGRect {
}
private final class ItemComponent: CombinedComponent {
let context: AccountContext?
let text: String
let font: UIFont
let color: UIColor
@ -338,12 +352,14 @@ private final class ItemComponent: CombinedComponent {
let selectionFraction: CGFloat
init(
context: AccountContext?,
text: String,
font: UIFont,
color: UIColor,
selectedColor: UIColor,
selectionFraction: CGFloat
) {
self.context = context
self.text = text
self.font = font
self.color = color
@ -352,6 +368,9 @@ private final class ItemComponent: CombinedComponent {
}
static func ==(lhs: ItemComponent, rhs: ItemComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.text != rhs.text {
return false
}
@ -371,17 +390,25 @@ private final class ItemComponent: CombinedComponent {
}
static var body: Body {
let title = Child(Text.self)
let selectedTitle = Child(Text.self)
let title = Child(MultilineTextWithEntitiesComponent.self)
let selectedTitle = Child(MultilineTextWithEntitiesComponent.self)
return { context in
let component = context.component
let attributedTitle = NSMutableAttributedString(string: component.text, font: component.font, textColor: component.color)
var range = (attributedTitle.string as NSString).range(of: "⭐️")
if range.location != NSNotFound {
attributedTitle.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .stars(tinted: false)), range: range)
}
let title = title.update(
component: Text(
text: component.text,
font: component.font,
color: component.color
component: MultilineTextWithEntitiesComponent(
context: component.context,
animationCache: component.context?.animationCache,
animationRenderer: component.context?.animationRenderer,
placeholderColor: .white,
text: .plain(attributedTitle)
),
availableSize: context.availableSize,
transition: .immediate
@ -391,11 +418,19 @@ private final class ItemComponent: CombinedComponent {
.opacity(1.0 - component.selectionFraction)
)
let selectedAttributedTitle = NSMutableAttributedString(string: component.text, font: component.font, textColor: component.selectedColor)
range = (selectedAttributedTitle.string as NSString).range(of: "⭐️")
if range.location != NSNotFound {
selectedAttributedTitle.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .stars(tinted: false)), range: range)
}
let selectedTitle = selectedTitle.update(
component: Text(
text: component.text,
font: component.font,
color: component.selectedColor
component: MultilineTextWithEntitiesComponent(
context: nil,
animationCache: nil,
animationRenderer: nil,
placeholderColor: .white,
text: .plain(selectedAttributedTitle)
),
availableSize: context.availableSize,
transition: .immediate

View File

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

View File

@ -0,0 +1,65 @@
%PDF-1.7
1 0 obj
<< >>
endobj
2 0 obj
<< /Filter /FlateDecode
/Length 3 0 R
>>
stream
x<01>ÍŽ$5„ïõ{F¢Öÿi_‰3ð­]<5D>Z‰çç ÛåòL<C3B2>uiw”3<E2809D>?áúüË—þz|ùýן>ýüÇñùþ÷ø~ü}øÓõç“»?®Õz·ëÕãõ¶|^||;™XògÙ¼é,,X.ìåÈgb£µËg-Îûz#åôÕµZ@|‰©qþQÎTb²ÎbßUZ0î̱˜y<CB9C>KöBªoÎ'ììrêO[­áÓ†äÜZð7²ØìâY,eoľ°¤¸J«·e²†Z‰¡¥dIH‰5Å@\-[*`5eWõ©ÅJò.Z Ä\¾dS åÌÕçf`É×ä"»8¦¤ <E28099>Yv®K¤E ¹b—Ʋ)<½ôÄ>±ra¹cÝIënà<È®£ÓŽ]Êÿ Ú®4â•ÈH•²<E280A2>ôí*Èãˆ:FEªWÙâUÈ…„«Ü yÜÜZØ tœÍ[ØjðB ˆjãq,²ØMŸI(ªpQŒ¨D;»ˆ(;<òPðgº>Sz³[I¼ÜØJbCf+†•„|ÍHWÛb½šÛ—ƒµZN9ƒ‡cÞ¼Â[Èãøóøz|;~û¿²¯E¸ÿ-L Kþ,w¯w»¶µ{aÏI}”úGZõjn_d‡GžŽye¼Ù-
líÎ'Ôv¾lÅCü̲ÏÃYeT™ãÖ|ÒXɤ`­GrjD³!ýìx#²Ó y¦¤¹é_˜Ç²†öé3[„0³Q¾ü˜ì¦ÉfÄ鮦_QiúY„¡ŠA<1A>Xô™•È—”¤»ºÚt5Cmº'iRQ6Ò¤è±C[X"O±¿¤Ò)°ŒÝ/Q®ê)0ÇÄAtt<74>
ñ“/ˆôÀLB¦Ê $G+-$ ]$ÕA”|ŽØŽazø?ÊX<C38A>°…P4Âu¶ÛÁ1
JEuß<EFBFBD> L­³tYŠQ½½=Z^&£D"3»tÎλçáÑÌÒ¼ÖÐ6¡Í{jóÊMÂ[G8ÓZ© ÑpH"q™+µß$z´'–Ø‚*1±-Ë—‰qkøÓ²Qá KqÔ'û:bp‰¶Z&"4oÞ<07>jÌ™¢…B)è<>·Jîì2Šâ~
µ€@ÙN<-6×o¸|ÂåR%p)éžÊ§÷ ¡U îekNPCU5nìÁJJº?_¸
wC ˜á`NÇx]tÏÊ€(„É þÇÔP˜ÃOŽ/F<>T¨ké!š
 °Ñ®sáUŸ^RC®ý"N)Ñ3³Ü/œL˜<ÊD½ø¤ðæš<18>öPLŽá§¼8Ð\'î#‰…ëm<C3AB>Úqľ)¾ôˆ™K9Þ`j=<™˜òžØÚ70Æ”7H£þƒXŒÊ8µÓ[ô£G}Oá~TÞ;cÉ®vi »<C2A0>)0ˆ CŽAîÛwŠövGOõÞîñ©ÞÛmOu†z¯o*8Ô{!ô‚ QT¯ý+D½&.ÈpcPy@ 6D"òþ«GRƒÜ—ÄG„”õ1>ú~´ˆÂùæ+ i{÷-&ù£éTA(OI†Kõì²)»Ž<C2BB>ƒ©_L£RÚþRªÊgœˆãÝ­š>B¡ÇÑÐs„óæ«t<06><><EFBFBD>4@®Äfª´|¤Ïœ<C38F>l_Ô$6ʶ!³´ ¡<>dC6»Õ¨íK|µsCfˉjÐ`C±/²¦èC.BMŠ1ʃvˆ²ëäì4‡®Uv8!t P´Ð|×]©ñ×ú'Ô¿“ŸT
endstream
endobj
3 0 obj
1308
endobj
4 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 10.000000 10.000000 ]
/Resources 1 0 R
/Contents 2 0 R
/Parent 5 0 R
>>
endobj
5 0 obj
<< /Kids [ 4 0 R ]
/Count 1
/Type /Pages
>>
endobj
6 0 obj
<< /Pages 5 0 R
/Type /Catalog
>>
endobj
xref
0 7
0000000000 65535 f
0000000010 00000 n
0000000034 00000 n
0000001426 00000 n
0000001449 00000 n
0000001622 00000 n
0000001696 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 6 0 R
/Size 7
>>
startxref
1755
%%EOF

View File

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

View File

@ -0,0 +1,83 @@
%PDF-1.7
1 0 obj
<< >>
endobj
2 0 obj
<< /Length 3 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 0.000000 2.784424 cm
0.000000 0.000000 0.000000 scn
62.376457 30.839119 m
33.623550 59.592026 l
31.548130 61.667446 30.510420 62.705158 29.299419 63.447258 c
28.225752 64.105202 27.055216 64.590057 25.830782 64.884018 c
24.449732 65.215576 22.982187 65.215576 20.047100 65.215576 c
7.725484 65.215576 l
5.302220 65.215576 4.090588 65.215576 3.529531 64.736389 c
3.042711 64.320602 2.784362 63.696896 2.834592 63.058659 c
2.892483 62.323093 3.749237 61.466339 5.462745 59.752831 c
62.537258 2.678318 l
64.250763 0.964813 65.107521 0.108055 65.843079 0.050171 c
66.481316 -0.000061 67.105026 0.258286 67.520813 0.745110 c
68.000000 1.306164 68.000000 2.517796 68.000000 4.941059 c
68.000000 17.262676 l
68.000000 20.197762 68.000000 21.665306 67.668442 23.046356 c
67.374481 24.270794 66.889626 25.441330 66.231682 26.514996 c
65.489578 27.725994 64.451874 28.763702 62.376457 30.839119 c
h
f
n
Q
endstream
endobj
3 0 obj
962
endobj
4 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 68.000000 68.000000 ]
/Resources 1 0 R
/Contents 2 0 R
/Parent 5 0 R
>>
endobj
5 0 obj
<< /Kids [ 4 0 R ]
/Count 1
/Type /Pages
>>
endobj
6 0 obj
<< /Pages 5 0 R
/Type /Catalog
>>
endobj
xref
0 7
0000000000 65535 f
0000000010 00000 n
0000000034 00000 n
0000001052 00000 n
0000001074 00000 n
0000001247 00000 n
0000001321 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 6 0 R
/Size 7
>>
startxref
1380
%%EOF

View File

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

View File

@ -0,0 +1,84 @@
%PDF-1.7
1 0 obj
<< >>
endobj
2 0 obj
<< /Length 3 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 0.000000 2.784363 cm
0.000000 0.000000 0.000000 scn
52.376450 30.839190 m
33.623550 49.592087 l
31.548130 51.667507 30.510420 52.705219 29.299419 53.447319 c
28.225752 54.105263 27.055216 54.590115 25.830782 54.884075 c
24.449732 55.215637 22.982187 55.215637 20.047100 55.215637 c
7.725484 55.215637 l
5.302220 55.215637 4.090588 55.215637 3.529531 54.736450 c
3.042711 54.320667 2.784362 53.696957 2.834592 53.058720 c
2.892483 52.323154 3.749236 51.466400 5.462744 49.752892 c
52.537258 2.678379 l
54.250763 0.964874 55.107517 0.108120 55.843082 0.050228 c
56.481319 0.000000 57.105030 0.258350 57.520813 0.745167 c
58.000000 1.306225 58.000000 2.517857 58.000000 4.941120 c
58.000000 17.262737 l
58.000000 20.197823 58.000000 21.665367 57.668438 23.046417 c
57.374477 24.270853 56.889626 25.441389 56.231682 26.515057 c
55.489586 27.726049 54.451885 28.763756 52.376484 30.839149 c
52.376450 30.839190 l
h
f
n
Q
endstream
endobj
3 0 obj
983
endobj
4 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 58.000000 58.000000 ]
/Resources 1 0 R
/Contents 2 0 R
/Parent 5 0 R
>>
endobj
5 0 obj
<< /Kids [ 4 0 R ]
/Count 1
/Type /Pages
>>
endobj
6 0 obj
<< /Pages 5 0 R
/Type /Catalog
>>
endobj
xref
0 7
0000000000 65535 f
0000000010 00000 n
0000000034 00000 n
0000001073 00000 n
0000001095 00000 n
0000001268 00000 n
0000001342 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 6 0 R
/Size 7
>>
startxref
1401
%%EOF

View File

@ -2365,7 +2365,7 @@ private func extractAccountManagerState(records: AccountRecordsView<TelegramAcco
if let primary = primary {
for context in contexts {
if let context = context, context.account.id == primary {
self.openChatWhenReady(accountId: nil, peerId: peerId, threadId: nil, storyId: nil)
self.openChatWhenReady(accountId: nil, peerId: peerId, threadId: nil, storyId: nil, openAppIfAny: true)
return
}
}
@ -2373,7 +2373,7 @@ private func extractAccountManagerState(records: AccountRecordsView<TelegramAcco
for context in contexts {
if let context = context {
self.openChatWhenReady(accountId: context.account.id, peerId: peerId, threadId: nil, storyId: nil)
self.openChatWhenReady(accountId: context.account.id, peerId: peerId, threadId: nil, storyId: nil, openAppIfAny: true)
return
}
}
@ -2459,7 +2459,7 @@ private func extractAccountManagerState(records: AccountRecordsView<TelegramAcco
}))
}
private func openChatWhenReady(accountId: AccountRecordId?, peerId: PeerId, threadId: Int64?, messageId: MessageId? = nil, activateInput: Bool = false, storyId: StoryId?) {
private func openChatWhenReady(accountId: AccountRecordId?, peerId: PeerId, threadId: Int64?, messageId: MessageId? = nil, activateInput: Bool = false, storyId: StoryId?, openAppIfAny: Bool = false) {
let signal = self.sharedContextPromise.get()
|> take(1)
|> deliverOnMainQueue
@ -2478,7 +2478,7 @@ private func extractAccountManagerState(records: AccountRecordsView<TelegramAcco
}
self.openChatWhenReadyDisposable.set((signal
|> deliverOnMainQueue).start(next: { context in
context.openChatWithPeerId(peerId: peerId, threadId: threadId, messageId: messageId, activateInput: activateInput, storyId: storyId)
context.openChatWithPeerId(peerId: peerId, threadId: threadId, messageId: messageId, activateInput: activateInput, storyId: storyId, openAppIfAny: openAppIfAny)
}))
}

View File

@ -894,7 +894,7 @@ final class AuthorizedApplicationContext {
}))
}
func openChatWithPeerId(peerId: PeerId, threadId: Int64?, messageId: MessageId? = nil, activateInput: Bool = false, storyId: StoryId?) {
func openChatWithPeerId(peerId: PeerId, threadId: Int64?, messageId: MessageId? = nil, activateInput: Bool = false, storyId: StoryId?, openAppIfAny: Bool = false) {
if let storyId {
var controllers = self.rootController.viewControllers
controllers = controllers.filter { c in
@ -945,7 +945,11 @@ final class AuthorizedApplicationContext {
chatLocation = .peer(peer)
}
self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: self.rootController, context: self.context, chatLocation: chatLocation, subject: isOutgoingMessage ? messageId.flatMap { .message(id: .id($0), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil, setupReply: false) } : nil, activateInput: activateInput ? .text : nil))
if openAppIfAny, let parentController = self.rootController.viewControllers.last as? ViewController {
self.context.sharedContext.openWebApp(context: self.context, parentController: parentController, updatedPresentationData: nil, peer: peer, threadId: nil, buttonText: "", url: "", simple: true, source: .generic, skipTermsOfService: true)
} else {
self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: self.rootController, context: self.context, chatLocation: chatLocation, subject: isOutgoingMessage ? messageId.flatMap { .message(id: .id($0), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil, setupReply: false) } : nil, activateInput: activateInput ? .text : nil))
}
})
}
}

View File

@ -242,7 +242,7 @@ func openWebAppImpl(context: AccountContext, parentController: ViewController, u
|> afterDisposed {
updateProgress()
})
|> deliverOnMainQueue).startStrict(next: { [weak parentController] result in
|> deliverOnMainQueue).startStandalone(next: { [weak parentController] result in
guard let parentController else {
return
}

View File

@ -1113,12 +1113,6 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
} else {
strongSelf.present(BotReceiptController(context: strongSelf.context, messageId: message.id), in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
}
/*for attribute in message.attributes {
if let attribute = attribute as? ReplyMessageAttribute {
//strongSelf.navigateToMessage(from: message.id, to: .id(attribute.messageId))
break
}
}*/
return true
case .setChatTheme:
strongSelf.presentThemeSelection()
@ -1185,6 +1179,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
let controller = PremiumIntroScreen(context: strongSelf.context, source: .gift(from: fromPeerId, to: toPeerId, duration: duration, giftCode: nil))
strongSelf.push(controller)
return true
case .starGift:
let controller = strongSelf.context.sharedContext.makeGiftViewScreen(context: strongSelf.context, message: EngineMessage(message))
strongSelf.push(controller)
return true
case .giftStars:
let controller = strongSelf.context.sharedContext.makeStarsGiftScreen(context: strongSelf.context, message: EngineMessage(message))
strongSelf.push(controller)
@ -1280,6 +1278,15 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
standalone = true
}
if let adAttribute = message.attributes.first(where: { $0 is AdMessageAttribute }) as? AdMessageAttribute {
if let file = message.media.first(where: { $0 is TelegramMediaFile}) as? TelegramMediaFile, file.isVideo && !file.isAnimated {
strongSelf.chatDisplayNode.historyNode.adMessagesContext?.markAction(opaqueId: adAttribute.opaqueId, media: true, fullscreen: false)
} else {
strongSelf.controllerInteraction?.activateAdAction(message.id, nil, true, false)
return true
}
}
return context.sharedContext.openChatMessage(OpenChatMessageParams(context: context, updatedPresentationData: strongSelf.updatedPresentationData, chatLocation: openChatLocation, chatFilterTag: chatFilterTag, chatLocationContextHolder: strongSelf.chatLocationContextHolder, message: message, mediaIndex: params.mediaIndex, standalone: standalone, reverseMessageGalleryOrder: false, mode: mode, navigationController: strongSelf.effectiveNavigationController, dismissInput: {
self?.chatDisplayNode.dismissInput()
}, present: { c, a, i in
@ -1391,7 +1398,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}
}, openAd: { [weak self] messageId in
if let strongSelf = self {
strongSelf.controllerInteraction?.activateAdAction(messageId, nil)
strongSelf.controllerInteraction?.activateAdAction(messageId, nil, true, true)
}
}, addContact: { [weak self] phoneNumber in
if let strongSelf = self {
@ -3903,7 +3910,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
return
}
self.openWebApp(buttonText: buttonText, url: url, simple: simple, source: source)
}, activateAdAction: { [weak self] messageId, progress in
}, activateAdAction: { [weak self] messageId, progress, media, fullscreen in
guard let self, let message = self.chatDisplayNode.historyNode.messageInCurrentHistoryView(messageId), let adAttribute = message.adAttribute else {
return
}
@ -3917,7 +3924,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}
}
self.chatDisplayNode.historyNode.adMessagesContext?.markAction(opaqueId: adAttribute.opaqueId, media: false, fullscreen: false)
self.chatDisplayNode.historyNode.adMessagesContext?.markAction(opaqueId: adAttribute.opaqueId, media: media, fullscreen: fullscreen)
self.controllerInteraction?.openUrl(ChatControllerInteraction.OpenUrl(url: adAttribute.url, concealed: false, external: true, progress: progress))
}, openRequestedPeerSelection: { [weak self] messageId, peerType, buttonId, maxQuantity in
guard let self else {

View File

@ -351,7 +351,8 @@ private func extractAssociatedData(
chatThemes: [TelegramTheme],
deviceContactsNumbers: Set<String>,
isInline: Bool,
showSensitiveContent: Bool
showSensitiveContent: Bool,
starGifts: [Int64 : TelegramMediaFile]
) -> ChatMessageItemAssociatedData {
var automaticDownloadPeerId: EnginePeer.Id?
var automaticMediaDownloadPeerType: MediaAutoDownloadPeerType = .channel
@ -406,7 +407,7 @@ private func extractAssociatedData(
automaticDownloadPeerId = message.peerId
}
return ChatMessageItemAssociatedData(automaticDownloadPeerType: automaticMediaDownloadPeerType, automaticDownloadPeerId: automaticDownloadPeerId, automaticDownloadNetworkType: automaticDownloadNetworkType, preferredStoryHighQuality: preferredStoryHighQuality, isRecentActions: false, subject: subject, contactsPeerIds: contactsPeerIds, channelDiscussionGroup: channelDiscussionGroup, animatedEmojiStickers: animatedEmojiStickers, additionalAnimatedEmojiStickers: additionalAnimatedEmojiStickers, currentlyPlayingMessageId: currentlyPlayingMessageId, isCopyProtectionEnabled: isCopyProtectionEnabled, availableReactions: availableReactions, availableMessageEffects: availableMessageEffects, savedMessageTags: savedMessageTags, defaultReaction: defaultReaction, isPremium: isPremium, accountPeer: accountPeer, alwaysDisplayTranscribeButton: alwaysDisplayTranscribeButton, topicAuthorId: topicAuthorId, hasBots: hasBots, translateToLanguage: translateToLanguage, maxReadStoryId: maxReadStoryId, recommendedChannels: recommendedChannels, audioTranscriptionTrial: audioTranscriptionTrial, chatThemes: chatThemes, deviceContactsNumbers: deviceContactsNumbers, isInline: isInline, showSensitiveContent: showSensitiveContent)
return ChatMessageItemAssociatedData(automaticDownloadPeerType: automaticMediaDownloadPeerType, automaticDownloadPeerId: automaticDownloadPeerId, automaticDownloadNetworkType: automaticDownloadNetworkType, preferredStoryHighQuality: preferredStoryHighQuality, isRecentActions: false, subject: subject, contactsPeerIds: contactsPeerIds, channelDiscussionGroup: channelDiscussionGroup, animatedEmojiStickers: animatedEmojiStickers, additionalAnimatedEmojiStickers: additionalAnimatedEmojiStickers, currentlyPlayingMessageId: currentlyPlayingMessageId, isCopyProtectionEnabled: isCopyProtectionEnabled, availableReactions: availableReactions, availableMessageEffects: availableMessageEffects, savedMessageTags: savedMessageTags, defaultReaction: defaultReaction, isPremium: isPremium, accountPeer: accountPeer, alwaysDisplayTranscribeButton: alwaysDisplayTranscribeButton, topicAuthorId: topicAuthorId, hasBots: hasBots, translateToLanguage: translateToLanguage, maxReadStoryId: maxReadStoryId, recommendedChannels: recommendedChannels, audioTranscriptionTrial: audioTranscriptionTrial, chatThemes: chatThemes, deviceContactsNumbers: deviceContactsNumbers, isInline: isInline, showSensitiveContent: showSensitiveContent, starGifts: starGifts)
}
private extension ChatHistoryLocationInput {
@ -1610,6 +1611,17 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto
}
|> distinctUntilChanged
let starGifts: Signal<[Int64 : TelegramMediaFile], NoError> = context.engine.payments.cachedStarGifts()
|> map { gifts in
var files: [Int64 : TelegramMediaFile] = [:]
if let gifts {
for gift in gifts {
files[gift.id] = gift.file
}
}
return files
}
let messageViewQueue = Queue.mainQueue()
let historyViewTransitionDisposable = combineLatest(queue: messageViewQueue,
historyViewUpdate,
@ -1636,8 +1648,9 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto
audioTranscriptionTrial,
chatThemes,
deviceContactsNumbers,
contentSettings
).startStrict(next: { [weak self] update, chatPresentationData, selectedMessages, updatingMedia, networkType, preferredStoryHighQuality, animatedEmojiStickers, additionalAnimatedEmojiStickers, customChannelDiscussionReadState, customThreadOutgoingReadState, availableReactions, availableMessageEffects, savedMessageTags, defaultReaction, accountPeer, suggestAudioTranscription, promises, topicAuthorId, translationState, maxReadStoryId, recommendedChannels, audioTranscriptionTrial, chatThemes, deviceContactsNumbers, contentSettings in
contentSettings,
starGifts
).startStrict(next: { [weak self] update, chatPresentationData, selectedMessages, updatingMedia, networkType, preferredStoryHighQuality, animatedEmojiStickers, additionalAnimatedEmojiStickers, customChannelDiscussionReadState, customThreadOutgoingReadState, availableReactions, availableMessageEffects, savedMessageTags, defaultReaction, accountPeer, suggestAudioTranscription, promises, topicAuthorId, translationState, maxReadStoryId, recommendedChannels, audioTranscriptionTrial, chatThemes, deviceContactsNumbers, contentSettings, starGifts in
let (historyAppearsCleared, pendingUnpinnedAllMessages, pendingRemovedMessages, currentlyPlayingMessageIdAndType, scrollToMessageId, chatHasBots, allAdMessages) = promises
func applyHole() {
@ -1856,7 +1869,7 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto
translateToLanguage = languageCode
}
let associatedData = extractAssociatedData(chatLocation: chatLocation, view: view, automaticDownloadNetworkType: networkType, preferredStoryHighQuality: preferredStoryHighQuality, animatedEmojiStickers: animatedEmojiStickers, additionalAnimatedEmojiStickers: additionalAnimatedEmojiStickers, subject: subject, currentlyPlayingMessageId: currentlyPlayingMessageIdAndType?.0, isCopyProtectionEnabled: isCopyProtectionEnabled, availableReactions: availableReactions, availableMessageEffects: availableMessageEffects, savedMessageTags: savedMessageTags, defaultReaction: defaultReaction, isPremium: isPremium, alwaysDisplayTranscribeButton: alwaysDisplayTranscribeButton, accountPeer: accountPeer, topicAuthorId: topicAuthorId, hasBots: chatHasBots, translateToLanguage: translateToLanguage, maxReadStoryId: maxReadStoryId, recommendedChannels: recommendedChannels, audioTranscriptionTrial: audioTranscriptionTrial, chatThemes: chatThemes, deviceContactsNumbers: deviceContactsNumbers, isInline: !rotated, showSensitiveContent: contentSettings.ignoreContentRestrictionReasons.contains("sensitive"))
let associatedData = extractAssociatedData(chatLocation: chatLocation, view: view, automaticDownloadNetworkType: networkType, preferredStoryHighQuality: preferredStoryHighQuality, animatedEmojiStickers: animatedEmojiStickers, additionalAnimatedEmojiStickers: additionalAnimatedEmojiStickers, subject: subject, currentlyPlayingMessageId: currentlyPlayingMessageIdAndType?.0, isCopyProtectionEnabled: isCopyProtectionEnabled, availableReactions: availableReactions, availableMessageEffects: availableMessageEffects, savedMessageTags: savedMessageTags, defaultReaction: defaultReaction, isPremium: isPremium, alwaysDisplayTranscribeButton: alwaysDisplayTranscribeButton, accountPeer: accountPeer, topicAuthorId: topicAuthorId, hasBots: chatHasBots, translateToLanguage: translateToLanguage, maxReadStoryId: maxReadStoryId, recommendedChannels: recommendedChannels, audioTranscriptionTrial: audioTranscriptionTrial, chatThemes: chatThemes, deviceContactsNumbers: deviceContactsNumbers, isInline: !rotated, showSensitiveContent: contentSettings.ignoreContentRestrictionReasons.contains("sensitive"), starGifts: starGifts)
var includeEmbeddedSavedChatInfo = false
if case let .replyThread(message) = chatLocation, message.peerId == context.account.peerId, !rotated {

View File

@ -165,7 +165,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu
}, openLargeEmojiInfo: { _, _, _ in
}, openJoinLink: { _ in
}, openWebView: { _, _, _, _ in
}, activateAdAction: { _, _ in
}, activateAdAction: { _, _, _, _ in
}, openRequestedPeerSelection: { _, _, _, _ in
}, saveMediaToFiles: { _ in
}, openNoAdsDemo: {

View File

@ -70,6 +70,8 @@ import StarsTransferScreen
import StarsTransactionScreen
import StarsWithdrawalScreen
import MiniAppListScreen
import GiftOptionsScreen
import GiftViewScreen
private final class AccountUserInterfaceInUseContext {
let subscribers = Bag<(Bool) -> Void>()
@ -1776,7 +1778,7 @@ public final class SharedAccountContextImpl: SharedAccountContext {
}, openLargeEmojiInfo: { _, _, _ in
}, openJoinLink: { _ in
}, openWebView: { _, _, _, _ in
}, activateAdAction: { _, _ in
}, activateAdAction: { _, _, _, _ in
}, openRequestedPeerSelection: { _, _, _, _ in
}, saveMediaToFiles: { _ in
}, openNoAdsDemo: {
@ -2202,26 +2204,22 @@ public final class SharedAccountContextImpl: SharedAccountContext {
public func makePremiumGiftController(context: AccountContext, source: PremiumGiftSource, completion: (([EnginePeer.Id]) -> Void)?) -> ViewController {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let limit: Int32 = 10
var reachedLimitImpl: ((Int32) -> Void)?
// let limit: Int32 = 10
// var reachedLimitImpl: ((Int32) -> Void)?
var presentBirthdayPickerImpl: (() -> Void)?
let mode: ContactMultiselectionControllerMode
var starsMode: ContactSelectionControllerMode = .generic
var currentBirthdays: [EnginePeer.Id: TelegramBirthday]?
if case let .chatList(birthdays) = source, let birthdays, !birthdays.isEmpty {
mode = .premiumGifting(birthdays: birthdays, selectToday: true, hasActions: true)
starsMode = .starsGifting(birthdays: birthdays, hasActions: true)
currentBirthdays = birthdays
} else if case let .settings(birthdays) = source, let birthdays, !birthdays.isEmpty {
mode = .premiumGifting(birthdays: birthdays, selectToday: false, hasActions: true)
currentBirthdays = birthdays
} else if case let .stars(birthdays) = source {
mode = .premiumGifting(birthdays: birthdays, selectToday: false, hasActions: false)
starsMode = .starsGifting(birthdays: birthdays, hasActions: false)
starsMode = .starsGifting(birthdays: birthdays, hasActions: true)
currentBirthdays = birthdays
} else {
mode = .premiumGifting(birthdays: nil, selectToday: false, hasActions: true)
starsMode = .starsGifting(birthdays: nil, hasActions: true)
}
let contactOptions: Signal<[ContactListAdditionalOption], NoError>
if currentBirthdays != nil || "".isEmpty {
contactOptions = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Birthday(id: context.account.peerId))
@ -2247,104 +2245,122 @@ public final class SharedAccountContextImpl: SharedAccountContext {
var openProfileImpl: ((EnginePeer) -> Void)?
var sendMessageImpl: ((EnginePeer) -> Void)?
//TODO:localize
let controller: ViewController
if case .stars = source {
let options = Promise<[StarsGiftOption]>()
options.set(context.engine.payments.starsGiftOptions(peerId: nil))
// if case .stars = source {
// let options = Promise<[StarsGiftOption]>()
// options.set(context.engine.payments.starsGiftOptions(peerId: nil))
let options = Promise<[PremiumGiftCodeOption]>()
options.set(context.engine.payments.premiumGiftCodeOptions(peerId: nil))
let contactsController = context.sharedContext.makeContactSelectionController(ContactSelectionControllerParams(
context: context,
mode: starsMode,
autoDismiss: false,
title: { strings in return strings.Stars_Purchase_GiftStars },
options: contactOptions
))
let _ = (contactsController.result
|> deliverOnMainQueue).start(next: { result in
if let (peers, _, _, _, _, _) = result, let contactPeer = peers.first, case let .peer(peer, _, _) = contactPeer {
completion?([peer.id])
title: { strings in return "Gift Premium or Stars" },
options: contactOptions,
openProfile: { peer in
openProfileImpl?(peer)
},
sendMessage: { peer in
sendMessageImpl?(peer)
}
})
controller = contactsController
} else {
let options = Promise<[PremiumGiftCodeOption]>()
options.set(context.engine.payments.premiumGiftCodeOptions(peerId: nil))
let contactsController = context.sharedContext.makeContactMultiselectionController(
ContactMultiselectionControllerParams(
context: context,
mode: mode,
options: contactOptions,
isPeerEnabled: { peer in
if case let .user(user) = peer, user.botInfo == nil && !peer.isService && !user.flags.contains(.isSupport) {
return true
} else {
return false
}
},
limit: limit,
reachedLimit: { limit in
reachedLimitImpl?(limit)
},
openProfile: { peer in
openProfileImpl?(peer)
},
sendMessage: { peer in
sendMessageImpl?(peer)
}
)
)
))
let _ = combineLatest(queue: Queue.mainQueue(), contactsController.result, options.get())
.startStandalone(next: { [weak contactsController] result, options in
guard let controller = contactsController else {
return
}
var peerIds: [PeerId] = []
if case let .result(peerIdsValue, _) = result {
peerIds = peerIdsValue.compactMap({ peerId in
if case let .peer(peerId) = peerId {
return peerId
} else {
return nil
}
})
}
guard !peerIds.isEmpty else {
return
}
let mappedOptions = options.filter { $0.users == 1 }.map { CachedPremiumGiftOption(months: $0.months, currency: $0.currency, amount: $0.amount, botUrl: "", storeProductId: $0.storeProductId) }
var pushImpl: ((ViewController) -> Void)?
var filterImpl: (() -> Void)?
let giftController = PremiumGiftScreen(context: context, peerIds: peerIds, options: mappedOptions, source: source, pushController: { c in
pushImpl?(c)
}, completion: {
filterImpl?()
if let (peers, _, _, _, _, _) = result, let contactPeer = peers.first, case let .peer(peer, _, _) = contactPeer {
let premiumOptions = options.filter { $0.users == 1 }.map { CachedPremiumGiftOption(months: $0.months, currency: $0.currency, amount: $0.amount, botUrl: "", storeProductId: $0.storeProductId) }
let giftController = GiftOptionsScreen(context: context, peerId: peer.id, premiumOptions: premiumOptions)
giftController.navigationPresentation = .modal
contactsController?.push(giftController)
// completion?([peer.id])
if case .chatList = source, let _ = currentBirthdays {
let _ = context.engine.notices.dismissServerProvidedSuggestion(suggestion: .todayBirthdays).startStandalone()
}
})
pushImpl = { [weak giftController] c in
giftController?.push(c)
}
filterImpl = { [weak giftController] in
if let navigationController = giftController?.navigationController as? NavigationController {
var controllers = navigationController.viewControllers
controllers = controllers.filter { !($0 is ContactMultiselectionController) && !($0 is PremiumGiftScreen) }
navigationController.setViewControllers(controllers, animated: true)
}
}
controller.push(giftController)
})
controller = contactsController
}
// } else {
// let options = Promise<[PremiumGiftCodeOption]>()
// options.set(context.engine.payments.premiumGiftCodeOptions(peerId: nil))
// let contactsController = context.sharedContext.makeContactMultiselectionController(
// ContactMultiselectionControllerParams(
// context: context,
// mode: mode,
// options: contactOptions,
// isPeerEnabled: { peer in
// if case let .user(user) = peer, user.botInfo == nil && !peer.isService && !user.flags.contains(.isSupport) {
// return true
// } else {
// return false
// }
// },
// limit: limit,
// reachedLimit: { limit in
// reachedLimitImpl?(limit)
// },
// openProfile: { peer in
// openProfileImpl?(peer)
// },
// sendMessage: { peer in
// sendMessageImpl?(peer)
// }
// )
// )
// let _ = combineLatest(queue: Queue.mainQueue(), contactsController.result, options.get())
// .startStandalone(next: { [weak contactsController] result, options in
// guard let controller = contactsController else {
// return
// }
// var peerIds: [PeerId] = []
// if case let .result(peerIdsValue, _) = result {
// peerIds = peerIdsValue.compactMap({ peerId in
// if case let .peer(peerId) = peerId {
// return peerId
// } else {
// return nil
// }
// })
// }
// guard !peerIds.isEmpty else {
// return
// }
//
// let mappedOptions = options.filter { $0.users == 1 }.map { CachedPremiumGiftOption(months: $0.months, currency: $0.currency, amount: $0.amount, botUrl: "", storeProductId: $0.storeProductId) }
// var pushImpl: ((ViewController) -> Void)?
// var filterImpl: (() -> Void)?
// let giftController = PremiumGiftScreen(context: context, peerIds: peerIds, options: mappedOptions, source: source, pushController: { c in
// pushImpl?(c)
// }, completion: {
// filterImpl?()
//
// if case .chatList = source, let _ = currentBirthdays {
// let _ = context.engine.notices.dismissServerProvidedSuggestion(suggestion: .todayBirthdays).startStandalone()
// }
// })
// pushImpl = { [weak giftController] c in
// giftController?.push(c)
// }
// filterImpl = { [weak giftController] in
// if let navigationController = giftController?.navigationController as? NavigationController {
// var controllers = navigationController.viewControllers
// controllers = controllers.filter { !($0 is ContactMultiselectionController) && !($0 is PremiumGiftScreen) }
// navigationController.setViewControllers(controllers, animated: true)
// }
// }
// controller.push(giftController)
// })
// controller = contactsController
// }
reachedLimitImpl = { [weak controller] limit in
guard let controller else {
return
}
HapticFeedback().error()
controller.present(UndoOverlayController(presentationData: presentationData, content: .info(title: nil, text: presentationData.strings.Premium_Gift_ContactSelection_MaximumReached("\(limit)").string, timeout: nil, customUndoText: nil), elevatedLayout: true, position: .bottom, animateInAsReplacement: false, action: { _ in return false }), in: .current)
}
// reachedLimitImpl = { [weak controller] limit in
// guard let controller else {
// return
// }
// HapticFeedback().error()
// controller.present(UndoOverlayController(presentationData: presentationData, content: .info(title: nil, text: presentationData.strings.Premium_Gift_ContactSelection_MaximumReached("\(limit)").string, timeout: nil, customUndoText: nil), elevatedLayout: true, position: .bottom, animateInAsReplacement: false, action: { _ in return false }), in: .current)
// }
sendMessageImpl = { [weak self, weak controller] peer in
guard let self, let controller, let navigationController = controller.navigationController as? NavigationController else {
@ -2795,6 +2811,10 @@ public final class SharedAccountContextImpl: SharedAccountContext {
return StarsTransactionScreen(context: context, subject: .boost(peerId, boost))
}
public func makeGiftViewScreen(context: AccountContext, message: EngineMessage) -> ViewController {
return GiftViewScreen(context: context, subject: .message(message))
}
public func makeMiniAppListScreenInitialData(context: AccountContext) -> Signal<MiniAppListScreenInitialData, NoError> {
return MiniAppListScreen.initialData(context: context)
}

View File

@ -186,12 +186,38 @@ private func manageableSpotlightContacts(appBasePath: String, accounts: Signal<[
return accounts
|> mapToSignal { accounts -> Signal<[[EnginePeer.Id: SpotlightIndexStorageItem]], NoError> in
return combineLatest(queue: queue, accounts.map { account -> Signal<[EnginePeer.Id: SpotlightIndexStorageItem], NoError> in
return TelegramEngine(account: account).data.subscribe(
TelegramEngine.EngineData.Item.Contacts.List(includePresences: false)
let engine = TelegramEngine(account: account)
let recentApps = engine.peers.recentApps()
|> mapToSignal { peerIds -> Signal<[EnginePeer], NoError> in
return engine.data.get(
EngineDataMap(
peerIds.map { peerId -> TelegramEngine.EngineData.Item.Peer.Peer in
return TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)
}
)
)
|> map { result -> [EnginePeer] in
var peers: [EnginePeer] = []
for (_, maybePeer) in result {
if let peer = maybePeer {
peers.append(peer)
}
}
return peers
}
}
return combineLatest(
engine.data.subscribe(
TelegramEngine.EngineData.Item.Contacts.List(includePresences: false)
),
recentApps
)
|> map { view -> [EnginePeer.Id: SpotlightIndexStorageItem] in
|> map { view, recentApps -> [EnginePeer.Id: SpotlightIndexStorageItem] in
var result: [EnginePeer.Id: SpotlightIndexStorageItem] = [:]
for peer in view.peers {
var peers: [EnginePeer] = []
peers.append(contentsOf: view.peers)
peers.append(contentsOf: recentApps)
for peer in peers {
if case let .user(user) = peer {
let avatarSourcePath = smallestImageRepresentation(user.photo).flatMap { representation -> String? in
let resourcePath = account.postbox.mediaBox.resourcePath(representation.resource)

View File

@ -409,6 +409,7 @@ public final class ChatTextInputTextCustomEmojiAttribute: NSObject, Codable {
case nameColors([UInt32])
case stars(tinted: Bool)
case ton
case animation(name: String)
}
public let interactivelySelectedFromPackId: ItemCollectionId?