mirror of
https://github.com/TelegramMessenger/Telegram-iOS.git
synced 2026-07-06 03:33:41 +02:00
Various fixes
This commit is contained in:
parent
4a44c61c8d
commit
aff6322def
62 changed files with 1386 additions and 819 deletions
|
|
@ -305,6 +305,7 @@ public enum ResolvedUrl {
|
|||
case startAttach(peerId: PeerId, payload: String?, choose: ResolvedBotChoosePeerTypes?)
|
||||
case invoice(slug: String, invoice: TelegramMediaInvoice?)
|
||||
case premiumOffer(reference: String?)
|
||||
case starsTopup(amount: Int64?)
|
||||
case chatFolder(slug: String)
|
||||
case story(peerId: PeerId, id: Int32)
|
||||
case boost(peerId: PeerId?, status: ChannelBoostStatus?, myBoostStatus: MyBoostStatus?)
|
||||
|
|
@ -755,7 +756,7 @@ public class MediaEditorTransitionOutExternalState {
|
|||
}
|
||||
|
||||
public protocol MediaEditorScreenResult {
|
||||
|
||||
var target: Stories.PendingTarget { get }
|
||||
}
|
||||
|
||||
public protocol TelegramRootControllerInterface: NavigationController {
|
||||
|
|
@ -963,7 +964,8 @@ public protocol SharedAccountContext: AnyObject {
|
|||
func presentContactsWarningSuppression(context: AccountContext, present: (ViewController, Any?) -> Void)
|
||||
func openImagePicker(context: AccountContext, completion: @escaping (UIImage) -> Void, present: @escaping (ViewController) -> Void)
|
||||
func openAddPeerMembers(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?, parentController: ViewController, groupPeer: Peer, selectAddMemberDisposable: MetaDisposable, addMemberDisposable: MetaDisposable)
|
||||
func openChatInstantPage(context: AccountContext, message: Message, sourcePeerType: MediaAutoDownloadPeerType?, navigationController: NavigationController)
|
||||
func makeInstantPageController(context: AccountContext, message: Message, sourcePeerType: MediaAutoDownloadPeerType?) -> ViewController?
|
||||
func makeInstantPageController(context: AccountContext, webPage: TelegramMediaWebpage, anchor: String?, sourceLocation: InstantPageSourceLocation) -> ViewController
|
||||
func openChatWallpaper(context: AccountContext, message: Message, present: @escaping (ViewController, Any?) -> Void)
|
||||
|
||||
func makeRecentSessionsController(context: AccountContext, activeSessionsContext: ActiveSessionsContext) -> ViewController & RecentSessionsController
|
||||
|
|
|
|||
|
|
@ -292,12 +292,12 @@ public struct ChatControllerInitialAttachBotStart {
|
|||
}
|
||||
|
||||
public struct ChatControllerInitialBotAppStart {
|
||||
public let botApp: BotApp
|
||||
public let botApp: BotApp?
|
||||
public let payload: String?
|
||||
public let justInstalled: Bool
|
||||
public let compact: Bool
|
||||
|
||||
public init(botApp: BotApp, payload: String?, justInstalled: Bool, compact: Bool) {
|
||||
public init(botApp: BotApp?, payload: String?, justInstalled: Bool, compact: Bool) {
|
||||
self.botApp = botApp
|
||||
self.payload = payload
|
||||
self.justInstalled = justInstalled
|
||||
|
|
|
|||
|
|
@ -123,7 +123,7 @@ public enum BoostSubject: Equatable {
|
|||
}
|
||||
|
||||
public enum StarsPurchasePurpose: Equatable {
|
||||
case generic
|
||||
case generic(requiredStars: Int64?)
|
||||
case transfer(peerId: EnginePeer.Id, requiredStars: Int64)
|
||||
case subscription(peerId: EnginePeer.Id, requiredStars: Int64, renew: Bool)
|
||||
case gift(peerId: EnginePeer.Id)
|
||||
|
|
|
|||
|
|
@ -653,7 +653,7 @@ public final class DefaultAnimatedStickerNodeImpl: ASDisplayNode, AnimatedSticke
|
|||
|
||||
strongSelf.frameUpdated(frame.index, frame.totalFrames)
|
||||
strongSelf.currentFrameIndex = frame.index
|
||||
strongSelf.currentFrameCount = frame.totalFrames;
|
||||
strongSelf.currentFrameCount = frame.totalFrames
|
||||
strongSelf.currentFrameRate = frameRate
|
||||
|
||||
if frame.isLastFrame {
|
||||
|
|
|
|||
|
|
@ -713,7 +713,7 @@ public class AttachmentController: ViewController, MinimizableController {
|
|||
}
|
||||
if case .ended = recognizer.state {
|
||||
if let lastController = self.currentControllers.last {
|
||||
if let controller = self.controller, controller.shouldMinimizeOnSwipe?(self.currentType) == true {
|
||||
if let controller = self.controller, let layout = self.validLayout, !layout.metrics.isTablet, controller.shouldMinimizeOnSwipe?(self.currentType) == true {
|
||||
self.minimize()
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ swift_library(
|
|||
"//submodules/SearchBarNode",
|
||||
"//submodules/TelegramUI/Components/SaveProgressScreen",
|
||||
"//submodules/TelegramUI/Components/ListActionItemComponent",
|
||||
"//submodules/Utils/DeviceModel",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
|
|
|||
|
|
@ -281,15 +281,7 @@ final class AddressBarContentComponent: Component {
|
|||
}
|
||||
let isActive = self.textField?.isFirstResponder ?? false
|
||||
|
||||
var title: String = ""
|
||||
if let parsedUrl = URL(string: component.url) {
|
||||
title = parsedUrl.host ?? component.url
|
||||
if title.hasPrefix("www.") {
|
||||
title.removeSubrange(title.startIndex ..< title.index(title.startIndex, offsetBy: 4))
|
||||
}
|
||||
title = title.idnaDecoded ?? title
|
||||
}
|
||||
|
||||
let title = getDisplayUrl(component.url, hostOnly: true)
|
||||
self.update(theme: component.theme, strings: component.strings, size: availableSize, isActive: isActive, title: title.lowercased(), isSecure: component.isSecure, collapseFraction: collapseFraction, isTablet: component.metrics.isTablet, transition: transition)
|
||||
|
||||
return availableSize
|
||||
|
|
@ -449,19 +441,7 @@ final class AddressBarContentComponent: Component {
|
|||
textField.addTarget(self, action: #selector(self.textFieldChanged(_:)), for: .editingChanged)
|
||||
}
|
||||
|
||||
var address = self.component?.url ?? ""
|
||||
if let components = URLComponents(string: address) {
|
||||
if #available(iOS 16.0, *), let encodedHost = components.encodedHost {
|
||||
if let decodedHost = components.host, encodedHost != decodedHost {
|
||||
address = address.replacingOccurrences(of: encodedHost, with: decodedHost)
|
||||
}
|
||||
} else if let encodedHost = components.host {
|
||||
if let decodedHost = components.host?.idnaDecoded, encodedHost != decodedHost {
|
||||
address = address.replacingOccurrences(of: encodedHost, with: decodedHost)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let address = getDisplayUrl(self.component?.url ?? "", trim: false)
|
||||
if textField.text != address {
|
||||
textField.text = address
|
||||
self.clearIconView.isHidden = address.isEmpty
|
||||
|
|
|
|||
|
|
@ -176,23 +176,7 @@ final class BrowserAddressListItemComponent: Component {
|
|||
if case let .Loaded(content) = component.webPage.content {
|
||||
title = content.title ?? content.url
|
||||
|
||||
var address = content.url
|
||||
if let components = URLComponents(string: address) {
|
||||
if #available(iOS 16.0, *), let encodedHost = components.encodedHost {
|
||||
if let decodedHost = components.host, encodedHost != decodedHost {
|
||||
address = address.replacingOccurrences(of: encodedHost, with: decodedHost)
|
||||
}
|
||||
} else if let encodedHost = components.host {
|
||||
if let decodedHost = components.host?.idnaDecoded, encodedHost != decodedHost {
|
||||
address = address.replacingOccurrences(of: encodedHost, with: decodedHost)
|
||||
}
|
||||
}
|
||||
}
|
||||
address = address.replacingOccurrences(of: "https://www.", with: "")
|
||||
address = address.replacingOccurrences(of: "https://", with: "")
|
||||
address = address.replacingOccurrences(of: "tonsite://", with: "")
|
||||
address = address.trimmingCharacters(in: CharacterSet(charactersIn: "/"))
|
||||
subtitle = address
|
||||
subtitle = getDisplayUrl(content.url)
|
||||
|
||||
parsedUrl = URL(string: content.url)
|
||||
|
||||
|
|
|
|||
|
|
@ -367,7 +367,7 @@ public final class BrowserBookmarksScreen: ViewController {
|
|||
|
||||
self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Back, style: .plain, target: nil, action: nil)
|
||||
|
||||
self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(self.cancelPressed))
|
||||
self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Close, style: .plain, target: self, action: #selector(self.cancelPressed))
|
||||
self.title = self.presentationData.strings.WebBrowser_Bookmarks_Title
|
||||
|
||||
self.searchContentNode = NavigationBarSearchContentNode(theme: self.presentationData.theme, placeholder: self.presentationData.strings.Common_Search, activate: { [weak self] in
|
||||
|
|
|
|||
|
|
@ -0,0 +1,299 @@
|
|||
import Foundation
|
||||
import UIKit
|
||||
import SwiftSignalKit
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import TelegramPresentationData
|
||||
import TelegramUIPreferences
|
||||
import AccountContext
|
||||
import AppBundle
|
||||
import PhotoResources
|
||||
import CheckNode
|
||||
import Markdown
|
||||
|
||||
private let textFont = Font.regular(13.0)
|
||||
private let boldTextFont = Font.semibold(13.0)
|
||||
|
||||
private func formattedText(_ text: String, color: UIColor, textAlignment: NSTextAlignment = .natural) -> NSAttributedString {
|
||||
return parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: color), bold: MarkdownAttributeSet(font: boldTextFont, textColor: color), link: MarkdownAttributeSet(font: textFont, textColor: color), linkAttribute: { _ in return nil}), textAlignment: textAlignment)
|
||||
}
|
||||
|
||||
private final class BrowserExceptionDomainAlertContentNode: AlertContentNode {
|
||||
private let strings: PresentationStrings
|
||||
private let domain: String
|
||||
|
||||
private let titleNode: ASTextNode
|
||||
private let textNode: ASTextNode
|
||||
|
||||
private let allowWriteCheckNode: InteractiveCheckNode
|
||||
private let allowWriteLabelNode: ASTextNode
|
||||
|
||||
private let actionNodesSeparator: ASDisplayNode
|
||||
private let actionNodes: [TextAlertContentActionNode]
|
||||
private let actionVerticalSeparators: [ASDisplayNode]
|
||||
|
||||
private var validLayout: CGSize?
|
||||
|
||||
private var iconDisposable: Disposable?
|
||||
|
||||
override var dismissOnOutsideTap: Bool {
|
||||
return self.isUserInteractionEnabled
|
||||
}
|
||||
|
||||
var allowWriteAccess: Bool = true {
|
||||
didSet {
|
||||
self.allowWriteCheckNode.setSelected(self.allowWriteAccess, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
init(account: Account, theme: AlertControllerTheme, ptheme: PresentationTheme, strings: PresentationStrings, domain: String, requestWriteAccess: Bool, actions: [TextAlertAction]) {
|
||||
self.strings = strings
|
||||
self.domain = domain
|
||||
|
||||
self.titleNode = ASTextNode()
|
||||
self.titleNode.maximumNumberOfLines = 0
|
||||
|
||||
self.textNode = ASTextNode()
|
||||
self.textNode.maximumNumberOfLines = 0
|
||||
|
||||
self.allowWriteCheckNode = InteractiveCheckNode(theme: CheckNodeTheme(backgroundColor: theme.accentColor, strokeColor: theme.contrastColor, borderColor: theme.controlBorderColor, overlayBorder: false, hasInset: false, hasShadow: false))
|
||||
self.allowWriteCheckNode.setSelected(true, animated: false)
|
||||
self.allowWriteLabelNode = ASTextNode()
|
||||
self.allowWriteLabelNode.maximumNumberOfLines = 4
|
||||
self.allowWriteLabelNode.isUserInteractionEnabled = 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)
|
||||
|
||||
if requestWriteAccess {
|
||||
self.addSubnode(self.allowWriteCheckNode)
|
||||
self.addSubnode(self.allowWriteLabelNode)
|
||||
}
|
||||
|
||||
self.addSubnode(self.actionNodesSeparator)
|
||||
|
||||
for actionNode in self.actionNodes {
|
||||
self.addSubnode(actionNode)
|
||||
}
|
||||
|
||||
for separatorNode in self.actionVerticalSeparators {
|
||||
self.addSubnode(separatorNode)
|
||||
}
|
||||
|
||||
self.allowWriteCheckNode.valueChanged = { [weak self] value in
|
||||
if let strongSelf = self {
|
||||
strongSelf.allowWriteAccess = !strongSelf.allowWriteAccess
|
||||
}
|
||||
}
|
||||
|
||||
self.updateTheme(theme)
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.iconDisposable?.dispose()
|
||||
}
|
||||
|
||||
override func didLoad() {
|
||||
super.didLoad()
|
||||
|
||||
self.allowWriteLabelNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.allowWriteTap(_:))))
|
||||
}
|
||||
|
||||
@objc private func allowWriteTap(_ gestureRecognizer: UITapGestureRecognizer) {
|
||||
if self.allowWriteCheckNode.isUserInteractionEnabled {
|
||||
self.allowWriteAccess = !self.allowWriteAccess
|
||||
}
|
||||
}
|
||||
|
||||
override func updateTheme(_ theme: AlertControllerTheme) {
|
||||
self.titleNode.attributedText = NSAttributedString(string: "Open in Browser", font: Font.bold(17.0), textColor: theme.primaryColor, paragraphAlignment: .center)
|
||||
self.textNode.attributedText = NSAttributedString(string: "Do you want to open this link in your default browser?", font: Font.regular(13.0), textColor: theme.primaryColor, paragraphAlignment: .center)
|
||||
|
||||
self.allowWriteLabelNode.attributedText = formattedText("Always open links from **\(self.domain)** in browser", color: theme.primaryColor)
|
||||
|
||||
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)
|
||||
|
||||
self.validLayout = size
|
||||
|
||||
var origin: CGPoint = CGPoint(x: 0.0, y: 20.0)
|
||||
|
||||
let titleSize = self.titleNode.measure(size)
|
||||
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 + 13.0
|
||||
|
||||
let textSize = self.textNode.measure(size)
|
||||
var textFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - textSize.width) / 2.0), y: origin.y), size: textSize)
|
||||
origin.y += textSize.height
|
||||
|
||||
var entriesHeight: CGFloat = 0.0
|
||||
|
||||
if self.allowWriteLabelNode.supernode != nil {
|
||||
origin.y += 16.0
|
||||
entriesHeight += 16.0
|
||||
|
||||
let checkSize = CGSize(width: 22.0, height: 22.0)
|
||||
let condensedSize = CGSize(width: size.width - 76.0, height: size.height)
|
||||
|
||||
let allowWriteSize = self.allowWriteLabelNode.measure(condensedSize)
|
||||
transition.updateFrame(node: self.allowWriteLabelNode, frame: CGRect(origin: CGPoint(x: 46.0, y: origin.y), size: allowWriteSize))
|
||||
transition.updateFrame(node: self.allowWriteCheckNode, frame: CGRect(origin: CGPoint(x: 12.0, y: origin.y - 2.0), size: checkSize))
|
||||
origin.y += allowWriteSize.height
|
||||
entriesHeight += allowWriteSize.height
|
||||
}
|
||||
|
||||
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: 18.0, right: 18.0)
|
||||
|
||||
var contentWidth = max(textSize.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 + entriesHeight + actionsHeight + 17.0 + 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
|
||||
}
|
||||
|
||||
textFrame.origin.x = floorToScreenPixels((resultSize.width - textFrame.width) / 2.0)
|
||||
transition.updateFrame(node: self.textNode, frame: textFrame)
|
||||
|
||||
return resultSize
|
||||
}
|
||||
}
|
||||
|
||||
public func browserExceptionDomainAlertController(context: AccountContext, domain: String, completion: @escaping (Bool) -> Void) -> AlertController {
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
let theme = presentationData.theme
|
||||
let strings = presentationData.strings
|
||||
|
||||
var dismissImpl: ((Bool) -> Void)?
|
||||
var getContentNodeImpl: (() -> BrowserExceptionDomainAlertContentNode?)?
|
||||
let actions: [TextAlertAction] = [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {
|
||||
dismissImpl?(true)
|
||||
}), TextAlertAction(type: .defaultAction, title: "Continue", action: {
|
||||
if let allowWriteAccess = getContentNodeImpl?()?.allowWriteAccess {
|
||||
completion(allowWriteAccess)
|
||||
} else {
|
||||
completion(false)
|
||||
}
|
||||
dismissImpl?(true)
|
||||
})]
|
||||
|
||||
let contentNode = BrowserExceptionDomainAlertContentNode(account: context.account, theme: AlertControllerTheme(presentationData: presentationData), ptheme: theme, strings: strings, domain: domain, requestWriteAccess: true, actions: actions)
|
||||
getContentNodeImpl = { [weak contentNode] in
|
||||
return contentNode
|
||||
}
|
||||
|
||||
let controller = AlertController(theme: AlertControllerTheme(presentationData: presentationData), contentNode: contentNode)
|
||||
dismissImpl = { [weak controller] animated in
|
||||
if animated {
|
||||
controller?.dismissAnimated()
|
||||
} else {
|
||||
controller?.dismiss()
|
||||
}
|
||||
}
|
||||
return controller
|
||||
}
|
||||
|
|
@ -414,8 +414,7 @@ final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDeleg
|
|||
if let state = self.initialState {
|
||||
didSetScrollOffset = true
|
||||
contentOffset = CGPoint(x: 0.0, y: CGFloat(state.contentOffset))
|
||||
}
|
||||
else if let anchor = self.initialAnchor, !anchor.isEmpty {
|
||||
} else if let anchor = self.initialAnchor, !anchor.isEmpty {
|
||||
self.initialAnchor = nil
|
||||
if let items = self.currentLayout?.items {
|
||||
didSetScrollOffset = true
|
||||
|
|
@ -1312,35 +1311,35 @@ final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDeleg
|
|||
}
|
||||
|
||||
private func presentReferenceView(item: InstantPageTextItem, referenceAnchor: String) {
|
||||
// guard let theme = self.theme, let webPage = self.webPage else {
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// var targetAnchor: InstantPageTextAnchorItem?
|
||||
// for (name, (line, _)) in item.anchors {
|
||||
// if name == referenceAnchor {
|
||||
// let anchors = item.lines[line].anchorItems
|
||||
// for anchor in anchors {
|
||||
// if anchor.name == referenceAnchor {
|
||||
// targetAnchor = anchor
|
||||
// break
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// guard let anchorText = targetAnchor?.anchorText else {
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// let controller = InstantPageReferenceController(context: self.context, sourceLocation: self.sourceLocation, theme: theme, webPage: webPage, anchorText: anchorText, openUrl: { [weak self] url in
|
||||
// self?.openUrl(url)
|
||||
// }, openUrlIn: { [weak self] url in
|
||||
// self?.openUrlIn(url)
|
||||
// }, present: { [weak self] c, a in
|
||||
// self?.present(c, a)
|
||||
// })
|
||||
// self.present(controller, nil)
|
||||
guard let webPage = self.webPage else {
|
||||
return
|
||||
}
|
||||
|
||||
var targetAnchor: InstantPageTextAnchorItem?
|
||||
for (name, (line, _)) in item.anchors {
|
||||
if name == referenceAnchor {
|
||||
let anchors = item.lines[line].anchorItems
|
||||
for anchor in anchors {
|
||||
if anchor.name == referenceAnchor {
|
||||
targetAnchor = anchor
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
guard let anchorText = targetAnchor?.anchorText else {
|
||||
return
|
||||
}
|
||||
|
||||
let controller = InstantPageReferenceController(context: self.context, sourceLocation: self.sourceLocation, theme: theme, webPage: webPage, anchorText: anchorText, openUrl: { [weak self] url in
|
||||
self?.openUrl(url)
|
||||
}, openUrlIn: { [weak self] url in
|
||||
self?.openUrlIn(url)
|
||||
}, present: { [weak self] c, a in
|
||||
self?.present(c, a)
|
||||
})
|
||||
self.present(controller, nil)
|
||||
}
|
||||
|
||||
private func scrollToAnchor(_ anchor: String) {
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import MinimizedContainer
|
|||
import InstantPageUI
|
||||
import NavigationStackComponent
|
||||
import LottieComponent
|
||||
import WebKit
|
||||
|
||||
private let settingsTag = GenericComponentViewTag()
|
||||
|
||||
|
|
@ -384,13 +385,13 @@ private final class BrowserScreenComponent: CombinedComponent {
|
|||
bottomInset: toolbarBottomInset,
|
||||
sideInset: environment.safeInsets.left,
|
||||
item: toolbarContent,
|
||||
collapseFraction: collapseFraction
|
||||
collapseFraction: 0.0
|
||||
),
|
||||
availableSize: context.availableSize,
|
||||
transition: context.transition
|
||||
)
|
||||
context.add(toolbar
|
||||
.position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height - toolbar.size.height / 2.0))
|
||||
.position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height - toolbar.size.height / 2.0 + toolbar.size.height * collapseFraction))
|
||||
.appear(ComponentTransition.Appear { _, view, transition in
|
||||
transition.animatePosition(view: view, from: CGPoint(x: 0.0, y: view.frame.height), to: CGPoint(), additive: true)
|
||||
})
|
||||
|
|
@ -489,13 +490,13 @@ public class BrowserScreen: ViewController, MinimizableController {
|
|||
case expand
|
||||
}
|
||||
|
||||
fileprivate final class Node: ViewControllerTracingNode {
|
||||
final class Node: ViewControllerTracingNode {
|
||||
private weak var controller: BrowserScreen?
|
||||
private let context: AccountContext
|
||||
|
||||
private let contentContainerView = UIView()
|
||||
fileprivate let contentNavigationContainer = ComponentView<Empty>()
|
||||
fileprivate var content: [BrowserContent] = []
|
||||
private(set) var content: [BrowserContent] = []
|
||||
fileprivate var contentState: BrowserContentState?
|
||||
private var contentStateDisposable = MetaDisposable()
|
||||
|
||||
|
|
@ -785,13 +786,14 @@ public class BrowserScreen: ViewController, MinimizableController {
|
|||
let browserContent: BrowserContent
|
||||
switch content {
|
||||
case let .webPage(url):
|
||||
let webContent = BrowserWebContent(context: self.context, presentationData: self.presentationData, url: url)
|
||||
let webContent = BrowserWebContent(context: self.context, presentationData: self.presentationData, url: url, preferredConfiguration: self.controller?.preferredConfiguration)
|
||||
webContent.cancelInteractiveTransitionGestures = { [weak self] in
|
||||
if let self, let view = self.controller?.view {
|
||||
cancelInteractiveTransitionGestures(view: view)
|
||||
}
|
||||
}
|
||||
browserContent = webContent
|
||||
self.controller?.preferredConfiguration = nil
|
||||
case let .instantPage(webPage, anchor, sourceLocation):
|
||||
let instantPageContent = BrowserInstantPageContent(context: self.context, presentationData: self.presentationData, webPage: webPage, anchor: anchor, url: webPage.content.url ?? "", sourceLocation: sourceLocation)
|
||||
instantPageContent.openPeer = { [weak self] peer in
|
||||
|
|
@ -846,7 +848,9 @@ public class BrowserScreen: ViewController, MinimizableController {
|
|||
return
|
||||
}
|
||||
if controller.isMinimized {
|
||||
|
||||
if let navigationController = controller.navigationController as? NavigationController, let minimizedContainer = navigationController.minimizedContainer {
|
||||
minimizedContainer.removeController(controller)
|
||||
}
|
||||
} else {
|
||||
controller.dismiss()
|
||||
}
|
||||
|
|
@ -1328,7 +1332,8 @@ public class BrowserScreen: ViewController, MinimizableController {
|
|||
right: layout.safeInsets.right
|
||||
),
|
||||
navigationBarHeight: navigationBarHeight,
|
||||
scrollingPanelOffsetFraction: self.scrollingPanelOffsetFraction
|
||||
scrollingPanelOffsetFraction: self.scrollingPanelOffsetFraction,
|
||||
hasBottomPanel: !layout.metrics.isTablet || self.presentationState.isSearching
|
||||
)
|
||||
))
|
||||
)
|
||||
|
|
@ -1372,7 +1377,7 @@ public class BrowserScreen: ViewController, MinimizableController {
|
|||
|
||||
private let context: AccountContext
|
||||
private let subject: Subject
|
||||
|
||||
private var preferredConfiguration: WKWebViewConfiguration?
|
||||
private var openPreviousOnClose = false
|
||||
|
||||
private var validLayout: ContainerViewLayout?
|
||||
|
|
@ -1389,9 +1394,20 @@ public class BrowserScreen: ViewController, MinimizableController {
|
|||
// "application/vnd.openxmlformats-officedocument.presentationml.presentation"
|
||||
]
|
||||
|
||||
public init(context: AccountContext, subject: Subject, openPreviousOnClose: Bool = false) {
|
||||
public init(context: AccountContext, subject: Subject, preferredConfiguration: WKWebViewConfiguration? = nil, openPreviousOnClose: Bool = false) {
|
||||
var subject = subject
|
||||
if case let .webPage(url) = subject, let parsedUrl = URL(string: url) {
|
||||
if parsedUrl.host?.hasSuffix(".ton") == true {
|
||||
var urlComponents = URLComponents(string: url)
|
||||
urlComponents?.scheme = "tonsite"
|
||||
if let updatedUrl = urlComponents?.url?.absoluteString {
|
||||
subject = .webPage(url: updatedUrl)
|
||||
}
|
||||
}
|
||||
}
|
||||
self.context = context
|
||||
self.subject = subject
|
||||
self.preferredConfiguration = preferredConfiguration
|
||||
self.openPreviousOnClose = openPreviousOnClose
|
||||
|
||||
super.init(navigationBarPresentationData: nil)
|
||||
|
|
@ -1409,7 +1425,7 @@ public class BrowserScreen: ViewController, MinimizableController {
|
|||
preconditionFailure()
|
||||
}
|
||||
|
||||
private var node: Node {
|
||||
var node: Node {
|
||||
return self.displayNode as! Node
|
||||
}
|
||||
|
||||
|
|
@ -1529,17 +1545,20 @@ private final class BrowserContentComponent: Component {
|
|||
let insets: UIEdgeInsets
|
||||
let navigationBarHeight: CGFloat
|
||||
let scrollingPanelOffsetFraction: CGFloat
|
||||
let hasBottomPanel: Bool
|
||||
|
||||
init(
|
||||
content: BrowserContent,
|
||||
insets: UIEdgeInsets,
|
||||
navigationBarHeight: CGFloat,
|
||||
scrollingPanelOffsetFraction: CGFloat
|
||||
scrollingPanelOffsetFraction: CGFloat,
|
||||
hasBottomPanel: Bool
|
||||
) {
|
||||
self.content = content
|
||||
self.insets = insets
|
||||
self.navigationBarHeight = navigationBarHeight
|
||||
self.scrollingPanelOffsetFraction = scrollingPanelOffsetFraction
|
||||
self.hasBottomPanel = hasBottomPanel
|
||||
}
|
||||
|
||||
static func ==(lhs: BrowserContentComponent, rhs: BrowserContentComponent) -> Bool {
|
||||
|
|
@ -1555,6 +1574,9 @@ private final class BrowserContentComponent: Component {
|
|||
if lhs.scrollingPanelOffsetFraction != rhs.scrollingPanelOffsetFraction {
|
||||
return false
|
||||
}
|
||||
if lhs.hasBottomPanel != rhs.hasBottomPanel {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
|
|
@ -1574,9 +1596,9 @@ private final class BrowserContentComponent: Component {
|
|||
|
||||
let collapsedHeight: CGFloat = 24.0
|
||||
let topInset: CGFloat = component.navigationBarHeight * (1.0 - component.scrollingPanelOffsetFraction) + (component.insets.top + collapsedHeight) * component.scrollingPanelOffsetFraction
|
||||
let bottomInset = (49.0 + component.insets.bottom) * (1.0 - component.scrollingPanelOffsetFraction)
|
||||
let bottomInset = component.hasBottomPanel ? (49.0 + component.insets.bottom) * (1.0 - component.scrollingPanelOffsetFraction) : 0.0
|
||||
let insets = UIEdgeInsets(top: topInset, left: component.insets.left, bottom: bottomInset, right: component.insets.right)
|
||||
let fullInsets = UIEdgeInsets(top: component.insets.top + component.navigationBarHeight, left: component.insets.left, bottom: 49.0 + component.insets.bottom, right: component.insets.right)
|
||||
let fullInsets = UIEdgeInsets(top: component.insets.top + component.navigationBarHeight, left: component.insets.left, bottom: component.hasBottomPanel ? 49.0 + component.insets.bottom : 0.0, right: component.insets.right)
|
||||
|
||||
component.content.updateLayout(size: availableSize, insets: insets, fullInsets: fullInsets, safeInsets: component.insets, transition: transition)
|
||||
transition.setFrame(view: component.content, frame: CGRect(origin: .zero, size: availableSize))
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import MultilineTextComponent
|
|||
import UrlEscaping
|
||||
import UrlHandling
|
||||
import SaveProgressScreen
|
||||
import DeviceModel
|
||||
|
||||
private final class TonSchemeHandler: NSObject, WKURLSchemeHandler {
|
||||
private final class PendingTask {
|
||||
|
|
@ -151,6 +152,22 @@ private class WeakScriptMessageHandler: NSObject, WKScriptMessageHandler {
|
|||
}
|
||||
}
|
||||
|
||||
private func computedUserAgent() -> String {
|
||||
func getFirmwareVersion() -> String? {
|
||||
var size = 0
|
||||
sysctlbyname("kern.osversion", nil, &size, nil, 0)
|
||||
|
||||
var str = [CChar](repeating: 0, count: size)
|
||||
sysctlbyname("kern.osversion", &str, &size, nil, 0)
|
||||
|
||||
return String(cString: str)
|
||||
}
|
||||
|
||||
let osVersion = UIDevice.current.systemVersion
|
||||
let firmwareVersion = getFirmwareVersion() ?? "15E148"
|
||||
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 {
|
||||
private let context: AccountContext
|
||||
private var presentationData: PresentationData
|
||||
|
|
@ -186,42 +203,49 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU
|
|||
|
||||
private var tempFile: TempBoxFile?
|
||||
|
||||
init(context: AccountContext, presentationData: PresentationData, url: String) {
|
||||
init(context: AccountContext, presentationData: PresentationData, url: String, preferredConfiguration: WKWebViewConfiguration? = nil) {
|
||||
self.context = context
|
||||
self.uuid = UUID()
|
||||
self.presentationData = presentationData
|
||||
|
||||
let configuration = WKWebViewConfiguration()
|
||||
|
||||
var proxyServerHost = "magic.org"
|
||||
if let data = context.currentAppConfiguration.with({ $0 }).data, let hostValue = data["ton_proxy_address"] as? String {
|
||||
proxyServerHost = hostValue
|
||||
}
|
||||
configuration.setURLSchemeHandler(TonSchemeHandler(proxyServerHost: proxyServerHost), forURLScheme: "tonsite")
|
||||
configuration.allowsInlineMediaPlayback = true
|
||||
if #available(iOSApplicationExtension 10.0, iOS 10.0, *) {
|
||||
configuration.mediaTypesRequiringUserActionForPlayback = []
|
||||
} else {
|
||||
configuration.mediaPlaybackRequiresUserAction = false
|
||||
}
|
||||
|
||||
let contentController = WKUserContentController()
|
||||
let videoScript = WKUserScript(source: videoSource, injectionTime: .atDocumentStart, forMainFrameOnly: false)
|
||||
contentController.addUserScript(videoScript)
|
||||
let touchScript = WKUserScript(source: setupTouchObservers, injectionTime: .atDocumentStart, forMainFrameOnly: false)
|
||||
contentController.addUserScript(touchScript)
|
||||
configuration.userContentController = contentController
|
||||
|
||||
var handleScriptMessageImpl: ((WKScriptMessage) -> Void)?
|
||||
let eventProxyScript = WKUserScript(source: eventProxySource, injectionTime: .atDocumentStart, forMainFrameOnly: false)
|
||||
contentController.addUserScript(eventProxyScript)
|
||||
contentController.add(WeakScriptMessageHandler { message in
|
||||
handleScriptMessageImpl?(message)
|
||||
}, name: "performAction")
|
||||
|
||||
let configuration: WKWebViewConfiguration
|
||||
if let preferredConfiguration {
|
||||
configuration = preferredConfiguration
|
||||
} else {
|
||||
configuration = WKWebViewConfiguration()
|
||||
var proxyServerHost = "magic.org"
|
||||
if let data = context.currentAppConfiguration.with({ $0 }).data, let hostValue = data["ton_proxy_address"] as? String {
|
||||
proxyServerHost = hostValue
|
||||
}
|
||||
configuration.setURLSchemeHandler(TonSchemeHandler(proxyServerHost: proxyServerHost), forURLScheme: "tonsite")
|
||||
configuration.allowsInlineMediaPlayback = true
|
||||
if #available(iOSApplicationExtension 10.0, iOS 10.0, *) {
|
||||
configuration.mediaTypesRequiringUserActionForPlayback = []
|
||||
} else {
|
||||
configuration.mediaPlaybackRequiresUserAction = false
|
||||
}
|
||||
|
||||
let contentController = WKUserContentController()
|
||||
let videoScript = WKUserScript(source: videoSource, injectionTime: .atDocumentStart, forMainFrameOnly: false)
|
||||
contentController.addUserScript(videoScript)
|
||||
let touchScript = WKUserScript(source: setupTouchObservers, injectionTime: .atDocumentStart, forMainFrameOnly: false)
|
||||
contentController.addUserScript(touchScript)
|
||||
|
||||
let eventProxyScript = WKUserScript(source: eventProxySource, injectionTime: .atDocumentStart, forMainFrameOnly: false)
|
||||
contentController.addUserScript(eventProxyScript)
|
||||
contentController.add(WeakScriptMessageHandler { message in
|
||||
handleScriptMessageImpl?(message)
|
||||
}, name: "performAction")
|
||||
|
||||
configuration.userContentController = contentController
|
||||
configuration.applicationNameForUserAgent = computedUserAgent()
|
||||
}
|
||||
|
||||
self.webView = WebView(frame: CGRect(), configuration: configuration)
|
||||
self.webView.allowsLinkPreview = true
|
||||
|
||||
|
||||
if #available(iOS 11.0, *) {
|
||||
self.webView.scrollView.contentInsetAdjustmentBehavior = .never
|
||||
}
|
||||
|
|
@ -239,18 +263,19 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU
|
|||
let request = URLRequest(url: parsedUrl)
|
||||
self.webView.load(request)
|
||||
|
||||
title = parsedUrl.host ?? ""
|
||||
title = getDisplayUrl(url, hostOnly: true)
|
||||
}
|
||||
|
||||
self.errorView = ComponentHostView()
|
||||
|
||||
self._state = BrowserContentState(title: title, url: url, estimatedProgress: 0.0, readingProgress: 0.0, contentType: .webPage)
|
||||
self._state = BrowserContentState(title: title, url: url, estimatedProgress: 0.1, readingProgress: 0.0, contentType: .webPage)
|
||||
self.statePromise = Promise<BrowserContentState>(self._state)
|
||||
|
||||
super.init(frame: .zero)
|
||||
|
||||
self.backgroundColor = presentationData.theme.list.plainBackgroundColor
|
||||
self.webView.backgroundColor = presentationData.theme.list.plainBackgroundColor
|
||||
self.webView.isOpaque = false
|
||||
|
||||
self.webView.allowsBackForwardNavigationGestures = true
|
||||
self.webView.scrollView.delegate = self
|
||||
|
|
@ -528,7 +553,14 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU
|
|||
|
||||
self.previousScrollingOffset = ScrollingOffsetState(value: self.webView.scrollView.contentOffset.y, isDraggingOrDecelerating: self.webView.scrollView.isDragging || self.webView.scrollView.isDecelerating)
|
||||
|
||||
let webViewFrame = CGRect(origin: CGPoint(x: insets.left, y: insets.top), size: CGSize(width: size.width - insets.left - insets.right, height: size.height - insets.top))
|
||||
let currentBounds = self.webView.scrollView.bounds
|
||||
let offsetToBottomEdge = max(0.0, self.webView.scrollView.contentSize.height - currentBounds.maxY)
|
||||
var bottomInset = insets.bottom
|
||||
if offsetToBottomEdge < 128.0 {
|
||||
bottomInset = fullInsets.bottom
|
||||
}
|
||||
|
||||
let webViewFrame = CGRect(origin: CGPoint(x: insets.left, y: insets.top), size: CGSize(width: size.width - insets.left - insets.right, height: size.height - insets.top - bottomInset))
|
||||
var refresh = false
|
||||
if self.webView.frame.width > 0 && webViewFrame.width != self.webView.frame.width {
|
||||
refresh = true
|
||||
|
|
@ -539,11 +571,10 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU
|
|||
self.webView.reloadInputViews()
|
||||
}
|
||||
|
||||
self.webView.scrollView.contentInset = UIEdgeInsets(top: 0.0, left: 0.0, bottom: fullInsets.bottom, right: 0.0)
|
||||
self.webView.customBottomInset = max(insets.bottom, safeInsets.bottom)
|
||||
self.webView.customBottomInset = safeInsets.bottom * (1.0 - insets.bottom / fullInsets.bottom)
|
||||
|
||||
self.webView.scrollView.scrollIndicatorInsets = UIEdgeInsets(top: 0.0, left: -insets.left, bottom: 0.0, right: -insets.right)
|
||||
self.webView.scrollView.horizontalScrollIndicatorInsets = UIEdgeInsets(top: 0.0, left: -insets.left, bottom: 0.0, right: -insets.right)
|
||||
// self.webView.scrollView.scrollIndicatorInsets = UIEdgeInsets(top: 0.0, left: -insets.left, bottom: 0.0, right: -insets.right)
|
||||
// self.webView.scrollView.horizontalScrollIndicatorInsets = UIEdgeInsets(top: 0.0, left: -insets.left, bottom: 0.0, right: -insets.right)
|
||||
|
||||
if let error = self.currentError {
|
||||
let errorSize = self.errorView.update(
|
||||
|
|
@ -681,7 +712,7 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU
|
|||
// })
|
||||
// } else {
|
||||
if let url = navigationAction.request.url?.absoluteString {
|
||||
if (navigationAction.targetFrame == nil || navigationAction.targetFrame?.isMainFrame == true) && (isTelegramMeLink(url) || isTelegraPhLink(url)) {
|
||||
if (navigationAction.targetFrame == nil || navigationAction.targetFrame?.isMainFrame == true) && (isTelegramMeLink(url) || isTelegraPhLink(url)) && !url.contains("/auth/push?") && !self._state.url.contains("/auth/push?") {
|
||||
decisionHandler(.cancel, preferences)
|
||||
self.minimize()
|
||||
self.openAppUrl(url)
|
||||
|
|
@ -725,7 +756,12 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU
|
|||
}
|
||||
|
||||
func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) {
|
||||
self.currentError = nil
|
||||
if let _ = self.currentError {
|
||||
self.currentError = nil
|
||||
if let (size, insets, fullInsets, safeInsets) = self.validLayout {
|
||||
self.updateLayout(size: size, insets: insets, fullInsets: fullInsets, safeInsets: safeInsets, transition: .immediate)
|
||||
}
|
||||
}
|
||||
self.updateFontState(self.currentFontState, force: true)
|
||||
}
|
||||
|
||||
|
|
@ -739,7 +775,7 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU
|
|||
}
|
||||
|
||||
func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) {
|
||||
if [-1003, -1100].contains((error as NSError).code) {
|
||||
if [-1003, -1100, 102].contains((error as NSError).code) {
|
||||
self.currentError = error
|
||||
} else {
|
||||
self.currentError = nil
|
||||
|
|
@ -756,7 +792,7 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU
|
|||
self.minimize()
|
||||
self.openAppUrl(url)
|
||||
} else {
|
||||
self.open(url: url, new: true)
|
||||
return self.open(url: url, configuration: configuration, new: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -764,7 +800,9 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU
|
|||
}
|
||||
|
||||
func webViewDidClose(_ webView: WKWebView) {
|
||||
self.close()
|
||||
Queue.mainQueue().after(0.5, {
|
||||
self.close()
|
||||
})
|
||||
}
|
||||
|
||||
@available(iOS 15.0, *)
|
||||
|
|
@ -895,17 +933,19 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU
|
|||
self.present(alertController, nil)
|
||||
}
|
||||
|
||||
private func open(url: String, new: Bool) {
|
||||
@discardableResult private func open(url: String, configuration: WKWebViewConfiguration? = nil, new: Bool) -> WKWebView? {
|
||||
let subject: BrowserScreen.Subject = .webPage(url: url)
|
||||
if new, let navigationController = self.getNavigationController() {
|
||||
navigationController._keepModalDismissProgress = true
|
||||
self.minimize()
|
||||
let controller = BrowserScreen(context: self.context, subject: subject, openPreviousOnClose: true)
|
||||
let controller = BrowserScreen(context: self.context, subject: subject, preferredConfiguration: configuration, openPreviousOnClose: true)
|
||||
navigationController._keepModalDismissProgress = true
|
||||
navigationController.pushViewController(controller)
|
||||
return (controller.node.content.last as? BrowserWebContent)?.webView
|
||||
} else {
|
||||
self.pushContent(subject)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
private func share(url: String) {
|
||||
|
|
|
|||
|
|
@ -108,3 +108,47 @@ func getPrimaryUrl(message: Message) -> String? {
|
|||
}
|
||||
return primaryUrl
|
||||
}
|
||||
|
||||
private let asciiChars = CharacterSet(charactersIn: "a".unicodeScalars.first! ... "z".unicodeScalars.first!)
|
||||
|
||||
func getDisplayUrl(_ url: String, hostOnly: Bool = false, trim: Bool = true) -> String {
|
||||
if hostOnly {
|
||||
var title = url
|
||||
if let parsedUrl = URL(string: url) {
|
||||
title = parsedUrl.host ?? url
|
||||
if title.hasPrefix("www.") {
|
||||
title.removeSubrange(title.startIndex ..< title.index(title.startIndex, offsetBy: 4))
|
||||
}
|
||||
if let decoded = title.idnaDecoded, title != decoded {
|
||||
if decoded.lowercased().rangeOfCharacter(from: asciiChars) == nil {
|
||||
title = decoded
|
||||
}
|
||||
}
|
||||
}
|
||||
return title
|
||||
} else {
|
||||
var address = url
|
||||
if let components = URLComponents(string: address) {
|
||||
if #available(iOS 16.0, *), let encodedHost = components.encodedHost {
|
||||
if let decodedHost = components.host, encodedHost != decodedHost {
|
||||
if decodedHost.lowercased().rangeOfCharacter(from: asciiChars) == nil {
|
||||
address = address.replacingOccurrences(of: encodedHost, with: decodedHost)
|
||||
}
|
||||
}
|
||||
} else if let encodedHost = components.host {
|
||||
if let decodedHost = components.host?.idnaDecoded, encodedHost != decodedHost {
|
||||
if decodedHost.lowercased().rangeOfCharacter(from: asciiChars) == nil {
|
||||
address = address.replacingOccurrences(of: encodedHost, with: decodedHost)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if trim {
|
||||
address = address.replacingOccurrences(of: "https://www.", with: "")
|
||||
address = address.replacingOccurrences(of: "https://", with: "")
|
||||
address = address.replacingOccurrences(of: "tonsite://", with: "")
|
||||
address = address.trimmingCharacters(in: CharacterSet(charactersIn: "/"))
|
||||
}
|
||||
return address
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@ swift_library(
|
|||
"//submodules/Display:Display",
|
||||
"//submodules/ImageBlur:ImageBlur",
|
||||
"//submodules/TelegramCore:TelegramCore",
|
||||
"//submodules/Utils/DeviceModel",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import SwiftSignalKit
|
|||
import AVFoundation
|
||||
import CoreImage
|
||||
import TelegramCore
|
||||
import DeviceModel
|
||||
|
||||
final class CameraSession {
|
||||
private let singleSession: AVCaptureSession?
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import Foundation
|
||||
import DeviceModel
|
||||
|
||||
public extension Camera {
|
||||
enum Metrics {
|
||||
|
|
@ -56,370 +57,3 @@ public extension Camera {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum DeviceModel: CaseIterable, Equatable {
|
||||
static var allCases: [DeviceModel] {
|
||||
return [
|
||||
.iPodTouch1,
|
||||
.iPodTouch2,
|
||||
.iPodTouch3,
|
||||
.iPodTouch4,
|
||||
.iPodTouch5,
|
||||
.iPodTouch6,
|
||||
.iPodTouch7,
|
||||
.iPhone,
|
||||
.iPhone3G,
|
||||
.iPhone3GS,
|
||||
.iPhone4,
|
||||
.iPhone4S,
|
||||
.iPhone5,
|
||||
.iPhone5C,
|
||||
.iPhone5S,
|
||||
.iPhone6,
|
||||
.iPhone6Plus,
|
||||
.iPhone6S,
|
||||
.iPhone6SPlus,
|
||||
.iPhoneSE,
|
||||
.iPhone7,
|
||||
.iPhone7Plus,
|
||||
.iPhone8,
|
||||
.iPhone8Plus,
|
||||
.iPhoneX,
|
||||
.iPhoneXS,
|
||||
.iPhoneXR,
|
||||
.iPhone11,
|
||||
.iPhone11Pro,
|
||||
.iPhone11ProMax,
|
||||
.iPhone12,
|
||||
.iPhone12Mini,
|
||||
.iPhone12Pro,
|
||||
.iPhone12ProMax,
|
||||
.iPhone13,
|
||||
.iPhone13Mini,
|
||||
.iPhone13Pro,
|
||||
.iPhone13ProMax,
|
||||
.iPhone14,
|
||||
.iPhone14Plus,
|
||||
.iPhone14Pro,
|
||||
.iPhone14ProMax,
|
||||
.iPhone15,
|
||||
.iPhone15Plus,
|
||||
.iPhone15Pro,
|
||||
.iPhone15ProMax
|
||||
]
|
||||
}
|
||||
|
||||
case iPodTouch1
|
||||
case iPodTouch2
|
||||
case iPodTouch3
|
||||
case iPodTouch4
|
||||
case iPodTouch5
|
||||
case iPodTouch6
|
||||
case iPodTouch7
|
||||
|
||||
case iPhone
|
||||
case iPhone3G
|
||||
case iPhone3GS
|
||||
|
||||
case iPhone4
|
||||
case iPhone4S
|
||||
|
||||
case iPhone5
|
||||
case iPhone5C
|
||||
case iPhone5S
|
||||
|
||||
case iPhone6
|
||||
case iPhone6Plus
|
||||
case iPhone6S
|
||||
case iPhone6SPlus
|
||||
|
||||
case iPhoneSE
|
||||
|
||||
case iPhone7
|
||||
case iPhone7Plus
|
||||
case iPhone8
|
||||
case iPhone8Plus
|
||||
|
||||
case iPhoneX
|
||||
case iPhoneXS
|
||||
case iPhoneXSMax
|
||||
case iPhoneXR
|
||||
|
||||
case iPhone11
|
||||
case iPhone11Pro
|
||||
case iPhone11ProMax
|
||||
|
||||
case iPhoneSE2ndGen
|
||||
|
||||
case iPhone12
|
||||
case iPhone12Mini
|
||||
case iPhone12Pro
|
||||
case iPhone12ProMax
|
||||
|
||||
case iPhone13
|
||||
case iPhone13Mini
|
||||
case iPhone13Pro
|
||||
case iPhone13ProMax
|
||||
|
||||
case iPhoneSE3rdGen
|
||||
|
||||
case iPhone14
|
||||
case iPhone14Plus
|
||||
case iPhone14Pro
|
||||
case iPhone14ProMax
|
||||
|
||||
case iPhone15
|
||||
case iPhone15Plus
|
||||
case iPhone15Pro
|
||||
case iPhone15ProMax
|
||||
|
||||
case unknown(String)
|
||||
|
||||
var modelId: [String] {
|
||||
switch self {
|
||||
case .iPodTouch1:
|
||||
return ["iPod1,1"]
|
||||
case .iPodTouch2:
|
||||
return ["iPod2,1"]
|
||||
case .iPodTouch3:
|
||||
return ["iPod3,1"]
|
||||
case .iPodTouch4:
|
||||
return ["iPod4,1"]
|
||||
case .iPodTouch5:
|
||||
return ["iPod5,1"]
|
||||
case .iPodTouch6:
|
||||
return ["iPod7,1"]
|
||||
case .iPodTouch7:
|
||||
return ["iPod9,1"]
|
||||
case .iPhone:
|
||||
return ["iPhone1,1"]
|
||||
case .iPhone3G:
|
||||
return ["iPhone1,2"]
|
||||
case .iPhone3GS:
|
||||
return ["iPhone2,1"]
|
||||
case .iPhone4:
|
||||
return ["iPhone3,1", "iPhone3,2", "iPhone3,3"]
|
||||
case .iPhone4S:
|
||||
return ["iPhone4,1", "iPhone4,2", "iPhone4,3"]
|
||||
case .iPhone5:
|
||||
return ["iPhone5,1", "iPhone5,2"]
|
||||
case .iPhone5C:
|
||||
return ["iPhone5,3", "iPhone5,4"]
|
||||
case .iPhone5S:
|
||||
return ["iPhone6,1", "iPhone6,2"]
|
||||
case .iPhone6:
|
||||
return ["iPhone7,2"]
|
||||
case .iPhone6Plus:
|
||||
return ["iPhone7,1"]
|
||||
case .iPhone6S:
|
||||
return ["iPhone8,1"]
|
||||
case .iPhone6SPlus:
|
||||
return ["iPhone8,2"]
|
||||
case .iPhoneSE:
|
||||
return ["iPhone8,4"]
|
||||
case .iPhone7:
|
||||
return ["iPhone9,1", "iPhone9,3"]
|
||||
case .iPhone7Plus:
|
||||
return ["iPhone9,2", "iPhone9,4"]
|
||||
case .iPhone8:
|
||||
return ["iPhone10,1", "iPhone10,4"]
|
||||
case .iPhone8Plus:
|
||||
return ["iPhone10,2", "iPhone10,5"]
|
||||
case .iPhoneX:
|
||||
return ["iPhone10,3", "iPhone10,6"]
|
||||
case .iPhoneXS:
|
||||
return ["iPhone11,2"]
|
||||
case .iPhoneXSMax:
|
||||
return ["iPhone11,4", "iPhone11,6"]
|
||||
case .iPhoneXR:
|
||||
return ["iPhone11,8"]
|
||||
case .iPhone11:
|
||||
return ["iPhone12,1"]
|
||||
case .iPhone11Pro:
|
||||
return ["iPhone12,3"]
|
||||
case .iPhone11ProMax:
|
||||
return ["iPhone12,5"]
|
||||
case .iPhoneSE2ndGen:
|
||||
return ["iPhone12,8"]
|
||||
case .iPhone12:
|
||||
return ["iPhone13,2"]
|
||||
case .iPhone12Mini:
|
||||
return ["iPhone13,1"]
|
||||
case .iPhone12Pro:
|
||||
return ["iPhone13,3"]
|
||||
case .iPhone12ProMax:
|
||||
return ["iPhone13,4"]
|
||||
case .iPhone13:
|
||||
return ["iPhone14,5"]
|
||||
case .iPhone13Mini:
|
||||
return ["iPhone14,4"]
|
||||
case .iPhone13Pro:
|
||||
return ["iPhone14,2"]
|
||||
case .iPhone13ProMax:
|
||||
return ["iPhone14,3"]
|
||||
case .iPhoneSE3rdGen:
|
||||
return ["iPhone14,6"]
|
||||
case .iPhone14:
|
||||
return ["iPhone14,7"]
|
||||
case .iPhone14Plus:
|
||||
return ["iPhone14,8"]
|
||||
case .iPhone14Pro:
|
||||
return ["iPhone15,2"]
|
||||
case .iPhone14ProMax:
|
||||
return ["iPhone15,3"]
|
||||
case .iPhone15:
|
||||
return ["iPhone15,4"]
|
||||
case .iPhone15Plus:
|
||||
return ["iPhone15,5"]
|
||||
case .iPhone15Pro:
|
||||
return ["iPhone16,1"]
|
||||
case .iPhone15ProMax:
|
||||
return ["iPhone16,2"]
|
||||
case let .unknown(modelId):
|
||||
return [modelId]
|
||||
}
|
||||
}
|
||||
|
||||
var modelName: String {
|
||||
switch self {
|
||||
case .iPodTouch1:
|
||||
return "iPod touch 1G"
|
||||
case .iPodTouch2:
|
||||
return "iPod touch 2G"
|
||||
case .iPodTouch3:
|
||||
return "iPod touch 3G"
|
||||
case .iPodTouch4:
|
||||
return "iPod touch 4G"
|
||||
case .iPodTouch5:
|
||||
return "iPod touch 5G"
|
||||
case .iPodTouch6:
|
||||
return "iPod touch 6G"
|
||||
case .iPodTouch7:
|
||||
return "iPod touch 7G"
|
||||
case .iPhone:
|
||||
return "iPhone"
|
||||
case .iPhone3G:
|
||||
return "iPhone 3G"
|
||||
case .iPhone3GS:
|
||||
return "iPhone 3GS"
|
||||
case .iPhone4:
|
||||
return "iPhone 4"
|
||||
case .iPhone4S:
|
||||
return "iPhone 4S"
|
||||
case .iPhone5:
|
||||
return "iPhone 5"
|
||||
case .iPhone5C:
|
||||
return "iPhone 5C"
|
||||
case .iPhone5S:
|
||||
return "iPhone 5S"
|
||||
case .iPhone6:
|
||||
return "iPhone 6"
|
||||
case .iPhone6Plus:
|
||||
return "iPhone 6 Plus"
|
||||
case .iPhone6S:
|
||||
return "iPhone 6S"
|
||||
case .iPhone6SPlus:
|
||||
return "iPhone 6S Plus"
|
||||
case .iPhoneSE:
|
||||
return "iPhone SE"
|
||||
case .iPhone7:
|
||||
return "iPhone 7"
|
||||
case .iPhone7Plus:
|
||||
return "iPhone 7 Plus"
|
||||
case .iPhone8:
|
||||
return "iPhone 8"
|
||||
case .iPhone8Plus:
|
||||
return "iPhone 8 Plus"
|
||||
case .iPhoneX:
|
||||
return "iPhone X"
|
||||
case .iPhoneXS:
|
||||
return "iPhone XS"
|
||||
case .iPhoneXSMax:
|
||||
return "iPhone XS Max"
|
||||
case .iPhoneXR:
|
||||
return "iPhone XR"
|
||||
case .iPhone11:
|
||||
return "iPhone 11"
|
||||
case .iPhone11Pro:
|
||||
return "iPhone 11 Pro"
|
||||
case .iPhone11ProMax:
|
||||
return "iPhone 11 Pro Max"
|
||||
case .iPhoneSE2ndGen:
|
||||
return "iPhone SE (2nd gen)"
|
||||
case .iPhone12:
|
||||
return "iPhone 12"
|
||||
case .iPhone12Mini:
|
||||
return "iPhone 12 mini"
|
||||
case .iPhone12Pro:
|
||||
return "iPhone 12 Pro"
|
||||
case .iPhone12ProMax:
|
||||
return "iPhone 12 Pro Max"
|
||||
case .iPhone13:
|
||||
return "iPhone 13"
|
||||
case .iPhone13Mini:
|
||||
return "iPhone 13 mini"
|
||||
case .iPhone13Pro:
|
||||
return "iPhone 13 Pro"
|
||||
case .iPhone13ProMax:
|
||||
return "iPhone 13 Pro Max"
|
||||
case .iPhoneSE3rdGen:
|
||||
return "iPhone SE (3rd gen)"
|
||||
case .iPhone14:
|
||||
return "iPhone 14"
|
||||
case .iPhone14Plus:
|
||||
return "iPhone 14 Plus"
|
||||
case .iPhone14Pro:
|
||||
return "iPhone 14 Pro"
|
||||
case .iPhone14ProMax:
|
||||
return "iPhone 14 Pro Max"
|
||||
case .iPhone15:
|
||||
return "iPhone 15"
|
||||
case .iPhone15Plus:
|
||||
return "iPhone 15 Plus"
|
||||
case .iPhone15Pro:
|
||||
return "iPhone 15 Pro"
|
||||
case .iPhone15ProMax:
|
||||
return "iPhone 15 Pro Max"
|
||||
case let .unknown(modelId):
|
||||
if modelId.hasPrefix("iPhone") {
|
||||
return "Unknown iPhone"
|
||||
} else if modelId.hasPrefix("iPod") {
|
||||
return "Unknown iPod"
|
||||
} else if modelId.hasPrefix("iPad") {
|
||||
return "Unknown iPad"
|
||||
} else {
|
||||
return "Unknown Device"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var isIpad: Bool {
|
||||
return self.modelId.first?.hasPrefix("iPad") ?? false
|
||||
}
|
||||
|
||||
static let current = DeviceModel()
|
||||
|
||||
private init() {
|
||||
var systemInfo = utsname()
|
||||
uname(&systemInfo)
|
||||
let modelCode = withUnsafePointer(to: &systemInfo.machine) {
|
||||
$0.withMemoryRebound(to: CChar.self, capacity: 1) {
|
||||
ptr in String.init(validatingUTF8: ptr)
|
||||
}
|
||||
}
|
||||
var result: DeviceModel?
|
||||
if let modelCode {
|
||||
for model in DeviceModel.allCases {
|
||||
if model.modelId.contains(modelCode) {
|
||||
result = model
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if let result {
|
||||
self = result
|
||||
} else {
|
||||
self = .unknown(modelCode ?? "")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2908,9 +2908,10 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
|
|||
}, openUrl: { url, _, _, message in
|
||||
interaction.openUrl(url)
|
||||
}, openInstantPage: { [weak self] message, data in
|
||||
if let (webpage, anchor) = instantPageAndAnchor(message: message) {
|
||||
let pageController = InstantPageController(context: context, webPage: webpage, sourceLocation: InstantPageSourceLocation(userLocation: .peer(message.id.peerId), peerType: .channel), anchor: anchor)
|
||||
self?.navigationController?.pushViewController(pageController)
|
||||
if let self, let navigationController = self.navigationController {
|
||||
if let controller = self.context.sharedContext.makeInstantPageController(context: self.context, message: message, sourcePeerType: .channel) {
|
||||
navigationController.pushViewController(controller)
|
||||
}
|
||||
}
|
||||
}, longTap: { action, message in
|
||||
}, getHiddenMedia: {
|
||||
|
|
|
|||
|
|
@ -433,7 +433,7 @@ public func chatTextLinkEditController(sharedContext: SharedAccountContext, upda
|
|||
return
|
||||
}
|
||||
let updatedLink = explicitUrl(contentNode.link)
|
||||
if !updatedLink.isEmpty && isValidUrl(updatedLink, validSchemes: ["http": true, "https": true, "tg": false, "ton": false]) {
|
||||
if !updatedLink.isEmpty && isValidUrl(updatedLink, validSchemes: ["http": true, "https": true, "tg": false, "ton": false, "tonsite": true]) {
|
||||
dismissImpl?(true)
|
||||
apply(updatedLink)
|
||||
} else if allowEmpty && contentNode.link.isEmpty {
|
||||
|
|
|
|||
|
|
@ -6,12 +6,15 @@ public protocol MinimizedContainer: ASDisplayNode {
|
|||
var controllers: [MinimizableController] { get }
|
||||
var isExpanded: Bool { get }
|
||||
|
||||
var willMaximize: (() -> Void)? { get set }
|
||||
var willMaximize: ((MinimizedContainer) -> Void)? { get set }
|
||||
var willDismiss: ((MinimizedContainer) -> Void)? { get set }
|
||||
var didDismiss: ((MinimizedContainer) -> Void)? { get set }
|
||||
|
||||
var statusBarStyle: StatusBarStyle { get }
|
||||
var statusBarStyleUpdated: (() -> Void)? { get set }
|
||||
|
||||
func addController(_ viewController: MinimizableController, topEdgeOffset: CGFloat?, beforeMaximize: @escaping (NavigationController, @escaping () -> Void) -> Void, transition: ContainedViewLayoutTransition)
|
||||
func removeController(_ viewController: MinimizableController)
|
||||
func maximizeController(_ viewController: MinimizableController, animated: Bool, completion: @escaping (Bool) -> Void)
|
||||
func collapse()
|
||||
func dismissAll(completion: @escaping () -> Void)
|
||||
|
|
|
|||
|
|
@ -153,13 +153,23 @@ open class NavigationController: UINavigationController, ContainableController,
|
|||
open var minimizedContainer: MinimizedContainer? {
|
||||
didSet {
|
||||
self.minimizedContainer?.navigationController = self
|
||||
self.minimizedContainer?.willMaximize = { [weak self] in
|
||||
self.minimizedContainer?.willMaximize = { [weak self] _ in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.isMaximizing = true
|
||||
self.updateContainersNonReentrant(transition: .animated(duration: 0.4, curve: .spring))
|
||||
}
|
||||
self.minimizedContainer?.willDismiss = { [weak self] _ in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.minimizedContainer = nil
|
||||
self.updateContainersNonReentrant(transition: .animated(duration: 0.4, curve: .spring))
|
||||
}
|
||||
self.minimizedContainer?.didDismiss = { minimizedContainer in
|
||||
minimizedContainer.removeFromSupernode()
|
||||
}
|
||||
self.minimizedContainer?.statusBarStyleUpdated = { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
|
|
|
|||
|
|
@ -209,7 +209,7 @@ public final class DrawingLinkEntityView: DrawingEntityView, UITextViewDelegate
|
|||
if !self.linkEntity.name.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
||||
string = self.linkEntity.name.trimmingCharacters(in: .whitespacesAndNewlines).uppercased()
|
||||
} else {
|
||||
string = self.linkEntity.url.uppercased()
|
||||
string = self.linkEntity.url.uppercased().replacingOccurrences(of: "http://", with: "").replacingOccurrences(of: "https://", with: "")
|
||||
}
|
||||
let text = NSMutableAttributedString(string: string)
|
||||
let range = NSMakeRange(0, text.length)
|
||||
|
|
|
|||
|
|
@ -624,6 +624,15 @@ public final class DrawingTextEntityView: DrawingEntityView, UITextViewDelegate
|
|||
}
|
||||
|
||||
func getRenderSubEntities() -> [DrawingEntity] {
|
||||
var explicitlyStaticStickers = Set<Int64>()
|
||||
if let customEmojiContainerView = self.customEmojiContainerView {
|
||||
for (key, view) in customEmojiContainerView.emojiLayers {
|
||||
if let view = view as? EmojiTextAttachmentView, let numFrames = view.contentLayer.numFrames, numFrames == 1 {
|
||||
explicitlyStaticStickers.insert(key.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let textSize = self.textView.bounds.size
|
||||
let textPosition = self.textEntity.position
|
||||
let scale = self.textEntity.scale
|
||||
|
|
@ -638,6 +647,9 @@ public final class DrawingTextEntityView: DrawingEntityView, UITextViewDelegate
|
|||
let emojiTextPosition = emojiRect.center.offsetBy(dx: -textSize.width / 2.0, dy: -textSize.height / 2.0)
|
||||
|
||||
let entity = DrawingStickerEntity(content: .file(.standalone(media: file), .sticker))
|
||||
if explicitlyStaticStickers.contains(file.fileId.id) {
|
||||
entity.isExplicitlyStatic = true
|
||||
}
|
||||
entity.referenceDrawingSize = CGSize(width: itemSize * 4.0, height: itemSize * 4.0)
|
||||
entity.scale = scale
|
||||
entity.position = textPosition.offsetBy(
|
||||
|
|
|
|||
|
|
@ -8,16 +8,6 @@ import TelegramPresentationData
|
|||
import TelegramUIPreferences
|
||||
import AccountContext
|
||||
|
||||
public struct InstantPageSourceLocation {
|
||||
public var userLocation: MediaResourceUserLocation
|
||||
public var peerType: MediaAutoDownloadPeerType
|
||||
|
||||
public init(userLocation: MediaResourceUserLocation, peerType: MediaAutoDownloadPeerType) {
|
||||
self.userLocation = userLocation
|
||||
self.peerType = peerType
|
||||
}
|
||||
}
|
||||
|
||||
public func instantPageAndAnchor(message: Message) -> (TelegramMediaWebpage, String?)? {
|
||||
for media in message.media {
|
||||
if let webpage = media as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content {
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import SwiftSignalKit
|
|||
import AccountContext
|
||||
import TelegramUIPreferences
|
||||
|
||||
final class InstantPageReferenceController: ViewController {
|
||||
public final class InstantPageReferenceController: ViewController {
|
||||
private var controllerNode: InstantPageReferenceControllerNode {
|
||||
return self.displayNode as! InstantPageReferenceControllerNode
|
||||
}
|
||||
|
|
@ -23,7 +23,7 @@ final class InstantPageReferenceController: ViewController {
|
|||
private let openUrlIn: (InstantPageUrlItem) -> Void
|
||||
private let present: (ViewController, Any?) -> Void
|
||||
|
||||
init(context: AccountContext, sourceLocation: InstantPageSourceLocation, theme: InstantPageTheme, webPage: TelegramMediaWebpage, anchorText: NSAttributedString, openUrl: @escaping (InstantPageUrlItem) -> Void, openUrlIn: @escaping (InstantPageUrlItem) -> Void, present: @escaping (ViewController, Any?) -> Void) {
|
||||
public init(context: AccountContext, sourceLocation: InstantPageSourceLocation, theme: InstantPageTheme, webPage: TelegramMediaWebpage, anchorText: NSAttributedString, openUrl: @escaping (InstantPageUrlItem) -> Void, openUrlIn: @escaping (InstantPageUrlItem) -> Void, present: @escaping (ViewController, Any?) -> Void) {
|
||||
self.context = context
|
||||
self.sourceLocation = sourceLocation
|
||||
self.theme = theme
|
||||
|
|
|
|||
|
|
@ -38,10 +38,10 @@ struct InstantPageTextImageItem {
|
|||
let id: EngineMedia.Id
|
||||
}
|
||||
|
||||
struct InstantPageTextAnchorItem {
|
||||
let name: String
|
||||
let anchorText: NSAttributedString?
|
||||
let empty: Bool
|
||||
public struct InstantPageTextAnchorItem {
|
||||
public let name: String
|
||||
public let anchorText: NSAttributedString?
|
||||
public let empty: Bool
|
||||
}
|
||||
|
||||
public struct InstantPageTextRangeRectEdge: Equatable {
|
||||
|
|
@ -63,7 +63,7 @@ public final class InstantPageTextLine {
|
|||
let strikethroughItems: [InstantPageTextStrikethroughItem]
|
||||
let markedItems: [InstantPageTextMarkedItem]
|
||||
let imageItems: [InstantPageTextImageItem]
|
||||
let anchorItems: [InstantPageTextAnchorItem]
|
||||
public let anchorItems: [InstantPageTextAnchorItem]
|
||||
let isRTL: Bool
|
||||
|
||||
init(line: CTLine, range: NSRange, frame: CGRect, strikethroughItems: [InstantPageTextStrikethroughItem], markedItems: [InstantPageTextMarkedItem], imageItems: [InstantPageTextImageItem], anchorItems: [InstantPageTextAnchorItem], isRTL: Bool) {
|
||||
|
|
|
|||
|
|
@ -477,16 +477,18 @@ public final class LegacyPaintEntityRenderer: NSObject, TGPhotoPaintEntityRender
|
|||
}
|
||||
|
||||
func lcm(_ x: Int64, _ y: Int64) -> Int64 {
|
||||
let x = max(x, 1)
|
||||
let y = max(y, 1)
|
||||
return x / gcd(x, y) * y
|
||||
}
|
||||
|
||||
|
||||
return combineLatest(durations)
|
||||
|> map { durations in
|
||||
var result: Double
|
||||
let minDuration: Double = 3.0
|
||||
if durations.count > 1 {
|
||||
let reduced = durations.reduce(1.0) { lhs, rhs -> Double in
|
||||
return Double(lcm(Int64(lhs * 10.0), Int64(rhs * 10.0)))
|
||||
return Double(lcm(Int64(lhs * 100.0), Int64(rhs * 100.0)))
|
||||
}
|
||||
result = min(6.0, Double(reduced) / 10.0)
|
||||
} else if let duration = durations.first {
|
||||
|
|
|
|||
|
|
@ -1104,7 +1104,7 @@ private final class TwoFactorDataInputTextNode: ASDisplayNode, UITextFieldDelega
|
|||
self.hideButtonNode.isHidden = confirmation
|
||||
case .email:
|
||||
self.inputNode.textField.keyboardType = .emailAddress
|
||||
self.inputNode.textField.returnKeyType = .done
|
||||
self.inputNode.textField.returnKeyType = .next
|
||||
self.hideButtonNode.isHidden = true
|
||||
|
||||
if #available(iOS 12.0, *) {
|
||||
|
|
@ -1134,7 +1134,7 @@ private final class TwoFactorDataInputTextNode: ASDisplayNode, UITextFieldDelega
|
|||
}
|
||||
case .hint:
|
||||
self.inputNode.textField.keyboardType = .asciiCapable
|
||||
self.inputNode.textField.returnKeyType = .done
|
||||
self.inputNode.textField.returnKeyType = .next
|
||||
self.hideButtonNode.isHidden = true
|
||||
|
||||
self.inputNode.textField.autocorrectionType = .no
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import AccountContext
|
|||
import InstantPageUI
|
||||
import InstantPageCache
|
||||
import UrlHandling
|
||||
import TelegramUIPreferences
|
||||
|
||||
func faqSearchableItems(context: AccountContext, resolvedUrl: Signal<ResolvedUrl?, NoError>, suggestAccountDeletion: Bool) -> Signal<[SettingsSearchableItem], NoError> {
|
||||
let strings = context.sharedContext.currentPresentationData.with { $0 }.strings
|
||||
|
|
@ -45,8 +46,9 @@ func faqSearchableItems(context: AccountContext, resolvedUrl: Signal<ResolvedUrl
|
|||
} else {
|
||||
nextIndex += 1
|
||||
}
|
||||
let item = SettingsSearchableItem(id: .faq(index), title: text.plainText, alternate: [], icon: .faq, breadcrumbs: [strings.SettingsSearch_FAQ, currentSection], present: { context, _, present in
|
||||
present(.push, InstantPageController(context: context, webPage: webPage, sourceLocation: InstantPageSourceLocation(userLocation: .other, peerType: .channel), anchor: anchor))
|
||||
let item = SettingsSearchableItem(id: .faq(index), title: text.plainText, alternate: [], icon: .faq, breadcrumbs: [strings.SettingsSearch_FAQ, currentSection], present: { context, _, present in
|
||||
let controller = context.sharedContext.makeInstantPageController(context: context, webPage: webPage, anchor: anchor, sourceLocation: InstantPageSourceLocation(userLocation: .other, peerType: .channel))
|
||||
present(.push, controller)
|
||||
})
|
||||
if index == 1 {
|
||||
results.insert(item, at: 0)
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import WebKit
|
|||
import LinkPresentation
|
||||
import CoreServices
|
||||
import PersistentStringHash
|
||||
import UrlHandling
|
||||
|
||||
private final class WebBrowserSettingsControllerArguments {
|
||||
let context: AccountContext
|
||||
|
|
@ -453,21 +454,6 @@ public func webBrowserSettingsController(context: AccountContext) -> ViewControl
|
|||
return controller
|
||||
}
|
||||
|
||||
private func cleanDomain(url: String) -> (domain: String, fullUrl: String) {
|
||||
if let parsedUrl = URL(string: url) {
|
||||
let host: String?
|
||||
let scheme = parsedUrl.scheme ?? "https"
|
||||
if #available(iOS 16.0, *) {
|
||||
host = parsedUrl.host(percentEncoded: true)?.lowercased()
|
||||
} else {
|
||||
host = parsedUrl.host?.lowercased()
|
||||
}
|
||||
return (host ?? url, "\(scheme)://\(host ?? "")")
|
||||
} else {
|
||||
return (url, url)
|
||||
}
|
||||
}
|
||||
|
||||
private func fetchDomainExceptionInfo(context: AccountContext, url: String) -> Signal<WebBrowserException, NoError> {
|
||||
let (domain, domainUrl) = cleanDomain(url: url)
|
||||
if #available(iOS 13.0, *), let url = URL(string: domainUrl) {
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ swift_library(
|
|||
"//submodules/TextFormat",
|
||||
"//submodules/CounterControllerTitleView",
|
||||
"//submodules/TelegramUI/Components/AdminUserActionsSheet",
|
||||
"//submodules/BrowserUI",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
|
|
|||
|
|
@ -320,7 +320,9 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode {
|
|||
self?.openUrl(url.url)
|
||||
}, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { [weak self] message, associatedData in
|
||||
if let strongSelf = self, let navigationController = strongSelf.getNavigationController() {
|
||||
strongSelf.context.sharedContext.openChatInstantPage(context: strongSelf.context, message: message, sourcePeerType: associatedData?.automaticDownloadPeerType, navigationController: navigationController)
|
||||
if let controller = strongSelf.context.sharedContext.makeInstantPageController(context: strongSelf.context, message: message, sourcePeerType: associatedData?.automaticDownloadPeerType) {
|
||||
navigationController.pushViewController(controller)
|
||||
}
|
||||
}
|
||||
}, openWallpaper: { [weak self] message in
|
||||
if let strongSelf = self{
|
||||
|
|
@ -1223,8 +1225,9 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode {
|
|||
}
|
||||
case .chatFolder:
|
||||
break
|
||||
case let .instantView(webpage, anchor):
|
||||
strongSelf.pushController(InstantPageController(context: strongSelf.context, webPage: webpage, sourceLocation: InstantPageSourceLocation(userLocation: .peer(strongSelf.peer.id), peerType: .channel), anchor: anchor))
|
||||
case let .instantView(webPage, anchor):
|
||||
let browserController = strongSelf.context.sharedContext.makeInstantPageController(context: strongSelf.context, webPage: webPage, anchor: anchor, sourceLocation: InstantPageSourceLocation(userLocation: .peer(strongSelf.peer.id), peerType: .channel))
|
||||
strongSelf.pushController(browserController)
|
||||
case let .join(link):
|
||||
strongSelf.presentController(JoinLinkPreviewController(context: strongSelf.context, link: link, navigateToPeer: { peer, peekData in
|
||||
if let strongSelf = self {
|
||||
|
|
@ -1257,6 +1260,8 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode {
|
|||
break
|
||||
case .premiumOffer:
|
||||
break
|
||||
case .starsTopup:
|
||||
break
|
||||
case let .joinVoiceChat(peerId, invite):
|
||||
strongSelf.presentController(VoiceChatJoinScreen(context: strongSelf.context, peerId: peerId, invite: invite, join: { call in
|
||||
}), .window(.root), nil)
|
||||
|
|
|
|||
|
|
@ -776,7 +776,7 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget {
|
|||
}
|
||||
|
||||
public final class EmojiTextAttachmentView: UIView {
|
||||
private let contentLayer: InlineStickerItemLayer
|
||||
public let contentLayer: InlineStickerItemLayer
|
||||
|
||||
public var isActive: Bool = true {
|
||||
didSet {
|
||||
|
|
@ -826,7 +826,7 @@ public final class EmojiTextAttachmentView: UIView {
|
|||
public final class CustomEmojiContainerView: UIView {
|
||||
private let emojiViewProvider: (ChatTextInputTextCustomEmojiAttribute) -> UIView?
|
||||
|
||||
private var emojiLayers: [InlineStickerItemLayer.Key: UIView] = [:]
|
||||
public private(set) var emojiLayers: [InlineStickerItemLayer.Key: UIView] = [:]
|
||||
|
||||
public init(emojiViewProvider: @escaping (ChatTextInputTextCustomEmojiAttribute) -> UIView?) {
|
||||
self.emojiViewProvider = emojiViewProvider
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import Foundation
|
||||
import TelegramCore
|
||||
import UrlEscaping
|
||||
|
||||
public func decodeCodableDrawingEntities(data: Data) -> [CodableDrawingEntity] {
|
||||
if let codableEntities = try? JSONDecoder().decode([CodableDrawingEntity].self, from: data) {
|
||||
|
|
@ -183,13 +184,9 @@ public enum CodableDrawingEntity: Equatable {
|
|||
return nil
|
||||
}
|
||||
case let .link(entity):
|
||||
var url = entity.url
|
||||
if !url.hasPrefix("http://") && !url.hasPrefix("https://") {
|
||||
url = "https://\(url)"
|
||||
}
|
||||
return .link(
|
||||
coordinates: coordinates,
|
||||
url: url
|
||||
url: explicitUrl(entity.url)
|
||||
)
|
||||
case let .weather(entity):
|
||||
let color: UInt32
|
||||
|
|
|
|||
|
|
@ -88,11 +88,15 @@ public final class DrawingTextEntity: DrawingEntity, Codable {
|
|||
return true
|
||||
}
|
||||
var isAnimated = false
|
||||
self.text.enumerateAttributes(in: NSMakeRange(0, self.text.length), options: [], using: { attributes, range, _ in
|
||||
if let _ = attributes[ChatTextInputAttributes.customEmoji] as? ChatTextInputTextCustomEmojiAttribute {
|
||||
isAnimated = true
|
||||
|
||||
if let renderSubEntities = self.renderSubEntities {
|
||||
for entity in renderSubEntities {
|
||||
if entity.isAnimated {
|
||||
isAnimated = true
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
return isAnimated
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -466,14 +466,21 @@ private final class CreateLinkSheetComponent: CombinedComponent {
|
|||
let text = !self.name.isEmpty ? self.name : self.link
|
||||
|
||||
var effectiveMedia: TelegramMediaWebpage?
|
||||
if let webpage = self.webpage, case .Loaded = webpage.content, !self.dismissed {
|
||||
var webpageHasLargeMedia = false
|
||||
if let webpage = self.webpage, case let .Loaded(content) = webpage.content, !self.dismissed {
|
||||
effectiveMedia = webpage
|
||||
|
||||
if let isMediaLargeByDefault = content.isMediaLargeByDefault, isMediaLargeByDefault {
|
||||
webpageHasLargeMedia = true
|
||||
} else {
|
||||
webpageHasLargeMedia = true
|
||||
}
|
||||
}
|
||||
|
||||
var attributes: [MessageAttribute] = []
|
||||
attributes.append(TextEntitiesMessageAttribute(entities: [.init(range: 0 ..< (text as NSString).length, type: .Url)]))
|
||||
if !self.dismissed {
|
||||
attributes.append(WebpagePreviewMessageAttribute(leadingPreview: !self.positionBelowText, forceLargeMedia: self.largeMedia, isManuallyAdded: false, isSafe: true))
|
||||
attributes.append(WebpagePreviewMessageAttribute(leadingPreview: !self.positionBelowText, forceLargeMedia: self.largeMedia ?? webpageHasLargeMedia, isManuallyAdded: false, isSafe: true))
|
||||
}
|
||||
|
||||
let peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(1))
|
||||
|
|
|
|||
|
|
@ -5940,13 +5940,10 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
|||
}
|
||||
self.push(controller)
|
||||
|
||||
editCoverImpl = { [weak self, weak controller] in
|
||||
editCoverImpl = { [weak self] in
|
||||
if let self {
|
||||
self.node.openCoverSelection(exclusive: false)
|
||||
}
|
||||
if let controller {
|
||||
controller.dismiss()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import UIKitRuntimeUtils
|
|||
|
||||
private let minimizedNavigationHeight: CGFloat = 44.0
|
||||
private let minimizedTopMargin: CGFloat = 3.0
|
||||
private let maximizeLastStandingController = false
|
||||
|
||||
final class ScrollViewImpl: UIScrollView {
|
||||
var shouldPassthrough: () -> Bool = { return false }
|
||||
|
|
@ -112,7 +113,7 @@ public class MinimizedContainerImpl: ASDisplayNode, MinimizedContainer, ASScroll
|
|||
self.snapshotContainerView.isUserInteractionEnabled = false
|
||||
|
||||
super.init()
|
||||
|
||||
|
||||
self.clipsToBounds = true
|
||||
self.cornerRadius = 10.0
|
||||
applySmoothRoundedCorners(self.layer)
|
||||
|
|
@ -308,7 +309,9 @@ public class MinimizedContainerImpl: ASDisplayNode, MinimizedContainer, ASScroll
|
|||
private var presentationDataDisposable: Disposable?
|
||||
|
||||
public private(set) var isExpanded: Bool = false
|
||||
public var willMaximize: (() -> Void)?
|
||||
public var willMaximize: ((MinimizedContainer) -> Void)?
|
||||
public var willDismiss: ((MinimizedContainer) -> Void)?
|
||||
public var didDismiss: ((MinimizedContainer) -> Void)?
|
||||
|
||||
public private(set) var statusBarStyle: StatusBarStyle = .White
|
||||
public var statusBarStyleUpdated: (() -> Void)?
|
||||
|
|
@ -500,10 +503,13 @@ public class MinimizedContainerImpl: ASDisplayNode, MinimizedContainer, ASScroll
|
|||
self.currentTransition = .dismiss(itemId: itemId)
|
||||
|
||||
self.items.removeAll(where: { $0.id == itemId })
|
||||
if self.items.count == 1 {
|
||||
if self.items.count == 1, maximizeLastStandingController {
|
||||
self.isExpanded = false
|
||||
self.willMaximize?()
|
||||
self.willMaximize?(self)
|
||||
needsLayout = false
|
||||
} else if self.items.count == 0 {
|
||||
self.willDismiss?(self)
|
||||
self.isExpanded = false
|
||||
}
|
||||
}
|
||||
if let item = self.items.first(where: { $0.id == itemId }), !item.controller.shouldDismissImmediately() {
|
||||
|
|
@ -558,6 +564,15 @@ public class MinimizedContainerImpl: ASDisplayNode, MinimizedContainer, ASScroll
|
|||
self.requestUpdate(transition: transition)
|
||||
}
|
||||
|
||||
public func removeController(_ viewController: MinimizableController) {
|
||||
guard let item = self.items.first(where: { $0.controller === viewController }) else {
|
||||
return
|
||||
}
|
||||
|
||||
self.items.removeAll(where: { $0.id == item.id })
|
||||
self.requestUpdate(transition: .animated(duration: 0.25, curve: .easeInOut))
|
||||
}
|
||||
|
||||
private enum Transition: Equatable {
|
||||
case minimize(itemId: AnyHashable)
|
||||
case maximize(itemId: AnyHashable)
|
||||
|
|
@ -759,7 +774,7 @@ public class MinimizedContainerImpl: ASDisplayNode, MinimizedContainer, ASScroll
|
|||
if let currentTransition = self.currentTransition {
|
||||
if currentTransition.matches(item: item) {
|
||||
continue
|
||||
} else if case .dismiss = currentTransition, self.items.count == 1 {
|
||||
} else if case .dismiss = currentTransition, self.items.count == 1 && maximizeLastStandingController {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
|
@ -789,10 +804,13 @@ public class MinimizedContainerImpl: ASDisplayNode, MinimizedContainer, ASScroll
|
|||
self.currentTransition = .dismiss(itemId: item.id)
|
||||
|
||||
self.items.removeAll(where: { $0.id == item.id })
|
||||
if self.items.count == 1 {
|
||||
if self.items.count == 1, maximizeLastStandingController {
|
||||
self.isExpanded = false
|
||||
self.willMaximize?()
|
||||
self.willMaximize?(self)
|
||||
needsLayout = false
|
||||
} else if self.items.count == 0 {
|
||||
self.isExpanded = false
|
||||
self.willDismiss?(self)
|
||||
}
|
||||
if needsLayout {
|
||||
self.requestUpdate(transition: .animated(duration: 0.4, curve: .spring))
|
||||
|
|
@ -1097,7 +1115,7 @@ public class MinimizedContainerImpl: ASDisplayNode, MinimizedContainer, ASScroll
|
|||
guard let dismissedItemNode = self.itemNodes[itemId] else {
|
||||
return
|
||||
}
|
||||
if self.items.count == 1 {
|
||||
if self.items.count == 1, maximizeLastStandingController {
|
||||
if let itemNode = self.itemNodes.first(where: { $0.0 != itemId })?.value, let navigationController = self.navigationController {
|
||||
itemNode.item.beforeMaximize(navigationController, { [weak self] in
|
||||
guard let self else {
|
||||
|
|
@ -1137,6 +1155,7 @@ public class MinimizedContainerImpl: ASDisplayNode, MinimizedContainer, ASScroll
|
|||
}
|
||||
transition.updatePosition(node: dismissedItemNode, position: CGPoint(x: -layout.size.width, y: dismissedItemNode.position.y))
|
||||
} else {
|
||||
let isLast = self.items.isEmpty
|
||||
transition.updatePosition(node: dismissedItemNode, position: CGPoint(x: -layout.size.width, y: dismissedItemNode.position.y), completion: { _ in
|
||||
self.isApplyingTransition = false
|
||||
if self.currentTransition == currentTransition {
|
||||
|
|
@ -1146,7 +1165,15 @@ public class MinimizedContainerImpl: ASDisplayNode, MinimizedContainer, ASScroll
|
|||
|
||||
self.itemNodes[itemId] = nil
|
||||
dismissedItemNode.removeFromSupernode()
|
||||
|
||||
if isLast {
|
||||
self.didDismiss?(self)
|
||||
}
|
||||
})
|
||||
if isLast {
|
||||
let dismissOffset = collapsedHeight(layout: layout)
|
||||
transition.updatePosition(layer: self.bottomEdgeView.layer, position: self.bottomEdgeView.layer.position.offsetBy(dx: 0.0, dy: dismissOffset))
|
||||
}
|
||||
}
|
||||
case .dismissAll:
|
||||
let dismissOffset = collapsedHeight(layout: layout)
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ final class MinimizedHeaderNode: ASDisplayNode {
|
|||
guard let self else {
|
||||
return
|
||||
}
|
||||
let titles = titles.compactMap { $0 }
|
||||
let titles = titles.compactMap { $0 }.filter { !$0.isEmpty }
|
||||
if titles.count == 1, let title = titles.first {
|
||||
self.title = title
|
||||
} else if let title = titles.last {
|
||||
|
|
|
|||
|
|
@ -113,7 +113,10 @@ func interitemSpacing(itemCount: Int, boundingSize: CGSize, insets: UIEdgeInsets
|
|||
|
||||
func frameForIndex(index: Int, size: CGSize, insets: UIEdgeInsets, itemCount: Int, boundingSize: CGSize) -> CGRect {
|
||||
let spacing = interitemSpacing(itemCount: itemCount, boundingSize: boundingSize, insets: insets)
|
||||
let y = additionalInsetTop + insets.top + spacing * CGFloat(index)
|
||||
var y = additionalInsetTop + insets.top + spacing * CGFloat(index)
|
||||
if itemCount == 1 {
|
||||
y += 72.0
|
||||
}
|
||||
let origin = CGPoint(x: insets.left, y: y)
|
||||
|
||||
return CGRect(origin: origin, size: CGSize(width: size.width - insets.left - insets.right, height: size.height))
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ private var nextRenderTargetId: Int64 = 1
|
|||
|
||||
open class MultiAnimationRenderTarget: SimpleLayer {
|
||||
public let id: Int64
|
||||
public var numFrames: Int?
|
||||
|
||||
let deinitCallbacks = Bag<() -> Void>()
|
||||
let updateStateCallbacks = Bag<() -> Void>()
|
||||
|
|
@ -545,6 +546,7 @@ public final class MultiAnimationRendererImpl: MultiAnimationRenderer {
|
|||
}
|
||||
|
||||
target.contents = loadedFrame.image.cgImage
|
||||
target.numFrames = item.numFrames
|
||||
|
||||
if let blurredRepresentationTarget = target.blurredRepresentationTarget {
|
||||
blurredRepresentationTarget.contents = loadedFrame.blurredRepresentation(color: target.blurredRepresentationBackgroundColor)?.cgImage
|
||||
|
|
@ -580,6 +582,7 @@ public final class MultiAnimationRendererImpl: MultiAnimationRenderer {
|
|||
completion(false, true)
|
||||
return
|
||||
}
|
||||
target.numFrames = item.numFrames
|
||||
if let loadedFrame = loadedFrame {
|
||||
if let cgImage = loadedFrame.image.cgImage {
|
||||
if hadIntermediateUpdate {
|
||||
|
|
|
|||
|
|
@ -3351,7 +3351,9 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
|
|||
}
|
||||
|
||||
if let foundGalleryMessage = foundGalleryMessage {
|
||||
strongSelf.context.sharedContext.openChatInstantPage(context: strongSelf.context, message: foundGalleryMessage, sourcePeerType: associatedData?.automaticDownloadPeerType, navigationController: navigationController)
|
||||
if let controller = strongSelf.context.sharedContext.makeInstantPageController(context: strongSelf.context, message: foundGalleryMessage, sourcePeerType: associatedData?.automaticDownloadPeerType) {
|
||||
navigationController.pushViewController(controller)
|
||||
}
|
||||
}
|
||||
}, openWallpaper: { _ in
|
||||
}, openTheme: { _ in
|
||||
|
|
|
|||
|
|
@ -1690,8 +1690,12 @@ final class ShareWithPeersScreenComponent: Component {
|
|||
title: item.title,
|
||||
image: item.image,
|
||||
hasNext: false,
|
||||
action: {
|
||||
action: { [weak self] in
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
}
|
||||
component.editCover()
|
||||
self.saveAndDismiss()
|
||||
}
|
||||
)),
|
||||
environment: {},
|
||||
|
|
@ -1993,6 +1997,36 @@ final class ShareWithPeersScreenComponent: Component {
|
|||
private var currentHasChannels: Bool?
|
||||
private var currentHasCover: Bool?
|
||||
|
||||
func saveAndDismiss() {
|
||||
guard let component = self.component, let environment = self.environment, let controller = environment.controller() as? ShareWithPeersScreen else {
|
||||
return
|
||||
}
|
||||
let base: EngineStoryPrivacy.Base
|
||||
if self.selectedCategories.contains(.everyone) {
|
||||
base = .everyone
|
||||
} else if self.selectedCategories.contains(.closeFriends) {
|
||||
base = .closeFriends
|
||||
} else if self.selectedCategories.contains(.contacts) {
|
||||
base = .contacts
|
||||
} else if self.selectedCategories.contains(.selectedContacts) {
|
||||
base = .nobody
|
||||
} else {
|
||||
base = .nobody
|
||||
}
|
||||
component.completion(
|
||||
self.sendAsPeerId,
|
||||
EngineStoryPrivacy(
|
||||
base: base,
|
||||
additionallyIncludePeers: self.selectedPeers
|
||||
),
|
||||
self.selectedOptions.contains(.screenshot),
|
||||
self.selectedOptions.contains(.pin),
|
||||
self.component?.stateContext.stateValue?.peers.filter { self.selectedPeers.contains($0.id) } ?? [],
|
||||
false
|
||||
)
|
||||
controller.requestDismiss()
|
||||
}
|
||||
|
||||
func update(component: ShareWithPeersScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<ViewControllerComponentContainer.Environment>, transition: ComponentTransition) -> CGSize {
|
||||
guard !self.isDismissed else {
|
||||
return availableSize
|
||||
|
|
@ -2475,33 +2509,10 @@ final class ShareWithPeersScreenComponent: Component {
|
|||
component: AnyComponent(Button(
|
||||
content: AnyComponent(Text(text: environment.strings.Common_Cancel, font: Font.regular(17.0), color: environment.theme.rootController.navigationBar.accentTextColor)),
|
||||
action: { [weak self] in
|
||||
guard let self, let environment = self.environment, let controller = environment.controller() as? ShareWithPeersScreen else {
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
let base: EngineStoryPrivacy.Base
|
||||
if self.selectedCategories.contains(.everyone) {
|
||||
base = .everyone
|
||||
} else if self.selectedCategories.contains(.closeFriends) {
|
||||
base = .closeFriends
|
||||
} else if self.selectedCategories.contains(.contacts) {
|
||||
base = .contacts
|
||||
} else if self.selectedCategories.contains(.selectedContacts) {
|
||||
base = .nobody
|
||||
} else {
|
||||
base = .nobody
|
||||
}
|
||||
component.completion(
|
||||
self.sendAsPeerId,
|
||||
EngineStoryPrivacy(
|
||||
base: base,
|
||||
additionallyIncludePeers: self.selectedPeers
|
||||
),
|
||||
self.selectedOptions.contains(.screenshot),
|
||||
self.selectedOptions.contains(.pin),
|
||||
self.component?.stateContext.stateValue?.peers.filter { self.selectedPeers.contains($0.id) } ?? [],
|
||||
false
|
||||
)
|
||||
controller.requestDismiss()
|
||||
self.saveAndDismiss()
|
||||
}
|
||||
).minSize(CGSize(width: navigationHeight, height: navigationHeight))),
|
||||
environment: {},
|
||||
|
|
|
|||
|
|
@ -208,7 +208,8 @@ private final class StarsPurchaseScreenContentComponent: CombinedComponent {
|
|||
|
||||
let textString: String
|
||||
switch context.component.purpose {
|
||||
case .generic:
|
||||
case let .generic(requiredStars):
|
||||
let _ = requiredStars
|
||||
textString = strings.Stars_Purchase_GetStarsInfo
|
||||
case .gift:
|
||||
textString = strings.Stars_Purchase_GiftInfo(component.peers.first?.value.compactDisplayTitle ?? "").string
|
||||
|
|
@ -299,8 +300,13 @@ private final class StarsPurchaseScreenContentComponent: CombinedComponent {
|
|||
if let products = state.products, let balance = context.component.balance {
|
||||
var minimumCount: Int64?
|
||||
if let requiredStars = context.component.purpose.requiredStars {
|
||||
minimumCount = requiredStars - balance
|
||||
if case .generic = context.component.purpose {
|
||||
minimumCount = requiredStars
|
||||
} else {
|
||||
minimumCount = requiredStars - balance
|
||||
}
|
||||
}
|
||||
|
||||
for product in products {
|
||||
if let minimumCount, minimumCount > product.count && !(items.isEmpty && product.id == products.last?.id) {
|
||||
continue
|
||||
|
|
@ -810,8 +816,12 @@ private final class StarsPurchaseScreenComponent: CombinedComponent {
|
|||
|
||||
let titleText: String
|
||||
switch context.component.purpose {
|
||||
case .generic:
|
||||
titleText = strings.Stars_Purchase_GetStars
|
||||
case let .generic(requiredStars):
|
||||
if let requiredStars {
|
||||
titleText = strings.Stars_Purchase_StarsNeeded(Int32(requiredStars))
|
||||
} else {
|
||||
titleText = strings.Stars_Purchase_GetStars
|
||||
}
|
||||
case .gift:
|
||||
titleText = strings.Stars_Purchase_GiftStars
|
||||
case let .transfer(_, requiredStars), let .subscription(_, requiredStars, _), let .unlockMedia(requiredStars):
|
||||
|
|
@ -1216,6 +1226,8 @@ private extension StarsPurchasePurpose {
|
|||
|
||||
var requiredStars: Int64? {
|
||||
switch self {
|
||||
case let .generic(requiredStars):
|
||||
return requiredStars
|
||||
case let .transfer(_, requiredStars):
|
||||
return requiredStars
|
||||
case let .subscription(_, requiredStars, _):
|
||||
|
|
|
|||
|
|
@ -806,7 +806,7 @@ public final class StarsTransactionsScreen: ViewControllerComponentContainer {
|
|||
guard let self else {
|
||||
return
|
||||
}
|
||||
let controller = context.sharedContext.makeStarsPurchaseScreen(context: context, starsContext: starsContext, options: options, purpose: .generic, completion: { [weak self] stars in
|
||||
let controller = context.sharedContext.makeStarsPurchaseScreen(context: context, starsContext: starsContext, options: options, purpose: .generic(requiredStars: nil), completion: { [weak self] stars in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -484,7 +484,7 @@ private final class SheetContent: CombinedComponent {
|
|||
} else if let peerId = state?.botPeer?.id {
|
||||
purpose = .transfer(peerId: peerId, requiredStars: invoice.totalAmount)
|
||||
} else {
|
||||
purpose = .generic
|
||||
purpose = .generic(requiredStars: nil)
|
||||
}
|
||||
let purchaseController = accountContext.sharedContext.makeStarsPurchaseScreen(
|
||||
context: accountContext,
|
||||
|
|
|
|||
|
|
@ -6118,7 +6118,7 @@ public final class StoryItemSetContainerComponent: Component {
|
|||
self.openStoryEditing()
|
||||
})))
|
||||
|
||||
if case .file = component.slice.item.storyItem.media {
|
||||
if case .file = component.slice.item.storyItem.media, component.slice.item.storyItem.isPinned {
|
||||
items.append(.action(ContextMenuActionItem(text: component.strings.Story_Context_EditCover, icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Stories/Context Menu/EditCover"), color: theme.contextMenu.primaryColor)
|
||||
}, action: { [weak self] _, a in
|
||||
|
|
@ -6316,7 +6316,7 @@ public final class StoryItemSetContainerComponent: Component {
|
|||
self.openStoryEditing()
|
||||
})))
|
||||
|
||||
if case .file = component.slice.item.storyItem.media {
|
||||
if case .file = component.slice.item.storyItem.media, component.slice.item.storyItem.isPinned {
|
||||
items.append(.action(ContextMenuActionItem(text: component.strings.Story_Context_EditCover, icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Stories/Context Menu/EditCover"), color: theme.contextMenu.primaryColor)
|
||||
}, action: { [weak self] _, a in
|
||||
|
|
|
|||
|
|
@ -402,156 +402,160 @@ public extension ChatControllerImpl {
|
|||
}
|
||||
}
|
||||
|
||||
func presentBotApp(botApp: BotApp, botPeer: EnginePeer, payload: String?, compact: Bool, concealed: Bool = false, commit: @escaping () -> Void = {}) {
|
||||
func presentBotApp(botApp: BotApp?, botPeer: EnginePeer, payload: String?, compact: Bool, concealed: Bool = false, commit: @escaping () -> Void = {}) {
|
||||
guard let peerId = self.chatLocation.peerId else {
|
||||
return
|
||||
}
|
||||
self.attachmentController?.dismiss(animated: true, completion: nil)
|
||||
|
||||
let openBotApp: (Bool, Bool) -> Void = { [weak self] allowWrite, justInstalled in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
commit()
|
||||
|
||||
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, {
|
||||
return $0.updatedTitlePanelContext {
|
||||
if !$0.contains(where: {
|
||||
switch $0 {
|
||||
case .requestInProgress:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}) {
|
||||
var updatedContexts = $0
|
||||
updatedContexts.append(.requestInProgress)
|
||||
return updatedContexts.sorted()
|
||||
}
|
||||
return $0
|
||||
}
|
||||
})
|
||||
|
||||
let updateProgress = { [weak self] in
|
||||
Queue.mainQueue().async {
|
||||
if let strongSelf = self {
|
||||
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, {
|
||||
return $0.updatedTitlePanelContext {
|
||||
if let index = $0.firstIndex(where: {
|
||||
switch $0 {
|
||||
case .requestInProgress:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}) {
|
||||
var updatedContexts = $0
|
||||
updatedContexts.remove(at: index)
|
||||
return updatedContexts
|
||||
}
|
||||
return $0
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let botAddress = botPeer.addressName ?? ""
|
||||
strongSelf.messageActionCallbackDisposable.set(((strongSelf.context.engine.messages.requestAppWebView(peerId: peerId, appReference: .id(id: botApp.id, accessHash: botApp.accessHash), payload: payload, themeParams: generateWebAppThemeParams(strongSelf.presentationData.theme), compact: compact, allowWrite: allowWrite)
|
||||
|> afterDisposed {
|
||||
updateProgress()
|
||||
})
|
||||
|> deliverOnMainQueue).startStrict(next: { [weak self] result in
|
||||
if let botApp {
|
||||
let openBotApp: (Bool, Bool) -> Void = { [weak self] allowWrite, justInstalled in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
let context = strongSelf.context
|
||||
let params = WebAppParameters(source: .generic, peerId: peerId, botId: botPeer.id, botName: botApp.title, botVerified: botPeer.isVerified, url: result.url, queryId: 0, payload: payload, buttonText: "", keepAliveSignal: nil, forceHasSettings: botApp.flags.contains(.hasSettings), fullSize: result.flags.contains(.fullSize))
|
||||
var presentImpl: ((ViewController, Any?) -> Void)?
|
||||
let controller = standaloneWebAppController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, params: params, threadId: strongSelf.chatLocation.threadId, openUrl: { [weak self] url, concealed, commit in
|
||||
ChatControllerImpl.botOpenUrl(context: context, peerId: peerId, controller: self, url: url, concealed: concealed, present: { c, a in
|
||||
presentImpl?(c, a)
|
||||
}, commit: commit)
|
||||
}, requestSwitchInline: { [weak self] query, chatTypes, completion in
|
||||
ChatControllerImpl.botRequestSwitchInline(context: context, controller: self, peerId: peerId, botAddress: botAddress, query: query, chatTypes: chatTypes, completion: completion)
|
||||
}, completion: { [weak self] in
|
||||
self?.chatDisplayNode.historyNode.scrollToEndOfHistory()
|
||||
}, getNavigationController: { [weak self] in
|
||||
return self?.effectiveNavigationController ?? context.sharedContext.mainWindow?.viewController as? NavigationController
|
||||
commit()
|
||||
|
||||
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, {
|
||||
return $0.updatedTitlePanelContext {
|
||||
if !$0.contains(where: {
|
||||
switch $0 {
|
||||
case .requestInProgress:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}) {
|
||||
var updatedContexts = $0
|
||||
updatedContexts.append(.requestInProgress)
|
||||
return updatedContexts.sorted()
|
||||
}
|
||||
return $0
|
||||
}
|
||||
})
|
||||
controller.navigationPresentation = .flatModal
|
||||
strongSelf.currentWebAppController = controller
|
||||
strongSelf.push(controller)
|
||||
|
||||
presentImpl = { [weak controller] c, a in
|
||||
controller?.present(c, in: .window(.root), with: a)
|
||||
}
|
||||
|
||||
if justInstalled {
|
||||
let content: UndoOverlayContent = .succeed(text: strongSelf.presentationData.strings.WebApp_ShortcutsSettingsAdded(botPeer.compactDisplayTitle).string, timeout: 5.0, customUndoText: nil)
|
||||
controller.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: content, elevatedLayout: false, position: .top, action: { _ in return false }), in: .current)
|
||||
}
|
||||
}, error: { [weak self] error in
|
||||
if let strongSelf = self {
|
||||
strongSelf.present(textAlertController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, title: nil, text: strongSelf.presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {
|
||||
})]), in: .window(.root))
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
let _ = combineLatest(
|
||||
queue: Queue.mainQueue(),
|
||||
ApplicationSpecificNotice.getBotGameNotice(accountManager: self.context.sharedContext.accountManager, peerId: botPeer.id),
|
||||
self.context.engine.messages.attachMenuBots(),
|
||||
self.context.engine.messages.getAttachMenuBot(botId: botPeer.id, cached: true)
|
||||
|> map(Optional.init)
|
||||
|> `catch` { _ -> Signal<AttachMenuBot?, NoError> in
|
||||
return .single(nil)
|
||||
}
|
||||
).startStandalone(next: { [weak self] noticed, attachMenuBots, attachMenuBot in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
|
||||
var isAttachMenuBotInstalled: Bool?
|
||||
if let _ = attachMenuBot {
|
||||
if let _ = attachMenuBots.first(where: { $0.peer.id == botPeer.id && !$0.flags.contains(.notActivated) }) {
|
||||
isAttachMenuBotInstalled = true
|
||||
} else {
|
||||
isAttachMenuBotInstalled = false
|
||||
}
|
||||
}
|
||||
|
||||
let context = self.context
|
||||
if !noticed || botApp.flags.contains(.notActivated) || isAttachMenuBotInstalled == false {
|
||||
if let isAttachMenuBotInstalled, let attachMenuBot {
|
||||
if !isAttachMenuBotInstalled {
|
||||
let controller = webAppTermsAlertController(context: context, updatedPresentationData: self.updatedPresentationData, bot: attachMenuBot, completion: { allowWrite in
|
||||
let _ = ApplicationSpecificNotice.setBotGameNotice(accountManager: context.sharedContext.accountManager, peerId: botPeer.id).startStandalone()
|
||||
let _ = (context.engine.messages.addBotToAttachMenu(botId: botPeer.id, allowWrite: allowWrite)
|
||||
|> deliverOnMainQueue).startStandalone(error: { _ in
|
||||
}, completed: {
|
||||
openBotApp(allowWrite, true)
|
||||
let updateProgress = { [weak self] in
|
||||
Queue.mainQueue().async {
|
||||
if let strongSelf = self {
|
||||
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, {
|
||||
return $0.updatedTitlePanelContext {
|
||||
if let index = $0.firstIndex(where: {
|
||||
switch $0 {
|
||||
case .requestInProgress:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}) {
|
||||
var updatedContexts = $0
|
||||
updatedContexts.remove(at: index)
|
||||
return updatedContexts
|
||||
}
|
||||
return $0
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let botAddress = botPeer.addressName ?? ""
|
||||
strongSelf.messageActionCallbackDisposable.set(((strongSelf.context.engine.messages.requestAppWebView(peerId: peerId, appReference: .id(id: botApp.id, accessHash: botApp.accessHash), payload: payload, themeParams: generateWebAppThemeParams(strongSelf.presentationData.theme), compact: compact, allowWrite: allowWrite)
|
||||
|> afterDisposed {
|
||||
updateProgress()
|
||||
})
|
||||
|> deliverOnMainQueue).startStrict(next: { [weak self] result in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
let context = strongSelf.context
|
||||
let params = WebAppParameters(source: .generic, peerId: peerId, botId: botPeer.id, botName: botApp.title, botVerified: botPeer.isVerified, url: result.url, queryId: 0, payload: payload, buttonText: "", keepAliveSignal: nil, forceHasSettings: botApp.flags.contains(.hasSettings), fullSize: result.flags.contains(.fullSize))
|
||||
var presentImpl: ((ViewController, Any?) -> Void)?
|
||||
let controller = standaloneWebAppController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, params: params, threadId: strongSelf.chatLocation.threadId, openUrl: { [weak self] url, concealed, commit in
|
||||
ChatControllerImpl.botOpenUrl(context: context, peerId: peerId, controller: self, url: url, concealed: concealed, present: { c, a in
|
||||
presentImpl?(c, a)
|
||||
}, commit: commit)
|
||||
}, requestSwitchInline: { [weak self] query, chatTypes, completion in
|
||||
ChatControllerImpl.botRequestSwitchInline(context: context, controller: self, peerId: peerId, botAddress: botAddress, query: query, chatTypes: chatTypes, completion: completion)
|
||||
}, completion: { [weak self] in
|
||||
self?.chatDisplayNode.historyNode.scrollToEndOfHistory()
|
||||
}, getNavigationController: { [weak self] in
|
||||
return self?.effectiveNavigationController ?? context.sharedContext.mainWindow?.viewController as? NavigationController
|
||||
})
|
||||
controller.navigationPresentation = .flatModal
|
||||
strongSelf.currentWebAppController = controller
|
||||
strongSelf.push(controller)
|
||||
|
||||
presentImpl = { [weak controller] c, a in
|
||||
controller?.present(c, in: .window(.root), with: a)
|
||||
}
|
||||
|
||||
if justInstalled {
|
||||
let content: UndoOverlayContent = .succeed(text: strongSelf.presentationData.strings.WebApp_ShortcutsSettingsAdded(botPeer.compactDisplayTitle).string, timeout: 5.0, customUndoText: nil)
|
||||
controller.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: content, elevatedLayout: false, position: .top, action: { _ in return false }), in: .current)
|
||||
}
|
||||
}, error: { [weak self] error in
|
||||
if let strongSelf = self {
|
||||
strongSelf.present(textAlertController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, title: nil, text: strongSelf.presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {
|
||||
})]), in: .window(.root))
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
let _ = combineLatest(
|
||||
queue: Queue.mainQueue(),
|
||||
ApplicationSpecificNotice.getBotGameNotice(accountManager: self.context.sharedContext.accountManager, peerId: botPeer.id),
|
||||
self.context.engine.messages.attachMenuBots(),
|
||||
self.context.engine.messages.getAttachMenuBot(botId: botPeer.id, cached: true)
|
||||
|> map(Optional.init)
|
||||
|> `catch` { _ -> Signal<AttachMenuBot?, NoError> in
|
||||
return .single(nil)
|
||||
}
|
||||
).startStandalone(next: { [weak self] noticed, attachMenuBots, attachMenuBot in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
|
||||
var isAttachMenuBotInstalled: Bool?
|
||||
if let _ = attachMenuBot {
|
||||
if let _ = attachMenuBots.first(where: { $0.peer.id == botPeer.id && !$0.flags.contains(.notActivated) }) {
|
||||
isAttachMenuBotInstalled = true
|
||||
} else {
|
||||
isAttachMenuBotInstalled = false
|
||||
}
|
||||
}
|
||||
|
||||
let context = self.context
|
||||
if !noticed || botApp.flags.contains(.notActivated) || isAttachMenuBotInstalled == false {
|
||||
if let isAttachMenuBotInstalled, let attachMenuBot {
|
||||
if !isAttachMenuBotInstalled {
|
||||
let controller = webAppTermsAlertController(context: context, updatedPresentationData: self.updatedPresentationData, bot: attachMenuBot, completion: { allowWrite in
|
||||
let _ = ApplicationSpecificNotice.setBotGameNotice(accountManager: context.sharedContext.accountManager, peerId: botPeer.id).startStandalone()
|
||||
let _ = (context.engine.messages.addBotToAttachMenu(botId: botPeer.id, allowWrite: allowWrite)
|
||||
|> deliverOnMainQueue).startStandalone(error: { _ in
|
||||
}, completed: {
|
||||
openBotApp(allowWrite, true)
|
||||
})
|
||||
})
|
||||
self.present(controller, in: .window(.root))
|
||||
} else {
|
||||
openBotApp(false, false)
|
||||
}
|
||||
} else {
|
||||
let controller = webAppLaunchConfirmationController(context: context, updatedPresentationData: self.updatedPresentationData, peer: botPeer, requestWriteAccess: botApp.flags.contains(.notActivated) && botApp.flags.contains(.requiresWriteAccess), completion: { allowWrite in
|
||||
let _ = ApplicationSpecificNotice.setBotGameNotice(accountManager: context.sharedContext.accountManager, peerId: botPeer.id).startStandalone()
|
||||
openBotApp(allowWrite, false)
|
||||
}, showMore: { [weak self] in
|
||||
if let self {
|
||||
self.openResolved(result: .peer(botPeer._asPeer(), .info(nil)), sourceMessageId: nil)
|
||||
}
|
||||
})
|
||||
self.present(controller, in: .window(.root))
|
||||
} else {
|
||||
openBotApp(false, false)
|
||||
}
|
||||
} else {
|
||||
let controller = webAppLaunchConfirmationController(context: context, updatedPresentationData: self.updatedPresentationData, peer: botPeer, requestWriteAccess: botApp.flags.contains(.notActivated) && botApp.flags.contains(.requiresWriteAccess), completion: { allowWrite in
|
||||
let _ = ApplicationSpecificNotice.setBotGameNotice(accountManager: context.sharedContext.accountManager, peerId: botPeer.id).startStandalone()
|
||||
openBotApp(allowWrite, false)
|
||||
}, showMore: { [weak self] in
|
||||
if let self {
|
||||
self.openResolved(result: .peer(botPeer._asPeer(), .info(nil)), sourceMessageId: nil)
|
||||
}
|
||||
})
|
||||
self.present(controller, in: .window(.root))
|
||||
openBotApp(false, false)
|
||||
}
|
||||
} else {
|
||||
openBotApp(false, false)
|
||||
}
|
||||
})
|
||||
})
|
||||
} else {
|
||||
self.context.sharedContext.openWebApp(context: self.context, parentController: self, updatedPresentationData: self.updatedPresentationData, peer: botPeer, threadId: nil, buttonText: "", url: "", simple: true, source: .generic, skipTermsOfService: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2532,8 +2532,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
|||
if let strongSelf = self, strongSelf.isNodeLoaded, let navigationController = strongSelf.effectiveNavigationController, let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(message.id) {
|
||||
let _ = strongSelf.presentVoiceMessageDiscardAlert(action: {
|
||||
strongSelf.chatDisplayNode.dismissInput()
|
||||
strongSelf.context.sharedContext.openChatInstantPage(context: strongSelf.context, message: message, sourcePeerType: associatedData?.automaticDownloadPeerType, navigationController: navigationController)
|
||||
|
||||
if let controller = strongSelf.context.sharedContext.makeInstantPageController(context: strongSelf.context, message: message, sourcePeerType: associatedData?.automaticDownloadPeerType) {
|
||||
navigationController.pushViewController(controller)
|
||||
}
|
||||
if case .overlay = strongSelf.presentationInterfaceState.mode {
|
||||
strongSelf.chatDisplayNode.dismissAsOverlay()
|
||||
}
|
||||
|
|
@ -9211,10 +9212,15 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
|||
let _ = (strongSelf.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId.id))
|
||||
|> deliverOnMainQueue).startStandalone(next: { [weak self] peer in
|
||||
if let strongSelf = self, let peer {
|
||||
strongSelf.presentBotApp(botApp: botAppStart.botApp, botPeer: peer, payload: botAppStart.payload, compact: botAppStart.compact, concealed: concealed, commit: {
|
||||
dismissWebAppControllers()
|
||||
if let botApp = botAppStart.botApp {
|
||||
strongSelf.presentBotApp(botApp: botApp, botPeer: peer, payload: botAppStart.payload, compact: botAppStart.compact, concealed: concealed, commit: {
|
||||
dismissWebAppControllers()
|
||||
commit()
|
||||
})
|
||||
} else {
|
||||
strongSelf.context.sharedContext.openWebApp(context: strongSelf.context, parentController: strongSelf, updatedPresentationData: strongSelf.updatedPresentationData, peer: peer, threadId: nil, buttonText: "", url: "", simple: true, source: .generic, skipTermsOfService: false)
|
||||
commit()
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
default:
|
||||
|
|
@ -9256,7 +9262,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
|||
break
|
||||
default:
|
||||
progress?.set(.single(false))
|
||||
self.context.sharedContext.openChatInstantPage(context: self.context, message: message, sourcePeerType: nil, navigationController: navigationController)
|
||||
if let controller = self.context.sharedContext.makeInstantPageController(context: self.context, message: message, sourcePeerType: nil) {
|
||||
navigationController.pushViewController(controller)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -381,18 +381,16 @@ func openChatMessageImpl(_ params: OpenChatMessageParams) -> Bool {
|
|||
return false
|
||||
}
|
||||
|
||||
func openChatInstantPageImpl(context: AccountContext, message: Message, sourcePeerType: MediaAutoDownloadPeerType?, navigationController: NavigationController) {
|
||||
if let (webpage, anchor) = instantPageAndAnchor(message: message) {
|
||||
let sourceLocation = InstantPageSourceLocation(userLocation: .peer(message.id.peerId), peerType: sourcePeerType ?? .channel)
|
||||
|
||||
let pageController: ViewController
|
||||
if context.sharedContext.immediateExperimentalUISettings.browserExperiment {
|
||||
pageController = BrowserScreen(context: context, subject: .instantPage(webPage: webpage, anchor: anchor, sourceLocation: sourceLocation))
|
||||
} else {
|
||||
pageController = InstantPageController(context: context, webPage: webpage, sourceLocation: sourceLocation, anchor: anchor)
|
||||
}
|
||||
navigationController.pushViewController(pageController)
|
||||
func makeInstantPageControllerImpl(context: AccountContext, message: Message, sourcePeerType: MediaAutoDownloadPeerType?) -> ViewController? {
|
||||
guard let (webpage, anchor) = instantPageAndAnchor(message: message) else {
|
||||
return nil
|
||||
}
|
||||
let sourceLocation = InstantPageSourceLocation(userLocation: .peer(message.id.peerId), peerType: sourcePeerType ?? .channel)
|
||||
return makeInstantPageControllerImpl(context: context, webPage: webpage, anchor: anchor, sourceLocation: sourceLocation)
|
||||
}
|
||||
|
||||
func makeInstantPageControllerImpl(context: AccountContext, webPage: TelegramMediaWebpage, anchor: String?, sourceLocation: InstantPageSourceLocation) -> ViewController {
|
||||
return BrowserScreen(context: context, subject: .instantPage(webPage: webPage, anchor: anchor, sourceLocation: sourceLocation))
|
||||
}
|
||||
|
||||
func openChatWallpaperImpl(context: AccountContext, message: Message, present: @escaping (ViewController, Any?) -> Void) {
|
||||
|
|
|
|||
|
|
@ -248,15 +248,10 @@ func openResolvedUrlImpl(
|
|||
}
|
||||
})
|
||||
present(controller, nil)
|
||||
case let .instantView(webpage, anchor):
|
||||
case let .instantView(webPage, anchor):
|
||||
let sourceLocation = InstantPageSourceLocation(userLocation: .other, peerType: .channel)
|
||||
let pageController: ViewController
|
||||
if context.sharedContext.immediateExperimentalUISettings.browserExperiment {
|
||||
pageController = BrowserScreen(context: context, subject: .instantPage(webPage: webpage, anchor: anchor, sourceLocation: sourceLocation))
|
||||
} else {
|
||||
pageController = InstantPageController(context: context, webPage: webpage, sourceLocation: sourceLocation, anchor: anchor)
|
||||
}
|
||||
navigationController?.pushViewController(pageController)
|
||||
let browserController = context.sharedContext.makeInstantPageController(context: context, webPage: webPage, anchor: anchor, sourceLocation: sourceLocation)
|
||||
navigationController?.pushViewController(browserController)
|
||||
case let .join(link):
|
||||
dismissInput()
|
||||
|
||||
|
|
@ -661,6 +656,14 @@ func openResolvedUrlImpl(
|
|||
if let navigationController = navigationController {
|
||||
navigationController.pushViewController(controller, animated: true)
|
||||
}
|
||||
case let .starsTopup(amount):
|
||||
dismissInput()
|
||||
if let starsContext = context.starsContext {
|
||||
let controller = context.sharedContext.makeStarsPurchaseScreen(context: context, starsContext: starsContext, options: [], purpose: .generic(requiredStars: amount), completion: { _ in })
|
||||
if let navigationController = navigationController {
|
||||
navigationController.pushViewController(controller, animated: true)
|
||||
}
|
||||
}
|
||||
case let .joinVoiceChat(peerId, invite):
|
||||
let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))
|
||||
|> deliverOnMainQueue).start(next: { peer in
|
||||
|
|
|
|||
|
|
@ -918,6 +918,20 @@ func openExternalUrlImpl(context: AccountContext, urlContext: OpenURLContext, ur
|
|||
}
|
||||
}
|
||||
handleResolvedUrl(.premiumMultiGift(reference: reference))
|
||||
} else if parsedUrl.host == "stars_topup" {
|
||||
var amount: Int64?
|
||||
if let components = URLComponents(string: "/?" + query) {
|
||||
if let queryItems = components.queryItems {
|
||||
for queryItem in queryItems {
|
||||
if let value = queryItem.value {
|
||||
if queryItem.name == "amount" {
|
||||
amount = Int64(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
handleResolvedUrl(.starsTopup(amount: amount))
|
||||
} else if parsedUrl.host == "addlist" {
|
||||
if let components = URLComponents(string: "/?" + query) {
|
||||
var slug: String?
|
||||
|
|
@ -1047,7 +1061,14 @@ func openExternalUrlImpl(context: AccountContext, urlContext: OpenURLContext, ur
|
|||
|
||||
let _ = (settings
|
||||
|> deliverOnMainQueue).startStandalone(next: { settings in
|
||||
if let defaultWebBrowser = settings.defaultWebBrowser, defaultWebBrowser != "inApp" {
|
||||
var isTonSite = false
|
||||
if let host = parsedUrl.host, host.lowercased().hasSuffix(".ton") {
|
||||
isTonSite = true
|
||||
} else if let scheme = parsedUrl.scheme, scheme.lowercased().hasPrefix("tonsite") {
|
||||
isTonSite = true
|
||||
}
|
||||
|
||||
if let defaultWebBrowser = settings.defaultWebBrowser, defaultWebBrowser != "inApp" && !isTonSite {
|
||||
let openInOptions = availableOpenInOptions(context: context, item: .url(url: url))
|
||||
if let option = openInOptions.first(where: { $0.identifier == settings.defaultWebBrowser }) {
|
||||
if case let .openUrl(openInUrl) = option.action() {
|
||||
|
|
@ -1067,8 +1088,8 @@ func openExternalUrlImpl(context: AccountContext, urlContext: OpenURLContext, ur
|
|||
break
|
||||
}
|
||||
}
|
||||
|
||||
if settings.defaultWebBrowser == nil && !isExceptedDomain {
|
||||
|
||||
if (settings.defaultWebBrowser == nil && !isExceptedDomain) || isTonSite {
|
||||
let controller = BrowserScreen(context: context, subject: .webPage(url: parsedUrl.absoluteString))
|
||||
navigationController?.pushViewController(controller)
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -1846,8 +1846,12 @@ public final class SharedAccountContextImpl: SharedAccountContext {
|
|||
})
|
||||
}
|
||||
|
||||
public func openChatInstantPage(context: AccountContext, message: Message, sourcePeerType: MediaAutoDownloadPeerType?, navigationController: NavigationController) {
|
||||
openChatInstantPageImpl(context: context, message: message, sourcePeerType: sourcePeerType, navigationController: navigationController)
|
||||
public func makeInstantPageController(context: AccountContext, message: Message, sourcePeerType: MediaAutoDownloadPeerType?) -> ViewController? {
|
||||
return makeInstantPageControllerImpl(context: context, message: message, sourcePeerType: sourcePeerType)
|
||||
}
|
||||
|
||||
public func makeInstantPageController(context: AccountContext, webPage: TelegramMediaWebpage, anchor: String?, sourceLocation: InstantPageSourceLocation) -> ViewController {
|
||||
return makeInstantPageControllerImpl(context: context, webPage: webPage, anchor: anchor, sourceLocation: sourceLocation)
|
||||
}
|
||||
|
||||
public func openChatWallpaper(context: AccountContext, message: Message, present: @escaping (ViewController, Any?) -> Void) {
|
||||
|
|
|
|||
|
|
@ -735,8 +735,22 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
|
|||
//Xcode 16
|
||||
#if canImport(ContactProvider)
|
||||
extension MediaEditorScreen.Result: @retroactive MediaEditorScreenResult {
|
||||
public var target: Stories.PendingTarget {
|
||||
if let sendAsPeerId = self.options.sendAsPeerId {
|
||||
return .peer(sendAsPeerId)
|
||||
} else {
|
||||
return .myStories
|
||||
}
|
||||
}
|
||||
}
|
||||
#else
|
||||
extension MediaEditorScreen.Result: MediaEditorScreenResult {
|
||||
public var target: Stories.PendingTarget {
|
||||
if let sendAsPeerId = self.options.sendAsPeerId {
|
||||
return .peer(sendAsPeerId)
|
||||
} else {
|
||||
return .myStories
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import JoinLinkPreviewUI
|
|||
import PresentationDataUtils
|
||||
import UrlWhitelist
|
||||
import UndoUI
|
||||
import BrowserUI
|
||||
|
||||
func handleTextLinkActionImpl(context: AccountContext, peerId: EnginePeer.Id?, navigateDisposable: MetaDisposable, controller: ViewController, action: TextLinkItemActionType, itemLink: TextLinkItem) {
|
||||
let presentImpl: (ViewController, Any?) -> Void = { controllerToPresent, _ in
|
||||
|
|
@ -87,8 +88,10 @@ func handleTextLinkActionImpl(context: AccountContext, peerId: EnginePeer.Id?, n
|
|||
case let .stickerPack(name, _):
|
||||
let packReference: StickerPackReference = .name(name)
|
||||
controller.present(StickerPackScreen(context: context, mainStickerPack: packReference, stickerPacks: [packReference], parentNavigationController: controller.navigationController as? NavigationController), in: .window(.root))
|
||||
case let .instantView(webpage, anchor):
|
||||
(controller.navigationController as? NavigationController)?.pushViewController(InstantPageController(context: context, webPage: webpage, sourceLocation: InstantPageSourceLocation(userLocation: peerId.flatMap(MediaResourceUserLocation.peer) ?? .other, peerType: .group), anchor: anchor))
|
||||
case let .instantView(webPage, anchor):
|
||||
let sourceLocation = InstantPageSourceLocation(userLocation: peerId.flatMap(MediaResourceUserLocation.peer) ?? .other, peerType: .group)
|
||||
let browserController = context.sharedContext.makeInstantPageController(context: context, webPage: webPage, anchor: anchor, sourceLocation: sourceLocation)
|
||||
(controller.navigationController as? NavigationController)?.pushViewController(browserController, animated: true)
|
||||
case let .join(link):
|
||||
controller.present(JoinLinkPreviewController(context: context, link: link, navigateToPeer: { peer, peekData in
|
||||
openResolvedPeerImpl(peer, .chat(textInputState: nil, subject: nil, peekData: peekData))
|
||||
|
|
|
|||
|
|
@ -491,6 +491,16 @@ public enum MediaAutoDownloadPeerType {
|
|||
case channel
|
||||
}
|
||||
|
||||
public struct InstantPageSourceLocation {
|
||||
public var userLocation: MediaResourceUserLocation
|
||||
public var peerType: MediaAutoDownloadPeerType
|
||||
|
||||
public init(userLocation: MediaResourceUserLocation, peerType: MediaAutoDownloadPeerType) {
|
||||
self.userLocation = userLocation
|
||||
self.peerType = peerType
|
||||
}
|
||||
}
|
||||
|
||||
public func effectiveAutodownloadCategories(settings: MediaAutoDownloadSettings, networkType: MediaAutoDownloadNetworkType) -> MediaAutoDownloadCategories {
|
||||
let connection = settings.connectionSettings(for: networkType)
|
||||
switch connection.preset {
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ public extension CharacterSet {
|
|||
}()
|
||||
}
|
||||
|
||||
public func isValidUrl(_ url: String, validSchemes: [String: Bool] = ["http": true, "https": true]) -> Bool {
|
||||
public func isValidUrl(_ url: String, validSchemes: [String: Bool] = ["http": true, "https": true, "tonsite": true]) -> Bool {
|
||||
if let escapedUrl = url.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed), let url = URL(string: escapedUrl), let scheme = url.scheme?.lowercased(), let requiresTopLevelDomain = validSchemes[scheme], let host = url.host, (!requiresTopLevelDomain || host.contains(".")) && url.user == nil {
|
||||
if requiresTopLevelDomain {
|
||||
let components = host.components(separatedBy: ".")
|
||||
|
|
@ -39,8 +39,12 @@ public func isValidUrl(_ url: String, validSchemes: [String: Bool] = ["http": tr
|
|||
|
||||
public func explicitUrl(_ url: String) -> String {
|
||||
var url = url
|
||||
if !url.hasPrefix("http") && !url.hasPrefix("https") && url.range(of: "://") == nil {
|
||||
url = "https://\(url)"
|
||||
if !url.lowercased().hasPrefix("http:") && !url.lowercased().hasPrefix("https:") && !url.lowercased().hasPrefix("tonsite:") && url.range(of: "://") == nil {
|
||||
if let parsedUrl = URL(string: "http://\(url)"), parsedUrl.host?.hasSuffix(".ton") == true {
|
||||
url = "tonsite://\(url)"
|
||||
} else {
|
||||
url = "https://\(url)"
|
||||
}
|
||||
}
|
||||
return url
|
||||
}
|
||||
|
|
|
|||
|
|
@ -292,7 +292,20 @@ public func parseInternalUrl(sharedContext: SharedAccountContext, query: String)
|
|||
}
|
||||
}
|
||||
return .startAttach(peerName, value, choose)
|
||||
} else if queryItem.name == "story" {
|
||||
} else if queryItem.name == "startapp" {
|
||||
var compact = false
|
||||
if let queryItems = components.queryItems {
|
||||
for queryItem in queryItems {
|
||||
if let value = queryItem.value {
|
||||
if queryItem.name == "mode", value == "compact" {
|
||||
compact = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return .peer(.name(peerName), .appStart("", queryItem.value, compact))
|
||||
} else if queryItem.name == "story" {
|
||||
if let id = Int32(value) {
|
||||
return .peer(.name(peerName), .story(id))
|
||||
}
|
||||
|
|
@ -321,6 +334,19 @@ public func parseInternalUrl(sharedContext: SharedAccountContext, query: String)
|
|||
return .peer(.name(peerName), .boost)
|
||||
} else if queryItem.name == "profile" {
|
||||
return .peer(.name(peerName), .profile)
|
||||
} else if queryItem.name == "startapp" {
|
||||
var compact = false
|
||||
if let queryItems = components.queryItems {
|
||||
for queryItem in queryItems {
|
||||
if let value = queryItem.value {
|
||||
if queryItem.name == "mode", value == "compact" {
|
||||
compact = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return .peer(.name(peerName), .appStart("", nil, compact))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -720,18 +746,26 @@ private func resolveInternalUrl(context: AccountContext, url: ParsedInternalUrl)
|
|||
}
|
||||
}
|
||||
case let .appStart(name, payload, compact):
|
||||
return .single(.progress) |> then(context.engine.messages.getBotApp(botId: peer.id, shortName: name, cached: false)
|
||||
|> map(Optional.init)
|
||||
|> `catch` { _ -> Signal<BotApp?, NoError> in
|
||||
return .single(nil)
|
||||
}
|
||||
|> mapToSignal { botApp -> Signal<ResolveInternalUrlResult, NoError> in
|
||||
if let botApp {
|
||||
return .single(.result(.peer(peer._asPeer(), .withBotApp(ChatControllerInitialBotAppStart(botApp: botApp, payload: payload, justInstalled: false, compact: compact)))))
|
||||
if name.isEmpty {
|
||||
if case let .user(user) = peer, let botInfo = user.botInfo, botInfo.flags.contains(.hasWebApp) {
|
||||
return .single(.result(.peer(peer._asPeer(), .withBotApp(ChatControllerInitialBotAppStart(botApp: nil, payload: payload, justInstalled: false, compact: compact)))))
|
||||
} else {
|
||||
return .single(.result(.peer(peer._asPeer(), .chat(textInputState: nil, subject: nil, peekData: nil))))
|
||||
}
|
||||
})
|
||||
} else {
|
||||
return .single(.progress) |> then(context.engine.messages.getBotApp(botId: peer.id, shortName: name, cached: false)
|
||||
|> map(Optional.init)
|
||||
|> `catch` { _ -> Signal<BotApp?, NoError> in
|
||||
return .single(nil)
|
||||
}
|
||||
|> mapToSignal { botApp -> Signal<ResolveInternalUrlResult, NoError> in
|
||||
if let botApp {
|
||||
return .single(.result(.peer(peer._asPeer(), .withBotApp(ChatControllerInitialBotAppStart(botApp: botApp, payload: payload, justInstalled: false, compact: compact)))))
|
||||
} else {
|
||||
return .single(.result(.peer(peer._asPeer(), .chat(textInputState: nil, subject: nil, peekData: nil))))
|
||||
}
|
||||
})
|
||||
}
|
||||
case let .channelMessage(id, timecode):
|
||||
if case let .channel(channel) = peer, channel.flags.contains(.isForum) {
|
||||
let messageId = MessageId(peerId: channel.id, namespace: Namespaces.Message.Cloud, id: id)
|
||||
|
|
@ -1299,3 +1333,18 @@ public func resolveInstantViewUrl(account: Account, url: String) -> Signal<Resol
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func cleanDomain(url: String) -> (domain: String, fullUrl: String) {
|
||||
if let parsedUrl = URL(string: url) {
|
||||
let host: String?
|
||||
let scheme = parsedUrl.scheme ?? "https"
|
||||
if #available(iOS 16.0, *) {
|
||||
host = parsedUrl.host(percentEncoded: true)?.lowercased()
|
||||
} else {
|
||||
host = parsedUrl.host?.lowercased()
|
||||
}
|
||||
return (host ?? url, "\(scheme)://\(host ?? "")")
|
||||
} else {
|
||||
return (url, url)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
20
submodules/Utils/DeviceModel/BUILD
Normal file
20
submodules/Utils/DeviceModel/BUILD
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "DeviceModel",
|
||||
module_name = "DeviceModel",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
"//submodules/SSignalKit/SwiftSignalKit",
|
||||
"//submodules/LegacyComponents",
|
||||
"//submodules/AccountContext",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
||||
368
submodules/Utils/DeviceModel/Sources/DeviceModel.swift
Normal file
368
submodules/Utils/DeviceModel/Sources/DeviceModel.swift
Normal file
|
|
@ -0,0 +1,368 @@
|
|||
import Foundation
|
||||
|
||||
public enum DeviceModel: CaseIterable, Equatable {
|
||||
public static var allCases: [DeviceModel] {
|
||||
return [
|
||||
.iPodTouch1,
|
||||
.iPodTouch2,
|
||||
.iPodTouch3,
|
||||
.iPodTouch4,
|
||||
.iPodTouch5,
|
||||
.iPodTouch6,
|
||||
.iPodTouch7,
|
||||
.iPhone,
|
||||
.iPhone3G,
|
||||
.iPhone3GS,
|
||||
.iPhone4,
|
||||
.iPhone4S,
|
||||
.iPhone5,
|
||||
.iPhone5C,
|
||||
.iPhone5S,
|
||||
.iPhone6,
|
||||
.iPhone6Plus,
|
||||
.iPhone6S,
|
||||
.iPhone6SPlus,
|
||||
.iPhoneSE,
|
||||
.iPhone7,
|
||||
.iPhone7Plus,
|
||||
.iPhone8,
|
||||
.iPhone8Plus,
|
||||
.iPhoneX,
|
||||
.iPhoneXS,
|
||||
.iPhoneXR,
|
||||
.iPhone11,
|
||||
.iPhone11Pro,
|
||||
.iPhone11ProMax,
|
||||
.iPhone12,
|
||||
.iPhone12Mini,
|
||||
.iPhone12Pro,
|
||||
.iPhone12ProMax,
|
||||
.iPhone13,
|
||||
.iPhone13Mini,
|
||||
.iPhone13Pro,
|
||||
.iPhone13ProMax,
|
||||
.iPhone14,
|
||||
.iPhone14Plus,
|
||||
.iPhone14Pro,
|
||||
.iPhone14ProMax,
|
||||
.iPhone15,
|
||||
.iPhone15Plus,
|
||||
.iPhone15Pro,
|
||||
.iPhone15ProMax
|
||||
]
|
||||
}
|
||||
|
||||
case iPodTouch1
|
||||
case iPodTouch2
|
||||
case iPodTouch3
|
||||
case iPodTouch4
|
||||
case iPodTouch5
|
||||
case iPodTouch6
|
||||
case iPodTouch7
|
||||
|
||||
case iPhone
|
||||
case iPhone3G
|
||||
case iPhone3GS
|
||||
|
||||
case iPhone4
|
||||
case iPhone4S
|
||||
|
||||
case iPhone5
|
||||
case iPhone5C
|
||||
case iPhone5S
|
||||
|
||||
case iPhone6
|
||||
case iPhone6Plus
|
||||
case iPhone6S
|
||||
case iPhone6SPlus
|
||||
|
||||
case iPhoneSE
|
||||
|
||||
case iPhone7
|
||||
case iPhone7Plus
|
||||
case iPhone8
|
||||
case iPhone8Plus
|
||||
|
||||
case iPhoneX
|
||||
case iPhoneXS
|
||||
case iPhoneXSMax
|
||||
case iPhoneXR
|
||||
|
||||
case iPhone11
|
||||
case iPhone11Pro
|
||||
case iPhone11ProMax
|
||||
|
||||
case iPhoneSE2ndGen
|
||||
|
||||
case iPhone12
|
||||
case iPhone12Mini
|
||||
case iPhone12Pro
|
||||
case iPhone12ProMax
|
||||
|
||||
case iPhone13
|
||||
case iPhone13Mini
|
||||
case iPhone13Pro
|
||||
case iPhone13ProMax
|
||||
|
||||
case iPhoneSE3rdGen
|
||||
|
||||
case iPhone14
|
||||
case iPhone14Plus
|
||||
case iPhone14Pro
|
||||
case iPhone14ProMax
|
||||
|
||||
case iPhone15
|
||||
case iPhone15Plus
|
||||
case iPhone15Pro
|
||||
case iPhone15ProMax
|
||||
|
||||
case unknown(String)
|
||||
|
||||
public var modelId: [String] {
|
||||
switch self {
|
||||
case .iPodTouch1:
|
||||
return ["iPod1,1"]
|
||||
case .iPodTouch2:
|
||||
return ["iPod2,1"]
|
||||
case .iPodTouch3:
|
||||
return ["iPod3,1"]
|
||||
case .iPodTouch4:
|
||||
return ["iPod4,1"]
|
||||
case .iPodTouch5:
|
||||
return ["iPod5,1"]
|
||||
case .iPodTouch6:
|
||||
return ["iPod7,1"]
|
||||
case .iPodTouch7:
|
||||
return ["iPod9,1"]
|
||||
case .iPhone:
|
||||
return ["iPhone1,1"]
|
||||
case .iPhone3G:
|
||||
return ["iPhone1,2"]
|
||||
case .iPhone3GS:
|
||||
return ["iPhone2,1"]
|
||||
case .iPhone4:
|
||||
return ["iPhone3,1", "iPhone3,2", "iPhone3,3"]
|
||||
case .iPhone4S:
|
||||
return ["iPhone4,1", "iPhone4,2", "iPhone4,3"]
|
||||
case .iPhone5:
|
||||
return ["iPhone5,1", "iPhone5,2"]
|
||||
case .iPhone5C:
|
||||
return ["iPhone5,3", "iPhone5,4"]
|
||||
case .iPhone5S:
|
||||
return ["iPhone6,1", "iPhone6,2"]
|
||||
case .iPhone6:
|
||||
return ["iPhone7,2"]
|
||||
case .iPhone6Plus:
|
||||
return ["iPhone7,1"]
|
||||
case .iPhone6S:
|
||||
return ["iPhone8,1"]
|
||||
case .iPhone6SPlus:
|
||||
return ["iPhone8,2"]
|
||||
case .iPhoneSE:
|
||||
return ["iPhone8,4"]
|
||||
case .iPhone7:
|
||||
return ["iPhone9,1", "iPhone9,3"]
|
||||
case .iPhone7Plus:
|
||||
return ["iPhone9,2", "iPhone9,4"]
|
||||
case .iPhone8:
|
||||
return ["iPhone10,1", "iPhone10,4"]
|
||||
case .iPhone8Plus:
|
||||
return ["iPhone10,2", "iPhone10,5"]
|
||||
case .iPhoneX:
|
||||
return ["iPhone10,3", "iPhone10,6"]
|
||||
case .iPhoneXS:
|
||||
return ["iPhone11,2"]
|
||||
case .iPhoneXSMax:
|
||||
return ["iPhone11,4", "iPhone11,6"]
|
||||
case .iPhoneXR:
|
||||
return ["iPhone11,8"]
|
||||
case .iPhone11:
|
||||
return ["iPhone12,1"]
|
||||
case .iPhone11Pro:
|
||||
return ["iPhone12,3"]
|
||||
case .iPhone11ProMax:
|
||||
return ["iPhone12,5"]
|
||||
case .iPhoneSE2ndGen:
|
||||
return ["iPhone12,8"]
|
||||
case .iPhone12:
|
||||
return ["iPhone13,2"]
|
||||
case .iPhone12Mini:
|
||||
return ["iPhone13,1"]
|
||||
case .iPhone12Pro:
|
||||
return ["iPhone13,3"]
|
||||
case .iPhone12ProMax:
|
||||
return ["iPhone13,4"]
|
||||
case .iPhone13:
|
||||
return ["iPhone14,5"]
|
||||
case .iPhone13Mini:
|
||||
return ["iPhone14,4"]
|
||||
case .iPhone13Pro:
|
||||
return ["iPhone14,2"]
|
||||
case .iPhone13ProMax:
|
||||
return ["iPhone14,3"]
|
||||
case .iPhoneSE3rdGen:
|
||||
return ["iPhone14,6"]
|
||||
case .iPhone14:
|
||||
return ["iPhone14,7"]
|
||||
case .iPhone14Plus:
|
||||
return ["iPhone14,8"]
|
||||
case .iPhone14Pro:
|
||||
return ["iPhone15,2"]
|
||||
case .iPhone14ProMax:
|
||||
return ["iPhone15,3"]
|
||||
case .iPhone15:
|
||||
return ["iPhone15,4"]
|
||||
case .iPhone15Plus:
|
||||
return ["iPhone15,5"]
|
||||
case .iPhone15Pro:
|
||||
return ["iPhone16,1"]
|
||||
case .iPhone15ProMax:
|
||||
return ["iPhone16,2"]
|
||||
case let .unknown(modelId):
|
||||
return [modelId]
|
||||
}
|
||||
}
|
||||
|
||||
public var modelName: String {
|
||||
switch self {
|
||||
case .iPodTouch1:
|
||||
return "iPod touch 1G"
|
||||
case .iPodTouch2:
|
||||
return "iPod touch 2G"
|
||||
case .iPodTouch3:
|
||||
return "iPod touch 3G"
|
||||
case .iPodTouch4:
|
||||
return "iPod touch 4G"
|
||||
case .iPodTouch5:
|
||||
return "iPod touch 5G"
|
||||
case .iPodTouch6:
|
||||
return "iPod touch 6G"
|
||||
case .iPodTouch7:
|
||||
return "iPod touch 7G"
|
||||
case .iPhone:
|
||||
return "iPhone"
|
||||
case .iPhone3G:
|
||||
return "iPhone 3G"
|
||||
case .iPhone3GS:
|
||||
return "iPhone 3GS"
|
||||
case .iPhone4:
|
||||
return "iPhone 4"
|
||||
case .iPhone4S:
|
||||
return "iPhone 4S"
|
||||
case .iPhone5:
|
||||
return "iPhone 5"
|
||||
case .iPhone5C:
|
||||
return "iPhone 5C"
|
||||
case .iPhone5S:
|
||||
return "iPhone 5S"
|
||||
case .iPhone6:
|
||||
return "iPhone 6"
|
||||
case .iPhone6Plus:
|
||||
return "iPhone 6 Plus"
|
||||
case .iPhone6S:
|
||||
return "iPhone 6S"
|
||||
case .iPhone6SPlus:
|
||||
return "iPhone 6S Plus"
|
||||
case .iPhoneSE:
|
||||
return "iPhone SE"
|
||||
case .iPhone7:
|
||||
return "iPhone 7"
|
||||
case .iPhone7Plus:
|
||||
return "iPhone 7 Plus"
|
||||
case .iPhone8:
|
||||
return "iPhone 8"
|
||||
case .iPhone8Plus:
|
||||
return "iPhone 8 Plus"
|
||||
case .iPhoneX:
|
||||
return "iPhone X"
|
||||
case .iPhoneXS:
|
||||
return "iPhone XS"
|
||||
case .iPhoneXSMax:
|
||||
return "iPhone XS Max"
|
||||
case .iPhoneXR:
|
||||
return "iPhone XR"
|
||||
case .iPhone11:
|
||||
return "iPhone 11"
|
||||
case .iPhone11Pro:
|
||||
return "iPhone 11 Pro"
|
||||
case .iPhone11ProMax:
|
||||
return "iPhone 11 Pro Max"
|
||||
case .iPhoneSE2ndGen:
|
||||
return "iPhone SE (2nd gen)"
|
||||
case .iPhone12:
|
||||
return "iPhone 12"
|
||||
case .iPhone12Mini:
|
||||
return "iPhone 12 mini"
|
||||
case .iPhone12Pro:
|
||||
return "iPhone 12 Pro"
|
||||
case .iPhone12ProMax:
|
||||
return "iPhone 12 Pro Max"
|
||||
case .iPhone13:
|
||||
return "iPhone 13"
|
||||
case .iPhone13Mini:
|
||||
return "iPhone 13 mini"
|
||||
case .iPhone13Pro:
|
||||
return "iPhone 13 Pro"
|
||||
case .iPhone13ProMax:
|
||||
return "iPhone 13 Pro Max"
|
||||
case .iPhoneSE3rdGen:
|
||||
return "iPhone SE (3rd gen)"
|
||||
case .iPhone14:
|
||||
return "iPhone 14"
|
||||
case .iPhone14Plus:
|
||||
return "iPhone 14 Plus"
|
||||
case .iPhone14Pro:
|
||||
return "iPhone 14 Pro"
|
||||
case .iPhone14ProMax:
|
||||
return "iPhone 14 Pro Max"
|
||||
case .iPhone15:
|
||||
return "iPhone 15"
|
||||
case .iPhone15Plus:
|
||||
return "iPhone 15 Plus"
|
||||
case .iPhone15Pro:
|
||||
return "iPhone 15 Pro"
|
||||
case .iPhone15ProMax:
|
||||
return "iPhone 15 Pro Max"
|
||||
case let .unknown(modelId):
|
||||
if modelId.hasPrefix("iPhone") {
|
||||
return "Unknown iPhone"
|
||||
} else if modelId.hasPrefix("iPod") {
|
||||
return "Unknown iPod"
|
||||
} else if modelId.hasPrefix("iPad") {
|
||||
return "Unknown iPad"
|
||||
} else {
|
||||
return "Unknown Device"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public var isIpad: Bool {
|
||||
return self.modelId.first?.hasPrefix("iPad") ?? false
|
||||
}
|
||||
|
||||
public static let current = DeviceModel()
|
||||
|
||||
private init() {
|
||||
var systemInfo = utsname()
|
||||
uname(&systemInfo)
|
||||
let modelCode = withUnsafePointer(to: &systemInfo.machine) {
|
||||
$0.withMemoryRebound(to: CChar.self, capacity: 1) {
|
||||
ptr in String.init(validatingUTF8: ptr)
|
||||
}
|
||||
}
|
||||
var result: DeviceModel?
|
||||
if let modelCode {
|
||||
for model in DeviceModel.allCases {
|
||||
if model.modelId.contains(modelCode) {
|
||||
result = model
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if let result {
|
||||
self = result
|
||||
} else {
|
||||
self = .unknown(modelCode ?? "")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -40,6 +40,7 @@ swift_library(
|
|||
"//submodules/ShareController",
|
||||
"//submodules/UndoUI",
|
||||
"//submodules/OverlayStatusController",
|
||||
"//submodules/TelegramUIPreferences",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ import ShareController
|
|||
import UndoUI
|
||||
import AvatarNode
|
||||
import OverlayStatusController
|
||||
import TelegramUIPreferences
|
||||
|
||||
private let durgerKingBotIds: [Int64] = [5104055776, 2200339955]
|
||||
|
||||
|
|
@ -333,9 +334,6 @@ public final class WebAppController: ViewController, AttachmentContainable {
|
|||
if let parsedUrl = URL(string: url) {
|
||||
self.webView?.load(URLRequest(url: parsedUrl))
|
||||
}
|
||||
|
||||
self.checkBotIdAndUrl(url)
|
||||
|
||||
if let keepAliveSignal = controller.keepAliveSignal {
|
||||
self.keepAliveDisposable = (keepAliveSignal
|
||||
|> deliverOnMainQueue).start(error: { [weak self] _ in
|
||||
|
|
@ -355,7 +353,6 @@ public final class WebAppController: ViewController, AttachmentContainable {
|
|||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.checkBotIdAndUrl(result.url)
|
||||
if let parsedUrl = URL(string: result.url) {
|
||||
strongSelf.queryId = result.queryId
|
||||
strongSelf.webView?.load(URLRequest(url: parsedUrl))
|
||||
|
|
@ -368,17 +365,16 @@ public final class WebAppController: ViewController, AttachmentContainable {
|
|||
guard let self, let controller = self.controller else {
|
||||
return
|
||||
}
|
||||
guard case let .peer(peer, params) = result, let peer, case let .withBotApp(appStart) = params else {
|
||||
guard case let .peer(peer, params) = result, let peer, case let .withBotApp(appStart) = params, let botApp = appStart.botApp else {
|
||||
controller.dismiss()
|
||||
return
|
||||
}
|
||||
let _ = (self.context.engine.messages.requestAppWebView(peerId: peer.id, appReference: .id(id: appStart.botApp.id, accessHash: appStart.botApp.accessHash), payload: appStart.payload, themeParams: generateWebAppThemeParams(self.presentationData.theme), compact: appStart.compact, allowWrite: true)
|
||||
let _ = (self.context.engine.messages.requestAppWebView(peerId: peer.id, appReference: .id(id: botApp.id, accessHash: botApp.accessHash), payload: appStart.payload, themeParams: generateWebAppThemeParams(self.presentationData.theme), compact: appStart.compact, allowWrite: true)
|
||||
|> deliverOnMainQueue).startStandalone(next: { [weak self] result in
|
||||
guard let self, let parsedUrl = URL(string: result.url) else {
|
||||
return
|
||||
}
|
||||
self.checkBotIdAndUrl(result.url)
|
||||
self.controller?.titleView?.title = WebAppTitle(title: appStart.botApp.title, counter: self.presentationData.strings.WebApp_Miniapp, isVerified: controller.botVerified)
|
||||
self.controller?.titleView?.title = WebAppTitle(title: botApp.title, counter: self.presentationData.strings.WebApp_Miniapp, isVerified: controller.botVerified)
|
||||
self.webView?.load(URLRequest(url: parsedUrl))
|
||||
})
|
||||
})
|
||||
|
|
@ -390,9 +386,7 @@ public final class WebAppController: ViewController, AttachmentContainable {
|
|||
}
|
||||
strongSelf.queryId = result.queryId
|
||||
strongSelf.webView?.load(URLRequest(url: parsedUrl))
|
||||
|
||||
strongSelf.checkBotIdAndUrl(result.url)
|
||||
|
||||
|
||||
if let keepAliveSignal = result.keepAliveSignal {
|
||||
strongSelf.keepAliveDisposable = (keepAliveSignal
|
||||
|> deliverOnMainQueue).start(error: { [weak self] _ in
|
||||
|
|
@ -412,14 +406,6 @@ public final class WebAppController: ViewController, AttachmentContainable {
|
|||
}
|
||||
}
|
||||
|
||||
func checkBotIdAndUrl(_ url: String) {
|
||||
// if url.hasPrefix("https://walletbot.me"), let botId = self.controller?.botId.id._internalGetInt64Value(), botId != 1985737506 {
|
||||
// let alertController = textAlertController(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, title: nil, text: "Bot id mismatch, please report steps to app developer", actions: [TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: {
|
||||
// })])
|
||||
// self.controller?.present(alertController, in: .window(.root))
|
||||
// }
|
||||
}
|
||||
|
||||
@objc fileprivate func mainButtonPressed() {
|
||||
if let mainButtonState = self.mainButtonState, !mainButtonState.isVisible || !mainButtonState.isEnabled {
|
||||
return
|
||||
|
|
@ -845,7 +831,7 @@ public final class WebAppController: ViewController, AttachmentContainable {
|
|||
}
|
||||
switch result {
|
||||
case let .instantView(webPage, anchor):
|
||||
let controller = InstantPageController(context: strongSelf.context, webPage: webPage, sourceLocation: InstantPageSourceLocation(userLocation: .other, peerType: .otherPrivate), anchor: anchor)
|
||||
let controller = strongSelf.context.sharedContext.makeInstantPageController(context: strongSelf.context, webPage: webPage, anchor: anchor, sourceLocation: InstantPageSourceLocation(userLocation: .other, peerType: .otherPrivate))
|
||||
strongSelf.controller?.getNavigationController()?.pushViewController(controller)
|
||||
default:
|
||||
strongSelf.context.sharedContext.openExternalUrl(context: strongSelf.context, urlContext: .generic, url: url, forceExternal: true, presentationData: strongSelf.context.sharedContext.currentPresentationData.with { $0 }, navigationController: nil, dismissInput: {})
|
||||
|
|
@ -1164,15 +1150,7 @@ public final class WebAppController: ViewController, AttachmentContainable {
|
|||
transitionOut: nil
|
||||
)
|
||||
let controller = self.context.sharedContext.makeStoryMediaEditorScreen(context: self.context, source: source, text: text, link: linkUrl.flatMap { ($0, linkName) }, completion: { result, commit in
|
||||
// let targetPeerId: EnginePeer.Id
|
||||
let target: Stories.PendingTarget
|
||||
// if let sendAsPeerId = result.options.sendAsPeerId {
|
||||
// target = .peer(sendAsPeerId)
|
||||
// targetPeerId = sendAsPeerId
|
||||
// } else {
|
||||
target = .myStories
|
||||
// targetPeerId = self.context.account.peerId
|
||||
// }
|
||||
let target: Stories.PendingTarget = result.target
|
||||
externalState.storyTarget = target
|
||||
|
||||
if let rootController = self.context.sharedContext.mainWindow?.viewController as? TelegramRootControllerInterface {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue