Various improvements

This commit is contained in:
Ilya Laktyushin 2024-07-23 22:01:56 +04:00
parent 31ba87fb0f
commit 1c7834ad57
43 changed files with 3867 additions and 459 deletions

View file

@ -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.";

View file

@ -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

View file

@ -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 }
}

View file

@ -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",

View file

@ -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)
}
}

View 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)
}
}

View file

@ -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)
}
}

View 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
}
}

View file

@ -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)
}

View 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() {
}
}

View file

@ -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()
}
}
}

View file

@ -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))

View 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() {
}
}

View 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
}
}

View file

@ -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

View file

@ -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)
}
}

View 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)
}
}

View file

@ -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
}
}

View file

@ -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
}
}
}
}

View 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)
}
}

View 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
}

View file

@ -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
}

View file

@ -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

View file

@ -65,6 +65,7 @@ public enum ViewControllerNavigationPresentation {
case flatModal
case standaloneModal
case modalInLargeLayout
case modalInCompactLayout
}
public enum TabBarItemContextActionType {

View file

@ -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 {

View file

@ -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")

View file

@ -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()

View file

@ -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()
}
}

View file

@ -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))
}
}

View file

@ -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,

View file

@ -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)

View file

@ -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

View file

@ -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)
}
}

View file

@ -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);
}
}

View file

@ -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)

View file

@ -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 }

View file

@ -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()

View file

@ -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

View file

@ -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)

View file

@ -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)
}

View file

@ -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")
}
}
}

View file

@ -39,6 +39,7 @@ swift_library(
"//submodules/TelegramUI/Components/EmojiStatusComponent",
"//submodules/ShareController",
"//submodules/UndoUI",
"//submodules/OverlayStatusController",
],
visibility = [
"//visibility:public",

View file

@ -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
}