Various improvements

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

View File

@@ -16,6 +16,7 @@ import OpenInExternalAppUI
import MultilineTextComponent
import MinimizedContainer
import InstantPageUI
import NavigationStackComponent
private let settingsTag = GenericComponentViewTag()
@@ -26,6 +27,7 @@ private final class BrowserScreenComponent: CombinedComponent {
let contentState: BrowserContentState?
let presentationState: BrowserPresentationState
let performAction: ActionSlot<BrowserScreen.Action>
let performHoldAction: (UIView, ContextGesture?, BrowserScreen.Action) -> Void
let panelCollapseFraction: CGFloat
init(
@@ -33,12 +35,14 @@ private final class BrowserScreenComponent: CombinedComponent {
contentState: BrowserContentState?,
presentationState: BrowserPresentationState,
performAction: ActionSlot<BrowserScreen.Action>,
performHoldAction: @escaping (UIView, ContextGesture?, BrowserScreen.Action) -> Void,
panelCollapseFraction: CGFloat
) {
self.context = context
self.contentState = contentState
self.presentationState = presentationState
self.performAction = performAction
self.performHoldAction = performHoldAction
self.panelCollapseFraction = panelCollapseFraction
}
@@ -72,7 +76,8 @@ private final class BrowserScreenComponent: CombinedComponent {
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>?
var navigationLeftItems: [AnyComponentWithIdentity<Empty>]
var navigationRightItems: [AnyComponentWithIdentity<Empty>]
@@ -172,7 +177,7 @@ private final class BrowserScreenComponent: CombinedComponent {
leftItems: navigationLeftItems,
rightItems: navigationRightItems,
centerItem: navigationContent,
readingProgress: 0.0,
readingProgress: context.component.contentState?.readingProgress ?? 0.0,
loadingProgress: context.component.contentState?.estimatedProgress,
collapseFraction: collapseFraction
),
@@ -206,7 +211,8 @@ private final class BrowserScreenComponent: CombinedComponent {
textColor: environment.theme.rootController.navigationBar.primaryTextColor,
canGoBack: context.component.contentState?.canGoBack ?? false,
canGoForward: context.component.contentState?.canGoForward ?? false,
performAction: performAction
performAction: performAction,
performHoldAction: performHoldAction
)
)
)
@@ -275,17 +281,18 @@ public class BrowserScreen: ViewController, MinimizableController {
private weak var controller: BrowserScreen?
private let context: AccountContext
private let contentContainerView: UIView
fileprivate var content: BrowserContent?
private let contentContainerView = UIView()
fileprivate let contentNavigationContainer = ComponentView<Empty>()
fileprivate var content: [BrowserContent] = []
private var contentState: BrowserContentState?
private var contentStateDisposable: Disposable?
fileprivate var contentState: BrowserContentState?
private var contentStateDisposable = MetaDisposable()
private var presentationState: BrowserPresentationState
private let performAction: ActionSlot<BrowserScreen.Action>
private let performAction = ActionSlot<BrowserScreen.Action>()
fileprivate let componentHost: ComponentView<ViewControllerComponentContainer.Environment>
fileprivate let componentHost = ComponentView<ViewControllerComponentContainer.Environment>()
private var presentationData: PresentationData
private var validLayout: (ContainerViewLayout, CGFloat)?
@@ -296,41 +303,13 @@ public class BrowserScreen: ViewController, MinimizableController {
self.presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
self.presentationState = BrowserPresentationState(fontSize: 100, fontIsSerif: false, isSearching: false, searchResultIndex: 0, searchResultCount: 0, searchQueryIsEmpty: true)
self.performAction = ActionSlot()
self.contentContainerView = UIView()
self.contentContainerView.clipsToBounds = true
self.componentHost = ComponentView<ViewControllerComponentContainer.Environment>()
super.init()
let content: BrowserContent
switch controller.subject {
case let .webPage(url):
content = BrowserWebContent(context: controller.context, url: url)
case let .instantPage(webPage, sourceLocation):
content = BrowserInstantPageContent(context: controller.context, webPage: webPage, url: webPage.content.url ?? "", sourceLocation: sourceLocation)
}
self.content = content
self.contentStateDisposable = (content.state
|> deliverOnMainQueue).start(next: { [weak self] state in
guard let strongSelf = self else {
return
}
strongSelf.controller?.title = state.title
strongSelf.contentState = state
strongSelf.requestLayout(transition: .easeInOut(duration: 0.25))
}).strict()
self.content?.onScrollingUpdate = { [weak self] update in
self?.onContentScrollingUpdate(update)
}
self.pushContent(controller.subject, transition: .immediate)
self.performAction.connect { [weak self] action in
guard let self, let content = self.content, let url = self.contentState?.url else {
guard let self, let content = self.content.last, let url = self.contentState?.url else {
return
}
switch action {
@@ -341,7 +320,11 @@ public class BrowserScreen: ViewController, MinimizableController {
case .stop:
content.stop()
case .navigateBack:
content.navigateBack()
if content.currentState.canGoBack {
content.navigateBack()
} else {
self.popContent(transition: .spring(duration: 0.4))
}
case .navigateForward:
content.navigateForward()
case .share:
@@ -458,22 +441,139 @@ public class BrowserScreen: ViewController, MinimizableController {
}
deinit {
self.contentStateDisposable?.dispose()
self.contentStateDisposable.dispose()
}
override func didLoad() {
super.didLoad()
self.contentContainerView.clipsToBounds = true
self.view.addSubview(self.contentContainerView)
if let content = self.content {
self.contentContainerView.addSubview(content)
}
}
func updatePresentationState(animated: Bool = false, _ f: (BrowserPresentationState) -> BrowserPresentationState) {
self.presentationState = f(self.presentationState)
self.requestLayout(transition: animated ? .easeInOut(duration: 0.2) : .immediate)
}
func pushContent(_ content: BrowserScreen.Subject, transition: ComponentTransition) {
let browserContent: BrowserContent
switch content {
case let .webPage(url):
browserContent = BrowserWebContent(context: self.context, url: url)
case let .instantPage(webPage, anchor, sourceLocation):
let instantPageContent = BrowserInstantPageContent(context: self.context, webPage: webPage, anchor: anchor, url: webPage.content.url ?? "", sourceLocation: sourceLocation)
instantPageContent.openPeer = { [weak self] peer in
guard let self else {
return
}
self.openPeer(peer)
}
browserContent = instantPageContent
}
browserContent.pushContent = { [weak self] content in
guard let self else {
return
}
self.pushContent(content, transition: .spring(duration: 0.4))
}
browserContent.present = { [weak self] c, a in
guard let self, let controller = self.controller else {
return
}
controller.present(c, in: .window(.root), with: a)
}
browserContent.presentInGlobalOverlay = { [weak self] c in
guard let self, let controller = self.controller else {
return
}
controller.presentInGlobalOverlay(c)
}
browserContent.getNavigationController = { [weak self] in
return self?.controller?.navigationController as? NavigationController
}
browserContent.minimize = { [weak self] in
guard let self else {
return
}
self.minimize()
}
self.content.append(browserContent)
self.requestLayout(transition: transition)
self.setupContentStateUpdates()
}
func popContent(transition: ComponentTransition) {
self.content.removeLast()
self.requestLayout(transition: transition)
self.setupContentStateUpdates()
}
func openPeer(_ peer: EnginePeer) {
guard let controller = self.controller, let navigationController = controller.navigationController as? NavigationController else {
return
}
self.minimize()
self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(peer), animated: true))
}
private func setupContentStateUpdates() {
for content in self.content {
content.onScrollingUpdate = { _ in }
}
guard let content = self.content.last else {
self.controller?.title = ""
self.contentState = nil
self.contentStateDisposable.set(nil)
self.requestLayout(transition: .easeInOut(duration: 0.25))
return
}
var previousState = BrowserContentState(title: "", url: "", estimatedProgress: 1.0, readingProgress: 0.0, contentType: .webPage, canGoBack: false, canGoForward: false, backList: [], forwardList: [])
if self.content.count > 1 {
for content in self.content.prefix(upTo: self.content.count - 1) {
var backList = previousState.backList
backList.append(BrowserContentState.HistoryItem(url: content.currentState.url, title: content.currentState.title, uuid: content.uuid))
previousState = previousState.withUpdatedBackList(backList)
}
}
self.contentStateDisposable.set((content.state
|> deliverOnMainQueue).startStrict(next: { [weak self] state in
guard let self else {
return
}
var backList = state.backList
backList.insert(contentsOf: previousState.backList, at: 0)
var canGoBack = state.canGoBack
if !backList.isEmpty {
canGoBack = true
}
let previousState = self.contentState
let state = state.withUpdatedCanGoBack(canGoBack).withUpdatedBackList(backList)
self.controller?.title = state.title
self.contentState = state
let transition: ComponentTransition
if let previousState, previousState.withUpdatedReadingProgress(state.readingProgress) == state {
transition = .immediate
} else {
transition = .easeInOut(duration: 0.25)
}
self.requestLayout(transition: transition)
}))
content.onScrollingUpdate = { [weak self] update in
self?.onContentScrollingUpdate(update)
}
}
func minimize() {
guard let controller = self.controller, let navigationController = controller.navigationController as? NavigationController else {
@@ -598,7 +698,7 @@ public class BrowserScreen: ViewController, MinimizableController {
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let result = super.hitTest(point, with: event)
if result == self.componentHost.view, let content = self.content {
if result == self.componentHost.view, let content = self.content.last {
return content.hitTest(self.view.convert(point, to: content), with: event)
}
return result
@@ -654,6 +754,51 @@ public class BrowserScreen: ViewController, MinimizableController {
}
}
func navigateTo(_ item: BrowserContentState.HistoryItem) {
if let _ = item.webItem {
if let last = self.content.last {
last.navigateTo(historyItem: item)
}
} else if let uuid = item.uuid {
var newContent = self.content
while newContent.last?.uuid != uuid {
newContent.removeLast()
}
self.content = newContent
self.requestLayout(transition: .spring(duration: 0.4))
}
}
func performHoldAction(view: UIView, gesture: ContextGesture?, action: BrowserScreen.Action) {
guard let controller = self.controller, let contentState = self.contentState else {
return
}
let source: ContextContentSource = .reference(BrowserReferenceContentSource(controller: controller, sourceView: view))
var items: [ContextMenuItem] = []
switch action {
case .navigateBack:
for item in contentState.backList {
items.append(.action(ContextMenuActionItem(text: item.title, textLayout: .secondLineWithValue(item.url), icon: { _ in return nil }, action: { [weak self] (_, action) in
self?.navigateTo(item)
action(.default)
})))
}
case .navigateForward:
for item in contentState.forwardList {
items.append(.action(ContextMenuActionItem(text: item.title, textLayout: .secondLineWithValue(item.url), icon: { _ in return nil }, action: { [weak self] (_, action) in
self?.navigateTo(item)
action(.default)
})))
}
default:
return
}
let contextController = ContextController(presentationData: self.presentationData, source: source, items: .single(ContextController.Items(content: .list(items))))
self.controller?.present(contextController, in: .window(.root))
}
func requestLayout(transition: ComponentTransition) {
if let (layout, navigationBarHeight) = self.validLayout {
self.containerLayoutUpdated(layout: layout, navigationBarHeight: navigationBarHeight, transition: transition)
@@ -694,6 +839,11 @@ public class BrowserScreen: ViewController, MinimizableController {
contentState: self.contentState,
presentationState: self.presentationState,
performAction: self.performAction,
performHoldAction: { [weak self] view, gesture, action in
if let self {
self.performHoldAction(view: view, gesture: gesture, action: action)
}
},
panelCollapseFraction: self.scrollingPanelOffsetFraction
)
),
@@ -711,12 +861,48 @@ public class BrowserScreen: ViewController, MinimizableController {
transition.setFrame(view: componentView, frame: CGRect(origin: .zero, size: componentSize))
}
transition.setFrame(view: self.contentContainerView, frame: CGRect(origin: .zero, size: layout.size))
if let content = self.content {
let collapsedHeight: CGFloat = 24.0
let topInset: CGFloat = environment.statusBarHeight + navigationBarHeight * (1.0 - self.scrollingPanelOffsetFraction) + collapsedHeight * self.scrollingPanelOffsetFraction
let bottomInset = 49.0 + layout.intrinsicInsets.bottom
content.updateLayout(size: layout.size, insets: UIEdgeInsets(top: topInset, left: layout.safeInsets.left, bottom: bottomInset, right: layout.safeInsets.right), transition: transition)
transition.setFrame(view: content, frame: CGRect(origin: .zero, size: layout.size))
var items: [AnyComponentWithIdentity<Empty>] = []
for content in self.content {
items.append(
AnyComponentWithIdentity(id: content.uuid, component: AnyComponent(
BrowserContentComponent(
content: content,
insets: UIEdgeInsets(
top: environment.statusBarHeight,
left: layout.safeInsets.left,
bottom: layout.intrinsicInsets.bottom,
right: layout.safeInsets.right
),
navigationBarHeight: navigationBarHeight,
scrollingPanelOffsetFraction: self.scrollingPanelOffsetFraction
)
))
)
}
let _ = self.contentNavigationContainer.update(
transition: transition,
component: AnyComponent(
NavigationStackComponent(
items: items,
requestPop: { [weak self] in
guard let self else {
return
}
self.popContent(transition: .spring(duration: 0.4))
}
)
),
environment: {},
containerSize: layout.size
)
let navigationFrame = CGRect(origin: .zero, size: layout.size)
if let view = self.contentNavigationContainer.view {
if view.superview == nil {
self.contentContainerView.addSubview(view)
}
transition.setFrame(view: view, frame: navigationFrame)
}
self.navigationBarHeight = environment.navigationHeight
@@ -726,7 +912,7 @@ public class BrowserScreen: ViewController, MinimizableController {
public enum Subject {
case webPage(url: String)
case instantPage(webPage: TelegramMediaWebpage, sourceLocation: InstantPageSourceLocation)
case instantPage(webPage: TelegramMediaWebpage, anchor: String?, sourceLocation: InstantPageSourceLocation)
}
private let context: AccountContext
@@ -743,7 +929,7 @@ public class BrowserScreen: ViewController, MinimizableController {
self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .allButUpsideDown)
self.scrollToTop = { [weak self] in
(self?.displayNode as? Node)?.content?.scrollToTop()
self?.node.content.last?.scrollToTop()
}
}
@@ -751,6 +937,10 @@ public class BrowserScreen: ViewController, MinimizableController {
preconditionFailure()
}
private var node: Node {
return self.displayNode as! Node
}
override public func loadDisplayNode() {
self.displayNode = Node(controller: self)
@@ -760,11 +950,30 @@ public class BrowserScreen: ViewController, MinimizableController {
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
(self.displayNode as! Node).containerLayoutUpdated(layout: layout, navigationBarHeight: self.navigationLayout(layout: layout).navigationFrame.height, transition: ComponentTransition(transition))
self.node.containerLayoutUpdated(layout: layout, navigationBarHeight: self.navigationLayout(layout: layout).navigationFrame.height, transition: ComponentTransition(transition))
}
public var isMinimized = false
public var isMinimizable = true
public var minimizedIcon: UIImage? {
if let contentState = self.node.contentState {
switch contentState.contentType {
case .webPage:
return contentState.favicon
case .instantPage:
return UIImage(bundleImageName: "Chat/Message/AttachedContentInstantIcon")?.withRenderingMode(.alwaysTemplate)
}
}
return nil
}
public var minimizedProgress: Float? {
if let contentState = self.node.contentState {
return Float(contentState.readingProgress)
}
return nil
}
}
private final class BrowserReferenceContentSource: ContextReferenceContentSource {
@@ -780,3 +989,70 @@ private final class BrowserReferenceContentSource: ContextReferenceContentSource
return ContextControllerReferenceViewInfo(referenceView: self.sourceView, contentAreaInScreenSpace: UIScreen.main.bounds)
}
}
private final class BrowserContentComponent: Component {
let content: BrowserContent
let insets: UIEdgeInsets
let navigationBarHeight: CGFloat
let scrollingPanelOffsetFraction: CGFloat
init(
content: BrowserContent,
insets: UIEdgeInsets,
navigationBarHeight: CGFloat,
scrollingPanelOffsetFraction: CGFloat
) {
self.content = content
self.insets = insets
self.navigationBarHeight = navigationBarHeight
self.scrollingPanelOffsetFraction = scrollingPanelOffsetFraction
}
static func ==(lhs: BrowserContentComponent, rhs: BrowserContentComponent) -> Bool {
if lhs.content.uuid != rhs.content.uuid {
return false
}
if lhs.insets != rhs.insets {
return false
}
if lhs.navigationBarHeight != rhs.navigationBarHeight {
return false
}
if lhs.scrollingPanelOffsetFraction != rhs.scrollingPanelOffsetFraction {
return false
}
return true
}
final class View: UIView {
init() {
super.init(frame: CGRect())
}
required init?(coder aDecoder: NSCoder) {
preconditionFailure()
}
func update(component: BrowserContentComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize {
if component.content.superview !== self {
self.addSubview(component.content)
}
let collapsedHeight: CGFloat = 24.0
let topInset: CGFloat = component.insets.top + component.navigationBarHeight * (1.0 - component.scrollingPanelOffsetFraction) + collapsedHeight * component.scrollingPanelOffsetFraction
let bottomInset = 49.0 + component.insets.bottom
component.content.updateLayout(size: availableSize, insets: UIEdgeInsets(top: topInset, left: component.insets.left, bottom: bottomInset, right: component.insets.right), transition: transition)
transition.setFrame(view: component.content, frame: CGRect(origin: .zero, size: availableSize))
return availableSize
}
}
func makeView() -> View {
return View()
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, transition: transition)
}
}