diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index b181677f70..5f8a32624e 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -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."; diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index 8c04d63ab5..602a697cde 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -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 diff --git a/submodules/AccountContext/Sources/ChatController.swift b/submodules/AccountContext/Sources/ChatController.swift index 6f38e5b00e..3840144291 100644 --- a/submodules/AccountContext/Sources/ChatController.swift +++ b/submodules/AccountContext/Sources/ChatController.swift @@ -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 } } diff --git a/submodules/BrowserUI/BUILD b/submodules/BrowserUI/BUILD index e28f412f44..d143a87202 100644 --- a/submodules/BrowserUI/BUILD +++ b/submodules/BrowserUI/BUILD @@ -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", diff --git a/submodules/BrowserUI/Sources/BrowserAddressBarComponent.swift b/submodules/BrowserUI/Sources/BrowserAddressBarComponent.swift index 51618066fd..679f3ea5c3 100644 --- a/submodules/BrowserUI/Sources/BrowserAddressBarComponent.swift +++ b/submodules/BrowserUI/Sources/BrowserAddressBarComponent.swift @@ -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 init( theme: PresentationTheme, strings: PresentationStrings, url: String, + isSecure: Bool, + isExpanded: Bool, performAction: ActionSlot ) { 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() + private var titleContent = ComponentView() 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, 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, transition: ComponentTransition) -> CGSize { - return view.update(component: self, availableSize: availableSize, transition: transition) + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, environment: environment, transition: transition) } } diff --git a/submodules/BrowserUI/Sources/BrowserAddressListComponent.swift b/submodules/BrowserUI/Sources/BrowserAddressListComponent.swift new file mode 100644 index 0000000000..b33621c4ee --- /dev/null +++ b/submodules/BrowserUI/Sources/BrowserAddressListComponent.swift @@ -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() + + private var visibleSectionHeaders: [Int: ComponentView] = [:] + private var visibleItems: [AnyHashable: ComponentView] = [:] + + 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 + 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 + 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, 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, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/BrowserUI/Sources/BrowserAddressListItemComponent.swift b/submodules/BrowserUI/Sources/BrowserAddressListItemComponent.swift new file mode 100644 index 0000000000..49ebcb2c20 --- /dev/null +++ b/submodules/BrowserUI/Sources/BrowserAddressListItemComponent.swift @@ -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() + private let subtitle = ComponentView() + + 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, 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, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/BrowserUI/Sources/BrowserBookmarksScreen.swift b/submodules/BrowserUI/Sources/BrowserBookmarksScreen.swift new file mode 100644 index 0000000000..1cf00e619e --- /dev/null +++ b/submodules/BrowserUI/Sources/BrowserBookmarksScreen.swift @@ -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(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 + } +} + diff --git a/submodules/BrowserUI/Sources/BrowserContent.swift b/submodules/BrowserUI/Sources/BrowserContent.swift index 7a6427c01d..cf7eea0583 100644 --- a/submodules/BrowserUI/Sources/BrowserContent.swift +++ b/submodules/BrowserUI/Sources/BrowserContent.swift @@ -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) } diff --git a/submodules/BrowserUI/Sources/BrowserDocumentContent.swift b/submodules/BrowserUI/Sources/BrowserDocumentContent.swift new file mode 100644 index 0000000000..375979e94e --- /dev/null +++ b/submodules/BrowserUI/Sources/BrowserDocumentContent.swift @@ -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 + + var currentState: BrowserContentState { + return self._state + } + var state: Signal { + 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(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() { + } +} diff --git a/submodules/BrowserUI/Sources/BrowserInstantPageContent.swift b/submodules/BrowserUI/Sources/BrowserInstantPageContent.swift index f9e9e721ef..b8da562cf6 100644 --- a/submodules/BrowserUI/Sources/BrowserInstantPageContent.swift +++ b/submodules/BrowserUI/Sources/BrowserInstantPageContent.swift @@ -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() + } + } } diff --git a/submodules/BrowserUI/Sources/BrowserNavigationBarComponent.swift b/submodules/BrowserUI/Sources/BrowserNavigationBarComponent.swift index c873ddbaef..d31d7fd6a1 100644 --- a/submodules/BrowserUI/Sources/BrowserNavigationBarComponent.swift +++ b/submodules/BrowserUI/Sources/BrowserNavigationBarComponent.swift @@ -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] let rightItems: [AnyComponentWithIdentity] - let centerItem: AnyComponentWithIdentity? + let centerItem: AnyComponentWithIdentity? let readingProgress: CGFloat let loadingProgress: Double? let collapseFraction: CGFloat @@ -32,7 +47,7 @@ final class BrowserNavigationBarComponent: CombinedComponent { sideInset: CGFloat, leftItems: [AnyComponentWithIdentity], rightItems: [AnyComponentWithIdentity], - centerItem: AnyComponentWithIdentity?, + centerItem: AnyComponentWithIdentity?, 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)) diff --git a/submodules/BrowserUI/Sources/BrowserPdfContent.swift b/submodules/BrowserUI/Sources/BrowserPdfContent.swift new file mode 100644 index 0000000000..470b350ae9 --- /dev/null +++ b/submodules/BrowserUI/Sources/BrowserPdfContent.swift @@ -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 + + var currentState: BrowserContentState { + return self._state + } + var state: Signal { + 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(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() { + } +} diff --git a/submodules/BrowserUI/Sources/BrowserRecentlyVisited.swift b/submodules/BrowserUI/Sources/BrowserRecentlyVisited.swift new file mode 100644 index 0000000000..fc8bb321f9 --- /dev/null +++ b/submodules/BrowserUI/Sources/BrowserRecentlyVisited.swift @@ -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 { + 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 { + if let itemId = RecentlyVisitedLinkItemId(url) { + return engine.orderedLists.removeItem(collectionId: ApplicationSpecificOrderedItemListCollectionId.browserRecentlyVisited, id: itemId.rawValue) + } else { + return .complete() + } +} + +func clearRecentlyVisitedLinks(engine: TelegramEngine) -> Signal { + 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 + } +} diff --git a/submodules/BrowserUI/Sources/BrowserScreen.swift b/submodules/BrowserUI/Sources/BrowserScreen.swift index f555d1a821..1899dcc14f 100644 --- a/submodules/BrowserUI/Sources/BrowserScreen.swift +++ b/submodules/BrowserUI/Sources/BrowserScreen.swift @@ -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? + let navigationContent: AnyComponentWithIdentity? var navigationLeftItems: [AnyComponentWithIdentity] var navigationRightItems: [AnyComponentWithIdentity] 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() 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 diff --git a/submodules/BrowserUI/Sources/BrowserSearchBarComponent.swift b/submodules/BrowserUI/Sources/BrowserSearchBarComponent.swift index d588b744f2..6e8974667f 100644 --- a/submodules/BrowserUI/Sources/BrowserSearchBarComponent.swift +++ b/submodules/BrowserUI/Sources/BrowserSearchBarComponent.swift @@ -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 @@ -351,7 +353,7 @@ final class SearchBarContentComponent: Component { return View() } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, transition: transition) } } diff --git a/submodules/BrowserUI/Sources/BrowserTitleBarComponent.swift b/submodules/BrowserUI/Sources/BrowserTitleBarComponent.swift new file mode 100644 index 0000000000..a362da7d61 --- /dev/null +++ b/submodules/BrowserUI/Sources/BrowserTitleBarComponent.swift @@ -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() + private var component: TitleBarContentComponent? + + init() { + super.init(frame: CGRect()) + } + + required public init?(coder: NSCoder) { + fatalError() + } + + func update(component: TitleBarContentComponent, availableSize: CGSize, environment: Environment, 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, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, environment: environment, transition: transition) + } +} diff --git a/submodules/BrowserUI/Sources/BrowserWebContent.swift b/submodules/BrowserUI/Sources/BrowserWebContent.swift index 1da55837da..e99d97666f 100644 --- a/submodules/BrowserUI/Sources/BrowserWebContent.swift +++ b/submodules/BrowserUI/Sources/BrowserWebContent.swift @@ -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 + } +} diff --git a/submodules/BrowserUI/Sources/Favicon.swift b/submodules/BrowserUI/Sources/Favicon.swift deleted file mode 100644 index fe0e8ae84a..0000000000 --- a/submodules/BrowserUI/Sources/Favicon.swift +++ /dev/null @@ -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 { - if let icon = faviconCache[url] { - return .single(icon) - } - return context.engine.resources.httpData(url: url) - |> map(Optional.init) - |> `catch` { _ -> Signal 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 - } - } - } -} diff --git a/submodules/BrowserUI/Sources/SectionHeaderComponent.swift b/submodules/BrowserUI/Sources/SectionHeaderComponent.swift new file mode 100644 index 0000000000..6ebe3e80c9 --- /dev/null +++ b/submodules/BrowserUI/Sources/SectionHeaderComponent.swift @@ -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() + private let backgroundView: BlurredBackgroundView + private let action = ComponentView() + + 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, 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, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/BrowserUI/Sources/Utils.swift b/submodules/BrowserUI/Sources/Utils.swift new file mode 100644 index 0000000000..7caff6dd87 --- /dev/null +++ b/submodules/BrowserUI/Sources/Utils.swift @@ -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 { + if let icon = faviconCache[url] { + return .single(icon) + } + return context.engine.resources.httpData(url: url) + |> map(Optional.init) + |> `catch` { _ -> Signal 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 +} diff --git a/submodules/Display/Source/Navigation/MinimizedContainer.swift b/submodules/Display/Source/Navigation/MinimizedContainer.swift index bc5cb3941b..58201ab9bd 100644 --- a/submodules/Display/Source/Navigation/MinimizedContainer.swift +++ b/submodules/Display/Source/Navigation/MinimizedContainer.swift @@ -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 } diff --git a/submodules/Display/Source/Navigation/NavigationLayout.swift b/submodules/Display/Source/Navigation/NavigationLayout.swift index 839c4f41ea..61cfc02b45 100644 --- a/submodules/Display/Source/Navigation/NavigationLayout.swift +++ b/submodules/Display/Source/Navigation/NavigationLayout.swift @@ -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 diff --git a/submodules/Display/Source/ViewController.swift b/submodules/Display/Source/ViewController.swift index 347ef7df37..0c3d1a7e92 100644 --- a/submodules/Display/Source/ViewController.swift +++ b/submodules/Display/Source/ViewController.swift @@ -65,6 +65,7 @@ public enum ViewControllerNavigationPresentation { case flatModal case standaloneModal case modalInLargeLayout + case modalInCompactLayout } public enum TabBarItemContextActionType { diff --git a/submodules/DrawingUI/Sources/DrawingScreen.swift b/submodules/DrawingUI/Sources/DrawingScreen.swift index dd4c749639..ca595aadc5 100644 --- a/submodules/DrawingUI/Sources/DrawingScreen.swift +++ b/submodules/DrawingUI/Sources/DrawingScreen.swift @@ -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 { diff --git a/submodules/OpenInExternalAppUI/Sources/OpenInOptions.swift b/submodules/OpenInExternalAppUI/Sources/OpenInOptions.swift index e359d7895b..cdc9c00032 100644 --- a/submodules/OpenInExternalAppUI/Sources/OpenInOptions.swift +++ b/submodules/OpenInExternalAppUI/Sources/OpenInOptions.swift @@ -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") diff --git a/submodules/SettingsUI/Sources/Data and Storage/WebBrowserDomainController.swift b/submodules/SettingsUI/Sources/Data and Storage/WebBrowserDomainController.swift index 2d439c746a..4d62514a07 100644 --- a/submodules/SettingsUI/Sources/Data and Storage/WebBrowserDomainController.swift +++ b/submodules/SettingsUI/Sources/Data and Storage/WebBrowserDomainController.swift @@ -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() diff --git a/submodules/SettingsUI/Sources/Data and Storage/WebBrowserDomainExceptionItem.swift b/submodules/SettingsUI/Sources/Data and Storage/WebBrowserDomainExceptionItem.swift index 572e65ed9f..3e66f1ff29 100644 --- a/submodules/SettingsUI/Sources/Data and Storage/WebBrowserDomainExceptionItem.swift +++ b/submodules/SettingsUI/Sources/Data and Storage/WebBrowserDomainExceptionItem.swift @@ -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?, (ListViewItemApply) -> Void)) -> Void) { + func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (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() + } } diff --git a/submodules/SettingsUI/Sources/Data and Storage/WebBrowserSettingsController.swift b/submodules/SettingsUI/Sources/Data and Storage/WebBrowserSettingsController.swift index eda596b35c..29a4b1b14e 100644 --- a/submodules/SettingsUI/Sources/Data and Storage/WebBrowserSettingsController.swift +++ b/submodules/SettingsUI/Sources/Data and Storage/WebBrowserSettingsController.swift @@ -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 { +private func fetchDomainExceptionInfo(context: AccountContext, url: String) -> Signal { 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)) } } diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/CodableDrawingEntity.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/CodableDrawingEntity.swift index c1aa31fd74..b930925be4 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/CodableDrawingEntity.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/CodableDrawingEntity.swift @@ -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, diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift index 944830b8bf..27a8139900 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift @@ -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) diff --git a/submodules/TelegramUI/Components/MinimizedContainer/Sources/MinimizedContainer.swift b/submodules/TelegramUI/Components/MinimizedContainer/Sources/MinimizedContainer.swift index 2bcb788957..8cb8ed781a 100644 --- a/submodules/TelegramUI/Components/MinimizedContainer/Sources/MinimizedContainer.swift +++ b/submodules/TelegramUI/Components/MinimizedContainer/Sources/MinimizedContainer.swift @@ -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 diff --git a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift index b16bd59352..1fe793e04b 100644 --- a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift +++ b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift @@ -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) } } diff --git a/submodules/TelegramUI/Resources/WebEmbed/UIWebViewSearch.js b/submodules/TelegramUI/Resources/WebEmbed/UIWebViewSearch.js index 20b3fe89e7..fcb419a5bd 100644 --- a/submodules/TelegramUI/Resources/WebEmbed/UIWebViewSearch.js +++ b/submodules/TelegramUI/Resources/WebEmbed/UIWebViewSearch.js @@ -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); + } +} diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenStorySharing.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenStorySharing.swift index 77353f1fad..20b2ba01a1 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenStorySharing.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenStorySharing.swift @@ -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) diff --git a/submodules/TelegramUI/Sources/OpenChatMessage.swift b/submodules/TelegramUI/Sources/OpenChatMessage.swift index 1422973870..c0e002509a 100644 --- a/submodules/TelegramUI/Sources/OpenChatMessage.swift +++ b/submodules/TelegramUI/Sources/OpenChatMessage.swift @@ -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 } diff --git a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift index 6625d8b06f..167e85acec 100644 --- a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift +++ b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift @@ -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() diff --git a/submodules/TelegramUI/Sources/OpenUrl.swift b/submodules/TelegramUI/Sources/OpenUrl.swift index e232ff65cc..c25368c74b 100644 --- a/submodules/TelegramUI/Sources/OpenUrl.swift +++ b/submodules/TelegramUI/Sources/OpenUrl.swift @@ -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 diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index 1afcc0ab6a..abc647f4f6 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -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 + 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) diff --git a/submodules/TelegramUIPreferences/Sources/PostboxKeys.swift b/submodules/TelegramUIPreferences/Sources/PostboxKeys.swift index cba2c06b8c..ebefd6a4f9 100644 --- a/submodules/TelegramUIPreferences/Sources/PostboxKeys.swift +++ b/submodules/TelegramUIPreferences/Sources/PostboxKeys.swift @@ -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) } diff --git a/submodules/TelegramUIPreferences/Sources/WebBrowserSettings.swift b/submodules/TelegramUIPreferences/Sources/WebBrowserSettings.swift index 9c45fb6edd..68a6760183 100644 --- a/submodules/TelegramUIPreferences/Sources/WebBrowserSettings.swift +++ b/submodules/TelegramUIPreferences/Sources/WebBrowserSettings.swift @@ -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") + } } } diff --git a/submodules/WebUI/BUILD b/submodules/WebUI/BUILD index a0017b0e91..70f24be4cf 100644 --- a/submodules/WebUI/BUILD +++ b/submodules/WebUI/BUILD @@ -39,6 +39,7 @@ swift_library( "//submodules/TelegramUI/Components/EmojiStatusComponent", "//submodules/ShareController", "//submodules/UndoUI", + "//submodules/OverlayStatusController", ], visibility = [ "//visibility:public", diff --git a/submodules/WebUI/Sources/WebAppController.swift b/submodules/WebUI/Sources/WebAppController.swift index 08f7b3bec2..1191fa54c3 100644 --- a/submodules/WebUI/Sources/WebAppController.swift +++ b/submodules/WebUI/Sources/WebAppController.swift @@ -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 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 }