mirror of
https://github.com/TelegramMessenger/Telegram-iOS.git
synced 2026-07-06 03:33:41 +02:00
Various improvements
This commit is contained in:
parent
31ba87fb0f
commit
1c7834ad57
43 changed files with 3867 additions and 459 deletions
|
|
@ -12581,6 +12581,23 @@ Sorry for the inconvenience.";
|
|||
"WebBrowser.Exceptions.Create.Text" = "Enter a domain that you don't want to be opened in the in-app browser.";
|
||||
"WebBrowser.Exceptions.Create.Placeholder" = "Enter URL";
|
||||
|
||||
"WebBrowser.Exceptions.ClearConfirmation.Text" = "Are you sure you want to clear this list?";
|
||||
"WebBrowser.Exceptions.ClearConfirmation.Clear" = "Clear";
|
||||
|
||||
"WebBrowser.Done" = "Done";
|
||||
|
||||
"AccessDenied.LocationWeather" = "Telegram needs access to your location so that you can add the weather widget to your stories.\n\nPlease go to Settings > Privacy > Location Services and set Telegram to ON.";
|
||||
|
||||
"Story.Editor.TooltipWeatherLimitValue_1" = "**%@** weather stickers";
|
||||
"Story.Editor.TooltipWeatherLimitValue_any" = "**%@** weather stickers";
|
||||
"Story.Editor.TooltipWeatherLimitText" = "You can't add more than %@ to a story.";
|
||||
|
||||
"WebBrowser.AddressPlaceholder" = "Enter URL";
|
||||
|
||||
"WebBrowser.Bookmarks.Title" = "Bookmarks";
|
||||
"WebBrowser.Bookmarks.BookmarkCurrent" = "Bookmark Current Page";
|
||||
|
||||
"Story.Privacy.ChooseCover" = "Choose Story Cover";
|
||||
"Story.Privacy.ChooseCoverInfo" = "Choose a frame from the story to show in your Profile.";
|
||||
"Story.Privacy.ChooseCoverChannelInfo" = "Choose a frame from the story to show in channel profile.";
|
||||
"Story.Privacy.ChooseCoverGroupInfo" = "Choose a frame from the story to show in group profile.";
|
||||
|
|
|
|||
|
|
@ -978,6 +978,8 @@ public protocol SharedAccountContext: AnyObject {
|
|||
|
||||
func makeMediaPickerScreen(context: AccountContext, hasSearch: Bool, completion: @escaping (Any) -> Void) -> ViewController
|
||||
|
||||
func makeStoryMediaEditorScreen(context: AccountContext, source: Any?, text: String?, link: String?, completion: @escaping (MediaEditorScreenResult, @escaping (@escaping () -> Void) -> Void) -> Void) -> ViewController
|
||||
|
||||
func makeBotPreviewEditorScreen(context: AccountContext, source: Any?, target: Stories.PendingTarget, transitionArguments: (UIView, CGRect, UIImage?)?, transitionOut: @escaping () -> BotPreviewEditorTransitionOut?, externalState: MediaEditorTransitionOutExternalState, completion: @escaping (MediaEditorScreenResult, @escaping (@escaping () -> Void) -> Void) -> Void, cancelled: @escaping () -> Void) -> ViewController
|
||||
|
||||
func makeStickerEditorScreen(context: AccountContext, source: Any?, intro: Bool, transitionArguments: (UIView, CGRect, UIImage?)?, completion: @escaping (TelegramMediaFile, [String], @escaping () -> Void) -> Void, cancelled: @escaping () -> Void) -> ViewController
|
||||
|
|
|
|||
|
|
@ -1187,4 +1187,6 @@ public protocol ChatHistoryListNode: ListView {
|
|||
func scrollToEndOfHistory()
|
||||
func updateLayout(transition: ContainedViewLayoutTransition, updateSizeAndInsets: ListViewUpdateSizeAndInsets)
|
||||
func messageInCurrentHistoryView(_ id: MessageId) -> Message?
|
||||
|
||||
var contentPositionChanged: (ListViewVisibleContentOffset) -> Void { get set }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,6 +39,13 @@ swift_library(
|
|||
"//submodules/Svg",
|
||||
"//submodules/PromptUI",
|
||||
"//submodules/TelegramUI/Components/LottieComponent",
|
||||
"//submodules/PhotoResources",
|
||||
"//submodules/TelegramUI/Components/ChatControllerInteraction",
|
||||
"//submodules/ChatPresentationInterfaceState",
|
||||
"//submodules/UrlWhitelist",
|
||||
"//submodules/TelegramUI/Components/Chat/ChatHistorySearchContainerNode",
|
||||
"//submodules/SearchUI",
|
||||
"//submodules/SearchBarNode",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
|
|
|||
|
|
@ -3,25 +3,36 @@ import UIKit
|
|||
import AsyncDisplayKit
|
||||
import Display
|
||||
import ComponentFlow
|
||||
import SwiftSignalKit
|
||||
import TelegramPresentationData
|
||||
import AccountContext
|
||||
import BundleIconComponent
|
||||
import MultilineTextComponent
|
||||
import UrlEscaping
|
||||
|
||||
final class AddressBarContentComponent: Component {
|
||||
public typealias EnvironmentType = BrowserNavigationBarEnvironment
|
||||
|
||||
let theme: PresentationTheme
|
||||
let strings: PresentationStrings
|
||||
let url: String
|
||||
let isSecure: Bool
|
||||
let isExpanded: Bool
|
||||
let performAction: ActionSlot<BrowserScreen.Action>
|
||||
|
||||
init(
|
||||
theme: PresentationTheme,
|
||||
strings: PresentationStrings,
|
||||
url: String,
|
||||
isSecure: Bool,
|
||||
isExpanded: Bool,
|
||||
performAction: ActionSlot<BrowserScreen.Action>
|
||||
) {
|
||||
self.theme = theme
|
||||
self.strings = strings
|
||||
self.url = url
|
||||
self.isSecure = isSecure
|
||||
self.isExpanded = isExpanded
|
||||
self.performAction = performAction
|
||||
}
|
||||
|
||||
|
|
@ -35,6 +46,12 @@ final class AddressBarContentComponent: Component {
|
|||
if lhs.url != rhs.url {
|
||||
return false
|
||||
}
|
||||
if lhs.isSecure != rhs.isSecure {
|
||||
return false
|
||||
}
|
||||
if lhs.isExpanded != rhs.isExpanded {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
|
|
@ -43,12 +60,24 @@ final class AddressBarContentComponent: Component {
|
|||
override func textRect(forBounds bounds: CGRect) -> CGRect {
|
||||
return bounds.integral
|
||||
}
|
||||
|
||||
override var canBecomeFirstResponder: Bool {
|
||||
var canBecomeFirstResponder = super.canBecomeFirstResponder
|
||||
if !canBecomeFirstResponder && self.alpha.isZero {
|
||||
canBecomeFirstResponder = true
|
||||
}
|
||||
return canBecomeFirstResponder
|
||||
}
|
||||
}
|
||||
|
||||
private struct Params: Equatable {
|
||||
var theme: PresentationTheme
|
||||
var strings: PresentationStrings
|
||||
var size: CGSize
|
||||
var isActive: Bool
|
||||
var title: String
|
||||
var isSecure: Bool
|
||||
var collapseFraction: CGFloat
|
||||
|
||||
static func ==(lhs: Params, rhs: Params) -> Bool {
|
||||
if lhs.theme !== rhs.theme {
|
||||
|
|
@ -60,14 +89,25 @@ final class AddressBarContentComponent: Component {
|
|||
if lhs.size != rhs.size {
|
||||
return false
|
||||
}
|
||||
if lhs.isActive != rhs.isActive {
|
||||
return false
|
||||
}
|
||||
if lhs.title != rhs.title {
|
||||
return false
|
||||
}
|
||||
if lhs.isSecure != rhs.isSecure {
|
||||
return false
|
||||
}
|
||||
if lhs.collapseFraction != rhs.collapseFraction {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
private let activated: (Bool) -> Void = { _ in }
|
||||
private let deactivated: (Bool) -> Void = { _ in }
|
||||
private let updateQuery: (String?) -> Void = { _ in }
|
||||
|
||||
|
||||
private let backgroundLayer: SimpleLayer
|
||||
|
||||
private let iconView: UIImageView
|
||||
|
|
@ -79,10 +119,11 @@ final class AddressBarContentComponent: Component {
|
|||
private let cancelButton: HighlightTrackingButton
|
||||
|
||||
private var placeholderContent = ComponentView<Empty>()
|
||||
private var titleContent = ComponentView<Empty>()
|
||||
|
||||
private var textFrame: CGRect?
|
||||
private var textField: TextField?
|
||||
|
||||
|
||||
private var tapRecognizer: UITapGestureRecognizer?
|
||||
|
||||
private var params: Params?
|
||||
|
|
@ -99,12 +140,12 @@ final class AddressBarContentComponent: Component {
|
|||
|
||||
self.clearIconView = UIImageView()
|
||||
self.clearIconButton = HighlightableButton()
|
||||
self.clearIconView.isHidden = true
|
||||
self.clearIconButton.isHidden = true
|
||||
self.clearIconView.isHidden = false
|
||||
self.clearIconButton.isHidden = false
|
||||
|
||||
self.cancelButtonTitle = ComponentView()
|
||||
self.cancelButton = HighlightTrackingButton()
|
||||
|
||||
|
||||
super.init(frame: CGRect())
|
||||
|
||||
self.layer.addSublayer(self.backgroundLayer)
|
||||
|
|
@ -156,76 +197,49 @@ final class AddressBarContentComponent: Component {
|
|||
}
|
||||
|
||||
@objc private func tapGesture(_ recognizer: UITapGestureRecognizer) {
|
||||
if case .ended = recognizer.state {
|
||||
self.activateTextInput()
|
||||
if case .ended = recognizer.state, let component = self.component, !component.isExpanded {
|
||||
component.performAction.invoke(.openAddressBar)
|
||||
}
|
||||
}
|
||||
|
||||
private func activateTextInput() {
|
||||
if self.textField == nil, let textFrame = self.textFrame {
|
||||
let backgroundFrame = self.backgroundLayer.frame
|
||||
let textFieldFrame = CGRect(origin: CGPoint(x: textFrame.minX, y: backgroundFrame.minY), size: CGSize(width: backgroundFrame.maxX - textFrame.minX, height: backgroundFrame.height))
|
||||
|
||||
let textField = TextField(frame: textFieldFrame)
|
||||
textField.autocorrectionType = .no
|
||||
textField.returnKeyType = .search
|
||||
self.textField = textField
|
||||
self.insertSubview(textField, belowSubview: self.clearIconView)
|
||||
textField.delegate = self
|
||||
textField.addTarget(self, action: #selector(self.textFieldChanged(_:)), for: .editingChanged)
|
||||
}
|
||||
|
||||
guard !(self.textField?.isFirstResponder ?? false) else {
|
||||
return
|
||||
}
|
||||
|
||||
self.activated(true)
|
||||
|
||||
self.textField?.becomeFirstResponder()
|
||||
if let textField = self.textField {
|
||||
textField.becomeFirstResponder()
|
||||
Queue.mainQueue().justDispatch {
|
||||
textField.selectAll(nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func deactivateTextInput() {
|
||||
self.textField?.endEditing(true)
|
||||
}
|
||||
|
||||
@objc private func cancelPressed() {
|
||||
self.updateQuery(nil)
|
||||
self.deactivated(self.textField?.isFirstResponder ?? false)
|
||||
|
||||
self.clearIconView.isHidden = true
|
||||
self.clearIconButton.isHidden = true
|
||||
|
||||
let textField = self.textField
|
||||
self.textField = nil
|
||||
|
||||
self.deactivated(textField?.isFirstResponder ?? false)
|
||||
|
||||
self.component?.performAction.invoke(.updateSearchActive(false))
|
||||
|
||||
if let textField {
|
||||
textField.resignFirstResponder()
|
||||
textField.removeFromSuperview()
|
||||
}
|
||||
self.component?.performAction.invoke(.closeAddressBar)
|
||||
}
|
||||
|
||||
@objc private func clearPressed() {
|
||||
self.updateQuery(nil)
|
||||
self.textField?.text = ""
|
||||
|
||||
self.clearIconView.isHidden = true
|
||||
self.clearIconButton.isHidden = true
|
||||
}
|
||||
|
||||
func deactivate() {
|
||||
if let text = self.textField?.text, !text.isEmpty {
|
||||
self.textField?.endEditing(true)
|
||||
} else {
|
||||
self.cancelPressed()
|
||||
guard let textField = self.textField else {
|
||||
return
|
||||
}
|
||||
textField.text = ""
|
||||
self.textFieldChanged(textField)
|
||||
}
|
||||
|
||||
|
||||
public func textFieldDidBeginEditing(_ textField: UITextField) {
|
||||
}
|
||||
|
||||
public func textFieldDidEndEditing(_ textField: UITextField) {
|
||||
}
|
||||
|
||||
|
||||
public func textFieldShouldReturn(_ textField: UITextField) -> Bool {
|
||||
if let component = self.component {
|
||||
component.performAction.invoke(.navigateTo(explicitUrl(textField.text ?? "")))
|
||||
}
|
||||
textField.endEditing(true)
|
||||
return false
|
||||
}
|
||||
|
|
@ -237,38 +251,53 @@ final class AddressBarContentComponent: Component {
|
|||
self.clearIconButton.isHidden = text.isEmpty
|
||||
self.placeholderContent.view?.isHidden = !text.isEmpty
|
||||
|
||||
self.updateQuery(text)
|
||||
|
||||
self.component?.performAction.invoke(.updateSearchQuery(text))
|
||||
|
||||
if let params = self.params {
|
||||
self.update(theme: params.theme, strings: params.strings, size: params.size, transition: .immediate)
|
||||
self.update(theme: params.theme, strings: params.strings, size: params.size, isActive: params.isActive, title: params.title, isSecure: params.isSecure, collapseFraction: params.collapseFraction, transition: .immediate)
|
||||
}
|
||||
}
|
||||
|
||||
func update(component: AddressBarContentComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize {
|
||||
func update(component: AddressBarContentComponent, availableSize: CGSize, environment: Environment<BrowserNavigationBarEnvironment>, transition: ComponentTransition) -> CGSize {
|
||||
let collapseFraction = environment[BrowserNavigationBarEnvironment.self].fraction
|
||||
|
||||
let wasExpanded = self.component?.isExpanded ?? false
|
||||
self.component = component
|
||||
|
||||
self.update(theme: component.theme, strings: component.strings, size: availableSize, transition: transition)
|
||||
if !wasExpanded && component.isExpanded {
|
||||
self.activateTextInput()
|
||||
}
|
||||
if wasExpanded && !component.isExpanded {
|
||||
self.deactivateTextInput()
|
||||
}
|
||||
let isActive = self.textField?.isFirstResponder ?? false
|
||||
|
||||
var title: String = ""
|
||||
if let parsedUrl = URL(string: component.url) {
|
||||
title = parsedUrl.host ?? component.url
|
||||
}
|
||||
self.update(theme: component.theme, strings: component.strings, size: availableSize, isActive: isActive, title: title.lowercased(), isSecure: component.isSecure, collapseFraction: collapseFraction, transition: transition)
|
||||
|
||||
return availableSize
|
||||
}
|
||||
|
||||
public func update(theme: PresentationTheme, strings: PresentationStrings, size: CGSize, transition: ComponentTransition) {
|
||||
public func update(theme: PresentationTheme, strings: PresentationStrings, size: CGSize, isActive: Bool, title: String, isSecure: Bool, collapseFraction: CGFloat, transition: ComponentTransition) {
|
||||
let params = Params(
|
||||
theme: theme,
|
||||
strings: strings,
|
||||
size: size
|
||||
size: size,
|
||||
isActive: isActive,
|
||||
title: title,
|
||||
isSecure: isSecure,
|
||||
collapseFraction: collapseFraction
|
||||
)
|
||||
|
||||
if self.params == params {
|
||||
return
|
||||
}
|
||||
|
||||
let isActiveWithText = true
|
||||
let isActiveWithText = self.component?.isExpanded ?? false
|
||||
|
||||
if self.params?.theme !== theme {
|
||||
self.iconView.image = generateTintedImage(image: UIImage(bundleImageName: "Components/Search Bar/Loupe"), color: .white)?.withRenderingMode(.alwaysTemplate)
|
||||
self.iconView.image = generateTintedImage(image: UIImage(bundleImageName: "Media Grid/Lock"), color: .white)?.withRenderingMode(.alwaysTemplate)
|
||||
self.iconView.tintColor = theme.rootController.navigationSearchBar.inputIconColor
|
||||
self.clearIconView.image = generateTintedImage(image: UIImage(bundleImageName: "Components/Search Bar/Clear"), color: .white)?.withRenderingMode(.alwaysTemplate)
|
||||
self.clearIconView.tintColor = theme.rootController.navigationSearchBar.inputClearButtonColor
|
||||
|
|
@ -280,10 +309,9 @@ final class AddressBarContentComponent: Component {
|
|||
let inputHeight: CGFloat = 36.0
|
||||
let topInset: CGFloat = (size.height - inputHeight) / 2.0
|
||||
|
||||
let sideTextInset: CGFloat = sideInset + 4.0 + 17.0
|
||||
|
||||
self.backgroundLayer.backgroundColor = theme.rootController.navigationSearchBar.inputFillColor.cgColor
|
||||
self.backgroundLayer.cornerRadius = 10.5
|
||||
transition.setAlpha(layer: self.backgroundLayer, alpha: max(0.0, min(1.0, 1.0 - collapseFraction * 1.5)))
|
||||
|
||||
let cancelTextSize = self.cancelButtonTitle.update(
|
||||
transition: .immediate,
|
||||
|
|
@ -306,35 +334,74 @@ final class AddressBarContentComponent: Component {
|
|||
|
||||
transition.setFrame(view: self.cancelButton, frame: CGRect(origin: CGPoint(x: backgroundFrame.maxX, y: 0.0), size: CGSize(width: cancelButtonSpacing + cancelTextSize.width, height: size.height)))
|
||||
|
||||
let textX: CGFloat = backgroundFrame.minX + sideTextInset
|
||||
let textX: CGFloat = backgroundFrame.minX + sideInset
|
||||
let textFrame = CGRect(origin: CGPoint(x: textX, y: backgroundFrame.minY), size: CGSize(width: backgroundFrame.maxX - textX, height: backgroundFrame.height))
|
||||
self.textFrame = textFrame
|
||||
|
||||
if let image = self.iconView.image {
|
||||
let iconFrame = CGRect(origin: CGPoint(x: backgroundFrame.minX + 5.0, y: backgroundFrame.minY + floor((backgroundFrame.height - image.size.height) / 2.0)), size: image.size)
|
||||
transition.setFrame(view: self.iconView, frame: iconFrame)
|
||||
}
|
||||
|
||||
|
||||
let placeholderSize = self.placeholderContent.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(
|
||||
Text(text: strings.Common_Search, font: Font.regular(17.0), color: theme.rootController.navigationSearchBar.inputPlaceholderTextColor)
|
||||
Text(text: strings.WebBrowser_AddressPlaceholder, font: Font.regular(17.0), color: theme.rootController.navigationSearchBar.inputPlaceholderTextColor)
|
||||
),
|
||||
environment: {},
|
||||
containerSize: size
|
||||
)
|
||||
if let placeholderContentView = self.placeholderContent.view {
|
||||
if placeholderContentView.superview == nil {
|
||||
placeholderContentView.alpha = 0.0
|
||||
placeholderContentView.isHidden = true
|
||||
self.addSubview(placeholderContentView)
|
||||
}
|
||||
let placeholderContentFrame = CGRect(origin: CGPoint(x: textFrame.minX, y: backgroundFrame.midY - placeholderSize.height / 2.0), size: placeholderSize)
|
||||
transition.setFrame(view: placeholderContentView, frame: placeholderContentFrame)
|
||||
transition.setAlpha(view: placeholderContentView, alpha: isActiveWithText ? 1.0 : 0.0)
|
||||
}
|
||||
|
||||
let titleSize = self.titleContent.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(
|
||||
MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(string: title, font: Font.regular(17.0), textColor: theme.rootController.navigationSearchBar.inputTextColor)),
|
||||
horizontalAlignment: .center,
|
||||
truncationType: .end,
|
||||
maximumNumberOfLines: 1
|
||||
)
|
||||
),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: size.width - 36.0, height: size.height)
|
||||
)
|
||||
var titleContentFrame = CGRect(origin: CGPoint(x: isActiveWithText ? textFrame.minX : backgroundFrame.midX - titleSize.width / 2.0, y: backgroundFrame.midY - titleSize.height / 2.0), size: titleSize)
|
||||
if isSecure && !isActiveWithText {
|
||||
titleContentFrame.origin.x += 7.0
|
||||
}
|
||||
var titleSizeChanged = false
|
||||
if let titleContentView = self.titleContent.view {
|
||||
if titleContentView.superview == nil {
|
||||
self.addSubview(titleContentView)
|
||||
}
|
||||
if titleContentView.frame.width != titleContentFrame.size.width {
|
||||
titleSizeChanged = true
|
||||
}
|
||||
transition.setPosition(view: titleContentView, position: titleContentFrame.center)
|
||||
titleContentView.bounds = CGRect(origin: .zero, size: titleContentFrame.size)
|
||||
transition.setAlpha(view: titleContentView, alpha: isActiveWithText ? 0.0 : 1.0)
|
||||
}
|
||||
|
||||
if let image = self.iconView.image {
|
||||
let iconFrame = CGRect(origin: CGPoint(x: titleContentFrame.minX - image.size.width - 3.0, y: backgroundFrame.minY + floor((backgroundFrame.height - image.size.height) / 2.0)), size: image.size)
|
||||
var iconTransition = transition
|
||||
if titleSizeChanged {
|
||||
iconTransition = .immediate
|
||||
}
|
||||
iconTransition.setFrame(view: self.iconView, frame: iconFrame)
|
||||
transition.setAlpha(view: self.iconView, alpha: isActiveWithText || !isSecure ? 0.0 : 1.0)
|
||||
}
|
||||
|
||||
if let image = self.clearIconView.image {
|
||||
let iconFrame = CGRect(origin: CGPoint(x: backgroundFrame.maxX - image.size.width - 4.0, y: backgroundFrame.minY + floor((backgroundFrame.height - image.size.height) / 2.0)), size: image.size)
|
||||
transition.setFrame(view: self.clearIconView, frame: iconFrame)
|
||||
transition.setFrame(view: self.clearIconButton, frame: iconFrame.insetBy(dx: -8.0, dy: -10.0))
|
||||
transition.setAlpha(view: self.clearIconView, alpha: isActiveWithText ? 1.0 : 0.0)
|
||||
self.clearIconButton.isUserInteractionEnabled = isActiveWithText
|
||||
}
|
||||
|
||||
if let cancelButtonTitleComponentView = self.cancelButtonTitle.view {
|
||||
|
|
@ -343,12 +410,33 @@ final class AddressBarContentComponent: Component {
|
|||
cancelButtonTitleComponentView.isUserInteractionEnabled = false
|
||||
}
|
||||
transition.setFrame(view: cancelButtonTitleComponentView, frame: CGRect(origin: CGPoint(x: backgroundFrame.maxX + cancelButtonSpacing, y: floor((size.height - cancelTextSize.height) / 2.0)), size: cancelTextSize))
|
||||
transition.setAlpha(view: cancelButtonTitleComponentView, alpha: isActiveWithText ? 1.0 : 0.0)
|
||||
}
|
||||
|
||||
if let textField = self.textField {
|
||||
textField.textColor = theme.rootController.navigationSearchBar.inputTextColor
|
||||
transition.setFrame(view: textField, frame: CGRect(origin: CGPoint(x: backgroundFrame.minX + sideTextInset, y: backgroundFrame.minY - UIScreenPixel), size: CGSize(width: backgroundFrame.width - sideTextInset - 32.0, height: backgroundFrame.height)))
|
||||
|
||||
let textFieldFrame = CGRect(origin: CGPoint(x: textFrame.minX, y: backgroundFrame.minY), size: CGSize(width: backgroundFrame.maxX - textFrame.minX, height: backgroundFrame.height))
|
||||
|
||||
let textField: TextField
|
||||
if let current = self.textField {
|
||||
textField = current
|
||||
} else {
|
||||
textField = TextField(frame: textFieldFrame)
|
||||
textField.autocapitalizationType = .none
|
||||
textField.autocorrectionType = .no
|
||||
textField.keyboardType = .URL
|
||||
textField.returnKeyType = .go
|
||||
self.insertSubview(textField, belowSubview: self.clearIconView)
|
||||
self.textField = textField
|
||||
|
||||
textField.delegate = self
|
||||
textField.addTarget(self, action: #selector(self.textFieldChanged(_:)), for: .editingChanged)
|
||||
}
|
||||
|
||||
textField.text = self.component?.url ?? ""
|
||||
|
||||
textField.textColor = theme.rootController.navigationSearchBar.inputTextColor
|
||||
transition.setFrame(view: textField, frame: CGRect(origin: CGPoint(x: backgroundFrame.minX + sideInset, y: backgroundFrame.minY - UIScreenPixel), size: CGSize(width: backgroundFrame.width - sideInset - 32.0, height: backgroundFrame.height)))
|
||||
transition.setAlpha(view: textField, alpha: isActiveWithText ? 1.0 : 0.0)
|
||||
textField.isUserInteractionEnabled = isActiveWithText
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -356,7 +444,7 @@ final class AddressBarContentComponent: Component {
|
|||
return View()
|
||||
}
|
||||
|
||||
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, transition: transition)
|
||||
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<BrowserNavigationBarEnvironment>, transition: ComponentTransition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, environment: environment, transition: transition)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
466
submodules/BrowserUI/Sources/BrowserAddressListComponent.swift
Normal file
466
submodules/BrowserUI/Sources/BrowserAddressListComponent.swift
Normal file
|
|
@ -0,0 +1,466 @@
|
|||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import ComponentFlow
|
||||
import SwiftSignalKit
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import AccountContext
|
||||
import TelegramPresentationData
|
||||
|
||||
final class BrowserAddressListComponent: Component {
|
||||
let context: AccountContext
|
||||
let theme: PresentationTheme
|
||||
let strings: PresentationStrings
|
||||
let navigateTo: (String) -> Void
|
||||
|
||||
init(
|
||||
context: AccountContext,
|
||||
theme: PresentationTheme,
|
||||
strings: PresentationStrings,
|
||||
navigateTo: @escaping (String) -> Void
|
||||
) {
|
||||
self.context = context
|
||||
self.theme = theme
|
||||
self.strings = strings
|
||||
self.navigateTo = navigateTo
|
||||
}
|
||||
|
||||
static func ==(lhs: BrowserAddressListComponent, rhs: BrowserAddressListComponent) -> Bool {
|
||||
if lhs.context !== rhs.context {
|
||||
return false
|
||||
}
|
||||
if lhs.theme !== rhs.theme {
|
||||
return false
|
||||
}
|
||||
if lhs.strings !== rhs.strings {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private struct ItemLayout: Equatable {
|
||||
struct Section: Equatable {
|
||||
var id: Int
|
||||
var insets: UIEdgeInsets
|
||||
var itemHeight: CGFloat
|
||||
var itemCount: Int
|
||||
|
||||
var totalHeight: CGFloat
|
||||
|
||||
init(
|
||||
id: Int,
|
||||
insets: UIEdgeInsets,
|
||||
itemHeight: CGFloat,
|
||||
itemCount: Int
|
||||
) {
|
||||
self.id = id
|
||||
self.insets = insets
|
||||
self.itemHeight = itemHeight
|
||||
self.itemCount = itemCount
|
||||
|
||||
self.totalHeight = insets.top + itemHeight * CGFloat(itemCount) + insets.bottom
|
||||
}
|
||||
}
|
||||
|
||||
var containerSize: CGSize
|
||||
var insets: UIEdgeInsets
|
||||
var sections: [Section]
|
||||
|
||||
var contentHeight: CGFloat
|
||||
|
||||
init(
|
||||
containerSize: CGSize,
|
||||
insets: UIEdgeInsets,
|
||||
sections: [Section]
|
||||
) {
|
||||
self.containerSize = containerSize
|
||||
self.insets = insets
|
||||
self.sections = sections
|
||||
|
||||
var contentHeight: CGFloat = 0.0
|
||||
for section in sections {
|
||||
contentHeight += section.totalHeight
|
||||
}
|
||||
self.contentHeight = contentHeight
|
||||
}
|
||||
}
|
||||
|
||||
private final class ScrollView: UIScrollView {
|
||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
return super.hitTest(point, with: event)
|
||||
}
|
||||
|
||||
override func touchesShouldCancel(in view: UIView) -> Bool {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
final class View: UIView, UIScrollViewDelegate {
|
||||
struct State {
|
||||
let recent: [TelegramMediaWebpage]
|
||||
let bookmarks: [Message]
|
||||
}
|
||||
|
||||
private let backgroundView = UIView()
|
||||
private let scrollView = ScrollView()
|
||||
private let itemContainerView = UIView()
|
||||
|
||||
private let addressTemplateItem = ComponentView<Empty>()
|
||||
|
||||
private var visibleSectionHeaders: [Int: ComponentView<Empty>] = [:]
|
||||
private var visibleItems: [AnyHashable: ComponentView<Empty>] = [:]
|
||||
|
||||
private var ignoreScrolling: Bool = false
|
||||
|
||||
private var component: BrowserAddressListComponent?
|
||||
private weak var state: EmptyComponentState?
|
||||
private var itemLayout: ItemLayout?
|
||||
|
||||
private var stateDisposable: Disposable?
|
||||
private var stateValue: State?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
|
||||
self.scrollView.alwaysBounceVertical = true
|
||||
self.scrollView.delegate = self
|
||||
self.scrollView.showsVerticalScrollIndicator = false
|
||||
|
||||
self.addSubview(self.backgroundView)
|
||||
self.addSubview(self.scrollView)
|
||||
self.scrollView.addSubview(self.itemContainerView)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError()
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.stateDisposable?.dispose()
|
||||
}
|
||||
|
||||
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||
if !self.ignoreScrolling {
|
||||
self.updateScrolling(transition: .immediate)
|
||||
}
|
||||
}
|
||||
|
||||
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
|
||||
self.endEditing(true)
|
||||
}
|
||||
|
||||
private func updateScrolling(transition: ComponentTransition) {
|
||||
guard let component = self.component, let itemLayout = self.itemLayout, let state = self.stateValue else {
|
||||
return
|
||||
}
|
||||
|
||||
var topOffset = -self.scrollView.bounds.minY
|
||||
topOffset = max(0.0, topOffset)
|
||||
|
||||
let visibleBounds = self.scrollView.bounds
|
||||
var visibleFrame = self.scrollView.frame
|
||||
visibleFrame.origin.x = 0.0
|
||||
|
||||
var validIds: [AnyHashable] = []
|
||||
var validSectionHeaders: [AnyHashable] = []
|
||||
var sectionOffset: CGFloat = 0.0
|
||||
|
||||
let sideInset: CGFloat = 0.0
|
||||
let containerInset: CGFloat = 0.0
|
||||
|
||||
for sectionIndex in 0 ..< itemLayout.sections.count {
|
||||
let section = itemLayout.sections[sectionIndex]
|
||||
|
||||
do {
|
||||
var sectionHeaderFrame = CGRect(origin: CGPoint(x: sideInset, y: sectionOffset - self.scrollView.bounds.minY), size: CGSize(width: itemLayout.containerSize.width, height: section.insets.top))
|
||||
|
||||
let sectionHeaderMinY = topOffset + containerInset
|
||||
let sectionHeaderMaxY = containerInset + sectionOffset - self.scrollView.bounds.minY + section.totalHeight - 28.0
|
||||
|
||||
sectionHeaderFrame.origin.y = max(sectionHeaderFrame.origin.y, sectionHeaderMinY)
|
||||
sectionHeaderFrame.origin.y = min(sectionHeaderFrame.origin.y, sectionHeaderMaxY)
|
||||
|
||||
if visibleFrame.intersects(sectionHeaderFrame) {
|
||||
validSectionHeaders.append(section.id)
|
||||
let sectionHeader: ComponentView<Empty>
|
||||
var sectionHeaderTransition = transition
|
||||
if let current = self.visibleSectionHeaders[section.id] {
|
||||
sectionHeader = current
|
||||
} else {
|
||||
if !transition.animation.isImmediate {
|
||||
sectionHeaderTransition = .immediate
|
||||
}
|
||||
sectionHeader = ComponentView()
|
||||
self.visibleSectionHeaders[section.id] = sectionHeader
|
||||
}
|
||||
|
||||
let sectionTitle: String
|
||||
if section.id == 0 {
|
||||
sectionTitle = "RECENTLY VISITED"
|
||||
} else if section.id == 1 {
|
||||
sectionTitle = "BOOKMARKS"
|
||||
} else {
|
||||
sectionTitle = ""
|
||||
}
|
||||
|
||||
let _ = sectionHeader.update(
|
||||
transition: sectionHeaderTransition,
|
||||
component: AnyComponent(SectionHeaderComponent(
|
||||
theme: component.theme,
|
||||
style: .plain,
|
||||
title: sectionTitle,
|
||||
actionTitle: section.id == 0 ? "Clear" : nil,
|
||||
action: { [weak self] in
|
||||
if let self, let component = self.component {
|
||||
let _ = clearRecentlyVisitedLinks(engine: component.context.engine).start()
|
||||
}
|
||||
}
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: sectionHeaderFrame.size
|
||||
)
|
||||
if let sectionHeaderView = sectionHeader.view {
|
||||
if sectionHeaderView.superview == nil {
|
||||
self.addSubview(sectionHeaderView)
|
||||
|
||||
if !transition.animation.isImmediate {
|
||||
sectionHeaderView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
|
||||
}
|
||||
}
|
||||
let sectionXOffset = self.scrollView.frame.minX
|
||||
sectionHeaderTransition.setFrame(view: sectionHeaderView, frame: sectionHeaderFrame.offsetBy(dx: sectionXOffset, dy: 0.0))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for i in 0 ..< section.itemCount {
|
||||
let itemFrame = CGRect(origin: CGPoint(x: sideInset, y: sectionOffset + section.insets.top + CGFloat(i) * section.itemHeight), size: CGSize(width: itemLayout.containerSize.width, height: section.itemHeight))
|
||||
if !visibleBounds.intersects(itemFrame) {
|
||||
continue
|
||||
}
|
||||
|
||||
var id = 0
|
||||
if section.id == 0 {
|
||||
id += i
|
||||
} else if section.id == 1 {
|
||||
id += 1000 + i
|
||||
}
|
||||
|
||||
let itemId = AnyHashable(id)
|
||||
validIds.append(itemId)
|
||||
|
||||
var itemTransition = transition
|
||||
let visibleItem: ComponentView<Empty>
|
||||
if let current = self.visibleItems[itemId] {
|
||||
visibleItem = current
|
||||
} else {
|
||||
visibleItem = ComponentView()
|
||||
if !transition.animation.isImmediate {
|
||||
itemTransition = .immediate
|
||||
}
|
||||
self.visibleItems[itemId] = visibleItem
|
||||
}
|
||||
|
||||
var webPage: TelegramMediaWebpage?
|
||||
var itemMessage: Message?
|
||||
|
||||
if section.id == 0 {
|
||||
webPage = state.recent[i]
|
||||
} else if section.id == 1 {
|
||||
let message = state.bookmarks[i]
|
||||
if let primaryUrl = getPrimaryUrl(message: message) {
|
||||
if let media = message.media.first(where: { $0 is TelegramMediaWebpage }) as? TelegramMediaWebpage {
|
||||
webPage = media
|
||||
} else {
|
||||
webPage = TelegramMediaWebpage(webpageId: MediaId(namespace: 0, id: 0), content: .Loaded(TelegramMediaWebpageLoadedContent(url: primaryUrl, displayUrl: "", hash: 0, type: nil, websiteName: "", title: message.text, text: "", embedUrl: nil, embedType: nil, embedSize: nil, duration: nil, author: nil, isMediaLargeByDefault: nil, image: nil, file: nil, story: nil, attributes: [], instantPage: nil)))
|
||||
}
|
||||
itemMessage = message
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
let navigateTo = component.navigateTo
|
||||
let _ = visibleItem.update(
|
||||
transition: itemTransition,
|
||||
component: AnyComponent(
|
||||
BrowserAddressListItemComponent(
|
||||
context: component.context,
|
||||
theme: component.theme,
|
||||
webPage: webPage!,
|
||||
message: itemMessage,
|
||||
hasNext: true,
|
||||
action: {
|
||||
if let url = webPage?.content.url {
|
||||
navigateTo(url)
|
||||
}
|
||||
})
|
||||
),
|
||||
environment: {},
|
||||
containerSize: itemFrame.size
|
||||
)
|
||||
if let itemView = visibleItem.view {
|
||||
if itemView.superview == nil {
|
||||
self.itemContainerView.addSubview(itemView)
|
||||
}
|
||||
itemTransition.setFrame(view: itemView, frame: itemFrame)
|
||||
}
|
||||
}
|
||||
|
||||
sectionOffset += section.totalHeight
|
||||
}
|
||||
|
||||
var removeIds: [AnyHashable] = []
|
||||
for (id, item) in self.visibleItems {
|
||||
if !validIds.contains(id) {
|
||||
removeIds.append(id)
|
||||
if let itemView = item.view {
|
||||
if !transition.animation.isImmediate {
|
||||
itemView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in
|
||||
itemView.removeFromSuperview()
|
||||
})
|
||||
} else {
|
||||
itemView.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for id in removeIds {
|
||||
self.visibleItems.removeValue(forKey: id)
|
||||
}
|
||||
|
||||
var removeSectionHeaderIds: [Int] = []
|
||||
for (id, item) in self.visibleSectionHeaders {
|
||||
if !validSectionHeaders.contains(id) {
|
||||
removeSectionHeaderIds.append(id)
|
||||
if let itemView = item.view {
|
||||
if !transition.animation.isImmediate {
|
||||
itemView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in
|
||||
itemView.removeFromSuperview()
|
||||
})
|
||||
} else {
|
||||
itemView.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for id in removeSectionHeaderIds {
|
||||
self.visibleSectionHeaders.removeValue(forKey: id)
|
||||
}
|
||||
}
|
||||
|
||||
func update(component: BrowserAddressListComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
||||
let themeUpdated = self.component?.theme !== component.theme
|
||||
|
||||
if self.component == nil {
|
||||
self.stateDisposable = combineLatest(queue: Queue.mainQueue(),
|
||||
recentlyVisitedLinks(engine: component.context.engine),
|
||||
component.context.account.viewTracker.aroundMessageHistoryViewForLocation(.peer(peerId: component.context.account.peerId, threadId: nil), index: .upperBound, anchorIndex: .upperBound, count: 100, fixedCombinedReadStates: nil, tag: .tag(.webPage))
|
||||
).start(next: { [weak self] recent, view in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
|
||||
var bookmarks: [Message] = []
|
||||
for entry in view.0.entries.reversed() {
|
||||
bookmarks.append(entry.message)
|
||||
}
|
||||
|
||||
self.stateValue = State(
|
||||
recent: recent,
|
||||
bookmarks: bookmarks
|
||||
)
|
||||
self.state?.updated(transition: .immediate)
|
||||
})
|
||||
}
|
||||
|
||||
self.component = component
|
||||
self.state = state
|
||||
|
||||
let resetScrolling = self.scrollView.bounds.width != availableSize.width
|
||||
if themeUpdated {
|
||||
self.backgroundView.backgroundColor = component.theme.list.plainBackgroundColor
|
||||
}
|
||||
|
||||
let itemsContainerWidth = availableSize.width
|
||||
let addressItemSize = self.addressTemplateItem.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(BrowserAddressListItemComponent(
|
||||
context: component.context,
|
||||
theme: component.theme,
|
||||
webPage: TelegramMediaWebpage(webpageId: EngineMedia.Id(namespace: 0, id: 0), content: .Loaded(TelegramMediaWebpageLoadedContent(url: "https://telegram.org", displayUrl: "https://telegram.org", hash: 0, type: nil, websiteName: "Telegram", title: "Telegram Telegram", text: "Telegram", embedUrl: nil, embedType: nil, embedSize: nil, duration: nil, author: nil, isMediaLargeByDefault: nil, image: nil, file: nil, story: nil, attributes: [], instantPage: nil))),
|
||||
message: nil,
|
||||
hasNext: true,
|
||||
action: {}
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: itemsContainerWidth, height: 1000.0)
|
||||
)
|
||||
|
||||
let _ = resetScrolling
|
||||
let _ = addressItemSize
|
||||
|
||||
|
||||
var sections: [ItemLayout.Section] = []
|
||||
if let state = self.stateValue {
|
||||
if !state.recent.isEmpty {
|
||||
sections.append(ItemLayout.Section(
|
||||
id: 0,
|
||||
insets: UIEdgeInsets(top: 28.0, left: 0.0, bottom: 0.0, right: 0.0),
|
||||
itemHeight: addressItemSize.height,
|
||||
itemCount: state.recent.count
|
||||
))
|
||||
}
|
||||
if !state.bookmarks.isEmpty {
|
||||
sections.append(ItemLayout.Section(
|
||||
id: 1,
|
||||
insets: UIEdgeInsets(top: 28.0, left: 0.0, bottom: 0.0, right: 0.0),
|
||||
itemHeight: addressItemSize.height,
|
||||
itemCount: state.bookmarks.count
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
let itemLayout = ItemLayout(containerSize: availableSize, insets: .zero, sections: sections)
|
||||
self.itemLayout = itemLayout
|
||||
|
||||
let containerWidth = availableSize.width
|
||||
let scrollContentHeight = max(itemLayout.contentHeight, availableSize.height)
|
||||
|
||||
self.ignoreScrolling = true
|
||||
transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: containerWidth, height: availableSize.height)))
|
||||
let contentSize = CGSize(width: containerWidth, height: scrollContentHeight)
|
||||
if contentSize != self.scrollView.contentSize {
|
||||
self.scrollView.contentSize = contentSize
|
||||
}
|
||||
// let contentInset: UIEdgeInsets = UIEdgeInsets(top: 0.0, left: 0.0, bottom: bottomPanelHeight + bottomPanelInset, right: 0.0)
|
||||
// let indicatorInset = UIEdgeInsets(top: max(itemLayout.containerInset, environment.safeInsets.top + navigationHeight), left: 0.0, bottom: contentInset.bottom, right: 0.0)
|
||||
// if indicatorInset != self.scrollView.scrollIndicatorInsets {
|
||||
// self.scrollView.scrollIndicatorInsets = indicatorInset
|
||||
// }
|
||||
// if contentInset != self.scrollView.contentInset {
|
||||
// self.scrollView.contentInset = contentInset
|
||||
// }
|
||||
if resetScrolling {
|
||||
self.scrollView.bounds = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: containerWidth, height: availableSize.height))
|
||||
}
|
||||
self.ignoreScrolling = false
|
||||
self.updateScrolling(transition: transition)
|
||||
|
||||
transition.setFrame(view: self.backgroundView, frame: CGRect(origin: .zero, size: availableSize))
|
||||
transition.setFrame(view: self.itemContainerView, frame: CGRect(origin: .zero, size: CGSize(width: containerWidth, height: scrollContentHeight)))
|
||||
|
||||
return availableSize
|
||||
}
|
||||
}
|
||||
|
||||
func makeView() -> View {
|
||||
return View(frame: CGRect())
|
||||
}
|
||||
|
||||
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,251 @@
|
|||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import ComponentFlow
|
||||
import SwiftSignalKit
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import MultilineTextComponent
|
||||
import TelegramPresentationData
|
||||
import PhotoResources
|
||||
import AccountContext
|
||||
|
||||
final class BrowserAddressListItemComponent: Component {
|
||||
let context: AccountContext
|
||||
let theme: PresentationTheme
|
||||
let webPage: TelegramMediaWebpage
|
||||
var message: Message?
|
||||
let hasNext: Bool
|
||||
let action: () -> Void
|
||||
|
||||
init(
|
||||
context: AccountContext,
|
||||
theme: PresentationTheme,
|
||||
webPage: TelegramMediaWebpage,
|
||||
message: Message?,
|
||||
hasNext: Bool,
|
||||
action: @escaping () -> Void
|
||||
) {
|
||||
self.context = context
|
||||
self.theme = theme
|
||||
self.webPage = webPage
|
||||
self.message = message
|
||||
self.hasNext = hasNext
|
||||
self.action = action
|
||||
}
|
||||
|
||||
static func ==(lhs: BrowserAddressListItemComponent, rhs: BrowserAddressListItemComponent) -> Bool {
|
||||
if lhs.theme !== rhs.theme {
|
||||
return false
|
||||
}
|
||||
if lhs.webPage != rhs.webPage {
|
||||
return false
|
||||
}
|
||||
if lhs.hasNext != rhs.hasNext {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
final class View: UIView {
|
||||
private let containerButton: HighlightTrackingButton
|
||||
|
||||
private var emptyIcon: UIImageView?
|
||||
private var icon = TransformImageNode()
|
||||
private let title = ComponentView<Empty>()
|
||||
private let subtitle = ComponentView<Empty>()
|
||||
|
||||
private let separatorLayer: SimpleLayer
|
||||
|
||||
private var component: BrowserAddressListItemComponent?
|
||||
private weak var state: EmptyComponentState?
|
||||
|
||||
private var currentIconImageRepresentation: TelegramMediaImageRepresentation?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.separatorLayer = SimpleLayer()
|
||||
|
||||
self.containerButton = HighlightTrackingButton()
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.layer.addSublayer(self.separatorLayer)
|
||||
self.addSubview(self.containerButton)
|
||||
|
||||
self.containerButton.addTarget(self, action: #selector(self.pressed), for: .touchUpInside)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
@objc private func pressed() {
|
||||
guard let component = self.component else {
|
||||
return
|
||||
}
|
||||
component.action()
|
||||
}
|
||||
|
||||
func update(component: BrowserAddressListItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
||||
let themeUpdated = self.component?.theme !== component.theme
|
||||
let currentIconImageRepresentation = self.currentIconImageRepresentation
|
||||
|
||||
let iconSize = CGSize(width: 40.0, height: 40.0)
|
||||
let height: CGFloat = 60.0
|
||||
let leftInset: CGFloat = 11.0 + iconSize.width + 11.0
|
||||
let rightInset: CGFloat = 16.0
|
||||
let titleSpacing: CGFloat = 2.0
|
||||
|
||||
let title: String
|
||||
let subtitle: String
|
||||
var iconImageReferenceAndRepresentation: (AnyMediaReference, TelegramMediaImageRepresentation)?
|
||||
var updateIconImageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>?
|
||||
|
||||
if case let .Loaded(content) = component.webPage.content {
|
||||
title = content.title ?? content.url
|
||||
subtitle = content.url
|
||||
|
||||
if let image = content.image {
|
||||
if let representation = imageRepresentationLargerThan(image.representations, size: PixelDimensions(width: 80, height: 80)) {
|
||||
if let message = component.message {
|
||||
iconImageReferenceAndRepresentation = (.message(message: MessageReference(message), media: image), representation)
|
||||
} else {
|
||||
iconImageReferenceAndRepresentation = (.standalone(media: image), representation)
|
||||
}
|
||||
}
|
||||
} else if let file = content.file {
|
||||
if let representation = smallestImageRepresentation(file.previewRepresentations) {
|
||||
if let message = component.message {
|
||||
iconImageReferenceAndRepresentation = (.message(message: MessageReference(message), media: file), representation)
|
||||
} else {
|
||||
iconImageReferenceAndRepresentation = (.standalone(media: file), representation)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if currentIconImageRepresentation != iconImageReferenceAndRepresentation?.1 {
|
||||
if let iconImageReferenceAndRepresentation = iconImageReferenceAndRepresentation {
|
||||
if let imageReference = iconImageReferenceAndRepresentation.0.concrete(TelegramMediaImage.self) {
|
||||
updateIconImageSignal = chatWebpageSnippetPhoto(account: component.context.account, userLocation: (component.message?.id.peerId).flatMap(MediaResourceUserLocation.peer) ?? .other, photoReference: imageReference)
|
||||
} else if let fileReference = iconImageReferenceAndRepresentation.0.concrete(TelegramMediaFile.self) {
|
||||
updateIconImageSignal = chatWebpageSnippetFile(account: component.context.account, userLocation: (component.message?.id.peerId).flatMap(MediaResourceUserLocation.peer) ?? .other, mediaReference: fileReference.abstract, representation: iconImageReferenceAndRepresentation.1)
|
||||
}
|
||||
} else {
|
||||
updateIconImageSignal = .complete()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
title = ""
|
||||
subtitle = ""
|
||||
}
|
||||
|
||||
self.component = component
|
||||
self.state = state
|
||||
self.currentIconImageRepresentation = iconImageReferenceAndRepresentation?.1
|
||||
|
||||
let titleSize = self.title.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(string: title, font: Font.semibold(17.0), textColor: component.theme.list.itemPrimaryTextColor))
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width - leftInset - rightInset, height: 100.0)
|
||||
)
|
||||
let subtitleSize = self.subtitle.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(string: subtitle, font: Font.regular(15.0), textColor: component.theme.list.itemAccentColor))
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width - leftInset - rightInset, height: 100.0)
|
||||
)
|
||||
|
||||
let centralContentHeight = titleSize.height + subtitleSize.height + titleSpacing
|
||||
|
||||
let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: floor((height - centralContentHeight) / 2.0)), size: titleSize)
|
||||
if let titleView = self.title.view {
|
||||
if titleView.superview == nil {
|
||||
titleView.isUserInteractionEnabled = false
|
||||
self.containerButton.addSubview(titleView)
|
||||
}
|
||||
titleView.frame = titleFrame
|
||||
}
|
||||
|
||||
let subtitleFrame = CGRect(origin: CGPoint(x: leftInset, y: titleFrame.maxY + titleSpacing), size: subtitleSize)
|
||||
if let subtitleView = self.subtitle.view {
|
||||
if subtitleView.superview == nil {
|
||||
subtitleView.isUserInteractionEnabled = false
|
||||
self.containerButton.addSubview(subtitleView)
|
||||
}
|
||||
subtitleView.frame = subtitleFrame
|
||||
}
|
||||
|
||||
|
||||
let iconFrame = CGRect(origin: CGPoint(x: 11.0, y: floorToScreenPixels((height - iconSize.height) / 2.0)), size: iconSize)
|
||||
|
||||
let iconImageLayout = self.icon.asyncLayout()
|
||||
var iconImageApply: (() -> Void)?
|
||||
if let iconImageReferenceAndRepresentation = iconImageReferenceAndRepresentation {
|
||||
let imageCorners = ImageCorners(radius: 6.0)
|
||||
let arguments = TransformImageArguments(corners: imageCorners, imageSize: iconImageReferenceAndRepresentation.1.dimensions.cgSize.aspectFilled(iconSize), boundingSize: iconSize, intrinsicInsets: UIEdgeInsets(), emptyColor: component.theme.list.mediaPlaceholderColor)
|
||||
iconImageApply = iconImageLayout(arguments)
|
||||
}
|
||||
|
||||
if let iconImageApply = iconImageApply {
|
||||
if let updateImageSignal = updateIconImageSignal {
|
||||
self.icon.setSignal(updateImageSignal)
|
||||
}
|
||||
|
||||
if self.icon.supernode == nil {
|
||||
self.addSubview(self.icon.view)
|
||||
self.icon.frame = iconFrame
|
||||
} else {
|
||||
transition.setFrame(view: self.icon.view, frame: iconFrame)
|
||||
}
|
||||
|
||||
iconImageApply()
|
||||
|
||||
// if strongSelf.iconTextBackgroundNode.supernode != nil {
|
||||
// strongSelf.iconTextBackgroundNode.removeFromSupernode()
|
||||
// }
|
||||
// if strongSelf.iconTextNode.supernode != nil {
|
||||
// strongSelf.iconTextNode.removeFromSupernode()
|
||||
// }
|
||||
} else {
|
||||
if self.icon.supernode != nil {
|
||||
self.icon.view.removeFromSuperview()
|
||||
}
|
||||
|
||||
// if strongSelf.iconTextBackgroundNode.supernode == nil {
|
||||
// strongSelf.iconTextBackgroundNode.image = applyIconTextBackgroundImage
|
||||
// strongSelf.offsetContainerNode.addSubnode(strongSelf.iconTextBackgroundNode)
|
||||
// strongSelf.iconTextBackgroundNode.frame = iconFrame
|
||||
// } else {
|
||||
// transition.updateFrame(node: strongSelf.iconTextBackgroundNode, frame: iconFrame)
|
||||
// }
|
||||
// if strongSelf.iconTextNode.supernode == nil {
|
||||
// strongSelf.offsetContainerNode.addSubnode(strongSelf.iconTextNode)
|
||||
// }
|
||||
}
|
||||
|
||||
if themeUpdated {
|
||||
self.separatorLayer.backgroundColor = component.theme.list.itemPlainSeparatorColor.cgColor
|
||||
}
|
||||
transition.setFrame(layer: self.separatorLayer, frame: CGRect(origin: CGPoint(x: leftInset, y: height), size: CGSize(width: availableSize.width - leftInset, height: UIScreenPixel)))
|
||||
self.separatorLayer.isHidden = !component.hasNext
|
||||
|
||||
let containerFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: availableSize.width, height: height))
|
||||
transition.setFrame(view: self.containerButton, frame: containerFrame)
|
||||
|
||||
return CGSize(width: availableSize.width, height: height)
|
||||
}
|
||||
}
|
||||
|
||||
func makeView() -> View {
|
||||
return View(frame: CGRect())
|
||||
}
|
||||
|
||||
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||
}
|
||||
}
|
||||
517
submodules/BrowserUI/Sources/BrowserBookmarksScreen.swift
Normal file
517
submodules/BrowserUI/Sources/BrowserBookmarksScreen.swift
Normal file
|
|
@ -0,0 +1,517 @@
|
|||
import Foundation
|
||||
import UIKit
|
||||
import AccountContext
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import SwiftSignalKit
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import TelegramPresentationData
|
||||
import PresentationDataUtils
|
||||
import ChatControllerInteraction
|
||||
import TelegramUIPreferences
|
||||
import ChatPresentationInterfaceState
|
||||
import TextFormat
|
||||
import UrlWhitelist
|
||||
import SearchUI
|
||||
import SearchBarNode
|
||||
import ChatHistorySearchContainerNode
|
||||
|
||||
public final class BrowserBookmarksScreen: ViewController {
|
||||
final class Node: ViewControllerTracingNode, ASScrollViewDelegate {
|
||||
private let context: AccountContext
|
||||
private var presentationData: PresentationData
|
||||
private weak var controller: BrowserBookmarksScreen?
|
||||
|
||||
private let controllerInteraction: ChatControllerInteraction
|
||||
private var searchDisplayController: SearchDisplayController?
|
||||
|
||||
fileprivate let historyNode: ChatHistoryListNode
|
||||
private let bottomPanelNode: BottomPanelNode
|
||||
|
||||
private var addedBookmark = false
|
||||
|
||||
private var validLayout: (ContainerViewLayout, CGFloat, CGFloat)?
|
||||
|
||||
init(context: AccountContext, controller: BrowserBookmarksScreen, presentationData: PresentationData) {
|
||||
self.context = context
|
||||
self.controller = controller
|
||||
self.presentationData = presentationData
|
||||
|
||||
var openMessageImpl: ((Message) -> Bool)?
|
||||
self.controllerInteraction = ChatControllerInteraction(openMessage: { message, _ in
|
||||
if let openMessageImpl = openMessageImpl {
|
||||
return openMessageImpl(message)
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}, openPeer: { _, _, _, _ in
|
||||
}, openPeerMention: { _, _ in
|
||||
}, openMessageContextMenu: { _, _, _, _, _, _ in
|
||||
}, openMessageReactionContextMenu: { _, _, _, _ in
|
||||
}, updateMessageReaction: { _, _, _, _ in
|
||||
}, activateMessagePinch: { _ in
|
||||
}, openMessageContextActions: { _, _, _, _ in
|
||||
}, navigateToMessage: { _, _, _ in
|
||||
}, navigateToMessageStandalone: { _ in
|
||||
}, navigateToThreadMessage: { _, _, _ in
|
||||
}, tapMessage: nil, clickThroughMessage: {
|
||||
}, toggleMessagesSelection: { _, _ in
|
||||
}, sendCurrentMessage: { _, _ in
|
||||
}, sendMessage: { _ in
|
||||
}, sendSticker: { _, _, _, _, _, _, _, _, _ in
|
||||
return false
|
||||
}, sendEmoji: { _, _, _ in
|
||||
}, sendGif: { _, _, _, _, _ in
|
||||
return false
|
||||
}, sendBotContextResultAsGif: { _, _, _, _, _, _ in
|
||||
return false
|
||||
}, requestMessageActionCallback: { _, _, _, _ in
|
||||
}, requestMessageActionUrlAuth: { _, _ in
|
||||
}, activateSwitchInline: { _, _, _ in
|
||||
}, openUrl: { [weak controller] url in
|
||||
if let controller {
|
||||
controller.openUrl(url.url)
|
||||
controller.dismiss()
|
||||
}
|
||||
}, shareCurrentLocation: {
|
||||
}, shareAccountContact: {
|
||||
}, sendBotCommand: { _, _ in
|
||||
}, openInstantPage: { message, _ in
|
||||
if let openMessageImpl = openMessageImpl {
|
||||
let _ = openMessageImpl(message)
|
||||
}
|
||||
}, openWallpaper: { _ in
|
||||
}, openTheme: {_ in
|
||||
}, openHashtag: { _, _ in
|
||||
}, updateInputState: { _ in
|
||||
}, updateInputMode: { _ in
|
||||
}, openMessageShareMenu: { _ in
|
||||
}, presentController: { _, _ in
|
||||
}, presentControllerInCurrent: { _, _ in
|
||||
}, navigationController: {
|
||||
return nil
|
||||
}, chatControllerNode: {
|
||||
return nil
|
||||
}, presentGlobalOverlayController: { _, _ in
|
||||
}, callPeer: { _, _ in
|
||||
}, longTap: { _, _ in
|
||||
}, openCheckoutOrReceipt: { _, _ in
|
||||
}, openSearch: {
|
||||
}, setupReply: { _ in
|
||||
}, canSetupReply: { _ in
|
||||
return .none
|
||||
}, canSendMessages: {
|
||||
return false
|
||||
}, navigateToFirstDateMessage: { _, _ in
|
||||
}, requestRedeliveryOfFailedMessages: { _ in
|
||||
}, addContact: { _ in
|
||||
}, rateCall: { _, _, _ in
|
||||
}, requestSelectMessagePollOptions: { _, _ in
|
||||
}, requestOpenMessagePollResults: { _, _ in
|
||||
}, openAppStorePage: {
|
||||
}, displayMessageTooltip: { _, _, _, _, _ in
|
||||
}, seekToTimecode: { _, _, _ in
|
||||
}, scheduleCurrentMessage: { _ in
|
||||
}, sendScheduledMessagesNow: { _ in
|
||||
}, editScheduledMessagesTime: { _ in
|
||||
}, performTextSelectionAction: { _, _, _, _ in
|
||||
}, displayImportedMessageTooltip: { _ in
|
||||
}, displaySwipeToReplyHint: {
|
||||
}, dismissReplyMarkupMessage: { _ in
|
||||
}, openMessagePollResults: { _, _ in
|
||||
}, openPollCreation: { _ in
|
||||
}, displayPollSolution: { _, _ in
|
||||
}, displayPsa: { _, _ in
|
||||
}, displayDiceTooltip: { _ in
|
||||
}, animateDiceSuccess: { _, _ in
|
||||
}, displayPremiumStickerTooltip: { _, _ in
|
||||
}, displayEmojiPackTooltip: { _, _ in
|
||||
}, openPeerContextMenu: { _, _, _, _, _ in
|
||||
}, openMessageReplies: { _, _, _ in
|
||||
}, openReplyThreadOriginalMessage: { _ in
|
||||
}, openMessageStats: { _ in
|
||||
}, editMessageMedia: { _, _ in
|
||||
}, copyText: { _ in
|
||||
}, displayUndo: { _ in
|
||||
}, isAnimatingMessage: { _ in
|
||||
return false
|
||||
}, getMessageTransitionNode: {
|
||||
return nil
|
||||
}, updateChoosingSticker: { _ in
|
||||
}, commitEmojiInteraction: { _, _, _, _ in
|
||||
}, openLargeEmojiInfo: { _, _, _ in
|
||||
}, openJoinLink: { _ in
|
||||
}, openWebView: { _, _, _, _ in
|
||||
}, activateAdAction: { _, _ in
|
||||
}, openRequestedPeerSelection: { _, _, _, _ in
|
||||
}, saveMediaToFiles: { _ in
|
||||
}, openNoAdsDemo: {
|
||||
}, openAdsInfo: {
|
||||
}, displayGiveawayParticipationStatus: { _ in
|
||||
}, openPremiumStatusInfo: { _, _, _, _ in
|
||||
}, openRecommendedChannelContextMenu: { _, _, _ in
|
||||
}, openGroupBoostInfo: { _, _ in
|
||||
}, openStickerEditor: {
|
||||
}, openAgeRestrictedMessageMedia: { _, _ in
|
||||
}, playMessageEffect: { _ in
|
||||
}, editMessageFactCheck: { _ in
|
||||
}, requestMessageUpdate: { _, _ in
|
||||
}, cancelInteractiveKeyboardGestures: {
|
||||
}, dismissTextInput: {
|
||||
}, scrollToMessageId: { _ in
|
||||
}, navigateToStory: { _, _ in
|
||||
}, attemptedNavigationToPrivateQuote: { _ in
|
||||
}, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(), presentationContext: ChatPresentationContext(context: context, backgroundNode: nil))
|
||||
|
||||
|
||||
let tagMask: MessageTags = .webPage
|
||||
let chatLocationContextHolder = Atomic<ChatLocationContextHolder?>(value: nil)
|
||||
self.historyNode = context.sharedContext.makeChatHistoryListNode(
|
||||
context: context,
|
||||
updatedPresentationData: (context.sharedContext.currentPresentationData.with({ $0 }), context.sharedContext.presentationData),
|
||||
chatLocation: .peer(id: context.account.peerId),
|
||||
chatLocationContextHolder: chatLocationContextHolder,
|
||||
tag: .tag(tagMask),
|
||||
source: .default,
|
||||
subject: nil,
|
||||
controllerInteraction: self.controllerInteraction,
|
||||
selectedMessages: .single(nil),
|
||||
mode: .list(
|
||||
search: false,
|
||||
reversed: false,
|
||||
reverseGroups: false,
|
||||
displayHeaders: .none,
|
||||
hintLinks: true,
|
||||
isGlobalSearch: false
|
||||
)
|
||||
)
|
||||
|
||||
var addBookmarkImpl: (() -> Void)?
|
||||
self.bottomPanelNode = BottomPanelNode(theme: presentationData.theme, strings: presentationData.strings, action: {
|
||||
addBookmarkImpl?()
|
||||
})
|
||||
|
||||
super.init()
|
||||
|
||||
self.backgroundColor = self.presentationData.theme.list.plainBackgroundColor
|
||||
|
||||
self.addSubnode(self.historyNode)
|
||||
self.addSubnode(self.bottomPanelNode)
|
||||
|
||||
openMessageImpl = { [weak controller] message in
|
||||
guard let controller else {
|
||||
return false
|
||||
}
|
||||
if let primaryUrl = getPrimaryUrl(message: message) {
|
||||
controller.openUrl(primaryUrl)
|
||||
}
|
||||
controller.dismiss()
|
||||
return true
|
||||
}
|
||||
|
||||
addBookmarkImpl = { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.controller?.addBookmark()
|
||||
self.addedBookmark = true
|
||||
if let (layout, navigationBarHeight, actualNavigationBarHeight) = self.validLayout {
|
||||
self.containerLayoutUpdated(layout: layout, navigationBarHeight: navigationBarHeight, actualNavigationBarHeight: actualNavigationBarHeight, transition: .animated(duration: 0.4, curve: .spring))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func activateSearch(placeholderNode: SearchBarPlaceholderNode) {
|
||||
guard let (layout, navigationBarHeight, _) = self.validLayout, let navigationBar = self.controller?.navigationBar else {
|
||||
return
|
||||
}
|
||||
let tagMask: MessageTags = .webPage
|
||||
|
||||
self.searchDisplayController = SearchDisplayController(presentationData: self.presentationData, mode: .list, placeholder: self.presentationData.strings.Common_Search, hasBackground: true, contentNode: ChatHistorySearchContainerNode(context: self.context, peerId: self.context.account.peerId, threadId: nil, tagMask: tagMask, interfaceInteraction: self.controllerInteraction), cancel: { [weak self] in
|
||||
self?.controller?.deactivateSearch()
|
||||
})
|
||||
|
||||
self.searchDisplayController?.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate)
|
||||
self.searchDisplayController?.activate(insertSubnode: { [weak self, weak placeholderNode] subnode, isSearchBar in
|
||||
if let strongSelf = self, let placeholderNode {
|
||||
if isSearchBar {
|
||||
placeholderNode.supernode?.insertSubnode(subnode, aboveSubnode: placeholderNode)
|
||||
} else {
|
||||
strongSelf.insertSubnode(subnode, belowSubnode: navigationBar)
|
||||
}
|
||||
}
|
||||
}, placeholder: placeholderNode)
|
||||
}
|
||||
|
||||
func deactivateSearch(placeholderNode: SearchBarPlaceholderNode) {
|
||||
guard let searchDisplayController = self.searchDisplayController else {
|
||||
return
|
||||
}
|
||||
self.searchDisplayController = nil
|
||||
searchDisplayController.deactivate(placeholder: placeholderNode)
|
||||
}
|
||||
|
||||
func scrollToTop() {
|
||||
self.historyNode.scrollToEndOfHistory()
|
||||
}
|
||||
|
||||
func containerLayoutUpdated(layout: ContainerViewLayout, navigationBarHeight: CGFloat, actualNavigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||
self.validLayout = (layout, navigationBarHeight, actualNavigationBarHeight)
|
||||
|
||||
let historyFrame = CGRect(origin: .zero, size: layout.size)
|
||||
transition.updateFrame(node: self.historyNode, frame: historyFrame)
|
||||
|
||||
var insets = layout.insets(options: [.input])
|
||||
insets.top += navigationBarHeight
|
||||
|
||||
var headerInsets = layout.insets(options: [.input])
|
||||
headerInsets.top += actualNavigationBarHeight
|
||||
|
||||
let panelHeight = self.bottomPanelNode.updateLayout(width: layout.size.width, sideInset: layout.safeInsets.left, bottomInset: insets.bottom, transition: transition)
|
||||
var panelOrigin: CGFloat = layout.size.height
|
||||
if !self.addedBookmark {
|
||||
panelOrigin -= panelHeight
|
||||
insets.bottom = panelHeight
|
||||
}
|
||||
let panelFrame = CGRect(origin: CGPoint(x: 0.0, y: panelOrigin), size: CGSize(width: layout.size.width, height: panelHeight))
|
||||
transition.updateFrame(node: self.bottomPanelNode, frame: panelFrame)
|
||||
|
||||
let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition)
|
||||
let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: historyFrame.size, insets: insets, headerInsets: headerInsets, duration: duration, curve: curve)
|
||||
self.historyNode.updateLayout(transition: transition, updateSizeAndInsets: updateSizeAndInsets)
|
||||
|
||||
if let searchDisplayController = self.searchDisplayController {
|
||||
searchDisplayController.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private let context: AccountContext
|
||||
private var presentationData: PresentationData
|
||||
private var presentationDataDisposable: Disposable?
|
||||
private let url: String
|
||||
private let openUrl: (String) -> Void
|
||||
private let addBookmark: () -> Void
|
||||
|
||||
private var controllerNode: Node {
|
||||
return self.displayNode as! Node
|
||||
}
|
||||
|
||||
private var searchContentNode: NavigationBarSearchContentNode?
|
||||
|
||||
private var validLayout: ContainerViewLayout?
|
||||
|
||||
private var node: Node {
|
||||
return self.displayNode as! Node
|
||||
}
|
||||
|
||||
public init(context: AccountContext, url: String, openUrl: @escaping (String) -> Void, addBookmark: @escaping () -> Void) {
|
||||
self.context = context
|
||||
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
self.url = url
|
||||
self.openUrl = openUrl
|
||||
self.addBookmark = addBookmark
|
||||
|
||||
super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationData: self.presentationData))
|
||||
|
||||
self.navigationPresentation = .modal
|
||||
self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait)
|
||||
|
||||
self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style
|
||||
|
||||
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.title = self.presentationData.strings.WebBrowser_Bookmarks_Title
|
||||
|
||||
self.searchContentNode = NavigationBarSearchContentNode(theme: self.presentationData.theme, placeholder: self.presentationData.strings.Common_Search, activate: { [weak self] in
|
||||
self?.activateSearch()
|
||||
})
|
||||
self.navigationBar?.setContentNode(self.searchContentNode, animated: false)
|
||||
|
||||
self.scrollToTop = { [weak self] in
|
||||
if let self {
|
||||
if let searchContentNode = self.searchContentNode {
|
||||
searchContentNode.updateExpansionProgress(1.0, animated: true)
|
||||
}
|
||||
self.node.scrollToTop()
|
||||
}
|
||||
}
|
||||
|
||||
self.presentationDataDisposable = (context.sharedContext.presentationData
|
||||
|> deliverOnMainQueue).start(next: { [weak self] presentationData in
|
||||
if let strongSelf = self {
|
||||
let previousTheme = strongSelf.presentationData.theme
|
||||
let previousStrings = strongSelf.presentationData.strings
|
||||
|
||||
strongSelf.presentationData = presentationData
|
||||
|
||||
if previousTheme !== presentationData.theme || previousStrings !== presentationData.strings {
|
||||
strongSelf.updateThemeAndStrings()
|
||||
}
|
||||
}
|
||||
}).strict()
|
||||
}
|
||||
|
||||
required init(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.presentationDataDisposable?.dispose()
|
||||
}
|
||||
|
||||
override public func loadDisplayNode() {
|
||||
self.displayNode = Node(context: self.context, controller: self, presentationData: self.presentationData)
|
||||
|
||||
self.node.historyNode.contentPositionChanged = { [weak self] offset in
|
||||
if let strongSelf = self, let searchContentNode = strongSelf.searchContentNode {
|
||||
searchContentNode.updateListVisibleContentOffset(offset)
|
||||
}
|
||||
}
|
||||
//
|
||||
// self.node.historyNode.didEndScrolling = { [weak self] _ in
|
||||
// if let strongSelf = self, let searchContentNode = strongSelf.searchContentNode {
|
||||
// let _ = fixNavigationSearchableListNodeScrolling(strongSelf.node.historyNode, searchNode: searchContentNode)
|
||||
// }
|
||||
// }
|
||||
|
||||
self.displayNodeDidLoad()
|
||||
}
|
||||
|
||||
private func updateThemeAndStrings() {
|
||||
self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style
|
||||
self.navigationBar?.updatePresentationData(NavigationBarPresentationData(presentationData: self.presentationData))
|
||||
self.searchContentNode?.updateThemeAndPlaceholder(theme: self.presentationData.theme, placeholder: self.presentationData.strings.Common_Search)
|
||||
}
|
||||
|
||||
fileprivate func activateSearch() {
|
||||
if self.displayNavigationBar {
|
||||
if let scrollToTop = self.scrollToTop {
|
||||
scrollToTop()
|
||||
}
|
||||
if let searchContentNode = self.searchContentNode {
|
||||
self.node.activateSearch(placeholderNode: searchContentNode.placeholderNode)
|
||||
}
|
||||
self.setDisplayNavigationBar(false, transition: .animated(duration: 0.5, curve: .spring))
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate func deactivateSearch() {
|
||||
if !self.displayNavigationBar {
|
||||
self.setDisplayNavigationBar(true, transition: .animated(duration: 0.5, curve: .spring))
|
||||
if let searchContentNode = self.searchContentNode {
|
||||
self.node.deactivateSearch(placeholderNode: searchContentNode.placeholderNode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
|
||||
super.containerLayoutUpdated(layout, transition: transition)
|
||||
|
||||
self.validLayout = layout
|
||||
|
||||
self.controllerNode.containerLayoutUpdated(layout: layout, navigationBarHeight: self.cleanNavigationHeight, actualNavigationBarHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition)
|
||||
}
|
||||
|
||||
@objc private func cancelPressed() {
|
||||
self.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
private class BottomPanelNode: ASDisplayNode {
|
||||
private let theme: PresentationTheme
|
||||
private let strings: PresentationStrings
|
||||
private let action: () -> Void
|
||||
|
||||
private let separatorNode: ASDisplayNode
|
||||
private let button: HighlightTrackingButtonNode
|
||||
private let iconNode: ASImageNode
|
||||
private let textNode: ImmediateTextNode
|
||||
|
||||
private var validLayout: (CGFloat, CGFloat, CGFloat)?
|
||||
|
||||
init(theme: PresentationTheme, strings: PresentationStrings, action: @escaping () -> Void) {
|
||||
self.theme = theme
|
||||
self.strings = strings
|
||||
self.action = action
|
||||
|
||||
self.separatorNode = ASDisplayNode()
|
||||
self.separatorNode.backgroundColor = theme.rootController.navigationBar.separatorColor
|
||||
|
||||
self.iconNode = ASImageNode()
|
||||
self.iconNode.displaysAsynchronously = false
|
||||
self.iconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat List/AddIcon"), color: theme.rootController.navigationBar.accentTextColor)
|
||||
self.iconNode.isUserInteractionEnabled = false
|
||||
|
||||
self.textNode = ImmediateTextNode()
|
||||
self.textNode.displaysAsynchronously = false
|
||||
self.textNode.attributedText = NSAttributedString(string: strings.WebBrowser_Bookmarks_BookmarkCurrent, font: Font.regular(17.0), textColor: theme.rootController.navigationBar.accentTextColor)
|
||||
self.textNode.isUserInteractionEnabled = false
|
||||
|
||||
self.button = HighlightTrackingButtonNode()
|
||||
|
||||
super.init()
|
||||
|
||||
self.backgroundColor = theme.rootController.navigationBar.opaqueBackgroundColor
|
||||
|
||||
self.addSubnode(self.button)
|
||||
self.addSubnode(self.separatorNode)
|
||||
self.addSubnode(self.iconNode)
|
||||
self.addSubnode(self.textNode)
|
||||
self.addSubnode(self.button)
|
||||
|
||||
self.button.highligthedChanged = { [weak self] highlighted in
|
||||
if let self {
|
||||
if highlighted {
|
||||
self.iconNode.layer.removeAnimation(forKey: "opacity")
|
||||
self.iconNode.alpha = 0.4
|
||||
|
||||
self.textNode.layer.removeAnimation(forKey: "opacity")
|
||||
self.textNode.alpha = 0.4
|
||||
} else {
|
||||
self.iconNode.alpha = 1.0
|
||||
self.iconNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
|
||||
|
||||
self.textNode.alpha = 1.0
|
||||
self.textNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
|
||||
}
|
||||
}
|
||||
}
|
||||
self.button.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside)
|
||||
}
|
||||
|
||||
@objc private func buttonPressed() {
|
||||
self.action()
|
||||
}
|
||||
|
||||
func updateLayout(width: CGFloat, sideInset: CGFloat, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat {
|
||||
self.validLayout = (width, sideInset, bottomInset)
|
||||
let topInset: CGFloat = 8.0
|
||||
var bottomInset = bottomInset
|
||||
bottomInset += topInset - (bottomInset.isZero ? 0.0 : 4.0)
|
||||
|
||||
let buttonHeight: CGFloat = 40.0
|
||||
let textSize = self.textNode.updateLayout(CGSize(width: width, height: 44.0))
|
||||
|
||||
let spacing: CGFloat = 8.0
|
||||
var contentWidth = textSize.width
|
||||
var contentOriginX = floorToScreenPixels((width - contentWidth) / 2.0)
|
||||
if let icon = self.iconNode.image {
|
||||
contentWidth += icon.size.width + spacing
|
||||
contentOriginX = floorToScreenPixels((width - contentWidth) / 2.0)
|
||||
transition.updateFrame(node: self.iconNode, frame: CGRect(origin: CGPoint(x: contentOriginX, y: 12.0 + UIScreenPixel), size: icon.size))
|
||||
contentOriginX += icon.size.width + spacing
|
||||
}
|
||||
let textFrame = CGRect(origin: CGPoint(x: contentOriginX, y: 17.0), size: textSize)
|
||||
transition.updateFrame(node: self.textNode, frame: textFrame)
|
||||
|
||||
transition.updateFrame(node: self.button, frame: textFrame.insetBy(dx: -10.0, dy: -10.0))
|
||||
|
||||
transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: width, height: UIScreenPixel)))
|
||||
|
||||
return topInset + buttonHeight + bottomInset
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -10,6 +10,7 @@ final class BrowserContentState: Equatable {
|
|||
enum ContentType: Equatable {
|
||||
case webPage
|
||||
case instantPage
|
||||
case document
|
||||
}
|
||||
|
||||
struct HistoryItem: Equatable {
|
||||
|
|
@ -39,6 +40,7 @@ final class BrowserContentState: Equatable {
|
|||
let readingProgress: Double
|
||||
let contentType: ContentType
|
||||
let favicon: UIImage?
|
||||
let isSecure: Bool
|
||||
|
||||
let canGoBack: Bool
|
||||
let canGoForward: Bool
|
||||
|
|
@ -53,6 +55,7 @@ final class BrowserContentState: Equatable {
|
|||
readingProgress: Double,
|
||||
contentType: ContentType,
|
||||
favicon: UIImage? = nil,
|
||||
isSecure: Bool = false,
|
||||
canGoBack: Bool = false,
|
||||
canGoForward: Bool = false,
|
||||
backList: [HistoryItem] = [],
|
||||
|
|
@ -64,6 +67,7 @@ final class BrowserContentState: Equatable {
|
|||
self.readingProgress = readingProgress
|
||||
self.contentType = contentType
|
||||
self.favicon = favicon
|
||||
self.isSecure = isSecure
|
||||
self.canGoBack = canGoBack
|
||||
self.canGoForward = canGoForward
|
||||
self.backList = backList
|
||||
|
|
@ -89,6 +93,9 @@ final class BrowserContentState: Equatable {
|
|||
if (lhs.favicon == nil) != (rhs.favicon == nil) {
|
||||
return false
|
||||
}
|
||||
if lhs.isSecure != rhs.isSecure {
|
||||
return false
|
||||
}
|
||||
if lhs.canGoBack != rhs.canGoBack {
|
||||
return false
|
||||
}
|
||||
|
|
@ -105,39 +112,43 @@ final class BrowserContentState: Equatable {
|
|||
}
|
||||
|
||||
func withUpdatedTitle(_ title: String) -> BrowserContentState {
|
||||
return BrowserContentState(title: title, url: self.url, estimatedProgress: self.estimatedProgress, readingProgress: self.readingProgress, contentType: self.contentType, favicon: self.favicon, canGoBack: self.canGoBack, canGoForward: self.canGoForward, backList: self.backList, forwardList: self.forwardList)
|
||||
return BrowserContentState(title: title, url: self.url, estimatedProgress: self.estimatedProgress, readingProgress: self.readingProgress, contentType: self.contentType, favicon: self.favicon, isSecure: self.isSecure, canGoBack: self.canGoBack, canGoForward: self.canGoForward, backList: self.backList, forwardList: self.forwardList)
|
||||
}
|
||||
|
||||
func withUpdatedUrl(_ url: String) -> BrowserContentState {
|
||||
return BrowserContentState(title: self.title, url: url, estimatedProgress: self.estimatedProgress, readingProgress: self.readingProgress, contentType: self.contentType, favicon: self.favicon, canGoBack: self.canGoBack, canGoForward: self.canGoForward, backList: self.backList, forwardList: self.forwardList)
|
||||
return BrowserContentState(title: self.title, url: url, estimatedProgress: self.estimatedProgress, readingProgress: self.readingProgress, contentType: self.contentType, favicon: self.favicon, isSecure: self.isSecure, canGoBack: self.canGoBack, canGoForward: self.canGoForward, backList: self.backList, forwardList: self.forwardList)
|
||||
}
|
||||
|
||||
func withUpdatedIsSecure(_ isSecure: Bool) -> BrowserContentState {
|
||||
return BrowserContentState(title: self.title, url: self.url, estimatedProgress: self.estimatedProgress, readingProgress: self.readingProgress, contentType: self.contentType, favicon: self.favicon, isSecure: isSecure, canGoBack: self.canGoBack, canGoForward: self.canGoForward, backList: self.backList, forwardList: self.forwardList)
|
||||
}
|
||||
|
||||
func withUpdatedEstimatedProgress(_ estimatedProgress: Double) -> BrowserContentState {
|
||||
return BrowserContentState(title: self.title, url: self.url, estimatedProgress: estimatedProgress, readingProgress: self.readingProgress, contentType: self.contentType, favicon: self.favicon, canGoBack: self.canGoBack, canGoForward: self.canGoForward, backList: self.backList, forwardList: self.forwardList)
|
||||
return BrowserContentState(title: self.title, url: self.url, estimatedProgress: estimatedProgress, readingProgress: self.readingProgress, contentType: self.contentType, favicon: self.favicon, isSecure: self.isSecure, canGoBack: self.canGoBack, canGoForward: self.canGoForward, backList: self.backList, forwardList: self.forwardList)
|
||||
}
|
||||
|
||||
func withUpdatedReadingProgress(_ readingProgress: Double) -> BrowserContentState {
|
||||
return BrowserContentState(title: self.title, url: self.url, estimatedProgress: self.estimatedProgress, readingProgress: readingProgress, contentType: self.contentType, favicon: self.favicon, canGoBack: self.canGoBack, canGoForward: self.canGoForward, backList: self.backList, forwardList: self.forwardList)
|
||||
return BrowserContentState(title: self.title, url: self.url, estimatedProgress: self.estimatedProgress, readingProgress: readingProgress, contentType: self.contentType, favicon: self.favicon, isSecure: self.isSecure, canGoBack: self.canGoBack, canGoForward: self.canGoForward, backList: self.backList, forwardList: self.forwardList)
|
||||
}
|
||||
|
||||
func withUpdatedFavicon(_ favicon: UIImage?) -> BrowserContentState {
|
||||
return BrowserContentState(title: self.title, url: self.url, estimatedProgress: self.estimatedProgress, readingProgress: self.readingProgress, contentType: self.contentType, favicon: favicon, canGoBack: self.canGoBack, canGoForward: self.canGoForward, backList: self.backList, forwardList: self.forwardList)
|
||||
return BrowserContentState(title: self.title, url: self.url, estimatedProgress: self.estimatedProgress, readingProgress: self.readingProgress, contentType: self.contentType, favicon: favicon, isSecure: self.isSecure, canGoBack: self.canGoBack, canGoForward: self.canGoForward, backList: self.backList, forwardList: self.forwardList)
|
||||
}
|
||||
|
||||
func withUpdatedCanGoBack(_ canGoBack: Bool) -> BrowserContentState {
|
||||
return BrowserContentState(title: self.title, url: self.url, estimatedProgress: self.estimatedProgress, readingProgress: self.readingProgress, contentType: self.contentType, favicon: self.favicon, canGoBack: canGoBack, canGoForward: self.canGoForward, backList: self.backList, forwardList: self.forwardList)
|
||||
return BrowserContentState(title: self.title, url: self.url, estimatedProgress: self.estimatedProgress, readingProgress: self.readingProgress, contentType: self.contentType, favicon: self.favicon, isSecure: self.isSecure, canGoBack: canGoBack, canGoForward: self.canGoForward, backList: self.backList, forwardList: self.forwardList)
|
||||
}
|
||||
|
||||
func withUpdatedCanGoForward(_ canGoForward: Bool) -> BrowserContentState {
|
||||
return BrowserContentState(title: self.title, url: self.url, estimatedProgress: self.estimatedProgress, readingProgress: self.readingProgress, contentType: self.contentType, favicon: self.favicon, canGoBack: self.canGoBack, canGoForward: canGoForward, backList: self.backList, forwardList: self.forwardList)
|
||||
return BrowserContentState(title: self.title, url: self.url, estimatedProgress: self.estimatedProgress, readingProgress: self.readingProgress, contentType: self.contentType, favicon: self.favicon, isSecure: self.isSecure, canGoBack: self.canGoBack, canGoForward: canGoForward, backList: self.backList, forwardList: self.forwardList)
|
||||
}
|
||||
|
||||
func withUpdatedBackList(_ backList: [HistoryItem]) -> BrowserContentState {
|
||||
return BrowserContentState(title: self.title, url: self.url, estimatedProgress: self.estimatedProgress, readingProgress: self.readingProgress, contentType: self.contentType, favicon: self.favicon, canGoBack: self.canGoBack, canGoForward: self.canGoForward, backList: backList, forwardList: self.forwardList)
|
||||
return BrowserContentState(title: self.title, url: self.url, estimatedProgress: self.estimatedProgress, readingProgress: self.readingProgress, contentType: self.contentType, favicon: self.favicon, isSecure: self.isSecure, canGoBack: self.canGoBack, canGoForward: self.canGoForward, backList: backList, forwardList: self.forwardList)
|
||||
}
|
||||
|
||||
func withUpdatedForwardList(_ forwardList: [HistoryItem]) -> BrowserContentState {
|
||||
return BrowserContentState(title: self.title, url: self.url, estimatedProgress: self.estimatedProgress, readingProgress: self.readingProgress, contentType: self.contentType, favicon: self.favicon, canGoBack: self.canGoBack, canGoForward: self.canGoForward, backList: self.backList, forwardList: forwardList)
|
||||
return BrowserContentState(title: self.title, url: self.url, estimatedProgress: self.estimatedProgress, readingProgress: self.readingProgress, contentType: self.contentType, favicon: self.favicon, isSecure: self.isSecure, canGoBack: self.canGoBack, canGoForward: self.canGoForward, backList: self.backList, forwardList: forwardList)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -173,6 +184,8 @@ protocol BrowserContent: UIView {
|
|||
|
||||
func scrollToTop()
|
||||
|
||||
func addToRecentlyVisited()
|
||||
|
||||
func updateLayout(size: CGSize, insets: UIEdgeInsets, transition: ComponentTransition)
|
||||
}
|
||||
|
||||
|
|
|
|||
471
submodules/BrowserUI/Sources/BrowserDocumentContent.swift
Normal file
471
submodules/BrowserUI/Sources/BrowserDocumentContent.swift
Normal file
|
|
@ -0,0 +1,471 @@
|
|||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import ComponentFlow
|
||||
import TelegramCore
|
||||
import Postbox
|
||||
import SwiftSignalKit
|
||||
import TelegramPresentationData
|
||||
import TelegramUIPreferences
|
||||
import PresentationDataUtils
|
||||
import AccountContext
|
||||
import WebKit
|
||||
import AppBundle
|
||||
import PromptUI
|
||||
import SafariServices
|
||||
import ShareController
|
||||
import UndoUI
|
||||
import UrlEscaping
|
||||
|
||||
|
||||
final class BrowserDocumentContent: UIView, BrowserContent, WKNavigationDelegate, WKUIDelegate, UIScrollViewDelegate {
|
||||
private let context: AccountContext
|
||||
private var presentationData: PresentationData
|
||||
|
||||
private let webView: WKWebView
|
||||
|
||||
let uuid: UUID
|
||||
|
||||
private var _state: BrowserContentState
|
||||
private let statePromise: Promise<BrowserContentState>
|
||||
|
||||
var currentState: BrowserContentState {
|
||||
return self._state
|
||||
}
|
||||
var state: Signal<BrowserContentState, NoError> {
|
||||
return self.statePromise.get()
|
||||
}
|
||||
|
||||
var pushContent: (BrowserScreen.Subject) -> Void = { _ in }
|
||||
var onScrollingUpdate: (ContentScrollingUpdate) -> Void = { _ in }
|
||||
var minimize: () -> Void = { }
|
||||
var close: () -> Void = { }
|
||||
var present: (ViewController, Any?) -> Void = { _, _ in }
|
||||
var presentInGlobalOverlay: (ViewController) -> Void = { _ in }
|
||||
var getNavigationController: () -> NavigationController? = { return nil }
|
||||
|
||||
private var tempFile: TempBoxFile?
|
||||
|
||||
init(context: AccountContext, presentationData: PresentationData, file: TelegramMediaFile) {
|
||||
self.context = context
|
||||
self.uuid = UUID()
|
||||
self.presentationData = presentationData
|
||||
|
||||
let configuration = WKWebViewConfiguration()
|
||||
self.webView = WKWebView(frame: CGRect(), configuration: configuration)
|
||||
self.webView.allowsLinkPreview = true
|
||||
|
||||
if #available(iOS 11.0, *) {
|
||||
self.webView.scrollView.contentInsetAdjustmentBehavior = .never
|
||||
}
|
||||
|
||||
var title: String = "file"
|
||||
if let path = self.context.account.postbox.mediaBox.completedResourcePath(file.resource) {
|
||||
var updatedPath = path
|
||||
if let fileName = file.fileName {
|
||||
let tempFile = TempBox.shared.file(path: path, fileName: fileName)
|
||||
updatedPath = tempFile.path
|
||||
self.tempFile = tempFile
|
||||
title = fileName
|
||||
}
|
||||
|
||||
let request = URLRequest(url: URL(fileURLWithPath: updatedPath))
|
||||
self.webView.load(request)
|
||||
}
|
||||
|
||||
self._state = BrowserContentState(title: title, url: "", estimatedProgress: 0.0, readingProgress: 0.0, contentType: .document)
|
||||
self.statePromise = Promise<BrowserContentState>(self._state)
|
||||
|
||||
super.init(frame: .zero)
|
||||
|
||||
self.webView.allowsBackForwardNavigationGestures = true
|
||||
self.webView.scrollView.delegate = self
|
||||
self.webView.scrollView.clipsToBounds = false
|
||||
self.webView.navigationDelegate = self
|
||||
self.webView.uiDelegate = self
|
||||
if #available(iOS 15.0, *) {
|
||||
self.backgroundColor = presentationData.theme.list.plainBackgroundColor
|
||||
self.webView.underPageBackgroundColor = presentationData.theme.list.plainBackgroundColor
|
||||
}
|
||||
self.addSubview(self.webView)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func updatePresentationData(_ presentationData: PresentationData) {
|
||||
self.presentationData = presentationData
|
||||
if #available(iOS 15.0, *) {
|
||||
self.backgroundColor = presentationData.theme.list.plainBackgroundColor
|
||||
self.webView.underPageBackgroundColor = presentationData.theme.list.plainBackgroundColor
|
||||
}
|
||||
if let (size, insets) = self.validLayout {
|
||||
self.updateLayout(size: size, insets: insets, transition: .immediate)
|
||||
}
|
||||
}
|
||||
|
||||
var currentFontState = BrowserPresentationState.FontState(size: 100, isSerif: false)
|
||||
func updateFontState(_ state: BrowserPresentationState.FontState) {
|
||||
self.updateFontState(state, force: false)
|
||||
}
|
||||
func updateFontState(_ state: BrowserPresentationState.FontState, force: Bool) {
|
||||
self.currentFontState = state
|
||||
|
||||
let fontFamily = state.isSerif ? "'Georgia, serif'" : "null"
|
||||
let textSizeAdjust = state.size != 100 ? "'\(state.size)%'" : "null"
|
||||
let js = "\(setupFontFunctions) setTelegramFontOverrides(\(fontFamily), \(textSizeAdjust))";
|
||||
self.webView.evaluateJavaScript(js) { _, _ in }
|
||||
}
|
||||
|
||||
private var didSetupSearch = false
|
||||
private func setupSearch(completion: @escaping () -> Void) {
|
||||
guard !self.didSetupSearch else {
|
||||
completion()
|
||||
return
|
||||
}
|
||||
|
||||
let bundle = getAppBundle()
|
||||
guard let scriptPath = bundle.path(forResource: "UIWebViewSearch", ofType: "js") else {
|
||||
return
|
||||
}
|
||||
guard let scriptData = try? Data(contentsOf: URL(fileURLWithPath: scriptPath)) else {
|
||||
return
|
||||
}
|
||||
guard let script = String(data: scriptData, encoding: .utf8) else {
|
||||
return
|
||||
}
|
||||
self.didSetupSearch = true
|
||||
self.webView.evaluateJavaScript(script, completionHandler: { _, error in
|
||||
if error != nil {
|
||||
print()
|
||||
}
|
||||
completion()
|
||||
})
|
||||
}
|
||||
|
||||
private var previousQuery: String?
|
||||
func setSearch(_ query: String?, completion: ((Int) -> Void)?) {
|
||||
guard self.previousQuery != query else {
|
||||
return
|
||||
}
|
||||
self.previousQuery = query
|
||||
self.setupSearch { [weak self] in
|
||||
if let query = query {
|
||||
let js = "uiWebview_HighlightAllOccurencesOfString('\(query)')"
|
||||
self?.webView.evaluateJavaScript(js, completionHandler: { [weak self] _, _ in
|
||||
let js = "uiWebview_SearchResultCount"
|
||||
self?.webView.evaluateJavaScript(js, completionHandler: { [weak self] result, _ in
|
||||
if let result = result as? NSNumber {
|
||||
self?.searchResultsCount = result.intValue
|
||||
completion?(result.intValue)
|
||||
} else {
|
||||
completion?(0)
|
||||
}
|
||||
})
|
||||
})
|
||||
} else {
|
||||
let js = "uiWebview_RemoveAllHighlights()"
|
||||
self?.webView.evaluateJavaScript(js, completionHandler: nil)
|
||||
|
||||
self?.currentSearchResult = 0
|
||||
self?.searchResultsCount = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var currentSearchResult: Int = 0
|
||||
private var searchResultsCount: Int = 0
|
||||
|
||||
func scrollToPreviousSearchResult(completion: ((Int, Int) -> Void)?) {
|
||||
let searchResultsCount = self.searchResultsCount
|
||||
var index = self.currentSearchResult - 1
|
||||
if index < 0 {
|
||||
index = searchResultsCount - 1
|
||||
}
|
||||
self.currentSearchResult = index
|
||||
|
||||
let js = "uiWebview_ScrollTo('\(searchResultsCount - index - 1)')"
|
||||
self.webView.evaluateJavaScript(js, completionHandler: { _, _ in
|
||||
completion?(index, searchResultsCount)
|
||||
})
|
||||
}
|
||||
|
||||
func scrollToNextSearchResult(completion: ((Int, Int) -> Void)?) {
|
||||
let searchResultsCount = self.searchResultsCount
|
||||
var index = self.currentSearchResult + 1
|
||||
if index >= searchResultsCount {
|
||||
index = 0
|
||||
}
|
||||
self.currentSearchResult = index
|
||||
|
||||
let js = "uiWebview_ScrollTo('\(searchResultsCount - index - 1)')"
|
||||
self.webView.evaluateJavaScript(js, completionHandler: { _, _ in
|
||||
completion?(index, searchResultsCount)
|
||||
})
|
||||
}
|
||||
|
||||
func stop() {
|
||||
self.webView.stopLoading()
|
||||
}
|
||||
|
||||
func reload() {
|
||||
self.webView.reload()
|
||||
}
|
||||
|
||||
func navigateBack() {
|
||||
self.webView.goBack()
|
||||
}
|
||||
|
||||
func navigateForward() {
|
||||
self.webView.goForward()
|
||||
}
|
||||
|
||||
func navigateTo(historyItem: BrowserContentState.HistoryItem) {
|
||||
if let webItem = historyItem.webItem {
|
||||
self.webView.go(to: webItem)
|
||||
}
|
||||
}
|
||||
|
||||
func navigateTo(address: String) {
|
||||
let finalUrl = explicitUrl(address)
|
||||
guard let url = URL(string: finalUrl) else {
|
||||
return
|
||||
}
|
||||
self.webView.load(URLRequest(url: url))
|
||||
}
|
||||
|
||||
func scrollToTop() {
|
||||
self.webView.scrollView.setContentOffset(CGPoint(x: 0.0, y: -self.webView.scrollView.contentInset.top), animated: true)
|
||||
}
|
||||
|
||||
private var validLayout: (CGSize, UIEdgeInsets)?
|
||||
func updateLayout(size: CGSize, insets: UIEdgeInsets, transition: ComponentTransition) {
|
||||
self.validLayout = (size, insets)
|
||||
|
||||
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 - insets.bottom))
|
||||
var refresh = false
|
||||
if self.webView.frame.width > 0 && webViewFrame.width != self.webView.frame.width {
|
||||
refresh = true
|
||||
}
|
||||
transition.setFrame(view: self.webView, frame: webViewFrame)
|
||||
|
||||
if refresh {
|
||||
self.webView.reloadInputViews()
|
||||
}
|
||||
|
||||
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(
|
||||
// transition: .immediate,
|
||||
// component: AnyComponent(
|
||||
// ErrorComponent(
|
||||
// theme: self.presentationData.theme,
|
||||
// title: self.presentationData.strings.Browser_ErrorTitle,
|
||||
// text: error.localizedDescription
|
||||
// )
|
||||
// ),
|
||||
// environment: {},
|
||||
// containerSize: CGSize(width: size.width - insets.left - insets.right - 72.0, height: size.height)
|
||||
// )
|
||||
// if self.errorView.superview == nil {
|
||||
// self.addSubview(self.errorView)
|
||||
// self.errorView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
|
||||
// }
|
||||
// self.errorView.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - errorSize.width) / 2.0), y: insets.top + floorToScreenPixels((size.height - insets.top - insets.bottom - errorSize.height) / 2.0)), size: errorSize)
|
||||
// } else if self.errorView.superview != nil {
|
||||
// self.errorView.removeFromSuperview()
|
||||
// }
|
||||
}
|
||||
|
||||
private func updateState(_ f: (BrowserContentState) -> BrowserContentState) {
|
||||
let updated = f(self._state)
|
||||
self._state = updated
|
||||
self.statePromise.set(.single(self._state))
|
||||
}
|
||||
|
||||
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
|
||||
if keyPath == "title" {
|
||||
self.updateState { $0.withUpdatedTitle(self.webView.title ?? "") }
|
||||
} else if keyPath == "URL" {
|
||||
self.updateState { $0.withUpdatedUrl(self.webView.url?.absoluteString ?? "") }
|
||||
self.didSetupSearch = false
|
||||
} else if keyPath == "estimatedProgress" {
|
||||
self.updateState { $0.withUpdatedEstimatedProgress(self.webView.estimatedProgress) }
|
||||
} else if keyPath == "canGoBack" {
|
||||
self.updateState { $0.withUpdatedCanGoBack(self.webView.canGoBack) }
|
||||
self.webView.disablesInteractiveTransitionGestureRecognizer = self.webView.canGoBack
|
||||
} else if keyPath == "canGoForward" {
|
||||
self.updateState { $0.withUpdatedCanGoForward(self.webView.canGoForward) }
|
||||
}
|
||||
}
|
||||
|
||||
private struct ScrollingOffsetState: Equatable {
|
||||
var value: CGFloat
|
||||
var isDraggingOrDecelerating: Bool
|
||||
}
|
||||
|
||||
private var previousScrollingOffset: ScrollingOffsetState?
|
||||
|
||||
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||
self.updateScrollingOffset(isReset: false, transition: .immediate)
|
||||
}
|
||||
|
||||
private func snapScrollingOffsetToInsets() {
|
||||
let transition = ComponentTransition(animation: .curve(duration: 0.4, curve: .spring))
|
||||
self.updateScrollingOffset(isReset: false, transition: transition)
|
||||
}
|
||||
|
||||
public func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
|
||||
if !decelerate {
|
||||
self.snapScrollingOffsetToInsets()
|
||||
}
|
||||
}
|
||||
|
||||
public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
|
||||
self.snapScrollingOffsetToInsets()
|
||||
}
|
||||
|
||||
private func updateScrollingOffset(isReset: Bool, transition: ComponentTransition) {
|
||||
let scrollView = self.webView.scrollView
|
||||
let isInteracting = scrollView.isDragging || scrollView.isDecelerating
|
||||
if let previousScrollingOffsetValue = self.previousScrollingOffset {
|
||||
let currentBounds = scrollView.bounds
|
||||
let offsetToTopEdge = max(0.0, currentBounds.minY - 0.0)
|
||||
let offsetToBottomEdge = max(0.0, scrollView.contentSize.height - currentBounds.maxY)
|
||||
|
||||
let relativeOffset = scrollView.contentOffset.y - previousScrollingOffsetValue.value
|
||||
self.onScrollingUpdate(ContentScrollingUpdate(
|
||||
relativeOffset: relativeOffset,
|
||||
absoluteOffsetToTopEdge: offsetToTopEdge,
|
||||
absoluteOffsetToBottomEdge: offsetToBottomEdge,
|
||||
isReset: isReset,
|
||||
isInteracting: isInteracting,
|
||||
transition: transition
|
||||
))
|
||||
}
|
||||
self.previousScrollingOffset = ScrollingOffsetState(value: scrollView.contentOffset.y, isDraggingOrDecelerating: isInteracting)
|
||||
|
||||
var readingProgress: CGFloat = 0.0
|
||||
if !scrollView.contentSize.height.isZero {
|
||||
let value = (scrollView.contentOffset.y + scrollView.contentInset.top) / (scrollView.contentSize.height - scrollView.bounds.size.height + scrollView.contentInset.top)
|
||||
readingProgress = max(0.0, min(1.0, value))
|
||||
}
|
||||
self.updateState {
|
||||
$0.withUpdatedReadingProgress(readingProgress)
|
||||
}
|
||||
}
|
||||
|
||||
func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) {
|
||||
// self.currentError = nil
|
||||
self.updateFontState(self.currentFontState, force: true)
|
||||
}
|
||||
|
||||
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
|
||||
self.updateState {
|
||||
$0
|
||||
.withUpdatedBackList(webView.backForwardList.backList.map { BrowserContentState.HistoryItem(webItem: $0) })
|
||||
.withUpdatedForwardList(webView.backForwardList.forwardList.map { BrowserContentState.HistoryItem(webItem: $0) })
|
||||
}
|
||||
}
|
||||
|
||||
// func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) {
|
||||
// if (error as NSError).code != -999 {
|
||||
// self.currentError = error
|
||||
// } else {
|
||||
// self.currentError = nil
|
||||
// }
|
||||
// if let (size, insets) = self.validLayout {
|
||||
// self.updateLayout(size: size, insets: insets, transition: .immediate)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
|
||||
// if (error as NSError).code != -999 {
|
||||
// self.currentError = error
|
||||
// } else {
|
||||
// self.currentError = nil
|
||||
// }
|
||||
// if let (size, insets) = self.validLayout {
|
||||
// self.updateLayout(size: size, insets: insets, transition: .immediate)
|
||||
// }
|
||||
// }
|
||||
|
||||
func webView(_ webView: WKWebView, createWebViewWith configuration: WKWebViewConfiguration, for navigationAction: WKNavigationAction, windowFeatures: WKWindowFeatures) -> WKWebView? {
|
||||
if navigationAction.targetFrame == nil {
|
||||
if let url = navigationAction.request.url?.absoluteString {
|
||||
self.open(url: url, new: true)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func webViewDidClose(_ webView: WKWebView) {
|
||||
self.close()
|
||||
}
|
||||
|
||||
@available(iOSApplicationExtension 15.0, iOS 15.0, *)
|
||||
func webView(_ webView: WKWebView, requestMediaCapturePermissionFor origin: WKSecurityOrigin, initiatedByFrame frame: WKFrameInfo, type: WKMediaCaptureType, decisionHandler: @escaping (WKPermissionDecision) -> Void) {
|
||||
decisionHandler(.prompt)
|
||||
}
|
||||
|
||||
|
||||
// @available(iOS 13.0, *)
|
||||
// func webView(_ webView: WKWebView, contextMenuConfigurationForElement elementInfo: WKContextMenuElementInfo, completionHandler: @escaping (UIContextMenuConfiguration?) -> Void) {
|
||||
// guard let url = elementInfo.linkURL else {
|
||||
// completionHandler(nil)
|
||||
// return
|
||||
// }
|
||||
// let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
|
||||
// let configuration = UIContextMenuConfiguration(identifier: nil, previewProvider: nil) { [weak self] _ in
|
||||
// return UIMenu(title: "", children: [
|
||||
// UIAction(title: presentationData.strings.Browser_ContextMenu_Open, image: generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Browser"), color: presentationData.theme.contextMenu.primaryColor), handler: { [weak self] _ in
|
||||
// self?.open(url: url.absoluteString, new: false)
|
||||
// }),
|
||||
// UIAction(title: presentationData.strings.Browser_ContextMenu_OpenInNewTab, image: generateTintedImage(image: UIImage(bundleImageName: "Instant View/NewTab"), color: presentationData.theme.contextMenu.primaryColor), handler: { [weak self] _ in
|
||||
// self?.open(url: url.absoluteString, new: true)
|
||||
// }),
|
||||
// UIAction(title: presentationData.strings.Browser_ContextMenu_AddToReadingList, image: generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/ReadingList"), color: presentationData.theme.contextMenu.primaryColor), handler: { _ in
|
||||
// let _ = try? SSReadingList.default()?.addItem(with: url, title: nil, previewText: nil)
|
||||
// }),
|
||||
// UIAction(title: presentationData.strings.Browser_ContextMenu_CopyLink, image: generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Copy"), color: presentationData.theme.contextMenu.primaryColor), handler: { [weak self] _ in
|
||||
// UIPasteboard.general.string = url.absoluteString
|
||||
// self?.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil)
|
||||
// }),
|
||||
// UIAction(title: presentationData.strings.Browser_ContextMenu_Share, image: generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Forward"), color: presentationData.theme.contextMenu.primaryColor), handler: { [weak self] _ in
|
||||
// self?.share(url: url.absoluteString)
|
||||
// })
|
||||
// ])
|
||||
// }
|
||||
// completionHandler(configuration)
|
||||
// }
|
||||
|
||||
private func open(url: String, new: Bool) {
|
||||
let subject: BrowserScreen.Subject = .webPage(url: url)
|
||||
if new, let navigationController = self.getNavigationController() {
|
||||
navigationController._keepModalDismissProgress = true
|
||||
self.minimize()
|
||||
let controller = BrowserScreen(context: self.context, subject: subject)
|
||||
navigationController._keepModalDismissProgress = true
|
||||
navigationController.pushViewController(controller)
|
||||
} else {
|
||||
self.pushContent(subject)
|
||||
}
|
||||
}
|
||||
|
||||
private func share(url: String) {
|
||||
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
|
||||
let shareController = ShareController(context: self.context, subject: .url(url))
|
||||
shareController.actionCompleted = { [weak self] in
|
||||
self?.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil)
|
||||
}
|
||||
self.present(shareController, nil)
|
||||
}
|
||||
|
||||
func addToRecentlyVisited() {
|
||||
}
|
||||
}
|
||||
|
|
@ -1378,4 +1378,10 @@ final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDeleg
|
|||
}
|
||||
self.updateVisibleItems(visibleBounds: self.scrollNode.view.bounds, animated: animated)
|
||||
}
|
||||
|
||||
func addToRecentlyVisited() {
|
||||
if let webPage = self.webPage {
|
||||
let _ = addRecentlyVisitedLink(engine: self.context.engine, webPage: webPage).startStandalone()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,21 @@ import ComponentFlow
|
|||
import BlurredBackgroundComponent
|
||||
import ContextUI
|
||||
|
||||
final class BrowserNavigationBarEnvironment: Equatable {
|
||||
public let fraction: CGFloat
|
||||
|
||||
public init(fraction: CGFloat) {
|
||||
self.fraction = fraction
|
||||
}
|
||||
|
||||
public static func ==(lhs: BrowserNavigationBarEnvironment, rhs: BrowserNavigationBarEnvironment) -> Bool {
|
||||
if lhs.fraction != rhs.fraction {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
final class BrowserNavigationBarComponent: CombinedComponent {
|
||||
let backgroundColor: UIColor
|
||||
let separatorColor: UIColor
|
||||
|
|
@ -16,7 +31,7 @@ final class BrowserNavigationBarComponent: CombinedComponent {
|
|||
let sideInset: CGFloat
|
||||
let leftItems: [AnyComponentWithIdentity<Empty>]
|
||||
let rightItems: [AnyComponentWithIdentity<Empty>]
|
||||
let centerItem: AnyComponentWithIdentity<Empty>?
|
||||
let centerItem: AnyComponentWithIdentity<BrowserNavigationBarEnvironment>?
|
||||
let readingProgress: CGFloat
|
||||
let loadingProgress: Double?
|
||||
let collapseFraction: CGFloat
|
||||
|
|
@ -32,7 +47,7 @@ final class BrowserNavigationBarComponent: CombinedComponent {
|
|||
sideInset: CGFloat,
|
||||
leftItems: [AnyComponentWithIdentity<Empty>],
|
||||
rightItems: [AnyComponentWithIdentity<Empty>],
|
||||
centerItem: AnyComponentWithIdentity<Empty>?,
|
||||
centerItem: AnyComponentWithIdentity<BrowserNavigationBarEnvironment>?,
|
||||
readingProgress: CGFloat,
|
||||
loadingProgress: Double?,
|
||||
collapseFraction: CGFloat
|
||||
|
|
@ -106,7 +121,7 @@ final class BrowserNavigationBarComponent: CombinedComponent {
|
|||
let loadingProgress = Child(LoadingProgressComponent.self)
|
||||
let leftItems = ChildMap(environment: Empty.self, keyedBy: AnyHashable.self)
|
||||
let rightItems = ChildMap(environment: Empty.self, keyedBy: AnyHashable.self)
|
||||
let centerItems = ChildMap(environment: Empty.self, keyedBy: AnyHashable.self)
|
||||
let centerItems = ChildMap(environment: BrowserNavigationBarEnvironment.self, keyedBy: AnyHashable.self)
|
||||
|
||||
return { context in
|
||||
var availableWidth = context.availableSize.width
|
||||
|
|
@ -169,16 +184,20 @@ final class BrowserNavigationBarComponent: CombinedComponent {
|
|||
}
|
||||
|
||||
if !leftItemList.isEmpty || !rightItemList.isEmpty {
|
||||
availableWidth -= 32.0
|
||||
availableWidth -= 14.0
|
||||
}
|
||||
|
||||
context.add(background
|
||||
.position(CGPoint(x: size.width / 2.0, y: size.height / 2.0))
|
||||
)
|
||||
|
||||
var readingProgressAlpha = context.component.collapseFraction
|
||||
if leftItemList.isEmpty && rightItemList.isEmpty {
|
||||
readingProgressAlpha = 0.0
|
||||
}
|
||||
context.add(readingProgress
|
||||
.position(CGPoint(x: readingProgress.size.width / 2.0, y: size.height / 2.0))
|
||||
.opacity(context.component.centerItem?.id == AnyHashable("search") ? 0.0 : 1.0)
|
||||
.opacity(readingProgressAlpha)
|
||||
)
|
||||
|
||||
context.add(separator
|
||||
|
|
@ -199,7 +218,7 @@ final class BrowserNavigationBarComponent: CombinedComponent {
|
|||
.appear(.default(scale: true, alpha: true))
|
||||
.disappear(.default(scale: true, alpha: true))
|
||||
)
|
||||
leftItemX -= item.size.width + 8.0
|
||||
leftItemX += item.size.width + 8.0
|
||||
centerLeftInset += item.size.width + 8.0
|
||||
}
|
||||
|
||||
|
|
@ -220,20 +239,27 @@ final class BrowserNavigationBarComponent: CombinedComponent {
|
|||
let maxCenterInset = max(centerLeftInset, centerRightInset)
|
||||
|
||||
if !leftItemList.isEmpty || !rightItemList.isEmpty {
|
||||
availableWidth -= 28.0
|
||||
availableWidth -= 20.0
|
||||
}
|
||||
|
||||
let environment = BrowserNavigationBarEnvironment(fraction: context.component.collapseFraction)
|
||||
|
||||
let centerItem = context.component.centerItem.flatMap { item in
|
||||
centerItems[item.id].update(
|
||||
component: item.component,
|
||||
environment: { environment },
|
||||
availableSize: CGSize(width: availableWidth, height: expandedHeight),
|
||||
transition: context.transition
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
var centerX = maxCenterInset + (context.availableSize.width - maxCenterInset * 2.0) / 2.0
|
||||
if "".isEmpty {
|
||||
centerX = centerLeftInset + (context.availableSize.width - centerLeftInset - centerRightInset) / 2.0
|
||||
}
|
||||
if let centerItem = centerItem {
|
||||
context.add(centerItem
|
||||
.position(CGPoint(x: maxCenterInset + (context.availableSize.width - maxCenterInset * 2.0) / 2.0, y: context.component.topInset + contentHeight / 2.0))
|
||||
.position(CGPoint(x: centerX, y: context.component.topInset + contentHeight / 2.0))
|
||||
.scale(1.0 - 0.35 * context.component.collapseFraction)
|
||||
.appear(.default(scale: false, alpha: true))
|
||||
.disappear(.default(scale: false, alpha: true))
|
||||
|
|
|
|||
463
submodules/BrowserUI/Sources/BrowserPdfContent.swift
Normal file
463
submodules/BrowserUI/Sources/BrowserPdfContent.swift
Normal file
|
|
@ -0,0 +1,463 @@
|
|||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import ComponentFlow
|
||||
import TelegramCore
|
||||
import Postbox
|
||||
import SwiftSignalKit
|
||||
import TelegramPresentationData
|
||||
import TelegramUIPreferences
|
||||
import PresentationDataUtils
|
||||
import AccountContext
|
||||
import WebKit
|
||||
import AppBundle
|
||||
import PromptUI
|
||||
import SafariServices
|
||||
import ShareController
|
||||
import UndoUI
|
||||
import UrlEscaping
|
||||
import PDFKit
|
||||
|
||||
final class BrowserPdfContent: UIView, BrowserContent, WKNavigationDelegate, WKUIDelegate, UIScrollViewDelegate {
|
||||
private let context: AccountContext
|
||||
private var presentationData: PresentationData
|
||||
|
||||
private let webView: PDFView
|
||||
private let scrollView: UIScrollView!
|
||||
|
||||
let uuid: UUID
|
||||
|
||||
private var _state: BrowserContentState
|
||||
private let statePromise: Promise<BrowserContentState>
|
||||
|
||||
var currentState: BrowserContentState {
|
||||
return self._state
|
||||
}
|
||||
var state: Signal<BrowserContentState, NoError> {
|
||||
return self.statePromise.get()
|
||||
}
|
||||
|
||||
var pushContent: (BrowserScreen.Subject) -> Void = { _ in }
|
||||
var onScrollingUpdate: (ContentScrollingUpdate) -> Void = { _ in }
|
||||
var minimize: () -> Void = { }
|
||||
var close: () -> Void = { }
|
||||
var present: (ViewController, Any?) -> Void = { _, _ in }
|
||||
var presentInGlobalOverlay: (ViewController) -> Void = { _ in }
|
||||
var getNavigationController: () -> NavigationController? = { return nil }
|
||||
|
||||
private var tempFile: TempBoxFile?
|
||||
|
||||
init(context: AccountContext, presentationData: PresentationData, file: TelegramMediaFile) {
|
||||
self.context = context
|
||||
self.uuid = UUID()
|
||||
self.presentationData = presentationData
|
||||
|
||||
self.webView = PDFView()
|
||||
self.webView.maxScaleFactor = 4.0;
|
||||
self.webView.minScaleFactor = self.webView.scaleFactorForSizeToFit
|
||||
self.webView.autoScales = true
|
||||
|
||||
var scrollView: UIScrollView?
|
||||
for view in self.webView.subviews {
|
||||
if let view = view as? UIScrollView {
|
||||
scrollView = view
|
||||
} else {
|
||||
for subview in view.subviews {
|
||||
if let subview = subview as? UIScrollView {
|
||||
scrollView = subview
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
self.scrollView = scrollView
|
||||
|
||||
var title: String = "file"
|
||||
if let path = self.context.account.postbox.mediaBox.completedResourcePath(file.resource), let data = try? Data(contentsOf: URL(fileURLWithPath: path), options: .mappedIfSafe) {
|
||||
// var updatedPath = path
|
||||
// if let fileName = file.fileName {
|
||||
// let tempFile = TempBox.shared.file(path: path, fileName: fileName)
|
||||
// updatedPath = tempFile.path
|
||||
// self.tempFile = tempFile
|
||||
// title = fileName
|
||||
// }
|
||||
|
||||
self.webView.document = PDFDocument(data: data)
|
||||
title = file.fileName ?? "file"
|
||||
}
|
||||
|
||||
self._state = BrowserContentState(title: title, url: "", estimatedProgress: 0.0, readingProgress: 0.0, contentType: .document)
|
||||
self.statePromise = Promise<BrowserContentState>(self._state)
|
||||
|
||||
super.init(frame: .zero)
|
||||
|
||||
if #available(iOS 15.0, *) {
|
||||
self.backgroundColor = presentationData.theme.list.plainBackgroundColor
|
||||
}
|
||||
self.addSubview(self.webView)
|
||||
|
||||
Queue.mainQueue().after(1.0) {
|
||||
scrollView?.delegate = self
|
||||
}
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func updatePresentationData(_ presentationData: PresentationData) {
|
||||
self.presentationData = presentationData
|
||||
if #available(iOS 15.0, *) {
|
||||
self.backgroundColor = presentationData.theme.list.plainBackgroundColor
|
||||
}
|
||||
if let (size, insets) = self.validLayout {
|
||||
self.updateLayout(size: size, insets: insets, transition: .immediate)
|
||||
}
|
||||
}
|
||||
|
||||
var currentFontState = BrowserPresentationState.FontState(size: 100, isSerif: false)
|
||||
func updateFontState(_ state: BrowserPresentationState.FontState) {
|
||||
self.updateFontState(state, force: false)
|
||||
}
|
||||
func updateFontState(_ state: BrowserPresentationState.FontState, force: Bool) {
|
||||
self.currentFontState = state
|
||||
|
||||
// let fontFamily = state.isSerif ? "'Georgia, serif'" : "null"
|
||||
// let textSizeAdjust = state.size != 100 ? "'\(state.size)%'" : "null"
|
||||
// let js = "\(setupFontFunctions) setTelegramFontOverrides(\(fontFamily), \(textSizeAdjust))";
|
||||
// self.webView.evaluateJavaScript(js) { _, _ in }
|
||||
}
|
||||
|
||||
private var didSetupSearch = false
|
||||
private func setupSearch(completion: @escaping () -> Void) {
|
||||
// guard !self.didSetupSearch else {
|
||||
// completion()
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// let bundle = getAppBundle()
|
||||
// guard let scriptPath = bundle.path(forResource: "UIWebViewSearch", ofType: "js") else {
|
||||
// return
|
||||
// }
|
||||
// guard let scriptData = try? Data(contentsOf: URL(fileURLWithPath: scriptPath)) else {
|
||||
// return
|
||||
// }
|
||||
// guard let script = String(data: scriptData, encoding: .utf8) else {
|
||||
// return
|
||||
// }
|
||||
// self.didSetupSearch = true
|
||||
// self.webView.evaluateJavaScript(script, completionHandler: { _, error in
|
||||
// if error != nil {
|
||||
// print()
|
||||
// }
|
||||
// completion()
|
||||
// })
|
||||
}
|
||||
|
||||
private var previousQuery: String?
|
||||
func setSearch(_ query: String?, completion: ((Int) -> Void)?) {
|
||||
// guard self.previousQuery != query else {
|
||||
// return
|
||||
// }
|
||||
// self.previousQuery = query
|
||||
// self.setupSearch { [weak self] in
|
||||
// if let query = query {
|
||||
// let js = "uiWebview_HighlightAllOccurencesOfString('\(query)')"
|
||||
// self?.webView.evaluateJavaScript(js, completionHandler: { [weak self] _, _ in
|
||||
// let js = "uiWebview_SearchResultCount"
|
||||
// self?.webView.evaluateJavaScript(js, completionHandler: { [weak self] result, _ in
|
||||
// if let result = result as? NSNumber {
|
||||
// self?.searchResultsCount = result.intValue
|
||||
// completion?(result.intValue)
|
||||
// } else {
|
||||
// completion?(0)
|
||||
// }
|
||||
// })
|
||||
// })
|
||||
// } else {
|
||||
// let js = "uiWebview_RemoveAllHighlights()"
|
||||
// self?.webView.evaluateJavaScript(js, completionHandler: nil)
|
||||
//
|
||||
// self?.currentSearchResult = 0
|
||||
// self?.searchResultsCount = 0
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
private var currentSearchResult: Int = 0
|
||||
private var searchResultsCount: Int = 0
|
||||
|
||||
func scrollToPreviousSearchResult(completion: ((Int, Int) -> Void)?) {
|
||||
// let searchResultsCount = self.searchResultsCount
|
||||
// var index = self.currentSearchResult - 1
|
||||
// if index < 0 {
|
||||
// index = searchResultsCount - 1
|
||||
// }
|
||||
// self.currentSearchResult = index
|
||||
//
|
||||
// let js = "uiWebview_ScrollTo('\(searchResultsCount - index - 1)')"
|
||||
// self.webView.evaluateJavaScript(js, completionHandler: { _, _ in
|
||||
// completion?(index, searchResultsCount)
|
||||
// })
|
||||
}
|
||||
|
||||
func scrollToNextSearchResult(completion: ((Int, Int) -> Void)?) {
|
||||
// let searchResultsCount = self.searchResultsCount
|
||||
// var index = self.currentSearchResult + 1
|
||||
// if index >= searchResultsCount {
|
||||
// index = 0
|
||||
// }
|
||||
// self.currentSearchResult = index
|
||||
//
|
||||
// let js = "uiWebview_ScrollTo('\(searchResultsCount - index - 1)')"
|
||||
// self.webView.evaluateJavaScript(js, completionHandler: { _, _ in
|
||||
// completion?(index, searchResultsCount)
|
||||
// })
|
||||
}
|
||||
|
||||
func stop() {
|
||||
// self.webView.stopLoading()
|
||||
}
|
||||
|
||||
func reload() {
|
||||
// self.webView.reload()
|
||||
}
|
||||
|
||||
func navigateBack() {
|
||||
// self.webView.goBack()
|
||||
}
|
||||
|
||||
func navigateForward() {
|
||||
// self.webView.goForward()
|
||||
}
|
||||
|
||||
func navigateTo(historyItem: BrowserContentState.HistoryItem) {
|
||||
// if let webItem = historyItem.webItem {
|
||||
// self.webView.go(to: webItem)
|
||||
// }
|
||||
}
|
||||
|
||||
func navigateTo(address: String) {
|
||||
// let finalUrl = explicitUrl(address)
|
||||
// guard let url = URL(string: finalUrl) else {
|
||||
// return
|
||||
// }
|
||||
// self.webView.load(URLRequest(url: url))
|
||||
}
|
||||
|
||||
func scrollToTop() {
|
||||
self.scrollView.setContentOffset(CGPoint(x: 0.0, y: -self.scrollView.contentInset.top), animated: true)
|
||||
}
|
||||
|
||||
private var validLayout: (CGSize, UIEdgeInsets)?
|
||||
func updateLayout(size: CGSize, insets: UIEdgeInsets, transition: ComponentTransition) {
|
||||
self.validLayout = (size, insets)
|
||||
|
||||
self.previousScrollingOffset = ScrollingOffsetState(value: self.scrollView.contentOffset.y, isDraggingOrDecelerating: self.scrollView.isDragging || self.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 - insets.bottom))
|
||||
var refresh = false
|
||||
if self.webView.frame.width > 0 && webViewFrame.width != self.webView.frame.width {
|
||||
refresh = true
|
||||
}
|
||||
transition.setFrame(view: self.webView, frame: webViewFrame)
|
||||
|
||||
if refresh {
|
||||
self.webView.reloadInputViews()
|
||||
}
|
||||
|
||||
// if let error = self.currentError {
|
||||
// let errorSize = self.errorView.update(
|
||||
// transition: .immediate,
|
||||
// component: AnyComponent(
|
||||
// ErrorComponent(
|
||||
// theme: self.presentationData.theme,
|
||||
// title: self.presentationData.strings.Browser_ErrorTitle,
|
||||
// text: error.localizedDescription
|
||||
// )
|
||||
// ),
|
||||
// environment: {},
|
||||
// containerSize: CGSize(width: size.width - insets.left - insets.right - 72.0, height: size.height)
|
||||
// )
|
||||
// if self.errorView.superview == nil {
|
||||
// self.addSubview(self.errorView)
|
||||
// self.errorView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
|
||||
// }
|
||||
// self.errorView.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - errorSize.width) / 2.0), y: insets.top + floorToScreenPixels((size.height - insets.top - insets.bottom - errorSize.height) / 2.0)), size: errorSize)
|
||||
// } else if self.errorView.superview != nil {
|
||||
// self.errorView.removeFromSuperview()
|
||||
// }
|
||||
}
|
||||
|
||||
private func updateState(_ f: (BrowserContentState) -> BrowserContentState) {
|
||||
let updated = f(self._state)
|
||||
self._state = updated
|
||||
self.statePromise.set(.single(self._state))
|
||||
}
|
||||
|
||||
private struct ScrollingOffsetState: Equatable {
|
||||
var value: CGFloat
|
||||
var isDraggingOrDecelerating: Bool
|
||||
}
|
||||
|
||||
private var previousScrollingOffset: ScrollingOffsetState?
|
||||
|
||||
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||
self.updateScrollingOffset(isReset: false, transition: .immediate)
|
||||
}
|
||||
|
||||
private func snapScrollingOffsetToInsets() {
|
||||
let transition = ComponentTransition(animation: .curve(duration: 0.4, curve: .spring))
|
||||
self.updateScrollingOffset(isReset: false, transition: transition)
|
||||
}
|
||||
|
||||
public func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
|
||||
if !decelerate {
|
||||
self.snapScrollingOffsetToInsets()
|
||||
}
|
||||
}
|
||||
|
||||
public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
|
||||
self.snapScrollingOffsetToInsets()
|
||||
}
|
||||
|
||||
private func updateScrollingOffset(isReset: Bool, transition: ComponentTransition) {
|
||||
guard let scrollView = self.scrollView else {
|
||||
return
|
||||
}
|
||||
let isInteracting = scrollView.isDragging || scrollView.isDecelerating
|
||||
if let previousScrollingOffsetValue = self.previousScrollingOffset {
|
||||
let currentBounds = scrollView.bounds
|
||||
let offsetToTopEdge = max(0.0, currentBounds.minY - 0.0)
|
||||
let offsetToBottomEdge = max(0.0, scrollView.contentSize.height - currentBounds.maxY)
|
||||
|
||||
let relativeOffset = scrollView.contentOffset.y - previousScrollingOffsetValue.value
|
||||
self.onScrollingUpdate(ContentScrollingUpdate(
|
||||
relativeOffset: relativeOffset,
|
||||
absoluteOffsetToTopEdge: offsetToTopEdge,
|
||||
absoluteOffsetToBottomEdge: offsetToBottomEdge,
|
||||
isReset: isReset,
|
||||
isInteracting: isInteracting,
|
||||
transition: transition
|
||||
))
|
||||
}
|
||||
self.previousScrollingOffset = ScrollingOffsetState(value: scrollView.contentOffset.y, isDraggingOrDecelerating: isInteracting)
|
||||
|
||||
var readingProgress: CGFloat = 0.0
|
||||
if !scrollView.contentSize.height.isZero {
|
||||
let value = (scrollView.contentOffset.y + scrollView.contentInset.top) / (scrollView.contentSize.height - scrollView.bounds.size.height + scrollView.contentInset.top)
|
||||
readingProgress = max(0.0, min(1.0, value))
|
||||
}
|
||||
self.updateState {
|
||||
$0.withUpdatedReadingProgress(readingProgress)
|
||||
}
|
||||
}
|
||||
|
||||
func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) {
|
||||
// self.currentError = nil
|
||||
self.updateFontState(self.currentFontState, force: true)
|
||||
}
|
||||
|
||||
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
|
||||
self.updateState {
|
||||
$0
|
||||
.withUpdatedBackList(webView.backForwardList.backList.map { BrowserContentState.HistoryItem(webItem: $0) })
|
||||
.withUpdatedForwardList(webView.backForwardList.forwardList.map { BrowserContentState.HistoryItem(webItem: $0) })
|
||||
}
|
||||
}
|
||||
|
||||
// func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) {
|
||||
// if (error as NSError).code != -999 {
|
||||
// self.currentError = error
|
||||
// } else {
|
||||
// self.currentError = nil
|
||||
// }
|
||||
// if let (size, insets) = self.validLayout {
|
||||
// self.updateLayout(size: size, insets: insets, transition: .immediate)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
|
||||
// if (error as NSError).code != -999 {
|
||||
// self.currentError = error
|
||||
// } else {
|
||||
// self.currentError = nil
|
||||
// }
|
||||
// if let (size, insets) = self.validLayout {
|
||||
// self.updateLayout(size: size, insets: insets, transition: .immediate)
|
||||
// }
|
||||
// }
|
||||
|
||||
func webView(_ webView: WKWebView, createWebViewWith configuration: WKWebViewConfiguration, for navigationAction: WKNavigationAction, windowFeatures: WKWindowFeatures) -> WKWebView? {
|
||||
if navigationAction.targetFrame == nil {
|
||||
if let url = navigationAction.request.url?.absoluteString {
|
||||
self.open(url: url, new: true)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func webViewDidClose(_ webView: WKWebView) {
|
||||
self.close()
|
||||
}
|
||||
|
||||
@available(iOSApplicationExtension 15.0, iOS 15.0, *)
|
||||
func webView(_ webView: WKWebView, requestMediaCapturePermissionFor origin: WKSecurityOrigin, initiatedByFrame frame: WKFrameInfo, type: WKMediaCaptureType, decisionHandler: @escaping (WKPermissionDecision) -> Void) {
|
||||
decisionHandler(.prompt)
|
||||
}
|
||||
|
||||
|
||||
// @available(iOS 13.0, *)
|
||||
// func webView(_ webView: WKWebView, contextMenuConfigurationForElement elementInfo: WKContextMenuElementInfo, completionHandler: @escaping (UIContextMenuConfiguration?) -> Void) {
|
||||
// guard let url = elementInfo.linkURL else {
|
||||
// completionHandler(nil)
|
||||
// return
|
||||
// }
|
||||
// let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
|
||||
// let configuration = UIContextMenuConfiguration(identifier: nil, previewProvider: nil) { [weak self] _ in
|
||||
// return UIMenu(title: "", children: [
|
||||
// UIAction(title: presentationData.strings.Browser_ContextMenu_Open, image: generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Browser"), color: presentationData.theme.contextMenu.primaryColor), handler: { [weak self] _ in
|
||||
// self?.open(url: url.absoluteString, new: false)
|
||||
// }),
|
||||
// UIAction(title: presentationData.strings.Browser_ContextMenu_OpenInNewTab, image: generateTintedImage(image: UIImage(bundleImageName: "Instant View/NewTab"), color: presentationData.theme.contextMenu.primaryColor), handler: { [weak self] _ in
|
||||
// self?.open(url: url.absoluteString, new: true)
|
||||
// }),
|
||||
// UIAction(title: presentationData.strings.Browser_ContextMenu_AddToReadingList, image: generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/ReadingList"), color: presentationData.theme.contextMenu.primaryColor), handler: { _ in
|
||||
// let _ = try? SSReadingList.default()?.addItem(with: url, title: nil, previewText: nil)
|
||||
// }),
|
||||
// UIAction(title: presentationData.strings.Browser_ContextMenu_CopyLink, image: generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Copy"), color: presentationData.theme.contextMenu.primaryColor), handler: { [weak self] _ in
|
||||
// UIPasteboard.general.string = url.absoluteString
|
||||
// self?.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil)
|
||||
// }),
|
||||
// UIAction(title: presentationData.strings.Browser_ContextMenu_Share, image: generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Forward"), color: presentationData.theme.contextMenu.primaryColor), handler: { [weak self] _ in
|
||||
// self?.share(url: url.absoluteString)
|
||||
// })
|
||||
// ])
|
||||
// }
|
||||
// completionHandler(configuration)
|
||||
// }
|
||||
|
||||
private func open(url: String, new: Bool) {
|
||||
let subject: BrowserScreen.Subject = .webPage(url: url)
|
||||
if new, let navigationController = self.getNavigationController() {
|
||||
navigationController._keepModalDismissProgress = true
|
||||
self.minimize()
|
||||
let controller = BrowserScreen(context: self.context, subject: subject)
|
||||
navigationController._keepModalDismissProgress = true
|
||||
navigationController.pushViewController(controller)
|
||||
} else {
|
||||
self.pushContent(subject)
|
||||
}
|
||||
}
|
||||
|
||||
private func share(url: String) {
|
||||
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
|
||||
let shareController = ShareController(context: self.context, subject: .url(url))
|
||||
shareController.actionCompleted = { [weak self] in
|
||||
self?.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil)
|
||||
}
|
||||
self.present(shareController, nil)
|
||||
}
|
||||
|
||||
func addToRecentlyVisited() {
|
||||
}
|
||||
}
|
||||
88
submodules/BrowserUI/Sources/BrowserRecentlyVisited.swift
Normal file
88
submodules/BrowserUI/Sources/BrowserRecentlyVisited.swift
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
import Foundation
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import SwiftSignalKit
|
||||
import TelegramUIPreferences
|
||||
|
||||
private struct RecentlyVisitedLinkItemId {
|
||||
public let rawValue: MemoryBuffer
|
||||
|
||||
var value: String {
|
||||
return String(data: self.rawValue.makeData(), encoding: .utf8) ?? ""
|
||||
}
|
||||
|
||||
init(_ rawValue: MemoryBuffer) {
|
||||
self.rawValue = rawValue
|
||||
}
|
||||
|
||||
init?(_ value: String) {
|
||||
if let data = value.data(using: .utf8) {
|
||||
self.rawValue = MemoryBuffer(data: data)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public final class RecentVisitedLinkItem: Codable {
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case webPage
|
||||
}
|
||||
|
||||
public let webPage: TelegramMediaWebpage
|
||||
|
||||
public init(webPage: TelegramMediaWebpage) {
|
||||
self.webPage = webPage
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
if let webPageData = try container.decodeIfPresent(Data.self, forKey: .webPage) {
|
||||
self.webPage = PostboxDecoder(buffer: MemoryBuffer(data: webPageData)).decodeRootObject() as! TelegramMediaWebpage
|
||||
} else {
|
||||
fatalError()
|
||||
}
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
let encoder = PostboxEncoder()
|
||||
encoder.encodeRootObject(self.webPage)
|
||||
let webPageData = encoder.makeData()
|
||||
try container.encode(webPageData, forKey: .webPage)
|
||||
}
|
||||
}
|
||||
|
||||
func addRecentlyVisitedLink(engine: TelegramEngine, webPage: TelegramMediaWebpage) -> Signal<Never, NoError> {
|
||||
if let url = webPage.content.url, let itemId = RecentlyVisitedLinkItemId(url) {
|
||||
return engine.orderedLists.addOrMoveToFirstPosition(collectionId: ApplicationSpecificOrderedItemListCollectionId.browserRecentlyVisited, id: itemId.rawValue, item: RecentVisitedLinkItem(webPage: webPage), removeTailIfCountExceeds: 10)
|
||||
} else {
|
||||
return .complete()
|
||||
}
|
||||
}
|
||||
|
||||
func removeRecentlyVisitedLink(engine: TelegramEngine, url: String) -> Signal<Never, NoError> {
|
||||
if let itemId = RecentlyVisitedLinkItemId(url) {
|
||||
return engine.orderedLists.removeItem(collectionId: ApplicationSpecificOrderedItemListCollectionId.browserRecentlyVisited, id: itemId.rawValue)
|
||||
} else {
|
||||
return .complete()
|
||||
}
|
||||
}
|
||||
|
||||
func clearRecentlyVisitedLinks(engine: TelegramEngine) -> Signal<Never, NoError> {
|
||||
return engine.orderedLists.clear(collectionId: ApplicationSpecificOrderedItemListCollectionId.browserRecentlyVisited)
|
||||
}
|
||||
|
||||
func recentlyVisitedLinks(engine: TelegramEngine) -> Signal<[TelegramMediaWebpage], NoError> {
|
||||
return engine.data.subscribe(TelegramEngine.EngineData.Item.OrderedLists.ListItems(collectionId: ApplicationSpecificOrderedItemListCollectionId.browserRecentlyVisited))
|
||||
|> map { items -> [TelegramMediaWebpage] in
|
||||
var result: [TelegramMediaWebpage] = []
|
||||
for item in items {
|
||||
if let link = item.contents.get(RecentVisitedLinkItem.self) {
|
||||
result.append(link.webPage)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
|
@ -73,13 +73,14 @@ private final class BrowserScreenComponent: CombinedComponent {
|
|||
static var body: Body {
|
||||
let navigationBar = Child(BrowserNavigationBarComponent.self)
|
||||
let toolbar = Child(BrowserToolbarComponent.self)
|
||||
let addressList = Child(BrowserAddressListComponent.self)
|
||||
|
||||
return { context in
|
||||
let environment = context.environment[ViewControllerComponentContainer.Environment.self].value
|
||||
let performAction = context.component.performAction
|
||||
let performHoldAction = context.component.performHoldAction
|
||||
|
||||
let navigationContent: AnyComponentWithIdentity<Empty>?
|
||||
let navigationContent: AnyComponentWithIdentity<BrowserNavigationBarEnvironment>?
|
||||
var navigationLeftItems: [AnyComponentWithIdentity<Empty>]
|
||||
var navigationRightItems: [AnyComponentWithIdentity<Empty>]
|
||||
if context.component.presentationState.isSearching {
|
||||
|
|
@ -96,51 +97,78 @@ private final class BrowserScreenComponent: CombinedComponent {
|
|||
navigationLeftItems = []
|
||||
navigationRightItems = []
|
||||
} else {
|
||||
let title = context.component.contentState?.title ?? ""
|
||||
navigationContent = AnyComponentWithIdentity(
|
||||
id: "title_\(title)",
|
||||
component: AnyComponent(
|
||||
MultilineTextComponent(text: .plain(NSAttributedString(string: title, font: Font.bold(17.0), textColor: environment.theme.rootController.navigationBar.primaryTextColor, paragraphAlignment: .center)), horizontalAlignment: .center, maximumNumberOfLines: 1)
|
||||
)
|
||||
)
|
||||
navigationLeftItems = [
|
||||
AnyComponentWithIdentity(
|
||||
id: "close",
|
||||
let contentType = context.component.contentState?.contentType ?? .instantPage
|
||||
switch contentType {
|
||||
case .webPage:
|
||||
navigationContent = AnyComponentWithIdentity(
|
||||
id: "addressBar",
|
||||
component: AnyComponent(
|
||||
Button(
|
||||
content: AnyComponent(
|
||||
MultilineTextComponent(text: .plain(NSAttributedString(string: environment.strings.WebBrowser_Done, font: Font.regular(17.0), textColor: environment.theme.rootController.navigationBar.accentTextColor, paragraphAlignment: .center)), horizontalAlignment: .left, maximumNumberOfLines: 1)
|
||||
),
|
||||
action: {
|
||||
performAction.invoke(.close)
|
||||
}
|
||||
AddressBarContentComponent(
|
||||
theme: environment.theme,
|
||||
strings: environment.strings,
|
||||
url: context.component.contentState?.url ?? "",
|
||||
isSecure: context.component.contentState?.isSecure ?? false,
|
||||
isExpanded: context.component.presentationState.addressFocused,
|
||||
performAction: performAction
|
||||
)
|
||||
)
|
||||
)
|
||||
]
|
||||
|
||||
navigationRightItems = [
|
||||
AnyComponentWithIdentity(
|
||||
id: "settings",
|
||||
case .instantPage, .document:
|
||||
let title = context.component.contentState?.title ?? ""
|
||||
navigationContent = AnyComponentWithIdentity(
|
||||
id: "titleBar_\(title)",
|
||||
component: AnyComponent(
|
||||
ReferenceButtonComponent(
|
||||
content: AnyComponent(
|
||||
LottieComponent(
|
||||
content: LottieComponent.AppBundleContent(
|
||||
name: "anim_moredots"
|
||||
),
|
||||
color: environment.theme.rootController.navigationBar.accentTextColor,
|
||||
size: CGSize(width: 30.0, height: 30.0)
|
||||
)
|
||||
),
|
||||
tag: settingsTag,
|
||||
action: {
|
||||
performAction.invoke(.openSettings)
|
||||
}
|
||||
TitleBarContentComponent(
|
||||
theme: environment.theme,
|
||||
title: title
|
||||
)
|
||||
)
|
||||
)
|
||||
]
|
||||
}
|
||||
|
||||
if context.component.presentationState.addressFocused {
|
||||
navigationLeftItems = []
|
||||
navigationRightItems = []
|
||||
} else {
|
||||
navigationLeftItems = [
|
||||
AnyComponentWithIdentity(
|
||||
id: "close",
|
||||
component: AnyComponent(
|
||||
Button(
|
||||
content: AnyComponent(
|
||||
MultilineTextComponent(text: .plain(NSAttributedString(string: environment.strings.WebBrowser_Done, font: Font.semibold(17.0), textColor: environment.theme.rootController.navigationBar.accentTextColor, paragraphAlignment: .center)), horizontalAlignment: .left, maximumNumberOfLines: 1)
|
||||
),
|
||||
action: {
|
||||
performAction.invoke(.close)
|
||||
}
|
||||
)
|
||||
)
|
||||
)
|
||||
]
|
||||
|
||||
navigationRightItems = [
|
||||
AnyComponentWithIdentity(
|
||||
id: "settings",
|
||||
component: AnyComponent(
|
||||
ReferenceButtonComponent(
|
||||
content: AnyComponent(
|
||||
LottieComponent(
|
||||
content: LottieComponent.AppBundleContent(
|
||||
name: "anim_moredots"
|
||||
),
|
||||
color: environment.theme.rootController.navigationBar.accentTextColor,
|
||||
size: CGSize(width: 30.0, height: 30.0)
|
||||
)
|
||||
),
|
||||
tag: settingsTag,
|
||||
action: {
|
||||
performAction.invoke(.openSettings)
|
||||
}
|
||||
)
|
||||
)
|
||||
)
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
let collapseFraction = context.component.presentationState.isSearching ? 0.0 : context.component.panelCollapseFraction
|
||||
|
|
@ -224,6 +252,26 @@ private final class BrowserScreenComponent: CombinedComponent {
|
|||
.position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height - toolbar.size.height / 2.0))
|
||||
)
|
||||
|
||||
if context.component.presentationState.addressFocused {
|
||||
let addressList = addressList.update(
|
||||
component: BrowserAddressListComponent(
|
||||
context: context.component.context,
|
||||
theme: environment.theme,
|
||||
strings: environment.strings,
|
||||
navigateTo: { url in
|
||||
performAction.invoke(.navigateTo(url))
|
||||
}
|
||||
),
|
||||
availableSize: CGSize(width: context.availableSize.width, height: context.availableSize.height - navigationBar.size.height - toolbar.size.height),
|
||||
transition: context.transition
|
||||
)
|
||||
context.add(addressList
|
||||
.position(CGPoint(x: context.availableSize.width / 2.0, y: navigationBar.size.height + addressList.size.height / 2.0))
|
||||
.appear(.default(alpha: true))
|
||||
.disappear(.default(alpha: true))
|
||||
)
|
||||
}
|
||||
|
||||
return context.availableSize
|
||||
}
|
||||
}
|
||||
|
|
@ -239,6 +287,7 @@ struct BrowserPresentationState: Equatable {
|
|||
var searchResultIndex: Int
|
||||
var searchResultCount: Int
|
||||
var searchQueryIsEmpty: Bool
|
||||
var addressFocused: Bool
|
||||
}
|
||||
|
||||
public class BrowserScreen: ViewController, MinimizableController {
|
||||
|
|
@ -262,6 +311,9 @@ public class BrowserScreen: ViewController, MinimizableController {
|
|||
case updateFontIsSerif(Bool)
|
||||
case addBookmark
|
||||
case openBookmarks
|
||||
case openAddressBar
|
||||
case closeAddressBar
|
||||
case navigateTo(String)
|
||||
}
|
||||
|
||||
fileprivate final class Node: ViewControllerTracingNode {
|
||||
|
|
@ -271,7 +323,6 @@ public class BrowserScreen: ViewController, MinimizableController {
|
|||
private let contentContainerView = UIView()
|
||||
fileprivate let contentNavigationContainer = ComponentView<Empty>()
|
||||
fileprivate var content: [BrowserContent] = []
|
||||
|
||||
fileprivate var contentState: BrowserContentState?
|
||||
private var contentStateDisposable = MetaDisposable()
|
||||
|
||||
|
|
@ -292,13 +343,20 @@ public class BrowserScreen: ViewController, MinimizableController {
|
|||
|
||||
self.presentationState = BrowserPresentationState(
|
||||
fontState: BrowserPresentationState.FontState(size: 100, isSerif: false),
|
||||
isSearching: false, searchResultIndex: 0, searchResultCount: 0, searchQueryIsEmpty: true
|
||||
isSearching: false,
|
||||
searchResultIndex: 0,
|
||||
searchResultCount: 0,
|
||||
searchQueryIsEmpty: true,
|
||||
addressFocused: false
|
||||
)
|
||||
|
||||
super.init()
|
||||
|
||||
self.pushContent(controller.subject, transition: .immediate)
|
||||
|
||||
if let content = self.content.last {
|
||||
content.addToRecentlyVisited()
|
||||
}
|
||||
|
||||
self.performAction.connect { [weak self] action in
|
||||
guard let self, let content = self.content.last, let url = self.contentState?.url else {
|
||||
return
|
||||
|
|
@ -341,7 +399,7 @@ public class BrowserScreen: ViewController, MinimizableController {
|
|||
let text: String
|
||||
var savedMessages = false
|
||||
if peerIds.count == 1, let peerId = peerIds.first, peerId == strongSelf.context.account.peerId {
|
||||
text = presentationData.strings.WebBrowser_LinkForwardTooltip_SavedMessages_One
|
||||
text = presentationData.strings.WebBrowser_LinkAddedToBookmarks
|
||||
savedMessages = true
|
||||
} else {
|
||||
if peers.count == 1, let peer = peers.first {
|
||||
|
|
@ -388,7 +446,7 @@ public class BrowserScreen: ViewController, MinimizableController {
|
|||
case .openSettings:
|
||||
self.openSettings()
|
||||
case let .updateSearchActive(active):
|
||||
self.updatePresentationState(animated: true, { state in
|
||||
self.updatePresentationState(transition: .easeInOut(duration: 0.2), { state in
|
||||
var updatedState = state
|
||||
updatedState.isSearching = active
|
||||
updatedState.searchQueryIsEmpty = true
|
||||
|
|
@ -485,10 +543,31 @@ public class BrowserScreen: ViewController, MinimizableController {
|
|||
content.updateFontState(self.presentationState.fontState)
|
||||
case .addBookmark:
|
||||
if let content = self.content.last {
|
||||
self.addBookmark(content.currentState.url)
|
||||
self.addBookmark(content.currentState.url, showArrow: true)
|
||||
}
|
||||
case .openBookmarks:
|
||||
break
|
||||
self.openBookmarks()
|
||||
case .openAddressBar:
|
||||
self.updatePresentationState(transition: .spring(duration: 0.4), { state in
|
||||
var updatedState = state
|
||||
updatedState.addressFocused = true
|
||||
return updatedState
|
||||
})
|
||||
case .closeAddressBar:
|
||||
self.updatePresentationState(transition: .spring(duration: 0.4), { state in
|
||||
var updatedState = state
|
||||
updatedState.addressFocused = false
|
||||
return updatedState
|
||||
})
|
||||
case let .navigateTo(address):
|
||||
if let content = self.content.last as? BrowserWebContent {
|
||||
content.navigateTo(address: address)
|
||||
}
|
||||
self.updatePresentationState(transition: .spring(duration: 0.4), { state in
|
||||
var updatedState = state
|
||||
updatedState.addressFocused = false
|
||||
return updatedState
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -517,9 +596,9 @@ public class BrowserScreen: ViewController, MinimizableController {
|
|||
self.view.addSubview(self.contentContainerView)
|
||||
}
|
||||
|
||||
func updatePresentationState(animated: Bool = false, _ f: (BrowserPresentationState) -> BrowserPresentationState) {
|
||||
func updatePresentationState(transition: ComponentTransition = .immediate, _ f: (BrowserPresentationState) -> BrowserPresentationState) {
|
||||
self.presentationState = f(self.presentationState)
|
||||
self.requestLayout(transition: animated ? .easeInOut(duration: 0.2) : .immediate)
|
||||
self.requestLayout(transition: transition)
|
||||
}
|
||||
|
||||
func pushContent(_ content: BrowserScreen.Subject, transition: ComponentTransition) {
|
||||
|
|
@ -536,6 +615,10 @@ public class BrowserScreen: ViewController, MinimizableController {
|
|||
self.openPeer(peer)
|
||||
}
|
||||
browserContent = instantPageContent
|
||||
case let .document(file):
|
||||
browserContent = BrowserDocumentContent(context: self.context, presentationData: self.presentationData, file: file)
|
||||
case let .pdfDocument(file):
|
||||
browserContent = BrowserPdfContent(context: self.context, presentationData: self.presentationData, file: file)
|
||||
}
|
||||
browserContent.pushContent = { [weak self] content in
|
||||
guard let self else {
|
||||
|
|
@ -596,7 +679,7 @@ public class BrowserScreen: ViewController, MinimizableController {
|
|||
self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(peer), animated: true))
|
||||
}
|
||||
|
||||
func addBookmark(_ url: String) {
|
||||
func addBookmark(_ url: String, showArrow: Bool) {
|
||||
let _ = enqueueMessages(
|
||||
account: self.context.account,
|
||||
peerId: self.context.account.peerId,
|
||||
|
|
@ -615,7 +698,9 @@ public class BrowserScreen: ViewController, MinimizableController {
|
|||
).start()
|
||||
|
||||
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
|
||||
self.controller?.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: true, text: presentationData.strings.WebBrowser_LinkAddedToBookmarks), elevatedLayout: false, animateInAsReplacement: true, action: { [weak self] action in
|
||||
|
||||
let lastController = self.controller?.navigationController?.viewControllers.last as? ViewController
|
||||
lastController?.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: true, text: presentationData.strings.WebBrowser_LinkAddedToBookmarks), elevatedLayout: false, animateInAsReplacement: true, action: { [weak self] action in
|
||||
if let self, action == .info {
|
||||
let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId))
|
||||
|> deliverOnMainQueue).start(next: { [weak self] peer in
|
||||
|
|
@ -708,6 +793,20 @@ public class BrowserScreen: ViewController, MinimizableController {
|
|||
}, animated: true)
|
||||
}
|
||||
|
||||
func openBookmarks() {
|
||||
guard let url = self.contentState?.url else {
|
||||
return
|
||||
}
|
||||
let controller = BrowserBookmarksScreen(context: self.context, url: url, openUrl: { [weak self] url in
|
||||
if let self {
|
||||
self.performAction.invoke(.navigateTo(url))
|
||||
}
|
||||
}, addBookmark: { [weak self] in
|
||||
self?.addBookmark(url, showArrow: false)
|
||||
})
|
||||
self.controller?.push(controller)
|
||||
}
|
||||
|
||||
func openSettings() {
|
||||
guard let referenceView = self.componentHost.findTaggedView(tag: settingsTag) as? ReferenceButtonComponent.View else {
|
||||
return
|
||||
|
|
@ -736,7 +835,7 @@ public class BrowserScreen: ViewController, MinimizableController {
|
|||
|
||||
let _ = (settings
|
||||
|> deliverOnMainQueue).start(next: { [weak self] settings in
|
||||
guard let self, let controller = self.controller else {
|
||||
guard let self, let controller = self.controller, let contentState = self.contentState else {
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -771,7 +870,7 @@ public class BrowserScreen: ViewController, MinimizableController {
|
|||
defaultWebBrowser = "safari"
|
||||
}
|
||||
|
||||
let url = self.contentState?.url ?? ""
|
||||
let url = contentState.url
|
||||
let openInOptions = availableOpenInOptions(context: self.context, item: .url(url: url))
|
||||
let openInTitle: String
|
||||
let openInUrl: String
|
||||
|
|
@ -787,40 +886,52 @@ public class BrowserScreen: ViewController, MinimizableController {
|
|||
openInUrl = url
|
||||
}
|
||||
|
||||
let items: [ContextMenuItem] = [
|
||||
.custom(fontItem, false),
|
||||
.action(ContextMenuActionItem(text: self.presentationData.strings.InstantPage_FontSanFrancisco, icon: forceIsSerif ? emptyIcon : checkIcon, action: { (controller, action) in
|
||||
var items: [ContextMenuItem] = []
|
||||
items.append(.custom(fontItem, false))
|
||||
|
||||
|
||||
items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.InstantPage_FontSanFrancisco, icon: forceIsSerif ? emptyIcon : checkIcon, action: { (controller, action) in
|
||||
performAction.invoke(.updateFontIsSerif(false))
|
||||
action(.default)
|
||||
})),
|
||||
.action(ContextMenuActionItem(text: self.presentationData.strings.InstantPage_FontNewYork, textFont: .custom(font: Font.with(size: 17.0, design: .serif, traits: []), height: nil, verticalOffset: nil), icon: forceIsSerif ? checkIcon : emptyIcon, action: { (controller, action) in
|
||||
})))
|
||||
|
||||
items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.InstantPage_FontNewYork, textFont: .custom(font: Font.with(size: 17.0, design: .serif, traits: []), height: nil, verticalOffset: nil), icon: forceIsSerif ? checkIcon : emptyIcon, action: { (controller, action) in
|
||||
performAction.invoke(.updateFontIsSerif(true))
|
||||
action(.default)
|
||||
})),
|
||||
.separator,
|
||||
.action(ContextMenuActionItem(text: self.presentationData.strings.WebBrowser_Reload, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Instant View/Settings/Reload"), color: theme.contextMenu.primaryColor) }, action: { (controller, action) in
|
||||
})))
|
||||
|
||||
items.append(.separator)
|
||||
|
||||
if case .webPage = contentState.contentType {
|
||||
items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.WebBrowser_Reload, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Instant View/Settings/Reload"), color: theme.contextMenu.primaryColor) }, action: { (controller, action) in
|
||||
performAction.invoke(.reload)
|
||||
action(.default)
|
||||
})),
|
||||
.action(ContextMenuActionItem(text: self.presentationData.strings.InstantPage_Search, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Instant View/Settings/Search"), color: theme.contextMenu.primaryColor) }, action: { (controller, action) in
|
||||
})))
|
||||
}
|
||||
if [.webPage, .instantPage].contains(contentState.contentType) {
|
||||
items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.InstantPage_Search, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Instant View/Settings/Search"), color: theme.contextMenu.primaryColor) }, action: { (controller, action) in
|
||||
performAction.invoke(.updateSearchActive(true))
|
||||
action(.default)
|
||||
})),
|
||||
.action(ContextMenuActionItem(text: self.presentationData.strings.WebBrowser_Share, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Share"), color: theme.contextMenu.primaryColor) }, action: { (controller, action) in
|
||||
performAction.invoke(.share)
|
||||
action(.default)
|
||||
})),
|
||||
.action(ContextMenuActionItem(text: self.presentationData.strings.WebBrowser_AddBookmark, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Fave"), color: theme.contextMenu.primaryColor) }, action: { (controller, action) in
|
||||
})))
|
||||
}
|
||||
|
||||
items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.WebBrowser_Share, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Share"), color: theme.contextMenu.primaryColor) }, action: { (controller, action) in
|
||||
performAction.invoke(.share)
|
||||
action(.default)
|
||||
})))
|
||||
|
||||
if [.webPage, .instantPage].contains(contentState.contentType) {
|
||||
items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.WebBrowser_AddBookmark, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Fave"), color: theme.contextMenu.primaryColor) }, action: { (controller, action) in
|
||||
performAction.invoke(.addBookmark)
|
||||
action(.default)
|
||||
})),
|
||||
.action(ContextMenuActionItem(text: self.presentationData.strings.InstantPage_OpenInBrowser(openInTitle).string, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Browser"), color: theme.contextMenu.primaryColor) }, action: { [weak self] (controller, action) in
|
||||
})))
|
||||
items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.InstantPage_OpenInBrowser(openInTitle).string, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Browser"), color: theme.contextMenu.primaryColor) }, action: { [weak self] (controller, action) in
|
||||
if let self {
|
||||
self.context.sharedContext.applicationBindings.openUrl(openInUrl)
|
||||
}
|
||||
action(.default)
|
||||
}))
|
||||
]
|
||||
})))
|
||||
}
|
||||
|
||||
let contextController = ContextController(presentationData: self.presentationData, source: source, items: .single(ContextController.Items(content: .list(items))))
|
||||
self.controller?.present(contextController, in: .window(.root))
|
||||
|
|
@ -1050,21 +1161,34 @@ public class BrowserScreen: ViewController, MinimizableController {
|
|||
public enum Subject {
|
||||
case webPage(url: String)
|
||||
case instantPage(webPage: TelegramMediaWebpage, anchor: String?, sourceLocation: InstantPageSourceLocation)
|
||||
case document(file: TelegramMediaFile)
|
||||
case pdfDocument(file: TelegramMediaFile)
|
||||
}
|
||||
|
||||
private let context: AccountContext
|
||||
private let subject: Subject
|
||||
|
||||
|
||||
var openPreviousOnClose = false
|
||||
|
||||
public static let supportedDocumentMimeTypes: [String] = [
|
||||
"text/plain",
|
||||
"text/rtf",
|
||||
"application/pdf",
|
||||
"application/msword",
|
||||
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||
"application/vnd.ms-excel",
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.template",
|
||||
"application/vnd.openxmlformats-officedocument.presentationml.presentation"
|
||||
]
|
||||
|
||||
public init(context: AccountContext, subject: Subject) {
|
||||
self.context = context
|
||||
self.subject = subject
|
||||
|
||||
super.init(navigationBarPresentationData: nil)
|
||||
|
||||
self.navigationPresentation = .modal
|
||||
self.navigationPresentation = .modalInCompactLayout
|
||||
|
||||
self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .allButUpsideDown)
|
||||
|
||||
|
|
@ -1107,6 +1231,8 @@ public class BrowserScreen: ViewController, MinimizableController {
|
|||
return contentState.favicon
|
||||
case .instantPage:
|
||||
return UIImage(bundleImageName: "Chat/Message/AttachedContentInstantIcon")?.withRenderingMode(.alwaysTemplate)
|
||||
case .document:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ import AccountContext
|
|||
import BundleIconComponent
|
||||
|
||||
final class SearchBarContentComponent: Component {
|
||||
public typealias EnvironmentType = BrowserNavigationBarEnvironment
|
||||
|
||||
let theme: PresentationTheme
|
||||
let strings: PresentationStrings
|
||||
let performAction: ActionSlot<BrowserScreen.Action>
|
||||
|
|
@ -351,7 +353,7 @@ final class SearchBarContentComponent: Component {
|
|||
return View()
|
||||
}
|
||||
|
||||
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
||||
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<BrowserNavigationBarEnvironment>, transition: ComponentTransition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, transition: transition)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
85
submodules/BrowserUI/Sources/BrowserTitleBarComponent.swift
Normal file
85
submodules/BrowserUI/Sources/BrowserTitleBarComponent.swift
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import ComponentFlow
|
||||
import SwiftSignalKit
|
||||
import TelegramPresentationData
|
||||
import AccountContext
|
||||
import BundleIconComponent
|
||||
import MultilineTextComponent
|
||||
import UrlEscaping
|
||||
|
||||
final class TitleBarContentComponent: Component {
|
||||
public typealias EnvironmentType = BrowserNavigationBarEnvironment
|
||||
|
||||
let theme: PresentationTheme
|
||||
let title: String
|
||||
|
||||
init(
|
||||
theme: PresentationTheme,
|
||||
title: String
|
||||
) {
|
||||
self.theme = theme
|
||||
self.title = title
|
||||
}
|
||||
|
||||
static func ==(lhs: TitleBarContentComponent, rhs: TitleBarContentComponent) -> Bool {
|
||||
if lhs.theme !== rhs.theme {
|
||||
return false
|
||||
}
|
||||
if lhs.title != rhs.title {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
final class View: UIView {
|
||||
private var titleContent = ComponentView<Empty>()
|
||||
private var component: TitleBarContentComponent?
|
||||
|
||||
init() {
|
||||
super.init(frame: CGRect())
|
||||
}
|
||||
|
||||
required public init?(coder: NSCoder) {
|
||||
fatalError()
|
||||
}
|
||||
|
||||
func update(component: TitleBarContentComponent, availableSize: CGSize, environment: Environment<BrowserNavigationBarEnvironment>, transition: ComponentTransition) -> CGSize {
|
||||
self.component = component
|
||||
|
||||
let titleSize = self.titleContent.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(
|
||||
MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(string: component.title, font: Font.semibold(17.0), textColor: component.theme.rootController.navigationBar.primaryTextColor)),
|
||||
horizontalAlignment: .center,
|
||||
truncationType: .end,
|
||||
maximumNumberOfLines: 1
|
||||
)
|
||||
),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width - 36.0, height: availableSize.height)
|
||||
)
|
||||
let titleContentFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - titleSize.width) / 2.0), y: floorToScreenPixels((availableSize.height - titleSize.height) / 2.0)), size: titleSize)
|
||||
if let titleContentView = self.titleContent.view {
|
||||
if titleContentView.superview == nil {
|
||||
self.addSubview(titleContentView)
|
||||
}
|
||||
transition.setPosition(view: titleContentView, position: titleContentFrame.center)
|
||||
titleContentView.bounds = CGRect(origin: .zero, size: titleContentFrame.size)
|
||||
}
|
||||
|
||||
return availableSize
|
||||
}
|
||||
}
|
||||
|
||||
func makeView() -> View {
|
||||
return View()
|
||||
}
|
||||
|
||||
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<BrowserNavigationBarEnvironment>, transition: ComponentTransition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, environment: environment, transition: transition)
|
||||
}
|
||||
}
|
||||
|
|
@ -17,6 +17,7 @@ import ShareController
|
|||
import UndoUI
|
||||
import LottieComponent
|
||||
import MultilineTextComponent
|
||||
import UrlEscaping
|
||||
|
||||
private final class TonSchemeHandler: NSObject, WKURLSchemeHandler {
|
||||
private final class PendingTask {
|
||||
|
|
@ -145,6 +146,8 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU
|
|||
var presentInGlobalOverlay: (ViewController) -> Void = { _ in }
|
||||
var getNavigationController: () -> NavigationController? = { return nil }
|
||||
|
||||
private var tempFile: TempBoxFile?
|
||||
|
||||
init(context: AccountContext, presentationData: PresentationData, url: String) {
|
||||
self.context = context
|
||||
self.uuid = UUID()
|
||||
|
|
@ -176,7 +179,15 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU
|
|||
}
|
||||
|
||||
var title: String = ""
|
||||
if let parsedUrl = URL(string: url) {
|
||||
if url.hasPrefix("file://") {
|
||||
var updatedPath = url
|
||||
let tempFile = TempBox.shared.file(path: url.replacingOccurrences(of: "file://", with: ""), fileName: "file.xlsx")
|
||||
updatedPath = tempFile.path
|
||||
self.tempFile = tempFile
|
||||
|
||||
let request = URLRequest(url: URL(fileURLWithPath: updatedPath))
|
||||
self.webView.load(request)
|
||||
} else if let parsedUrl = URL(string: url) {
|
||||
let request = URLRequest(url: parsedUrl)
|
||||
self.webView.load(request)
|
||||
|
||||
|
|
@ -201,6 +212,7 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU
|
|||
self.webView.addObserver(self, forKeyPath: #keyPath(WKWebView.estimatedProgress), options: [], context: nil)
|
||||
self.webView.addObserver(self, forKeyPath: #keyPath(WKWebView.canGoBack), options: [], context: nil)
|
||||
self.webView.addObserver(self, forKeyPath: #keyPath(WKWebView.canGoForward), options: [], context: nil)
|
||||
self.webView.addObserver(self, forKeyPath: #keyPath(WKWebView.hasOnlySecureContent), options: [], context: nil)
|
||||
if #available(iOS 15.0, *) {
|
||||
self.backgroundColor = presentationData.theme.list.plainBackgroundColor
|
||||
self.webView.underPageBackgroundColor = presentationData.theme.list.plainBackgroundColor
|
||||
|
|
@ -221,6 +233,7 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU
|
|||
self.webView.removeObserver(self, forKeyPath: #keyPath(WKWebView.estimatedProgress))
|
||||
self.webView.removeObserver(self, forKeyPath: #keyPath(WKWebView.canGoBack))
|
||||
self.webView.removeObserver(self, forKeyPath: #keyPath(WKWebView.canGoForward))
|
||||
self.webView.removeObserver(self, forKeyPath: #keyPath(WKWebView.hasOnlySecureContent))
|
||||
|
||||
self.faviconDisposable.dispose()
|
||||
}
|
||||
|
|
@ -236,41 +249,6 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU
|
|||
}
|
||||
}
|
||||
|
||||
private let setupFontFunctions = """
|
||||
(function() {
|
||||
const styleId = 'telegram-font-overrides';
|
||||
|
||||
function setTelegramFontOverrides(font, textSizeAdjust) {
|
||||
let style = document.getElementById(styleId);
|
||||
|
||||
if (!style) {
|
||||
style = document.createElement('style');
|
||||
style.id = styleId;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
let cssRules = '* {';
|
||||
if (font !== null) {
|
||||
cssRules += `
|
||||
font-family: ${font} !important;
|
||||
`;
|
||||
}
|
||||
if (textSizeAdjust !== null) {
|
||||
cssRules += `
|
||||
-webkit-text-size-adjust: ${textSizeAdjust} !important;
|
||||
`;
|
||||
}
|
||||
cssRules += '}';
|
||||
|
||||
style.innerHTML = cssRules;
|
||||
|
||||
if (font === null && textSizeAdjust === null) {
|
||||
style.parentNode.removeChild(style);
|
||||
}
|
||||
}
|
||||
window.setTelegramFontOverrides = setTelegramFontOverrides;
|
||||
})();
|
||||
"""
|
||||
|
||||
var currentFontState = BrowserPresentationState.FontState(size: 100, isSerif: false)
|
||||
func updateFontState(_ state: BrowserPresentationState.FontState) {
|
||||
|
|
@ -311,65 +289,113 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU
|
|||
})
|
||||
}
|
||||
|
||||
private var findSession: Any?
|
||||
private var previousQuery: String?
|
||||
func setSearch(_ query: String?, completion: ((Int) -> Void)?) {
|
||||
guard self.previousQuery != query else {
|
||||
return
|
||||
}
|
||||
self.previousQuery = query
|
||||
self.setupSearch { [weak self] in
|
||||
if let query = query {
|
||||
let js = "uiWebview_HighlightAllOccurencesOfString('\(query)')"
|
||||
self?.webView.evaluateJavaScript(js, completionHandler: { [weak self] _, _ in
|
||||
let js = "uiWebview_SearchResultCount"
|
||||
self?.webView.evaluateJavaScript(js, completionHandler: { [weak self] result, _ in
|
||||
if let result = result as? NSNumber {
|
||||
self?.searchResultsCount = result.intValue
|
||||
completion?(result.intValue)
|
||||
} else {
|
||||
completion?(0)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
if #available(iOS 16.0, *), !"".isEmpty {
|
||||
if let query {
|
||||
var findSession: UIFindSession?
|
||||
if let current = self.findSession as? UIFindSession {
|
||||
findSession = current
|
||||
} else {
|
||||
self.webView.isFindInteractionEnabled = true
|
||||
|
||||
if let findInteraction = self.webView.findInteraction, let webView = self.webView as? UIFindInteractionDelegate, let session = webView.findInteraction(findInteraction, sessionFor: self.webView) {
|
||||
// session.setValue(findInteraction, forKey: "_parentInteraction")
|
||||
// findInteraction.setValue(session, forKey: "_activeFindSession")
|
||||
findSession = session
|
||||
self.findSession = session
|
||||
|
||||
webView.findInteraction?(findInteraction, didBegin: session)
|
||||
}
|
||||
}
|
||||
if let findSession {
|
||||
findSession.performSearch(query: query, options: BrowserSearchOptions())
|
||||
self.webView.findInteraction?.updateResultCount()
|
||||
completion?(findSession.resultCount)
|
||||
}
|
||||
} else {
|
||||
let js = "uiWebview_RemoveAllHighlights()"
|
||||
self?.webView.evaluateJavaScript(js, completionHandler: nil)
|
||||
|
||||
self?.currentSearchResult = 0
|
||||
self?.searchResultsCount = 0
|
||||
if let findInteraction = self.webView.findInteraction, let webView = self.webView as? UIFindInteractionDelegate, let session = self.findSession as? UIFindSession {
|
||||
webView.findInteraction?(findInteraction, didEnd: session)
|
||||
self.findSession = nil
|
||||
self.webView.isFindInteractionEnabled = false
|
||||
}
|
||||
}
|
||||
} else {
|
||||
self.setupSearch { [weak self] in
|
||||
if let query, !query.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
||||
let js = "uiWebview_HighlightAllOccurencesOfString('\(query)')"
|
||||
self?.webView.evaluateJavaScript(js, completionHandler: { [weak self] _, _ in
|
||||
let js = "uiWebview_SearchResultCount"
|
||||
self?.webView.evaluateJavaScript(js, completionHandler: { [weak self] result, _ in
|
||||
if let result = result as? NSNumber {
|
||||
self?.searchResultsCount = result.intValue
|
||||
completion?(result.intValue)
|
||||
} else {
|
||||
completion?(0)
|
||||
}
|
||||
})
|
||||
})
|
||||
} else {
|
||||
let js = "uiWebview_RemoveAllHighlights()"
|
||||
self?.webView.evaluateJavaScript(js, completionHandler: nil)
|
||||
|
||||
self?.currentSearchResult = 0
|
||||
self?.searchResultsCount = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.previousQuery = query
|
||||
}
|
||||
|
||||
private var currentSearchResult: Int = 0
|
||||
private var searchResultsCount: Int = 0
|
||||
|
||||
func scrollToPreviousSearchResult(completion: ((Int, Int) -> Void)?) {
|
||||
let searchResultsCount = self.searchResultsCount
|
||||
var index = self.currentSearchResult - 1
|
||||
if index < 0 {
|
||||
index = searchResultsCount - 1
|
||||
if #available(iOS 16.0, *), !"".isEmpty {
|
||||
if let session = self.findSession as? UIFindSession {
|
||||
session.highlightNextResult(in: .backward)
|
||||
completion?(session.highlightedResultIndex, session.resultCount)
|
||||
}
|
||||
} else {
|
||||
let searchResultsCount = self.searchResultsCount
|
||||
var index = self.currentSearchResult - 1
|
||||
if index < 0 {
|
||||
index = searchResultsCount - 1
|
||||
}
|
||||
self.currentSearchResult = index
|
||||
|
||||
let js = "uiWebview_ScrollTo('\(searchResultsCount - index - 1)')"
|
||||
self.webView.evaluateJavaScript(js, completionHandler: { _, _ in
|
||||
completion?(index, searchResultsCount)
|
||||
})
|
||||
}
|
||||
self.currentSearchResult = index
|
||||
|
||||
let js = "uiWebview_ScrollTo('\(searchResultsCount - index - 1)')"
|
||||
self.webView.evaluateJavaScript(js, completionHandler: { _, _ in
|
||||
completion?(index, searchResultsCount)
|
||||
})
|
||||
}
|
||||
|
||||
func scrollToNextSearchResult(completion: ((Int, Int) -> Void)?) {
|
||||
let searchResultsCount = self.searchResultsCount
|
||||
var index = self.currentSearchResult + 1
|
||||
if index >= searchResultsCount {
|
||||
index = 0
|
||||
if #available(iOS 16.0, *), !"".isEmpty {
|
||||
if let session = self.findSession as? UIFindSession {
|
||||
session.highlightNextResult(in: .forward)
|
||||
completion?(session.highlightedResultIndex, session.resultCount)
|
||||
}
|
||||
} else {
|
||||
let searchResultsCount = self.searchResultsCount
|
||||
var index = self.currentSearchResult + 1
|
||||
if index >= searchResultsCount {
|
||||
index = 0
|
||||
}
|
||||
self.currentSearchResult = index
|
||||
|
||||
let js = "uiWebview_ScrollTo('\(searchResultsCount - index - 1)')"
|
||||
self.webView.evaluateJavaScript(js, completionHandler: { _, _ in
|
||||
completion?(index, searchResultsCount)
|
||||
})
|
||||
}
|
||||
self.currentSearchResult = index
|
||||
|
||||
let js = "uiWebview_ScrollTo('\(searchResultsCount - index - 1)')"
|
||||
self.webView.evaluateJavaScript(js, completionHandler: { _, _ in
|
||||
completion?(index, searchResultsCount)
|
||||
})
|
||||
}
|
||||
|
||||
func stop() {
|
||||
|
|
@ -394,6 +420,14 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU
|
|||
}
|
||||
}
|
||||
|
||||
func navigateTo(address: String) {
|
||||
let finalUrl = explicitUrl(address)
|
||||
guard let url = URL(string: finalUrl) else {
|
||||
return
|
||||
}
|
||||
self.webView.load(URLRequest(url: url))
|
||||
}
|
||||
|
||||
func scrollToTop() {
|
||||
self.webView.scrollView.setContentOffset(CGPoint(x: 0.0, y: -self.webView.scrollView.contentInset.top), animated: true)
|
||||
}
|
||||
|
|
@ -458,8 +492,10 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU
|
|||
} else if keyPath == "canGoBack" {
|
||||
self.updateState { $0.withUpdatedCanGoBack(self.webView.canGoBack) }
|
||||
self.webView.disablesInteractiveTransitionGestureRecognizer = self.webView.canGoBack
|
||||
} else if keyPath == "canGoForward" {
|
||||
} else if keyPath == "canGoForward" {
|
||||
self.updateState { $0.withUpdatedCanGoForward(self.webView.canGoForward) }
|
||||
} else if keyPath == "hasOnlySecureContent" {
|
||||
self.updateState { $0.withUpdatedIsSecure(self.webView.hasOnlySecureContent) }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -694,6 +730,9 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU
|
|||
}
|
||||
|
||||
private func parseFavicon() {
|
||||
let addToRecentsWhenReady = self.addToRecentsWhenReady
|
||||
self.addToRecentsWhenReady = false
|
||||
|
||||
struct Favicon: Equatable, Hashable {
|
||||
let url: String
|
||||
let dimensions: PixelDimensions?
|
||||
|
|
@ -774,10 +813,64 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU
|
|||
return
|
||||
}
|
||||
self.updateState { $0.withUpdatedFavicon(favicon) }
|
||||
|
||||
if addToRecentsWhenReady {
|
||||
var image: TelegramMediaImage?
|
||||
|
||||
if let favicon, let imageData = favicon.pngData() {
|
||||
let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max))
|
||||
self.context.account.postbox.mediaBox.storeResourceData(resource.id, data: imageData)
|
||||
image = TelegramMediaImage(
|
||||
imageId: MediaId(namespace: Namespaces.Media.LocalImage, id: Int64.random(in: Int64.min ... Int64.max)),
|
||||
representations: [
|
||||
TelegramMediaImageRepresentation(
|
||||
dimensions: PixelDimensions(width: Int32(favicon.size.width), height: Int32(favicon.size.height)),
|
||||
resource: resource,
|
||||
progressiveSizes: [],
|
||||
immediateThumbnailData: nil,
|
||||
hasVideo: false,
|
||||
isPersonal: false
|
||||
)
|
||||
],
|
||||
immediateThumbnailData: nil,
|
||||
reference: nil,
|
||||
partialReference: nil,
|
||||
flags: []
|
||||
)
|
||||
}
|
||||
|
||||
let webPage = TelegramMediaWebpage(webpageId: MediaId(namespace: 0, id: 0), content: .Loaded(TelegramMediaWebpageLoadedContent(
|
||||
url: self._state.url,
|
||||
displayUrl: self._state.url,
|
||||
hash: 0,
|
||||
type: "",
|
||||
websiteName: self._state.title,
|
||||
title: self._state.title,
|
||||
text: nil,
|
||||
embedUrl: nil,
|
||||
embedType: nil,
|
||||
embedSize: nil,
|
||||
duration: nil,
|
||||
author: nil,
|
||||
isMediaLargeByDefault: nil,
|
||||
image: image,
|
||||
file: nil,
|
||||
story: nil,
|
||||
attributes: [],
|
||||
instantPage: nil))
|
||||
)
|
||||
|
||||
let _ = addRecentlyVisitedLink(engine: self.context.engine, webPage: webPage).startStandalone()
|
||||
}
|
||||
}))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private var addToRecentsWhenReady = false
|
||||
func addToRecentlyVisited() {
|
||||
self.addToRecentsWhenReady = true
|
||||
}
|
||||
}
|
||||
|
||||
private final class ErrorComponent: CombinedComponent {
|
||||
|
|
@ -873,3 +966,50 @@ private final class ErrorComponent: CombinedComponent {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
let setupFontFunctions = """
|
||||
(function() {
|
||||
const styleId = 'telegram-font-overrides';
|
||||
|
||||
function setTelegramFontOverrides(font, textSizeAdjust) {
|
||||
let style = document.getElementById(styleId);
|
||||
|
||||
if (!style) {
|
||||
style = document.createElement('style');
|
||||
style.id = styleId;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
let cssRules = '* {';
|
||||
if (font !== null) {
|
||||
cssRules += `
|
||||
font-family: ${font} !important;
|
||||
`;
|
||||
}
|
||||
if (textSizeAdjust !== null) {
|
||||
cssRules += `
|
||||
-webkit-text-size-adjust: ${textSizeAdjust} !important;
|
||||
`;
|
||||
}
|
||||
cssRules += '}';
|
||||
|
||||
style.innerHTML = cssRules;
|
||||
|
||||
if (font === null && textSizeAdjust === null) {
|
||||
style.parentNode.removeChild(style);
|
||||
}
|
||||
}
|
||||
window.setTelegramFontOverrides = setTelegramFontOverrides;
|
||||
})();
|
||||
"""
|
||||
|
||||
@available(iOS 16.0, *)
|
||||
final class BrowserSearchOptions: UITextSearchOptions {
|
||||
override var wordMatchMethod: UITextSearchOptions.WordMatchMethod {
|
||||
return .contains
|
||||
}
|
||||
|
||||
override var stringCompareOptions: NSString.CompareOptions {
|
||||
return .caseInsensitive
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,38 +0,0 @@
|
|||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import SwiftSignalKit
|
||||
import TelegramCore
|
||||
import AccountContext
|
||||
import Svg
|
||||
|
||||
private var faviconCache: [String: UIImage] = [:]
|
||||
func fetchFavicon(context: AccountContext, url: String, size: CGSize) -> Signal<UIImage?, NoError> {
|
||||
if let icon = faviconCache[url] {
|
||||
return .single(icon)
|
||||
}
|
||||
return context.engine.resources.httpData(url: url)
|
||||
|> map(Optional.init)
|
||||
|> `catch` { _ -> Signal<Data?, NoError> in
|
||||
return .single(nil)
|
||||
}
|
||||
|> map { data in
|
||||
if let data {
|
||||
if let image = UIImage(data: data) {
|
||||
return image
|
||||
} else if url.lowercased().contains(".svg"), let preparedData = prepareSvgImage(data, false), let image = renderPreparedImage(preparedData, size, .clear, UIScreenScale, false) {
|
||||
return image
|
||||
}
|
||||
return nil
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|> beforeNext { image in
|
||||
if let image {
|
||||
Queue.mainQueue().async {
|
||||
faviconCache[url] = image
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
168
submodules/BrowserUI/Sources/SectionHeaderComponent.swift
Normal file
168
submodules/BrowserUI/Sources/SectionHeaderComponent.swift
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import ComponentFlow
|
||||
import TelegramPresentationData
|
||||
import MultilineTextComponent
|
||||
|
||||
final class SectionHeaderComponent: Component {
|
||||
enum Style {
|
||||
case blocks
|
||||
case plain
|
||||
}
|
||||
let theme: PresentationTheme
|
||||
let style: Style
|
||||
let title: String
|
||||
let actionTitle: String?
|
||||
let action: (() -> Void)?
|
||||
|
||||
init(
|
||||
theme: PresentationTheme,
|
||||
style: Style,
|
||||
title: String,
|
||||
actionTitle: String?,
|
||||
action: (() -> Void)?
|
||||
) {
|
||||
self.theme = theme
|
||||
self.style = style
|
||||
self.title = title
|
||||
self.actionTitle = actionTitle
|
||||
self.action = action
|
||||
}
|
||||
|
||||
static func ==(lhs: SectionHeaderComponent, rhs: SectionHeaderComponent) -> Bool {
|
||||
if lhs.theme !== rhs.theme {
|
||||
return false
|
||||
}
|
||||
if lhs.style != rhs.style {
|
||||
return false
|
||||
}
|
||||
if lhs.title != rhs.title {
|
||||
return false
|
||||
}
|
||||
if lhs.actionTitle != rhs.actionTitle {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
final class View: UIView {
|
||||
private let title = ComponentView<Empty>()
|
||||
private let backgroundView: BlurredBackgroundView
|
||||
private let action = ComponentView<Empty>()
|
||||
|
||||
private var component: SectionHeaderComponent?
|
||||
private weak var state: EmptyComponentState?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.backgroundView = BlurredBackgroundView(color: .clear, enableBlur: true)
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.addSubview(self.backgroundView)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func update(component: SectionHeaderComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
||||
let themeUpdated = self.component?.theme !== component.theme
|
||||
|
||||
self.component = component
|
||||
self.state = state
|
||||
|
||||
let height: CGFloat = 28.0
|
||||
let leftInset: CGFloat = 16.0
|
||||
let rightInset: CGFloat = 0.0
|
||||
|
||||
let previousTitleFrame = self.title.view?.frame
|
||||
|
||||
if themeUpdated {
|
||||
switch component.style {
|
||||
case .plain:
|
||||
self.backgroundView.isHidden = false
|
||||
self.backgroundView.updateColor(color: component.theme.rootController.navigationBar.blurredBackgroundColor, transition: .immediate)
|
||||
case .blocks:
|
||||
self.backgroundView.isHidden = true
|
||||
}
|
||||
}
|
||||
|
||||
let titleSize = self.title.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(string: component.title, font: Font.regular(13.0), textColor: component.theme.list.itemSecondaryTextColor))
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width - leftInset - rightInset, height: 100.0)
|
||||
)
|
||||
|
||||
let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: floor((height - titleSize.height) / 2.0)), size: titleSize)
|
||||
if let titleView = self.title.view {
|
||||
if titleView.superview == nil {
|
||||
titleView.isUserInteractionEnabled = false
|
||||
self.addSubview(titleView)
|
||||
}
|
||||
titleView.frame = titleFrame
|
||||
if let previousTitleFrame, previousTitleFrame.origin.x != titleFrame.origin.x {
|
||||
transition.animatePosition(view: titleView, from: CGPoint(x: previousTitleFrame.origin.x - titleFrame.origin.x, y: 0.0), to: CGPoint(), additive: true)
|
||||
}
|
||||
}
|
||||
|
||||
if let actionTitle = component.actionTitle {
|
||||
let actionSize = self.action.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(
|
||||
Button(content: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(string: actionTitle, font: Font.regular(13.0), textColor: component.theme.list.itemSecondaryTextColor))
|
||||
)), action: { [weak self] in
|
||||
if let self, let component = self.component {
|
||||
component.action?()
|
||||
}
|
||||
})
|
||||
),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width - leftInset - rightInset, height: 100.0)
|
||||
)
|
||||
if let view = self.action.view {
|
||||
if view.superview == nil {
|
||||
self.addSubview(view)
|
||||
if !transition.animation.isImmediate {
|
||||
view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
view.layer.animateScale(from: 0.01, to: 1.0, duration: 0.2)
|
||||
}
|
||||
}
|
||||
let actionFrame = CGRect(origin: CGPoint(x: availableSize.width - leftInset - actionSize.width, y: floor((height - titleSize.height) / 2.0)), size: actionSize)
|
||||
view.frame = actionFrame
|
||||
}
|
||||
} else if let view = self.action.view, view.superview != nil {
|
||||
if !transition.animation.isImmediate {
|
||||
view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { finished in
|
||||
if finished {
|
||||
view.removeFromSuperview()
|
||||
view.layer.removeAllAnimations()
|
||||
}
|
||||
})
|
||||
view.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false)
|
||||
} else {
|
||||
view.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
|
||||
let size = CGSize(width: availableSize.width, height: height)
|
||||
|
||||
transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(), size: size))
|
||||
self.backgroundView.update(size: size, transition: transition.containedViewLayoutTransition)
|
||||
|
||||
return size
|
||||
}
|
||||
}
|
||||
|
||||
func makeView() -> View {
|
||||
return View(frame: CGRect())
|
||||
}
|
||||
|
||||
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||
}
|
||||
}
|
||||
110
submodules/BrowserUI/Sources/Utils.swift
Normal file
110
submodules/BrowserUI/Sources/Utils.swift
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import SwiftSignalKit
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import AccountContext
|
||||
import TextFormat
|
||||
import UrlWhitelist
|
||||
import Svg
|
||||
|
||||
private var faviconCache: [String: UIImage] = [:]
|
||||
func fetchFavicon(context: AccountContext, url: String, size: CGSize) -> Signal<UIImage?, NoError> {
|
||||
if let icon = faviconCache[url] {
|
||||
return .single(icon)
|
||||
}
|
||||
return context.engine.resources.httpData(url: url)
|
||||
|> map(Optional.init)
|
||||
|> `catch` { _ -> Signal<Data?, NoError> in
|
||||
return .single(nil)
|
||||
}
|
||||
|> map { data in
|
||||
if let data {
|
||||
if let image = UIImage(data: data) {
|
||||
return image
|
||||
} else if url.lowercased().contains(".svg"), let preparedData = prepareSvgImage(data, false), let image = renderPreparedImage(preparedData, size, .clear, UIScreenScale, false) {
|
||||
return image
|
||||
}
|
||||
return nil
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|> beforeNext { image in
|
||||
if let image {
|
||||
Queue.mainQueue().async {
|
||||
faviconCache[url] = image
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getPrimaryUrl(message: Message) -> String? {
|
||||
var primaryUrl: String?
|
||||
if let webPage = message.media.first(where: { $0 is TelegramMediaWebpage }) as? TelegramMediaWebpage, let url = webPage.content.url {
|
||||
primaryUrl = url
|
||||
} else {
|
||||
var entities = message.textEntitiesAttribute?.entities
|
||||
if entities == nil {
|
||||
let parsedEntities = generateTextEntities(message.text, enabledTypes: .all)
|
||||
if !parsedEntities.isEmpty {
|
||||
entities = parsedEntities
|
||||
}
|
||||
}
|
||||
|
||||
if let entities {
|
||||
loop: for entity in entities {
|
||||
switch entity.type {
|
||||
case .Url, .Email:
|
||||
var range = NSRange(location: entity.range.lowerBound, length: entity.range.upperBound - entity.range.lowerBound)
|
||||
let nsString = message.text as NSString
|
||||
if range.location + range.length > nsString.length {
|
||||
range.location = max(0, nsString.length - range.length)
|
||||
range.length = nsString.length - range.location
|
||||
}
|
||||
let tempUrlString = nsString.substring(with: range)
|
||||
|
||||
var (urlString, concealed) = parseUrl(url: tempUrlString, wasConcealed: false)
|
||||
var parsedUrl = URL(string: urlString)
|
||||
if (parsedUrl == nil || parsedUrl!.host == nil || parsedUrl!.host!.isEmpty) && !urlString.contains("@") {
|
||||
urlString = "http://" + urlString
|
||||
parsedUrl = URL(string: urlString)
|
||||
}
|
||||
var host: String? = concealed ? urlString : parsedUrl?.host
|
||||
if host == nil {
|
||||
host = urlString
|
||||
}
|
||||
if let _ = parsedUrl, let _ = host {
|
||||
primaryUrl = urlString
|
||||
}
|
||||
break loop
|
||||
case let .TextUrl(url):
|
||||
let messageText = message.text
|
||||
|
||||
var range = NSRange(location: entity.range.lowerBound, length: entity.range.upperBound - entity.range.lowerBound)
|
||||
let nsString = messageText as NSString
|
||||
if range.location + range.length > nsString.length {
|
||||
range.location = max(0, nsString.length - range.length)
|
||||
range.length = nsString.length - range.location
|
||||
}
|
||||
|
||||
var (urlString, concealed) = parseUrl(url: url, wasConcealed: false)
|
||||
var parsedUrl = URL(string: urlString)
|
||||
if (parsedUrl == nil || parsedUrl!.host == nil || parsedUrl!.host!.isEmpty) && !urlString.contains("@") {
|
||||
urlString = "http://" + urlString
|
||||
parsedUrl = URL(string: urlString)
|
||||
}
|
||||
let host: String? = concealed ? urlString : parsedUrl?.host
|
||||
if let _ = parsedUrl, let _ = host {
|
||||
primaryUrl = urlString
|
||||
}
|
||||
break loop
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return primaryUrl
|
||||
}
|
||||
|
|
@ -30,6 +30,10 @@ public protocol MinimizableController: ViewController {
|
|||
|
||||
func requestMinimize(topEdgeOffset: CGFloat?, initialVelocity: CGFloat?)
|
||||
func makeContentSnapshotView() -> UIView?
|
||||
|
||||
func prepareContentSnapshotView()
|
||||
func resetContentSnapshotView()
|
||||
|
||||
func shouldDismissImmediately() -> Bool
|
||||
}
|
||||
|
||||
|
|
@ -66,6 +70,14 @@ public extension MinimizableController {
|
|||
return self.displayNode.view.snapshotView(afterScreenUpdates: false)
|
||||
}
|
||||
|
||||
func prepareContentSnapshotView() {
|
||||
|
||||
}
|
||||
|
||||
func resetContentSnapshotView() {
|
||||
|
||||
}
|
||||
|
||||
func shouldDismissImmediately() -> Bool {
|
||||
return true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -50,6 +50,15 @@ func makeNavigationLayout(mode: NavigationControllerMode, layout: ContainerViewL
|
|||
case .regular:
|
||||
requiresModal = true
|
||||
}
|
||||
case .modalInCompactLayout:
|
||||
switch layout.metrics.widthClass {
|
||||
case .compact:
|
||||
requiresModal = true
|
||||
case .regular:
|
||||
requiresModal = true
|
||||
beginsModal = true
|
||||
isFlat = true
|
||||
}
|
||||
}
|
||||
if requiresModal {
|
||||
controller._presentedInModal = true
|
||||
|
|
|
|||
|
|
@ -65,6 +65,7 @@ public enum ViewControllerNavigationPresentation {
|
|||
case flatModal
|
||||
case standaloneModal
|
||||
case modalInLargeLayout
|
||||
case modalInCompactLayout
|
||||
}
|
||||
|
||||
public enum TabBarItemContextActionType {
|
||||
|
|
|
|||
|
|
@ -3212,10 +3212,12 @@ public final class DrawingToolsInteraction {
|
|||
self.isActive = false
|
||||
}
|
||||
|
||||
public func insertEntity(_ entity: DrawingEntity, scale: CGFloat? = nil, position: CGPoint? = nil) {
|
||||
public func insertEntity(_ entity: DrawingEntity, scale: CGFloat? = nil, position: CGPoint? = nil, select: Bool = true) {
|
||||
self.entitiesView.prepareNewEntity(entity, scale: scale, position: position)
|
||||
self.entitiesView.add(entity)
|
||||
self.entitiesView.selectEntity(entity, animate: !(entity is DrawingTextEntity))
|
||||
if select {
|
||||
self.entitiesView.selectEntity(entity, animate: !(entity is DrawingTextEntity))
|
||||
}
|
||||
|
||||
if let entityView = self.entitiesView.getView(for: entity.uuid) {
|
||||
if let textEntityView = entityView as? DrawingTextEntityView {
|
||||
|
|
|
|||
|
|
@ -286,7 +286,7 @@ private func allOpenInOptions(context: AccountContext, item: OpenInItem) -> [Ope
|
|||
}))
|
||||
}
|
||||
|
||||
options.append(OpenInOption(identifier: "2gis", application: .other(title: "2GIS", identifier: 481627348, scheme: "dgis", store: nil), action: {
|
||||
options.append(OpenInOption(identifier: "2gis", application: .other(title: "2GIS", identifier: 481627348, scheme: "dgis", store: "ru"), action: {
|
||||
let coordinates = "\(lon),\(lat)"
|
||||
if let _ = directions {
|
||||
return .openUrl(url: "dgis://2gis.ru/routeSearch/to/\(coordinates)/go")
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import TelegramCore
|
|||
import TelegramPresentationData
|
||||
import AccountContext
|
||||
import UrlEscaping
|
||||
import ActivityIndicator
|
||||
|
||||
private final class WebBrowserDomainInputFieldNode: ASDisplayNode, ASEditableTextNodeDelegate {
|
||||
private var theme: PresentationTheme
|
||||
|
|
@ -116,7 +117,12 @@ private final class WebBrowserDomainInputFieldNode: ASDisplayNode, ASEditableTex
|
|||
private let domainRegex = try? NSRegularExpression(pattern: "^(https?://)?([a-zA-Z0-9-]+\\.?)*([a-zA-Z]*)?(:)?(/)?$", options: [])
|
||||
private let pathRegex = try? NSRegularExpression(pattern: "^(https?://)?([a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,6}/", options: [])
|
||||
|
||||
var inProgress = false
|
||||
|
||||
func editableTextNode(_ editableTextNode: ASEditableTextNode, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
|
||||
if self.inProgress {
|
||||
return false
|
||||
}
|
||||
if text == "\n" {
|
||||
self.complete?()
|
||||
return false
|
||||
|
|
@ -168,6 +174,7 @@ private final class WebBrowserDomainAlertContentNode: AlertContentNode {
|
|||
|
||||
private let titleNode: ASTextNode
|
||||
private let textNode: ASTextNode
|
||||
let activityIndicator: ActivityIndicator
|
||||
let inputFieldNode: WebBrowserDomainInputFieldNode
|
||||
|
||||
private let actionNodesSeparator: ASDisplayNode
|
||||
|
|
@ -198,6 +205,9 @@ private final class WebBrowserDomainAlertContentNode: AlertContentNode {
|
|||
self.textNode = ASTextNode()
|
||||
self.textNode.maximumNumberOfLines = 2
|
||||
|
||||
self.activityIndicator = ActivityIndicator(type: .custom(ptheme.rootController.navigationBar.secondaryTextColor, 20.0, 1.5, false), speed: .slow)
|
||||
self.activityIndicator.isHidden = true
|
||||
|
||||
self.inputFieldNode = WebBrowserDomainInputFieldNode(theme: ptheme, placeholder: strings.WebBrowser_Exceptions_Create_Placeholder)
|
||||
self.inputFieldNode.text = ""
|
||||
|
||||
|
|
@ -224,7 +234,8 @@ private final class WebBrowserDomainAlertContentNode: AlertContentNode {
|
|||
self.addSubnode(self.textNode)
|
||||
|
||||
self.addSubnode(self.inputFieldNode)
|
||||
|
||||
self.addSubnode(self.activityIndicator)
|
||||
|
||||
self.addSubnode(self.actionNodesSeparator)
|
||||
|
||||
for actionNode in self.actionNodes {
|
||||
|
|
@ -335,9 +346,13 @@ private final class WebBrowserDomainAlertContentNode: AlertContentNode {
|
|||
let inputFieldWidth = resultWidth
|
||||
let inputFieldHeight = self.inputFieldNode.updateLayout(width: inputFieldWidth, transition: transition)
|
||||
let inputHeight = inputFieldHeight
|
||||
transition.updateFrame(node: self.inputFieldNode, frame: CGRect(x: 0.0, y: origin.y, width: resultWidth, height: inputFieldHeight))
|
||||
let inputFrame = CGRect(x: 0.0, y: origin.y, width: resultWidth, height: inputFieldHeight)
|
||||
transition.updateFrame(node: self.inputFieldNode, frame: inputFrame)
|
||||
transition.updateAlpha(node: self.inputFieldNode, alpha: inputHeight > 0.0 ? 1.0 : 0.0)
|
||||
|
||||
let activitySize = CGSize(width: 20.0, height: 20.0)
|
||||
transition.updateFrame(node: self.activityIndicator, frame: CGRect(origin: CGPoint(x: inputFrame.maxX - activitySize.width - 23.0, y: inputFrame.midY - activitySize.height / 2.0 - 3.0), size: activitySize))
|
||||
|
||||
let resultSize = CGSize(width: resultWidth, height: titleSize.height + textSize.height + spacing + inputHeight + actionsHeight + insets.top + insets.bottom)
|
||||
|
||||
transition.updateFrame(node: self.actionNodesSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel)))
|
||||
|
|
@ -404,11 +419,16 @@ public func webBrowserDomainController(context: AccountContext, updatedPresentat
|
|||
var dismissImpl: ((Bool) -> Void)?
|
||||
var applyImpl: (() -> Void)?
|
||||
|
||||
var inProgress = false
|
||||
let actions: [TextAlertAction] = [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {
|
||||
dismissImpl?(true)
|
||||
apply(nil)
|
||||
if !inProgress {
|
||||
dismissImpl?(true)
|
||||
apply(nil)
|
||||
}
|
||||
}), TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Done, action: {
|
||||
applyImpl?()
|
||||
if !inProgress {
|
||||
applyImpl?()
|
||||
}
|
||||
})]
|
||||
|
||||
let contentNode = WebBrowserDomainAlertContentNode(theme: AlertControllerTheme(presentationData: presentationData), ptheme: presentationData.theme, strings: presentationData.strings, actions: actions)
|
||||
|
|
@ -419,9 +439,12 @@ public func webBrowserDomainController(context: AccountContext, updatedPresentat
|
|||
guard let contentNode = contentNode else {
|
||||
return
|
||||
}
|
||||
inProgress = true
|
||||
contentNode.inputFieldNode.inProgress = true
|
||||
contentNode.activityIndicator.isHidden = false
|
||||
|
||||
let updatedLink = explicitUrl(contentNode.link)
|
||||
if !updatedLink.isEmpty && isValidUrl(updatedLink, validSchemes: ["http": true, "https": true]) {
|
||||
dismissImpl?(true)
|
||||
apply(updatedLink)
|
||||
} else {
|
||||
contentNode.animateError()
|
||||
|
|
|
|||
|
|
@ -7,32 +7,43 @@ import TelegramPresentationData
|
|||
import TelegramCore
|
||||
import AccountContext
|
||||
import ItemListUI
|
||||
import PhotoResources
|
||||
|
||||
public class WebBrowserDomainExceptionItem: ListViewItem, ItemListItem {
|
||||
private enum RevealOptionKey: Int32 {
|
||||
case delete
|
||||
}
|
||||
|
||||
final class WebBrowserDomainExceptionItem: ListViewItem, ItemListItem {
|
||||
let presentationData: ItemListPresentationData
|
||||
let context: AccountContext?
|
||||
let context: AccountContext
|
||||
let title: String
|
||||
let label: String
|
||||
public let sectionId: ItemListSectionId
|
||||
let icon: TelegramMediaImage?
|
||||
let sectionId: ItemListSectionId
|
||||
let style: ItemListStyle
|
||||
let deleted: (() -> Void)?
|
||||
|
||||
public init(
|
||||
init(
|
||||
presentationData: ItemListPresentationData,
|
||||
context: AccountContext? = nil,
|
||||
context: AccountContext,
|
||||
title: String,
|
||||
label: String,
|
||||
icon: TelegramMediaImage?,
|
||||
sectionId: ItemListSectionId,
|
||||
style: ItemListStyle
|
||||
style: ItemListStyle,
|
||||
deleted: (() -> Void)?
|
||||
) {
|
||||
self.presentationData = presentationData
|
||||
self.context = context
|
||||
self.title = title
|
||||
self.label = label
|
||||
self.icon = icon
|
||||
self.sectionId = sectionId
|
||||
self.style = style
|
||||
self.deleted = deleted
|
||||
}
|
||||
|
||||
public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
|
||||
func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
|
||||
async {
|
||||
let node = WebBrowserDomainExceptionItemNode()
|
||||
let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
|
||||
|
|
@ -48,7 +59,7 @@ public class WebBrowserDomainExceptionItem: ListViewItem, ItemListItem {
|
|||
}
|
||||
}
|
||||
|
||||
public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
|
||||
func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
|
||||
Queue.mainQueue().async {
|
||||
if let nodeValue = node() as? WebBrowserDomainExceptionItemNode {
|
||||
let makeLayout = nodeValue.asyncLayout()
|
||||
|
|
@ -65,33 +76,34 @@ public class WebBrowserDomainExceptionItem: ListViewItem, ItemListItem {
|
|||
}
|
||||
}
|
||||
|
||||
public var selectable: Bool = false
|
||||
var selectable: Bool = false
|
||||
|
||||
public func selected(listView: ListView){
|
||||
func selected(listView: ListView){
|
||||
}
|
||||
}
|
||||
|
||||
public class WebBrowserDomainExceptionItemNode: ListViewItemNode, ItemListItemNode {
|
||||
final class WebBrowserDomainExceptionItemNode: ItemListRevealOptionsItemNode, ItemListItemNode {
|
||||
private let backgroundNode: ASDisplayNode
|
||||
private let topStripeNode: ASDisplayNode
|
||||
private let bottomStripeNode: ASDisplayNode
|
||||
private let maskNode: ASImageNode
|
||||
|
||||
let iconNode: ASImageNode
|
||||
let iconNode: TransformImageNode
|
||||
let titleNode: TextNode
|
||||
let labelNode: TextNode
|
||||
|
||||
private let activateArea: AccessibilityAreaNode
|
||||
|
||||
private var item: WebBrowserDomainExceptionItem?
|
||||
private var layoutParams: ListViewItemLayoutParams?
|
||||
|
||||
override public var canBeSelected: Bool {
|
||||
return false
|
||||
}
|
||||
|
||||
public var tag: ItemListItemTag? = nil
|
||||
var tag: ItemListItemTag? = nil
|
||||
|
||||
public init() {
|
||||
init() {
|
||||
self.backgroundNode = ASDisplayNode()
|
||||
self.backgroundNode.isLayerBacked = true
|
||||
self.backgroundNode.backgroundColor = .white
|
||||
|
|
@ -105,7 +117,7 @@ public class WebBrowserDomainExceptionItemNode: ListViewItemNode, ItemListItemNo
|
|||
self.bottomStripeNode = ASDisplayNode()
|
||||
self.bottomStripeNode.isLayerBacked = true
|
||||
|
||||
self.iconNode = ASImageNode()
|
||||
self.iconNode = TransformImageNode()
|
||||
self.iconNode.isLayerBacked = true
|
||||
self.iconNode.displaysAsynchronously = false
|
||||
|
||||
|
|
@ -117,15 +129,16 @@ public class WebBrowserDomainExceptionItemNode: ListViewItemNode, ItemListItemNo
|
|||
|
||||
self.activateArea = AccessibilityAreaNode()
|
||||
|
||||
super.init(layerBacked: false, dynamicBounce: false)
|
||||
super.init(layerBacked: false, dynamicBounce: false, rotated: false, seeThrough: false)
|
||||
|
||||
self.addSubnode(self.iconNode)
|
||||
self.addSubnode(self.titleNode)
|
||||
self.addSubnode(self.labelNode)
|
||||
|
||||
self.addSubnode(self.activateArea)
|
||||
}
|
||||
|
||||
public func asyncLayout() -> (_ item: WebBrowserDomainExceptionItem, _ params: ListViewItemLayoutParams, _ insets: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
|
||||
func asyncLayout() -> (_ item: WebBrowserDomainExceptionItem, _ params: ListViewItemLayoutParams, _ insets: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
|
||||
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
|
||||
let makeLabelLayout = TextNode.asyncLayout(self.labelNode)
|
||||
|
||||
|
|
@ -143,7 +156,7 @@ public class WebBrowserDomainExceptionItemNode: ListViewItemNode, ItemListItemNo
|
|||
let itemBackgroundColor: UIColor
|
||||
let itemSeparatorColor: UIColor
|
||||
|
||||
let leftInset = 16.0 + params.leftInset + 43.0
|
||||
let leftInset = 16.0 + params.leftInset + 46.0
|
||||
|
||||
let titleColor: UIColor = item.presentationData.theme.list.itemPrimaryTextColor
|
||||
let labelColor: UIColor = item.presentationData.theme.list.itemAccentColor
|
||||
|
|
@ -180,6 +193,7 @@ public class WebBrowserDomainExceptionItemNode: ListViewItemNode, ItemListItemNo
|
|||
return (ListViewItemNodeLayout(contentSize: contentSize, insets: insets), { [weak self] in
|
||||
if let strongSelf = self {
|
||||
strongSelf.item = item
|
||||
strongSelf.layoutParams = params
|
||||
|
||||
strongSelf.activateArea.frame = CGRect(origin: CGPoint(x: params.leftInset, y: 0.0), size: CGSize(width: params.width - params.leftInset - params.rightInset, height: layout.contentSize.height))
|
||||
strongSelf.activateArea.accessibilityLabel = item.title
|
||||
|
|
@ -191,6 +205,15 @@ public class WebBrowserDomainExceptionItemNode: ListViewItemNode, ItemListItemNo
|
|||
strongSelf.backgroundNode.backgroundColor = itemBackgroundColor
|
||||
}
|
||||
|
||||
let iconSize = CGSize(width: 40.0, height: 40.0)
|
||||
var imageSize = iconSize
|
||||
if currentItem?.icon?.id != item.icon?.id, let icon = item.icon {
|
||||
strongSelf.iconNode.setSignal(chatMessagePhoto(postbox: item.context.account.postbox, userLocation: .other, photoReference: .standalone(media: icon)))
|
||||
}
|
||||
if let icon = item.icon, let dimensions = largestImageRepresentation(icon.representations)?.dimensions.cgSize {
|
||||
imageSize = dimensions.aspectFilled(imageSize)
|
||||
}
|
||||
|
||||
let _ = titleApply()
|
||||
let _ = labelApply()
|
||||
|
||||
|
|
@ -256,25 +279,69 @@ public class WebBrowserDomainExceptionItemNode: ListViewItemNode, ItemListItemNo
|
|||
centralContentHeight += titleSpacing
|
||||
centralContentHeight += labelLayout.size.height
|
||||
|
||||
let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: floor((height - centralContentHeight) / 2.0)), size: titleLayout.size)
|
||||
let titleFrame = CGRect(origin: CGPoint(x: leftInset + strongSelf.revealOffset, y: floor((height - centralContentHeight) / 2.0)), size: titleLayout.size)
|
||||
strongSelf.titleNode.frame = titleFrame
|
||||
|
||||
let labelFrame = CGRect(origin: CGPoint(x: leftInset, y: titleFrame.maxY + titleSpacing), size: labelLayout.size)
|
||||
let labelFrame = CGRect(origin: CGPoint(x: leftInset + strongSelf.revealOffset, y: titleFrame.maxY + titleSpacing), size: labelLayout.size)
|
||||
strongSelf.labelNode.frame = labelFrame
|
||||
|
||||
let iconFrame = CGRect(origin: CGPoint(x: params.leftInset + 11.0 + strongSelf.revealOffset, y: floorToScreenPixels((contentSize.height - iconSize.height) / 2.0)), size: iconSize)
|
||||
strongSelf.iconNode.frame = iconFrame
|
||||
|
||||
strongSelf.iconNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(radius: 7.0), imageSize: imageSize, boundingSize: iconSize, intrinsicInsets: .zero))()
|
||||
|
||||
strongSelf.updateLayout(size: layout.contentSize, leftInset: params.leftInset, rightInset: params.rightInset)
|
||||
|
||||
var revealOptions: [ItemListRevealOption] = []
|
||||
revealOptions.append(ItemListRevealOption(key: RevealOptionKey.delete.rawValue, title: item.presentationData.strings.Common_Delete, icon: .none, color: item.presentationData.theme.list.itemDisclosureActions.destructive.fillColor, textColor: item.presentationData.theme.list.itemDisclosureActions.destructive.foregroundColor))
|
||||
strongSelf.setRevealOptions((left: [], right: revealOptions))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
override public func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) {
|
||||
override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) {
|
||||
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
|
||||
}
|
||||
|
||||
override public func animateAdded(_ currentTimestamp: Double, duration: Double) {
|
||||
override func animateAdded(_ currentTimestamp: Double, duration: Double) {
|
||||
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
}
|
||||
|
||||
override public func animateRemoved(_ currentTimestamp: Double, duration: Double) {
|
||||
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
|
||||
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
|
||||
}
|
||||
|
||||
override func updateRevealOffset(offset: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||
super.updateRevealOffset(offset: offset, transition: transition)
|
||||
|
||||
if let params = self.layoutParams {
|
||||
let leftInset: CGFloat = 16.0 + params.leftInset + 46.0
|
||||
|
||||
var iconFrame = self.iconNode.frame
|
||||
iconFrame.origin.x = params.leftInset + 11.0 + offset
|
||||
transition.updateFrame(node: self.iconNode, frame: iconFrame)
|
||||
|
||||
var titleFrame = self.titleNode.frame
|
||||
titleFrame.origin.x = leftInset + offset
|
||||
transition.updateFrame(node: self.titleNode, frame: titleFrame)
|
||||
|
||||
var subtitleFrame = self.labelNode.frame
|
||||
subtitleFrame.origin.x = leftInset + offset
|
||||
transition.updateFrame(node: self.labelNode, frame: subtitleFrame)
|
||||
}
|
||||
}
|
||||
|
||||
override func revealOptionSelected(_ option: ItemListRevealOption, animated: Bool) {
|
||||
if let item = self.item {
|
||||
switch option.key {
|
||||
case RevealOptionKey.delete.rawValue:
|
||||
item.deleted?()
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
self.setRevealOptionsOpened(false, animated: true)
|
||||
self.revealOptionsInteractivelyClosed()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import Postbox
|
|||
import TelegramCore
|
||||
import TelegramPresentationData
|
||||
import TelegramUIPreferences
|
||||
import PresentationDataUtils
|
||||
import ItemListUI
|
||||
import AccountContext
|
||||
import OpenInExternalAppUI
|
||||
|
|
@ -13,12 +14,15 @@ import ItemListPeerActionItem
|
|||
import UndoUI
|
||||
import WebKit
|
||||
import LinkPresentation
|
||||
import CoreServices
|
||||
import PersistentStringHash
|
||||
|
||||
private final class WebBrowserSettingsControllerArguments {
|
||||
let context: AccountContext
|
||||
let updateDefaultBrowser: (String?) -> Void
|
||||
let clearCookies: () -> Void
|
||||
let addException: () -> Void
|
||||
let removeException: (String) -> Void
|
||||
let clearExceptions: () -> Void
|
||||
|
||||
init(
|
||||
|
|
@ -26,12 +30,14 @@ private final class WebBrowserSettingsControllerArguments {
|
|||
updateDefaultBrowser: @escaping (String?) -> Void,
|
||||
clearCookies: @escaping () -> Void,
|
||||
addException: @escaping () -> Void,
|
||||
removeException: @escaping (String) -> Void,
|
||||
clearExceptions: @escaping () -> Void
|
||||
) {
|
||||
self.context = context
|
||||
self.updateDefaultBrowser = updateDefaultBrowser
|
||||
self.clearCookies = clearCookies
|
||||
self.addException = addException
|
||||
self.removeException = removeException
|
||||
self.clearExceptions = clearExceptions
|
||||
}
|
||||
}
|
||||
|
|
@ -66,7 +72,30 @@ private enum WebBrowserSettingsControllerEntry: ItemListNodeEntry {
|
|||
}
|
||||
}
|
||||
|
||||
var stableId: Int32 {
|
||||
var stableId: UInt64 {
|
||||
switch self {
|
||||
case .browserHeader:
|
||||
return 0
|
||||
case let .browser(_, _, _, _, _, index):
|
||||
return UInt64(1 + index)
|
||||
case .clearCookies:
|
||||
return 102
|
||||
case .clearCookiesInfo:
|
||||
return 103
|
||||
case .exceptionsHeader:
|
||||
return 104
|
||||
case .exceptionsAdd:
|
||||
return 105
|
||||
case let .exception(_, _, exception):
|
||||
return 2000 + exception.domain.persistentHashValue
|
||||
case .exceptionsClear:
|
||||
return 1000
|
||||
case .exceptionsInfo:
|
||||
return 1001
|
||||
}
|
||||
}
|
||||
|
||||
var sortId: Int32 {
|
||||
switch self {
|
||||
case .browserHeader:
|
||||
return 0
|
||||
|
|
@ -149,7 +178,7 @@ private enum WebBrowserSettingsControllerEntry: ItemListNodeEntry {
|
|||
}
|
||||
|
||||
static func <(lhs: WebBrowserSettingsControllerEntry, rhs: WebBrowserSettingsControllerEntry) -> Bool {
|
||||
return lhs.stableId < rhs.stableId
|
||||
return lhs.sortId < rhs.sortId
|
||||
}
|
||||
|
||||
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
|
||||
|
|
@ -170,7 +199,9 @@ private enum WebBrowserSettingsControllerEntry: ItemListNodeEntry {
|
|||
case let .exceptionsHeader(_, text):
|
||||
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
|
||||
case let .exception(_, _, exception):
|
||||
return WebBrowserDomainExceptionItem(presentationData: presentationData, context: arguments.context, title: exception.title, label: exception.domain, sectionId: self.section, style: .blocks)
|
||||
return WebBrowserDomainExceptionItem(presentationData: presentationData, context: arguments.context, title: exception.title, label: exception.domain, icon: exception.icon, sectionId: self.section, style: .blocks, deleted: {
|
||||
arguments.removeException(exception.domain)
|
||||
})
|
||||
case let .exceptionsAdd(_, text):
|
||||
return ItemListPeerActionItem(presentationData: presentationData, icon: PresentationResourcesItemList.plusIconImage(presentationData.theme), title: text, sectionId: self.section, height: .generic, color: .accent, editing: false, action: {
|
||||
arguments.addException()
|
||||
|
|
@ -207,7 +238,7 @@ private func webBrowserSettingsControllerEntries(context: AccountContext, presen
|
|||
entries.append(.exceptionsAdd(presentationData.theme, presentationData.strings.WebBrowser_Exceptions_AddException))
|
||||
|
||||
var exceptionIndex: Int32 = 0
|
||||
for exception in settings.exceptions {
|
||||
for exception in settings.exceptions.reversed() {
|
||||
entries.append(.exception(exceptionIndex, presentationData.theme, exception))
|
||||
exceptionIndex += 1
|
||||
}
|
||||
|
|
@ -225,6 +256,7 @@ private func webBrowserSettingsControllerEntries(context: AccountContext, presen
|
|||
public func webBrowserSettingsController(context: AccountContext) -> ViewController {
|
||||
var clearCookiesImpl: (() -> Void)?
|
||||
var addExceptionImpl: (() -> Void)?
|
||||
var removeExceptionImpl: ((String) -> Void)?
|
||||
var clearExceptionsImpl: (() -> Void)?
|
||||
|
||||
let arguments = WebBrowserSettingsControllerArguments(
|
||||
|
|
@ -240,6 +272,9 @@ public func webBrowserSettingsController(context: AccountContext) -> ViewControl
|
|||
addException: {
|
||||
addExceptionImpl?()
|
||||
},
|
||||
removeException: { domain in
|
||||
removeExceptionImpl?(domain)
|
||||
},
|
||||
clearExceptions: {
|
||||
clearExceptionsImpl?()
|
||||
}
|
||||
|
|
@ -261,6 +296,9 @@ public func webBrowserSettingsController(context: AccountContext) -> ViewControl
|
|||
if previousSettings.defaultWebBrowser != settings.defaultWebBrowser {
|
||||
animateChanges = true
|
||||
}
|
||||
if previousSettings.exceptions.count != settings.exceptions.count {
|
||||
animateChanges = true
|
||||
}
|
||||
}
|
||||
|
||||
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.WebBrowser_Title), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back))
|
||||
|
|
@ -290,9 +328,11 @@ public func webBrowserSettingsController(context: AccountContext) -> ViewControl
|
|||
}
|
||||
|
||||
addExceptionImpl = { [weak controller] in
|
||||
var dismissImpl: (() -> Void)?
|
||||
let linkController = webBrowserDomainController(context: context, apply: { url in
|
||||
if let url {
|
||||
let _ = fetchDomainExceptionInfo(url: url).startStandalone(next: { newException in
|
||||
let _ = (fetchDomainExceptionInfo(context: context, url: url)
|
||||
|> deliverOnMainQueue).startStandalone(next: { newException in
|
||||
let _ = updateWebBrowserSettingsInteractively(accountManager: context.sharedContext.accountManager, { currentSettings in
|
||||
var currentExceptions = currentSettings.exceptions
|
||||
for exception in currentExceptions {
|
||||
|
|
@ -303,18 +343,44 @@ public func webBrowserSettingsController(context: AccountContext) -> ViewControl
|
|||
currentExceptions.append(newException)
|
||||
return currentSettings.withUpdatedExceptions(currentExceptions)
|
||||
}).start()
|
||||
dismissImpl?()
|
||||
})
|
||||
}
|
||||
})
|
||||
dismissImpl = { [weak linkController] in
|
||||
linkController?.view.endEditing(true)
|
||||
linkController?.dismissAnimated()
|
||||
}
|
||||
controller?.present(linkController, in: .window(.root))
|
||||
}
|
||||
|
||||
clearExceptionsImpl = {
|
||||
removeExceptionImpl = { domain in
|
||||
let _ = updateWebBrowserSettingsInteractively(accountManager: context.sharedContext.accountManager, { currentSettings in
|
||||
return currentSettings.withUpdatedExceptions([])
|
||||
let updatedExceptions = currentSettings.exceptions.filter { $0.domain != domain }
|
||||
return currentSettings.withUpdatedExceptions(updatedExceptions)
|
||||
}).start()
|
||||
}
|
||||
|
||||
clearExceptionsImpl = { [weak controller] in
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
|
||||
let alertController = textAlertController(
|
||||
context: context,
|
||||
updatedPresentationData: nil,
|
||||
title: nil,
|
||||
text: presentationData.strings.WebBrowser_Exceptions_ClearConfirmation_Text,
|
||||
actions: [
|
||||
TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {}),
|
||||
TextAlertAction(type: .defaultAction, title: presentationData.strings.WebBrowser_Exceptions_ClearConfirmation_Clear, action: {
|
||||
let _ = updateWebBrowserSettingsInteractively(accountManager: context.sharedContext.accountManager, { currentSettings in
|
||||
return currentSettings.withUpdatedExceptions([])
|
||||
}).start()
|
||||
})
|
||||
]
|
||||
)
|
||||
controller?.present(alertController, in: .window(.root))
|
||||
}
|
||||
|
||||
return controller
|
||||
}
|
||||
|
||||
|
|
@ -333,22 +399,59 @@ private func cleanDomain(url: String) -> (domain: String, fullUrl: String) {
|
|||
}
|
||||
}
|
||||
|
||||
private func fetchDomainExceptionInfo(url: String) -> Signal<WebBrowserException, NoError> {
|
||||
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) {
|
||||
return Signal { subscriber in
|
||||
let metadataProvider = LPMetadataProvider()
|
||||
metadataProvider.shouldFetchSubresources = true
|
||||
metadataProvider.startFetchingMetadata(for: url, completionHandler: { metadata, _ in
|
||||
let title = metadata?.value(forKey: "_siteName") as? String ?? metadata?.title
|
||||
subscriber.putNext(WebBrowserException(domain: domain, title: title ?? domain))
|
||||
subscriber.putCompletion()
|
||||
let completeWithImage: (Data?) -> Void = { imageData in
|
||||
var image: TelegramMediaImage?
|
||||
if let imageData, let parsedImage = UIImage(data: imageData) {
|
||||
let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max))
|
||||
context.account.postbox.mediaBox.storeResourceData(resource.id, data: imageData)
|
||||
image = TelegramMediaImage(
|
||||
imageId: MediaId(namespace: Namespaces.Media.LocalImage, id: Int64.random(in: Int64.min ... Int64.max)),
|
||||
representations: [
|
||||
TelegramMediaImageRepresentation(
|
||||
dimensions: PixelDimensions(width: Int32(parsedImage.size.width), height: Int32(parsedImage.size.height)),
|
||||
resource: resource,
|
||||
progressiveSizes: [],
|
||||
immediateThumbnailData: nil,
|
||||
hasVideo: false,
|
||||
isPersonal: false
|
||||
)
|
||||
],
|
||||
immediateThumbnailData: nil,
|
||||
reference: nil,
|
||||
partialReference: nil,
|
||||
flags: []
|
||||
)
|
||||
}
|
||||
|
||||
let title = metadata?.value(forKey: "_siteName") as? String ?? metadata?.title
|
||||
subscriber.putNext(WebBrowserException(domain: domain, title: title ?? domain, icon: image))
|
||||
subscriber.putCompletion()
|
||||
}
|
||||
|
||||
if let imageProvider = metadata?.iconProvider {
|
||||
imageProvider.loadFileRepresentation(forTypeIdentifier: kUTTypeImage as String, completionHandler: { imageUrl, _ in
|
||||
guard let imageUrl, let imageData = try? Data(contentsOf: imageUrl) else {
|
||||
completeWithImage(nil)
|
||||
return
|
||||
}
|
||||
completeWithImage(imageData)
|
||||
})
|
||||
} else {
|
||||
completeWithImage(nil)
|
||||
}
|
||||
})
|
||||
return ActionDisposable {
|
||||
metadataProvider.cancel()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return .single(WebBrowserException(domain: domain, title: domain))
|
||||
return .single(WebBrowserException(domain: domain, title: domain, icon: nil))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -192,7 +192,17 @@ public enum CodableDrawingEntity: Equatable {
|
|||
url: url
|
||||
)
|
||||
case let .weather(entity):
|
||||
let color: UInt32 = 0xffffffff
|
||||
let color: UInt32
|
||||
switch entity.style {
|
||||
case .white:
|
||||
color = 0xffffffff
|
||||
case .black:
|
||||
color = 0xff000000
|
||||
case .transparent:
|
||||
color = 0x51000000
|
||||
case .custom:
|
||||
color = entity.color.toUIColor().argb
|
||||
}
|
||||
return .weather(
|
||||
coordinates: coordinates,
|
||||
emoji: entity.emoji,
|
||||
|
|
|
|||
|
|
@ -3155,6 +3155,10 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let initialLink = controller.initialLink {
|
||||
self.addInitialLink(initialLink)
|
||||
}
|
||||
}
|
||||
|
||||
private var initialMaskScale: CGFloat = .zero
|
||||
|
|
@ -4559,6 +4563,45 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
|||
controller.push(linkController)
|
||||
}
|
||||
|
||||
func addInitialLink(_ link: String) {
|
||||
guard self.context.isPremium else {
|
||||
return
|
||||
}
|
||||
let text = link
|
||||
|
||||
var attributes: [MessageAttribute] = []
|
||||
attributes.append(TextEntitiesMessageAttribute(entities: [.init(range: 0 ..< (text as NSString).length, type: .Url)]))
|
||||
|
||||
// attributes.append(WebpagePreviewMessageAttribute(leadingPreview: !self.positionBelowText, forceLargeMedia: self.largeMedia, isManuallyAdded: false, isSafe: true))
|
||||
|
||||
let effectiveMedia: TelegramMediaWebpage? = nil
|
||||
// if let webpage = self.webpage, case .Loaded = webpage.content {
|
||||
// effectiveMedia = webpage
|
||||
// }
|
||||
|
||||
let peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(1))
|
||||
let message = Message(stableId: 1, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 0, flags: [.Incoming], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: nil, text: text, attributes: attributes, media: effectiveMedia.flatMap { [$0] } ?? [], peers: SimpleDictionary(), associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:])
|
||||
|
||||
let renderer = DrawingMessageRenderer(context: self.context, messages: [message], parentView: self.view, isLink: true)
|
||||
renderer.render(completion: { [weak self] renderResult in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
let result = CreateLinkScreen.Result(
|
||||
url: link,
|
||||
name: "",
|
||||
webpage: effectiveMedia,
|
||||
positionBelowText: false,
|
||||
largeMedia: nil,
|
||||
image: effectiveMedia != nil ? renderResult.dayImage : nil,
|
||||
nightImage: effectiveMedia != nil ? renderResult.nightImage : nil
|
||||
)
|
||||
|
||||
let entity = DrawingLinkEntity(url: result.url, name: result.name, webpage: result.webpage, positionBelowText: result.positionBelowText, largeMedia: result.largeMedia, style: .white)
|
||||
self.interaction?.insertEntity(entity, position: CGPoint(x: storyDimensions.width / 2.0, y: storyDimensions.width / 3.0 * 4.0), select: false)
|
||||
})
|
||||
}
|
||||
|
||||
func addReaction() {
|
||||
guard let controller = self.controller else {
|
||||
return
|
||||
|
|
@ -4625,7 +4668,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
|||
}
|
||||
}
|
||||
if currentWeatherCount >= maxWeatherCount {
|
||||
self.controller?.hapticFeedback.error()
|
||||
self.controller?.presentWeatherLimitTooltip()
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -5535,6 +5578,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
|||
fileprivate let initialPrivacy: EngineStoryPrivacy?
|
||||
fileprivate let initialMediaAreas: [MediaArea]?
|
||||
fileprivate let initialVideoPosition: Double?
|
||||
fileprivate let initialLink: String?
|
||||
|
||||
fileprivate let transitionIn: TransitionIn?
|
||||
fileprivate let transitionOut: (Bool, Bool?) -> TransitionOut?
|
||||
|
|
@ -5569,6 +5613,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
|||
initialPrivacy: EngineStoryPrivacy? = nil,
|
||||
initialMediaAreas: [MediaArea]? = nil,
|
||||
initialVideoPosition: Double? = nil,
|
||||
initialLink: String? = nil,
|
||||
transitionIn: TransitionIn?,
|
||||
transitionOut: @escaping (Bool, Bool?) -> TransitionOut?,
|
||||
completion: @escaping (MediaEditorScreen.Result, @escaping (@escaping () -> Void) -> Void) -> Void
|
||||
|
|
@ -5583,6 +5628,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
|||
self.initialPrivacy = initialPrivacy
|
||||
self.initialMediaAreas = initialMediaAreas
|
||||
self.initialVideoPosition = initialVideoPosition
|
||||
self.initialLink = initialLink
|
||||
self.transitionIn = transitionIn
|
||||
self.transitionOut = transitionOut
|
||||
self.completion = completion
|
||||
|
|
@ -6216,6 +6262,29 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
|||
})
|
||||
self.present(controller, in: .window(.root))
|
||||
}
|
||||
|
||||
fileprivate func presentWeatherLimitTooltip() {
|
||||
self.hapticFeedback.impact(.light)
|
||||
|
||||
self.dismissAllTooltips()
|
||||
|
||||
let context = self.context
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
let limit: Int32 = 3
|
||||
|
||||
let value = presentationData.strings.Story_Editor_TooltipWeatherLimitValue(limit)
|
||||
let content: UndoOverlayContent = .info(
|
||||
title: nil,
|
||||
text: presentationData.strings.Story_Editor_TooltipWeatherLimitText(value).string,
|
||||
timeout: nil,
|
||||
customUndoText: nil
|
||||
)
|
||||
|
||||
let controller = UndoOverlayController(presentationData: presentationData, content: content, elevatedLayout: true, position: .top, animateInAsReplacement: false, action: { _ in
|
||||
return true
|
||||
})
|
||||
self.present(controller, in: .window(.root))
|
||||
}
|
||||
|
||||
func maybePresentDiscardAlert() {
|
||||
self.hapticFeedback.impact(.light)
|
||||
|
|
|
|||
|
|
@ -615,6 +615,9 @@ public class MinimizedContainerImpl: ASDisplayNode, MinimizedContainer, ASScroll
|
|||
guard !self.items.isEmpty && !self.isExpanded && self.currentTransition == nil else {
|
||||
return
|
||||
}
|
||||
|
||||
// self.scrollView.contentOffset = CGPoint(x: 0.0, y: max(0.0, self.scrollView.contentSize.height - self.scrollView.bounds.height))
|
||||
|
||||
if self.items.count == 1, let item = self.items.first {
|
||||
if let navigationController = self.navigationController {
|
||||
item.beforeMaximize(navigationController, { [weak self] in
|
||||
|
|
|
|||
|
|
@ -992,7 +992,7 @@ final class ShareWithPeersScreenComponent: Component {
|
|||
|
||||
let sectionTitle: String
|
||||
if section.id == 0, case .stories = component.stateContext.subject {
|
||||
sectionTitle = environment.strings.Story_Privacy_PostStoryAsHeader
|
||||
sectionTitle = component.coverItem == nil ? environment.strings.Story_Privacy_PostStoryAsHeader : ""
|
||||
} else if section.id == 2 {
|
||||
sectionTitle = environment.strings.Story_Privacy_WhoCanViewHeader
|
||||
} else if section.id == 1 {
|
||||
|
|
@ -1721,10 +1721,9 @@ final class ShareWithPeersScreenComponent: Component {
|
|||
self.visibleSectionFooters[section.id] = sectionFooter
|
||||
}
|
||||
|
||||
var footerText = "Choose a frame from the story to show in your Profile."
|
||||
|
||||
var footerText = environment.strings.Story_Privacy_ChooseCoverInfo
|
||||
if let sendAsPeerId = self.sendAsPeerId, sendAsPeerId.isGroupOrChannel == true {
|
||||
footerText = isSendAsGroup ? "Choose a frame from the story to show in group profile.": "Choose a frame from the story to show in channel profile."
|
||||
footerText = isSendAsGroup ? environment.strings.Story_Privacy_ChooseCoverGroupInfo : environment.strings.Story_Privacy_ChooseCoverChannelInfo
|
||||
}
|
||||
|
||||
let footerSize = sectionFooter.update(
|
||||
|
|
@ -2043,8 +2042,13 @@ final class ShareWithPeersScreenComponent: Component {
|
|||
var sideInset: CGFloat = 0.0
|
||||
if case .stories = component.stateContext.subject {
|
||||
sideInset = 16.0
|
||||
self.scrollView.isScrollEnabled = false
|
||||
self.dismissPanGesture?.isEnabled = true
|
||||
if availableSize.width < 393.0 && hasCover {
|
||||
self.scrollView.isScrollEnabled = true
|
||||
self.dismissPanGesture?.isEnabled = false
|
||||
} else {
|
||||
self.scrollView.isScrollEnabled = false
|
||||
self.dismissPanGesture?.isEnabled = true
|
||||
}
|
||||
} else if case .peers = component.stateContext.subject {
|
||||
sideInset = 16.0
|
||||
self.dismissPanGesture?.isEnabled = false
|
||||
|
|
@ -2417,7 +2421,7 @@ final class ShareWithPeersScreenComponent: Component {
|
|||
if !editing && hasChannels {
|
||||
sections.append(ItemLayout.Section(
|
||||
id: 0,
|
||||
insets: UIEdgeInsets(top: 28.0, left: 0.0, bottom: 0.0, right: 0.0),
|
||||
insets: UIEdgeInsets(top: component.coverItem == nil ? 28.0 : 12.0, left: 0.0, bottom: 0.0, right: 0.0),
|
||||
itemHeight: peerItemSize.height,
|
||||
itemCount: 1
|
||||
))
|
||||
|
|
@ -2461,7 +2465,7 @@ final class ShareWithPeersScreenComponent: Component {
|
|||
} else {
|
||||
containerInset += 10.0
|
||||
}
|
||||
|
||||
|
||||
var navigationHeight: CGFloat = 56.0
|
||||
let navigationSideInset: CGFloat = 16.0
|
||||
var navigationButtonsWidth: CGFloat = 0.0
|
||||
|
|
@ -2599,9 +2603,9 @@ final class ShareWithPeersScreenComponent: Component {
|
|||
}
|
||||
navigationHeight += navigationTextFieldFrame.height
|
||||
|
||||
if case .stories = component.stateContext.subject {
|
||||
navigationHeight += 16.0
|
||||
}
|
||||
// if case .stories = component.stateContext.subject {
|
||||
// navigationHeight += 16.0
|
||||
// }
|
||||
|
||||
let topInset: CGFloat
|
||||
if environment.inputHeight != 0.0 || !self.navigationTextFieldState.text.isEmpty {
|
||||
|
|
@ -2905,7 +2909,7 @@ final class ShareWithPeersScreenComponent: Component {
|
|||
bottomPanelInset = 8.0
|
||||
transition.setFrame(view: self.bottomBackgroundView, frame: CGRect(origin: CGPoint(x: containerSideInset, y: availableSize.height - bottomPanelHeight - 8.0), size: CGSize(width: containerWidth, height: bottomPanelHeight + bottomPanelInset)))
|
||||
self.bottomBackgroundView.update(size: self.bottomBackgroundView.bounds.size, transition: transition.containedViewLayoutTransition)
|
||||
transition.setFrame(layer: self.bottomSeparatorLayer, frame: CGRect(origin: CGPoint(x: containerSideInset + sideInset, y: availableSize.height - bottomPanelHeight - bottomPanelInset - UIScreenPixel), size: CGSize(width: containerWidth, height: UIScreenPixel)))
|
||||
transition.setFrame(layer: self.bottomSeparatorLayer, frame: CGRect(origin: CGPoint(x: containerSideInset, y: availableSize.height - bottomPanelHeight - bottomPanelInset - UIScreenPixel), size: CGSize(width: containerWidth, height: UIScreenPixel)))
|
||||
}
|
||||
|
||||
let itemContainerSize = CGSize(width: itemsContainerWidth, height: availableSize.height)
|
||||
|
|
@ -3146,7 +3150,7 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer {
|
|||
}
|
||||
|
||||
if !editing || pin, coverImage != nil {
|
||||
coverItem = ShareWithPeersScreenComponent.CoverItem(id: .choose, title: "Choose Story Cover", image: coverImage)
|
||||
coverItem = ShareWithPeersScreenComponent.CoverItem(id: .choose, title: presentationData.strings.Story_Privacy_ChooseCover, image: coverImage)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -38,12 +38,42 @@ function uiWebview_HighlightAllOccurencesOfStringForElement(element,keyword) {
|
|||
span.appendChild(text);
|
||||
|
||||
span.setAttribute("class","uiWebviewHighlight");
|
||||
span.style.position = "relative";
|
||||
span.style.display = "inline-block";
|
||||
span.style.backgroundColor="#ffe438";
|
||||
span.style.color="black";
|
||||
span.style.borderRadius="3px";
|
||||
span.style.scrollMargin="44px";
|
||||
span.style.zIndex = "1001"; // Ensure highlights are above the overlay
|
||||
|
||||
index--;
|
||||
span.setAttribute("id", "SEARCH WORD"+(index));
|
||||
|
||||
var beforeStyle = document.createElement('style');
|
||||
beforeStyle.innerHTML = `
|
||||
.uiWebviewHighlight::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
bottom: 0px;
|
||||
left: -2px;
|
||||
right: -2px;
|
||||
background-color: #ffe438;
|
||||
z-index: -1;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.dark-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.22);
|
||||
z-index: 1000;
|
||||
pointer-events: none;
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(beforeStyle);
|
||||
|
||||
text = document.createTextNode(value.substr(idx+keyword.length));
|
||||
element.deleteData(idx, value.length - idx);
|
||||
|
|
@ -68,6 +98,7 @@ function uiWebview_HighlightAllOccurencesOfStringForElement(element,keyword) {
|
|||
// the main entry point to start the search
|
||||
function uiWebview_HighlightAllOccurencesOfString(keyword) {
|
||||
uiWebview_RemoveAllHighlights();
|
||||
uiWebview_AddDarkOverlay();
|
||||
uiWebview_HighlightAllOccurencesOfStringForElement(document.body, keyword.toLowerCase());
|
||||
}
|
||||
|
||||
|
|
@ -100,9 +131,24 @@ function uiWebview_RemoveAllHighlightsForElement(element) {
|
|||
function uiWebview_RemoveAllHighlights() {
|
||||
uiWebview_SearchResultCount = 0;
|
||||
uiWebview_RemoveAllHighlightsForElement(document.body);
|
||||
uiWebview_RemoveDarkOverlay();
|
||||
}
|
||||
|
||||
function uiWebview_ScrollTo(idx) {
|
||||
var scrollTo = document.getElementById("SEARCH WORD" + idx);
|
||||
if (scrollTo) scrollTo.scrollIntoView();
|
||||
}
|
||||
|
||||
function uiWebview_AddDarkOverlay() {
|
||||
var overlay = document.createElement('div');
|
||||
overlay.classList.add('dark-overlay');
|
||||
overlay.setAttribute('id', 'dark-overlay');
|
||||
document.body.appendChild(overlay);
|
||||
}
|
||||
|
||||
function uiWebview_RemoveDarkOverlay() {
|
||||
var overlay = document.getElementById('dark-overlay');
|
||||
if (overlay) {
|
||||
document.body.removeChild(overlay);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,113 +16,16 @@ import TextFormat
|
|||
import TelegramBaseController
|
||||
import AccountContext
|
||||
import TelegramStringFormatting
|
||||
import OverlayStatusController
|
||||
import DeviceLocationManager
|
||||
import ShareController
|
||||
import UrlEscaping
|
||||
import ContextUI
|
||||
import ComposePollUI
|
||||
import AlertUI
|
||||
import PresentationDataUtils
|
||||
import UndoUI
|
||||
import TelegramCallsUI
|
||||
import TelegramNotices
|
||||
import GameUI
|
||||
import ScreenCaptureDetection
|
||||
import GalleryUI
|
||||
import OpenInExternalAppUI
|
||||
import LegacyUI
|
||||
import InstantPageUI
|
||||
import LocationUI
|
||||
import BotPaymentsUI
|
||||
import DeleteChatPeerActionSheetItem
|
||||
import HashtagSearchUI
|
||||
import LegacyMediaPickerUI
|
||||
import Emoji
|
||||
import PeerAvatarGalleryUI
|
||||
import PeerInfoUI
|
||||
import RaiseToListen
|
||||
import UrlHandling
|
||||
import AvatarNode
|
||||
import AppBundle
|
||||
import LocalizedPeerData
|
||||
import PhoneNumberFormat
|
||||
import SettingsUI
|
||||
import UrlWhitelist
|
||||
import TelegramIntents
|
||||
import TooltipUI
|
||||
import StatisticsUI
|
||||
import MediaResources
|
||||
import GalleryData
|
||||
import ChatInterfaceState
|
||||
import InviteLinksUI
|
||||
import Markdown
|
||||
import TelegramPermissionsUI
|
||||
import Speak
|
||||
import TranslateUI
|
||||
import UniversalMediaPlayer
|
||||
import WallpaperBackgroundNode
|
||||
import ChatListUI
|
||||
import CalendarMessageScreen
|
||||
import ReactionSelectionNode
|
||||
import ReactionListContextMenuContent
|
||||
import AttachmentUI
|
||||
import AttachmentTextInputPanelNode
|
||||
import MediaPickerUI
|
||||
import ChatPresentationInterfaceState
|
||||
import Pasteboard
|
||||
import ChatSendMessageActionUI
|
||||
import ChatTextLinkEditUI
|
||||
import WebUI
|
||||
import PremiumUI
|
||||
import ImageTransparency
|
||||
import StickerPackPreviewUI
|
||||
import TextNodeWithEntities
|
||||
import EntityKeyboard
|
||||
import ChatTitleView
|
||||
import EmojiStatusComponent
|
||||
import ChatTimerScreen
|
||||
import MediaPasteboardUI
|
||||
import ChatListHeaderComponent
|
||||
import ChatControllerInteraction
|
||||
import FeaturedStickersScreen
|
||||
import ChatEntityKeyboardInputNode
|
||||
import StorageUsageScreen
|
||||
import AvatarEditorScreen
|
||||
import ChatScheduleTimeController
|
||||
import ICloudResources
|
||||
import StoryContainerScreen
|
||||
import MoreHeaderButton
|
||||
import VolumeButtons
|
||||
import ChatAvatarNavigationNode
|
||||
import ChatContextQuery
|
||||
import PeerReportScreen
|
||||
import PeerSelectionController
|
||||
import SaveToCameraRoll
|
||||
import ChatMessageDateAndStatusNode
|
||||
import ReplyAccessoryPanelNode
|
||||
import TextSelectionNode
|
||||
import ChatMessagePollBubbleContentNode
|
||||
import ChatMessageItem
|
||||
import ChatMessageItemImpl
|
||||
import ChatMessageItemView
|
||||
import ChatMessageItemCommon
|
||||
import ChatMessageAnimatedStickerItemNode
|
||||
import ChatMessageBubbleItemNode
|
||||
import ChatNavigationButton
|
||||
import WebsiteType
|
||||
import ChatQrCodeScreen
|
||||
import PeerInfoScreen
|
||||
import MediaEditorScreen
|
||||
import WallpaperGalleryScreen
|
||||
import WallpaperGridScreen
|
||||
import VideoMessageCameraScreen
|
||||
import TopMessageReactions
|
||||
import AudioWaveform
|
||||
import PeerNameColorScreen
|
||||
import ChatEmptyNode
|
||||
import ChatMediaInputStickerGridItem
|
||||
import AdsInfoScreen
|
||||
|
||||
extension ChatControllerImpl {
|
||||
func openStorySharing(messages: [Message]) {
|
||||
|
|
@ -187,7 +90,6 @@ extension ChatControllerImpl {
|
|||
}
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
)
|
||||
self.push(controller)
|
||||
|
|
|
|||
|
|
@ -231,7 +231,18 @@ func openChatMessageImpl(_ params: OpenChatMessageParams) -> Bool {
|
|||
params.present(controller, nil)
|
||||
} else if let rootController = params.navigationController?.view.window?.rootViewController {
|
||||
let proceed = {
|
||||
presentDocumentPreviewController(rootController: rootController, theme: presentationData.theme, strings: presentationData.strings, postbox: params.context.account.postbox, file: file, canShare: !params.message.isCopyProtected())
|
||||
if params.context.sharedContext.immediateExperimentalUISettings.browserExperiment && BrowserScreen.supportedDocumentMimeTypes.contains(file.mimeType) {
|
||||
let subject: BrowserScreen.Subject
|
||||
if file.mimeType == "application/pdf" {
|
||||
subject = .pdfDocument(file: file)
|
||||
} else {
|
||||
subject = .document(file: file)
|
||||
}
|
||||
let controller = BrowserScreen(context: params.context, subject: subject)
|
||||
params.navigationController?.pushViewController(controller)
|
||||
} else {
|
||||
presentDocumentPreviewController(rootController: rootController, theme: presentationData.theme, strings: presentationData.strings, postbox: params.context.account.postbox, file: file, canShare: !params.message.isCopyProtected())
|
||||
}
|
||||
}
|
||||
if file.mimeType.contains("image/svg") {
|
||||
let presentationData = params.context.sharedContext.currentPresentationData.with { $0 }
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ import StoryContainerScreen
|
|||
import WallpaperGalleryScreen
|
||||
import TelegramStringFormatting
|
||||
import TextFormat
|
||||
import BrowserUI
|
||||
|
||||
private func defaultNavigationForPeerId(_ peerId: PeerId?, navigation: ChatControllerInteractionNavigateToPeer) -> ChatControllerInteractionNavigateToPeer {
|
||||
if case .default = navigation {
|
||||
|
|
@ -248,7 +249,14 @@ func openResolvedUrlImpl(
|
|||
})
|
||||
present(controller, nil)
|
||||
case let .instantView(webpage, anchor):
|
||||
navigationController?.pushViewController(InstantPageController(context: context, webPage: webpage, sourceLocation: InstantPageSourceLocation(userLocation: .other, peerType: .channel), anchor: 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)
|
||||
case let .join(link):
|
||||
dismissInput()
|
||||
|
||||
|
|
|
|||
|
|
@ -1039,18 +1039,18 @@ func openExternalUrlImpl(context: AccountContext, urlContext: OpenURLContext, ur
|
|||
return settings
|
||||
}
|
||||
|
||||
var isCompact = false
|
||||
if let metrics = navigationController?.validLayout?.metrics, case .compact = metrics.widthClass {
|
||||
isCompact = true
|
||||
}
|
||||
// var isCompact = false
|
||||
// if let metrics = navigationController?.validLayout?.metrics, case .compact = metrics.widthClass {
|
||||
// isCompact = true
|
||||
// }
|
||||
|
||||
let _ = (settings
|
||||
|> deliverOnMainQueue).startStandalone(next: { settings in
|
||||
if let defaultWebBrowser = settings.defaultWebBrowser, defaultWebBrowser != "inApp" {
|
||||
let openInOptions = availableOpenInOptions(context: context, item: .url(url: url))
|
||||
if let option = openInOptions.first(where: { $0.identifier == settings.defaultWebBrowser }) {
|
||||
if case let .openUrl(url) = option.action() {
|
||||
context.sharedContext.applicationBindings.openUrl(url)
|
||||
if case let .openUrl(openInUrl) = option.action() {
|
||||
context.sharedContext.applicationBindings.openUrl(openInUrl)
|
||||
} else {
|
||||
context.sharedContext.applicationBindings.openUrl(url)
|
||||
}
|
||||
|
|
@ -1058,11 +1058,20 @@ func openExternalUrlImpl(context: AccountContext, urlContext: OpenURLContext, ur
|
|||
context.sharedContext.applicationBindings.openUrl(url)
|
||||
}
|
||||
} else {
|
||||
if settings.defaultWebBrowser == nil && isCompact {
|
||||
var isExceptedDomain = false
|
||||
let host = ".\((parsedUrl.host ?? "").lowercased())"
|
||||
for exception in settings.exceptions {
|
||||
if host.hasSuffix(".\(exception.domain)") {
|
||||
isExceptedDomain = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if settings.defaultWebBrowser == nil && !isExceptedDomain {
|
||||
let controller = BrowserScreen(context: context, subject: .webPage(url: parsedUrl.absoluteString))
|
||||
navigationController?.pushViewController(controller)
|
||||
} else {
|
||||
if let window = navigationController?.view.window {
|
||||
if let window = navigationController?.view.window, !isExceptedDomain {
|
||||
let controller = SFSafariViewController(url: parsedUrl)
|
||||
controller.preferredBarTintColor = presentationData.theme.rootController.navigationBar.opaqueBackgroundColor
|
||||
controller.preferredControlTintColor = presentationData.theme.rootController.navigationBar.accentTextColor
|
||||
|
|
|
|||
|
|
@ -2652,6 +2652,35 @@ public final class SharedAccountContextImpl: SharedAccountContext {
|
|||
}
|
||||
return editorController
|
||||
}
|
||||
|
||||
public func makeStoryMediaEditorScreen(context: AccountContext, source: Any?, text: String?, link: String?, completion: @escaping (MediaEditorScreenResult, @escaping (@escaping () -> Void) -> Void) -> Void) -> ViewController {
|
||||
let subject: Signal<MediaEditorScreen.Subject?, NoError>
|
||||
if let image = source as? UIImage {
|
||||
subject = .single(.image(image, PixelDimensions(image.size), nil, .bottomRight))
|
||||
} else if let path = source as? String {
|
||||
subject = .single(.video(path, nil, false, nil, nil, PixelDimensions(width: 1080, height: 1920), 0.0, [], .bottomRight))
|
||||
} else {
|
||||
subject = .single(.empty(PixelDimensions(width: 1080, height: 1920)))
|
||||
}
|
||||
let editorController = MediaEditorScreen(
|
||||
context: context,
|
||||
mode: .storyEditor,
|
||||
subject: subject,
|
||||
customTarget: nil,
|
||||
initialCaption: text.flatMap { NSAttributedString(string: $0) },
|
||||
initialLink: link,
|
||||
transitionIn: nil,
|
||||
transitionOut: { finished, isNew in
|
||||
return nil
|
||||
}, completion: { result, commit in
|
||||
completion(result, commit)
|
||||
} as (MediaEditorScreen.Result, @escaping (@escaping () -> Void) -> Void) -> Void
|
||||
)
|
||||
// editorController.cancelled = { _ in
|
||||
// cancelled()
|
||||
// }
|
||||
return editorController
|
||||
}
|
||||
|
||||
public func makeMediaPickerScreen(context: AccountContext, hasSearch: Bool, completion: @escaping (Any) -> Void) -> ViewController {
|
||||
return mediaPickerController(context: context, hasSearch: hasSearch, completion: completion)
|
||||
|
|
|
|||
|
|
@ -104,6 +104,7 @@ private enum ApplicationSpecificOrderedItemListCollectionIdValues: Int32 {
|
|||
case storyDrafts = 4
|
||||
case storySources = 5
|
||||
case hashtagSearchRecentQueries = 6
|
||||
case browserRecentlyVisited = 7
|
||||
}
|
||||
|
||||
public struct ApplicationSpecificOrderedItemListCollectionId {
|
||||
|
|
@ -114,4 +115,5 @@ public struct ApplicationSpecificOrderedItemListCollectionId {
|
|||
public static let storyDrafts = applicationSpecificOrderedItemListCollectionId(ApplicationSpecificOrderedItemListCollectionIdValues.storyDrafts.rawValue)
|
||||
public static let storySources = applicationSpecificOrderedItemListCollectionId(ApplicationSpecificOrderedItemListCollectionIdValues.storySources.rawValue)
|
||||
public static let hashtagSearchRecentQueries = applicationSpecificOrderedItemListCollectionId(ApplicationSpecificOrderedItemListCollectionIdValues.hashtagSearchRecentQueries.rawValue)
|
||||
public static let browserRecentlyVisited = applicationSpecificOrderedItemListCollectionId(ApplicationSpecificOrderedItemListCollectionIdValues.browserRecentlyVisited.rawValue)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,10 +6,12 @@ import SwiftSignalKit
|
|||
public struct WebBrowserException: Codable, Equatable {
|
||||
public let domain: String
|
||||
public let title: String
|
||||
public let icon: TelegramMediaImage?
|
||||
|
||||
public init(domain: String, title: String) {
|
||||
public init(domain: String, title: String, icon: TelegramMediaImage?) {
|
||||
self.domain = domain
|
||||
self.title = title
|
||||
self.icon = icon
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
|
|
@ -17,6 +19,7 @@ public struct WebBrowserException: Codable, Equatable {
|
|||
|
||||
self.domain = try container.decode(String.self, forKey: "domain")
|
||||
self.title = try container.decode(String.self, forKey: "title")
|
||||
self.icon = try container.decodeIfPresent(TelegramMediaImage.self, forKey: "icon")
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
|
|
@ -24,6 +27,11 @@ public struct WebBrowserException: Codable, Equatable {
|
|||
|
||||
try container.encode(self.domain, forKey: "domain")
|
||||
try container.encode(self.title, forKey: "title")
|
||||
if let icon = self.icon {
|
||||
try container.encode(icon, forKey: "icon")
|
||||
} else {
|
||||
try container.encodeNil(forKey: "icon")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ swift_library(
|
|||
"//submodules/TelegramUI/Components/EmojiStatusComponent",
|
||||
"//submodules/ShareController",
|
||||
"//submodules/UndoUI",
|
||||
"//submodules/OverlayStatusController",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ import OpenInExternalAppUI
|
|||
import ShareController
|
||||
import UndoUI
|
||||
import AvatarNode
|
||||
import OverlayStatusController
|
||||
|
||||
private let durgerKingBotIds: [Int64] = [5104055776, 2200339955]
|
||||
|
||||
|
|
@ -1100,6 +1101,84 @@ public final class WebAppController: ViewController, AttachmentContainable {
|
|||
if let json = json, let isPanGestureEnabled = json["allow_vertical_swipe"] as? Bool {
|
||||
self.controller?._isPanGestureEnabled = isPanGestureEnabled
|
||||
}
|
||||
case "web_app_share_to_story":
|
||||
if let json = json, let mediaUrl = json["media_url"] as? String {
|
||||
let text = json["text"] as? String
|
||||
let link = json["widget_link"] as? String
|
||||
|
||||
enum FetchResult {
|
||||
case result(Data)
|
||||
case progress(Float)
|
||||
}
|
||||
|
||||
let controller = OverlayStatusController(theme: self.presentationData.theme, type: .loading(cancelled: {
|
||||
|
||||
}))
|
||||
self.controller?.present(controller, in: .window(.root))
|
||||
|
||||
let _ = (fetchHttpResource(url: mediaUrl)
|
||||
|> map(Optional.init)
|
||||
|> `catch` { error in
|
||||
return .single(nil)
|
||||
}
|
||||
|> mapToSignal { value -> Signal<FetchResult, NoError> in
|
||||
if case let .dataPart(_, data, _, complete) = value, complete {
|
||||
return .single(.result(data))
|
||||
} else if case let .progressUpdated(progress) = value {
|
||||
return .single(.progress(progress))
|
||||
} else {
|
||||
return .complete()
|
||||
}
|
||||
}
|
||||
|> deliverOnMainQueue).start(next: { [weak self, weak controller] next in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
controller?.dismiss()
|
||||
|
||||
switch next {
|
||||
case let .result(data):
|
||||
var source: Any?
|
||||
if let image = UIImage(data: data) {
|
||||
source = image
|
||||
} else {
|
||||
let tempFile = TempBox.shared.tempFile(fileName: "image.mp4")
|
||||
if let _ = try? data.write(to: URL(fileURLWithPath: tempFile.path), options: .atomic) {
|
||||
source = tempFile.path
|
||||
}
|
||||
}
|
||||
if let source {
|
||||
let externalState = MediaEditorTransitionOutExternalState(
|
||||
storyTarget: nil,
|
||||
isForcedTarget: false,
|
||||
isPeerArchived: false,
|
||||
transitionOut: nil
|
||||
)
|
||||
let controller = self.context.sharedContext.makeStoryMediaEditorScreen(context: self.context, source: source, text: text, link: link, 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
|
||||
// }
|
||||
externalState.storyTarget = target
|
||||
|
||||
if let rootController = self.context.sharedContext.mainWindow?.viewController as? TelegramRootControllerInterface {
|
||||
rootController.proceedWithStoryUpload(target: target, result: result, existingMedia: nil, forwardInfo: nil, externalState: externalState, commit: commit)
|
||||
}
|
||||
})
|
||||
if let navigationController = self.controller?.getNavigationController() {
|
||||
navigationController.pushViewController(controller)
|
||||
}
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
})
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue