From 345616704acdec5304f8843d5ad3037c48dc907d Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Mon, 22 Jul 2024 07:11:46 +0400 Subject: [PATCH] Browser improvements --- .../Telegram-iOS/en.lproj/Localizable.strings | 26 + .../Sources/BrowserAddressBarComponent.swift | 362 ++++++++++++++ .../BrowserUI/Sources/BrowserScreen.swift | 63 ++- .../Sources/BrowserSearchBarComponent.swift | 8 +- .../Sources/BrowserToolbarComponent.swift | 42 +- submodules/Camera/Sources/VideoRecorder.swift | 6 +- .../Sources/LocationViewController.swift | 1 - .../DataAndStorageSettingsController.swift | 34 +- .../WebBrowserDomainController.swift | 448 ++++++++++++++++++ .../WebBrowserDomainExceptionItem.swift | 280 +++++++++++ .../WebBrowserSettingsController.swift | 300 +++++++++++- .../Resources/PresentationResourceKey.swift | 1 + .../PresentationResourcesItemList.swift | 6 + .../Sources/MediaCoverScreen.swift | 13 + .../Sources/MediaEditorScreen.swift | 48 +- .../Sources/MediaScrubberComponent.swift | 62 ++- .../Sources/ShareWithPeersScreen.swift | 8 +- .../Sources/VideoMessageCameraScreen.swift | 18 +- .../Bookmark.imageset/Bookmark.pdf | Bin 0 -> 2667 bytes .../Contents.json | 2 +- .../Instant View/Browser.imageset/Browser.pdf | Bin 0 -> 2160 bytes .../Browser.imageset/Contents.json | 12 + .../Instant View/CloseIcon.imageset/Close.pdf | 83 ++++ .../CloseIcon.imageset/Contents.json | 2 +- .../Instant View/CloseIcon.imageset/cross.pdf | 83 ---- .../Browser.imageset/ic_lt_safari.pdf | Bin 4345 -> 0 bytes submodules/TelegramUI/Sources/OpenUrl.swift | 2 +- .../Sources/WebBrowserSettings.swift | 63 ++- .../Sources/UndoOverlayControllerNode.swift | 4 +- 29 files changed, 1811 insertions(+), 166 deletions(-) create mode 100644 submodules/BrowserUI/Sources/BrowserAddressBarComponent.swift create mode 100644 submodules/SettingsUI/Sources/Data and Storage/WebBrowserDomainController.swift create mode 100644 submodules/SettingsUI/Sources/Data and Storage/WebBrowserDomainExceptionItem.swift create mode 100644 submodules/TelegramUI/Images.xcassets/Instant View/Bookmark.imageset/Bookmark.pdf rename submodules/TelegramUI/Images.xcassets/Instant View/{Settings/Browser.imageset => Bookmark.imageset}/Contents.json (75%) create mode 100644 submodules/TelegramUI/Images.xcassets/Instant View/Browser.imageset/Browser.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Instant View/Browser.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Instant View/CloseIcon.imageset/Close.pdf delete mode 100644 submodules/TelegramUI/Images.xcassets/Instant View/CloseIcon.imageset/cross.pdf delete mode 100644 submodules/TelegramUI/Images.xcassets/Instant View/Settings/Browser.imageset/ic_lt_safari.pdf diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 77e7011c33..d36db734bd 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -12552,3 +12552,29 @@ Sorry for the inconvenience."; "Stars.Gift.Sent.Title" = "Sent Gift"; "Stars.Gift.Sent.Text" = "With Stars, %@ will be able to unlock content and services on Telegram. [See Examples >]()"; + +"WebBrowser.AddBookmark" = "Add Bookmark"; + +"WebBrowser.LinkAddedToBookmarks" = "Link added to [Bookmarks]() and **Saved Messages**."; + +"WebBrowser.AddressBar.RecentlyVisited" = "RECENTLY VISITED"; +"WebBrowser.AddressBar.RecentlyVisited.Clear" = "Clear"; + +"WebBrowser.AddressBar.Bookmarks" = "BOOKMARKS"; + +"WebBrowser.OpenLinksIn.Title" = "OPEN LINKS IN"; +"WebBrowser.AutoLogin" = "Auto-Login via Telegram"; +"WebBrowser.AutoLogin.Info" = "Use your Telegram account to automatically log in to websites opened in the in-app browser."; + +"WebBrowser.ClearCookies" = "Clear Cookies"; +"WebBrowser.ClearCookies.Info" = "Delete all cookies in the Telegram in-app browser. This action will sign you out of most websites."; +"WebBrowser.ClearCookies.Succeed" = "Cookies cleared."; + +"WebBrowser.Exceptions.Title" = "NEVER OPEN IN THE IN-APP BROWSER"; +"WebBrowser.Exceptions.AddException" = "Add Website"; +"WebBrowser.Exceptions.Clear" = "Clear List"; +"WebBrowser.Exceptions.Info" = "These websites will be always opened in your default browser."; + +"WebBrowser.Exceptions.Create.Title" = "Add Website"; +"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"; diff --git a/submodules/BrowserUI/Sources/BrowserAddressBarComponent.swift b/submodules/BrowserUI/Sources/BrowserAddressBarComponent.swift new file mode 100644 index 0000000000..51618066fd --- /dev/null +++ b/submodules/BrowserUI/Sources/BrowserAddressBarComponent.swift @@ -0,0 +1,362 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display +import ComponentFlow +import TelegramPresentationData +import AccountContext +import BundleIconComponent + +final class AddressBarContentComponent: Component { + let theme: PresentationTheme + let strings: PresentationStrings + let url: String + let performAction: ActionSlot + + init( + theme: PresentationTheme, + strings: PresentationStrings, + url: String, + performAction: ActionSlot + ) { + self.theme = theme + self.strings = strings + self.url = url + self.performAction = performAction + } + + static func ==(lhs: AddressBarContentComponent, rhs: AddressBarContentComponent) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + if lhs.strings !== rhs.strings { + return false + } + if lhs.url != rhs.url { + return false + } + return true + } + + final class View: UIView, UITextFieldDelegate { + private final class TextField: UITextField { + override func textRect(forBounds bounds: CGRect) -> CGRect { + return bounds.integral + } + } + + private struct Params: Equatable { + var theme: PresentationTheme + var strings: PresentationStrings + var size: CGSize + + static func ==(lhs: Params, rhs: Params) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + if lhs.strings !== rhs.strings { + return false + } + if lhs.size != rhs.size { + 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 + + private let clearIconView: UIImageView + private let clearIconButton: HighlightTrackingButton + + private let cancelButtonTitle: ComponentView + private let cancelButton: HighlightTrackingButton + + private var placeholderContent = ComponentView() + + private var textFrame: CGRect? + private var textField: TextField? + + private var tapRecognizer: UITapGestureRecognizer? + + private var params: Params? + private var component: AddressBarContentComponent? + + public var wantsDisplayBelowKeyboard: Bool { + return self.textField != nil + } + + init() { + self.backgroundLayer = SimpleLayer() + + self.iconView = UIImageView() + + self.clearIconView = UIImageView() + self.clearIconButton = HighlightableButton() + self.clearIconView.isHidden = true + self.clearIconButton.isHidden = true + + self.cancelButtonTitle = ComponentView() + self.cancelButton = HighlightTrackingButton() + + super.init(frame: CGRect()) + + self.layer.addSublayer(self.backgroundLayer) + + self.addSubview(self.iconView) + self.addSubview(self.clearIconView) + self.addSubview(self.clearIconButton) + + self.addSubview(self.cancelButton) + self.clipsToBounds = true + + let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))) + self.tapRecognizer = tapRecognizer + self.addGestureRecognizer(tapRecognizer) + + self.cancelButton.highligthedChanged = { [weak self] highlighted in + if let strongSelf = self { + if highlighted { + if let cancelButtonTitleView = strongSelf.cancelButtonTitle.view { + cancelButtonTitleView.layer.removeAnimation(forKey: "opacity") + cancelButtonTitleView.alpha = 0.4 + } + } else { + if let cancelButtonTitleView = strongSelf.cancelButtonTitle.view { + cancelButtonTitleView.alpha = 1.0 + cancelButtonTitleView.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) + } + } + } + } + self.cancelButton.addTarget(self, action: #selector(self.cancelPressed), for: .touchUpInside) + + self.clearIconButton.highligthedChanged = { [weak self] highlighted in + if let strongSelf = self { + if highlighted { + strongSelf.clearIconView.layer.removeAnimation(forKey: "opacity") + strongSelf.clearIconView.alpha = 0.4 + } else { + strongSelf.clearIconView.alpha = 1.0 + strongSelf.clearIconView.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) + } + } + } + self.clearIconButton.addTarget(self, action: #selector(self.clearPressed), for: .touchUpInside) + } + + required public init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc private func tapGesture(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state { + self.activateTextInput() + } + } + + 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() + } + + @objc private func cancelPressed() { + self.updateQuery(nil) + + 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() + } + } + + @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() + } + } + + public func textFieldDidBeginEditing(_ textField: UITextField) { + } + + public func textFieldDidEndEditing(_ textField: UITextField) { + } + + public func textFieldShouldReturn(_ textField: UITextField) -> Bool { + textField.endEditing(true) + return false + } + + @objc private func textFieldChanged(_ textField: UITextField) { + let text = textField.text ?? "" + + self.clearIconView.isHidden = text.isEmpty + 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) + } + } + + func update(component: AddressBarContentComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize { + self.component = component + + self.update(theme: component.theme, strings: component.strings, size: availableSize, transition: transition) + + return availableSize + } + + public func update(theme: PresentationTheme, strings: PresentationStrings, size: CGSize, transition: ComponentTransition) { + let params = Params( + theme: theme, + strings: strings, + size: size + ) + + if self.params == params { + return + } + + let isActiveWithText = true + + if self.params?.theme !== theme { + self.iconView.image = generateTintedImage(image: UIImage(bundleImageName: "Components/Search Bar/Loupe"), 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 + } + + self.params = params + + let sideInset: CGFloat = 10.0 + 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 + + let cancelTextSize = self.cancelButtonTitle.update( + transition: .immediate, + component: AnyComponent(Text( + text: strings.Common_Cancel, + font: Font.regular(17.0), + color: theme.rootController.navigationBar.accentTextColor + )), + environment: {}, + containerSize: CGSize(width: size.width - 32.0, height: 100.0) + ) + + let cancelButtonSpacing: CGFloat = 8.0 + + var backgroundFrame = CGRect(origin: CGPoint(x: sideInset, y: topInset), size: CGSize(width: size.width - sideInset * 2.0, height: inputHeight)) + if isActiveWithText { + backgroundFrame.size.width -= cancelTextSize.width + cancelButtonSpacing + } + transition.setFrame(layer: self.backgroundLayer, frame: backgroundFrame) + + 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 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) + ), + environment: {}, + containerSize: size + ) + if let placeholderContentView = self.placeholderContent.view { + if placeholderContentView.superview == nil { + 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) + } + + 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)) + } + + if let cancelButtonTitleComponentView = self.cancelButtonTitle.view { + if cancelButtonTitleComponentView.superview == nil { + self.addSubview(cancelButtonTitleComponentView) + 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)) + } + + 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))) + } + } + } + + 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, transition: transition) + } +} diff --git a/submodules/BrowserUI/Sources/BrowserScreen.swift b/submodules/BrowserUI/Sources/BrowserScreen.swift index 8b19afc5c7..e6115c2fde 100644 --- a/submodules/BrowserUI/Sources/BrowserScreen.swift +++ b/submodules/BrowserUI/Sources/BrowserScreen.swift @@ -109,7 +109,10 @@ private final class BrowserScreenComponent: CombinedComponent { component: AnyComponent( Button( content: AnyComponent( - MultilineTextComponent(text: .plain(NSAttributedString(string: environment.strings.Common_Close, font: Font.regular(17.0), textColor: environment.theme.rootController.navigationBar.primaryTextColor, paragraphAlignment: .center)), horizontalAlignment: .left, maximumNumberOfLines: 1) + BundleIconComponent( + name: "Instant View/CloseIcon", + tintColor: environment.theme.rootController.navigationBar.accentTextColor + ) ), action: { performAction.invoke(.close) @@ -130,7 +133,7 @@ private final class BrowserScreenComponent: CombinedComponent { content: LottieComponent.AppBundleContent( name: "anim_moredots" ), - color: environment.theme.rootController.navigationBar.primaryTextColor, + color: environment.theme.rootController.navigationBar.accentTextColor, size: CGSize(width: 30.0, height: 30.0) ) ), @@ -150,7 +153,7 @@ private final class BrowserScreenComponent: CombinedComponent { ReferenceButtonComponent( content: AnyComponent( BundleIconComponent( - name: isLoading ? "Instant View/CloseIcon" : "Chat/Context Menu/Reload", + name: isLoading ? "Instant View/Close" : "Chat/Context Menu/Reload", tintColor: environment.theme.rootController.navigationBar.primaryTextColor ) ), @@ -211,6 +214,7 @@ private final class BrowserScreenComponent: CombinedComponent { id: "navigation", component: AnyComponent( NavigationToolbarContentComponent( + accentColor: environment.theme.rootController.navigationBar.accentTextColor, textColor: environment.theme.rootController.navigationBar.primaryTextColor, canGoBack: context.component.contentState?.canGoBack ?? false, canGoForward: context.component.contentState?.canGoForward ?? false, @@ -281,6 +285,8 @@ public class BrowserScreen: ViewController, MinimizableController { case increaseFontSize case resetFontSize case updateFontIsSerif(Bool) + case addBookmark + case openBookmarks } fileprivate final class Node: ViewControllerTracingNode { @@ -502,6 +508,12 @@ public class BrowserScreen: ViewController, MinimizableController { return updatedState }) content.updateFontState(self.presentationState.fontState) + case .addBookmark: + if let content = self.content.last { + self.addBookmark(content.currentState.url) + } + case .openBookmarks: + break } } @@ -609,6 +621,43 @@ public class BrowserScreen: ViewController, MinimizableController { self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(peer), animated: true)) } + func addBookmark(_ url: String) { + let _ = enqueueMessages( + account: self.context.account, + peerId: self.context.account.peerId, + messages: [.message( + text: url, + attributes: [], + inlineStickers: [:], + mediaReference: nil, + threadId: nil, + replyToMessageId: nil, + replyToStoryId: nil, + localGroupingKey: nil, + correlationId: nil, + bubbleUpEmojiOrStickersets: [] + )] + ).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 + 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 + guard let self, let peer else { + return + } + guard let navigationController = self.controller?.navigationController as? NavigationController else { + return + } + self.minimize() + self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(peer), forceOpenChat: true)) + }) + } + return false + }), in: .current) + } + private func setupContentStateUpdates() { for content in self.content { content.onScrollingUpdate = { _ in } @@ -777,7 +826,11 @@ public class BrowserScreen: ViewController, MinimizableController { performAction.invoke(.updateSearchActive(true)) action(.default) })), - .action(ContextMenuActionItem(text: self.presentationData.strings.InstantPage_OpenInBrowser(openInTitle).string, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Instant View/Settings/Browser"), color: theme.contextMenu.primaryColor) }, action: { [weak self] (controller, action) in + .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 if let self { self.context.sharedContext.applicationBindings.openUrl(openInUrl) } @@ -1074,7 +1127,7 @@ public class BrowserScreen: ViewController, MinimizableController { } return nil } - + public var minimizedProgress: Float? { if let contentState = self.node.contentState { return Float(contentState.readingProgress) diff --git a/submodules/BrowserUI/Sources/BrowserSearchBarComponent.swift b/submodules/BrowserUI/Sources/BrowserSearchBarComponent.swift index c49fa13540..d588b744f2 100644 --- a/submodules/BrowserUI/Sources/BrowserSearchBarComponent.swift +++ b/submodules/BrowserUI/Sources/BrowserSearchBarComponent.swift @@ -33,7 +33,7 @@ final class SearchBarContentComponent: Component { } final class View: UIView, UITextFieldDelegate { - private final class EmojiSearchTextField: UITextField { + private final class SearchTextField: UITextField { override func textRect(forBounds bounds: CGRect) -> CGRect { return bounds.integral } @@ -75,7 +75,7 @@ final class SearchBarContentComponent: Component { private var placeholderContent = ComponentView() private var textFrame: CGRect? - private var textField: EmojiSearchTextField? + private var textField: SearchTextField? private var tapRecognizer: UITapGestureRecognizer? @@ -160,7 +160,7 @@ final class SearchBarContentComponent: Component { 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 = EmojiSearchTextField(frame: textFieldFrame) + let textField = SearchTextField(frame: textFieldFrame) textField.autocorrectionType = .no textField.returnKeyType = .search self.textField = textField @@ -285,7 +285,7 @@ final class SearchBarContentComponent: Component { component: AnyComponent(Text( text: strings.Common_Cancel, font: Font.regular(17.0), - color: theme.rootController.navigationBar.primaryTextColor + color: theme.rootController.navigationBar.accentTextColor )), environment: {}, containerSize: CGSize(width: size.width - 32.0, height: 100.0) diff --git a/submodules/BrowserUI/Sources/BrowserToolbarComponent.swift b/submodules/BrowserUI/Sources/BrowserToolbarComponent.swift index 7a752140ae..2c35387e73 100644 --- a/submodules/BrowserUI/Sources/BrowserToolbarComponent.swift +++ b/submodules/BrowserUI/Sources/BrowserToolbarComponent.swift @@ -120,6 +120,7 @@ final class BrowserToolbarComponent: CombinedComponent { } final class NavigationToolbarContentComponent: CombinedComponent { + let accentColor: UIColor let textColor: UIColor let canGoBack: Bool let canGoForward: Bool @@ -127,12 +128,14 @@ final class NavigationToolbarContentComponent: CombinedComponent { let performHoldAction: (UIView, ContextGesture?, BrowserScreen.Action) -> Void init( + accentColor: UIColor, textColor: UIColor, canGoBack: Bool, canGoForward: Bool, performAction: ActionSlot, performHoldAction: @escaping (UIView, ContextGesture?, BrowserScreen.Action) -> Void ) { + self.accentColor = accentColor self.textColor = textColor self.canGoBack = canGoBack self.canGoForward = canGoForward @@ -141,6 +144,9 @@ final class NavigationToolbarContentComponent: CombinedComponent { } static func ==(lhs: NavigationToolbarContentComponent, rhs: NavigationToolbarContentComponent) -> Bool { + if lhs.accentColor != rhs.accentColor { + return false + } if lhs.textColor != rhs.textColor { return false } @@ -157,6 +163,7 @@ final class NavigationToolbarContentComponent: CombinedComponent { let back = Child(ContextReferenceButtonComponent.self) let forward = Child(ContextReferenceButtonComponent.self) let share = Child(Button.self) + let bookmark = Child(Button.self) let openIn = Child(Button.self) return { context in @@ -166,7 +173,7 @@ final class NavigationToolbarContentComponent: CombinedComponent { let sideInset: CGFloat = 5.0 let buttonSize = CGSize(width: 50.0, height: availableSize.height) - let spacing = (availableSize.width - buttonSize.width * 4.0 - sideInset * 2.0) / 3.0 + let spacing = (availableSize.width - buttonSize.width * 5.0 - sideInset * 2.0) / 4.0 let canGoBack = context.component.canGoBack let back = back.update( @@ -174,7 +181,7 @@ final class NavigationToolbarContentComponent: CombinedComponent { content: AnyComponent( BundleIconComponent( name: "Instant View/Back", - tintColor: canGoBack ? context.component.textColor : context.component.textColor.withAlphaComponent(0.4) + tintColor: canGoBack ? context.component.accentColor : context.component.accentColor.withAlphaComponent(0.4) ) ), minSize: buttonSize, @@ -202,7 +209,7 @@ final class NavigationToolbarContentComponent: CombinedComponent { content: AnyComponent( BundleIconComponent( name: "Instant View/Forward", - tintColor: canGoForward ? context.component.textColor : context.component.textColor.withAlphaComponent(0.4) + tintColor: canGoForward ? context.component.accentColor : context.component.accentColor.withAlphaComponent(0.4) ) ), minSize: buttonSize, @@ -229,7 +236,7 @@ final class NavigationToolbarContentComponent: CombinedComponent { content: AnyComponent( BundleIconComponent( name: "Chat List/NavigationShare", - tintColor: context.component.textColor + tintColor: context.component.accentColor ) ), action: { @@ -243,23 +250,42 @@ final class NavigationToolbarContentComponent: CombinedComponent { .position(CGPoint(x: sideInset + back.size.width + spacing + forward.size.width + spacing + share.size.width / 2.0, y: availableSize.height / 2.0)) ) + let bookmark = bookmark.update( + component: Button( + content: AnyComponent( + BundleIconComponent( + name: "Instant View/Bookmark", + tintColor: context.component.accentColor + ) + ), + action: { + performAction.invoke(.openBookmarks) + } + ).minSize(buttonSize), + availableSize: buttonSize, + transition: .easeInOut(duration: 0.2) + ) + context.add(bookmark + .position(CGPoint(x: sideInset + back.size.width + spacing + forward.size.width + spacing + share.size.width + spacing + bookmark.size.width / 2.0, y: availableSize.height / 2.0)) + ) + let openIn = openIn.update( component: Button( content: AnyComponent( BundleIconComponent( - name: "Instant View/Minimize", - tintColor: context.component.textColor + name: "Instant View/Browser", + tintColor: context.component.accentColor ) ), action: { - performAction.invoke(.minimize) + performAction.invoke(.openIn) } ).minSize(buttonSize), availableSize: buttonSize, transition: .easeInOut(duration: 0.2) ) context.add(openIn - .position(CGPoint(x: sideInset + back.size.width + spacing + forward.size.width + spacing + share.size.width + spacing + openIn.size.width / 2.0, y: availableSize.height / 2.0)) + .position(CGPoint(x: sideInset + back.size.width + spacing + forward.size.width + spacing + share.size.width + spacing + bookmark.size.width + spacing + openIn.size.width / 2.0, y: availableSize.height / 2.0)) ) return availableSize diff --git a/submodules/Camera/Sources/VideoRecorder.swift b/submodules/Camera/Sources/VideoRecorder.swift index e785db1834..09dd80a039 100644 --- a/submodules/Camera/Sources/VideoRecorder.swift +++ b/submodules/Camera/Sources/VideoRecorder.swift @@ -204,9 +204,9 @@ private final class VideoRecorderImpl { if let videoInput = self.videoInput { let time = CACurrentMediaTime() - if let previousPresentationTime = self.previousPresentationTime, let previousAppendTime = self.previousAppendTime { - print("appending \(presentationTime.seconds) (\(presentationTime.seconds - previousPresentationTime) ) on \(time) (\(time - previousAppendTime)") - } +// if let previousPresentationTime = self.previousPresentationTime, let previousAppendTime = self.previousAppendTime { +// print("appending \(presentationTime.seconds) (\(presentationTime.seconds - previousPresentationTime) ) on \(time) (\(time - previousAppendTime)") +// } self.previousPresentationTime = presentationTime.seconds self.previousAppendTime = time diff --git a/submodules/LocationUI/Sources/LocationViewController.swift b/submodules/LocationUI/Sources/LocationViewController.swift index 35fb7bb4d4..173b93567c 100644 --- a/submodules/LocationUI/Sources/LocationViewController.swift +++ b/submodules/LocationUI/Sources/LocationViewController.swift @@ -80,7 +80,6 @@ public final class LocationViewController: ViewController { private let isStoryLocation: Bool private let locationManager = LocationManager() - private var permissionDisposable: Disposable? private var interaction: LocationViewInteraction? diff --git a/submodules/SettingsUI/Sources/Data and Storage/DataAndStorageSettingsController.swift b/submodules/SettingsUI/Sources/Data and Storage/DataAndStorageSettingsController.swift index d3a2363d15..adf77392b4 100644 --- a/submodules/SettingsUI/Sources/Data and Storage/DataAndStorageSettingsController.swift +++ b/submodules/SettingsUI/Sources/Data and Storage/DataAndStorageSettingsController.swift @@ -100,9 +100,9 @@ private enum DataAndStorageEntry: ItemListNodeEntry { case useLessVoiceData(PresentationTheme, String, Bool) case useLessVoiceDataInfo(PresentationTheme, String) case otherHeader(PresentationTheme, String) + case openLinksIn(PresentationTheme, String, String) case shareSheet(PresentationTheme, String) case saveEditedPhotos(PresentationTheme, String, Bool) - case openLinksIn(PresentationTheme, String, String) case pauseMusicOnRecording(PresentationTheme, String, Bool) case raiseToListen(PresentationTheme, String, Bool) case raiseToListenInfo(PresentationTheme, String) @@ -123,7 +123,7 @@ private enum DataAndStorageEntry: ItemListNodeEntry { return DataAndStorageSection.backgroundDownload.rawValue case .useLessVoiceData, .useLessVoiceDataInfo: return DataAndStorageSection.voiceCalls.rawValue - case .otherHeader, .shareSheet, .saveEditedPhotos, .openLinksIn, .pauseMusicOnRecording, .raiseToListen, .raiseToListenInfo: + case .otherHeader, .openLinksIn, .shareSheet, .saveEditedPhotos, .pauseMusicOnRecording, .raiseToListen, .raiseToListenInfo: return DataAndStorageSection.other.rawValue case .connectionHeader, .connectionProxy: return DataAndStorageSection.connection.rawValue @@ -162,11 +162,11 @@ private enum DataAndStorageEntry: ItemListNodeEntry { return 24 case .otherHeader: return 29 - case .shareSheet: - return 30 - case .saveEditedPhotos: - return 31 case .openLinksIn: + return 30 + case .shareSheet: + return 31 + case .saveEditedPhotos: return 32 case .pauseMusicOnRecording: return 33 @@ -257,6 +257,12 @@ private enum DataAndStorageEntry: ItemListNodeEntry { } else { return false } + case let .openLinksIn(lhsTheme, lhsText, lhsValue): + if case let .openLinksIn(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { + return true + } else { + return false + } case let .shareSheet(lhsTheme, lhsText): if case let .shareSheet(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true @@ -269,12 +275,6 @@ private enum DataAndStorageEntry: ItemListNodeEntry { } else { return false } - case let .openLinksIn(lhsTheme, lhsText, lhsValue): - if case let .openLinksIn(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { - return true - } else { - return false - } case let .pauseMusicOnRecording(lhsTheme, lhsText, lhsValue): if case let .pauseMusicOnRecording(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { return true @@ -386,6 +386,10 @@ private enum DataAndStorageEntry: ItemListNodeEntry { return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) case let .otherHeader(_, text): return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) + case let .openLinksIn(_, text, value): + return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, action: { + arguments.openBrowserSelection() + }) case let .shareSheet(_, text): return ItemListDisclosureItem(presentationData: presentationData, title: text, label: "", sectionId: self.section, style: .blocks, action: { arguments.openIntents() @@ -394,10 +398,6 @@ private enum DataAndStorageEntry: ItemListNodeEntry { return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, sectionId: self.section, style: .blocks, updated: { value in arguments.toggleSaveEditedPhotos(value) }, tag: DataAndStorageEntryTag.saveEditedPhotos) - case let .openLinksIn(_, text, value): - return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, action: { - arguments.openBrowserSelection() - }) case let .pauseMusicOnRecording(_, text, value): return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, sectionId: self.section, style: .blocks, updated: { value in arguments.togglePauseMusicOnRecording(value) @@ -618,11 +618,11 @@ private func dataAndStorageControllerEntries(state: DataAndStorageControllerStat entries.append(.useLessVoiceDataInfo(presentationData.theme, presentationData.strings.CallSettings_UseLessDataLongDescription)) entries.append(.otherHeader(presentationData.theme, presentationData.strings.ChatSettings_Other)) + entries.append(.openLinksIn(presentationData.theme, presentationData.strings.ChatSettings_OpenLinksIn, defaultWebBrowser)) if #available(iOSApplicationExtension 13.2, iOS 13.2, *) { entries.append(.shareSheet(presentationData.theme, presentationData.strings.ChatSettings_IntentsSettings)) } entries.append(.saveEditedPhotos(presentationData.theme, presentationData.strings.Settings_SaveEditedPhotos, data.generatedMediaStoreSettings.storeEditedPhotos)) - entries.append(.openLinksIn(presentationData.theme, presentationData.strings.ChatSettings_OpenLinksIn, defaultWebBrowser)) entries.append(.pauseMusicOnRecording(presentationData.theme, presentationData.strings.Settings_PauseMusicOnRecording, data.mediaInputSettings.pauseMusicOnRecording)) entries.append(.raiseToListen(presentationData.theme, presentationData.strings.Settings_RaiseToListen, data.mediaInputSettings.enableRaiseToSpeak)) entries.append(.raiseToListenInfo(presentationData.theme, presentationData.strings.Settings_RaiseToListenInfo)) diff --git a/submodules/SettingsUI/Sources/Data and Storage/WebBrowserDomainController.swift b/submodules/SettingsUI/Sources/Data and Storage/WebBrowserDomainController.swift new file mode 100644 index 0000000000..2d439c746a --- /dev/null +++ b/submodules/SettingsUI/Sources/Data and Storage/WebBrowserDomainController.swift @@ -0,0 +1,448 @@ +import Foundation +import UIKit +import SwiftSignalKit +import AsyncDisplayKit +import Display +import TelegramCore +import TelegramPresentationData +import AccountContext +import UrlEscaping + +private final class WebBrowserDomainInputFieldNode: ASDisplayNode, ASEditableTextNodeDelegate { + private var theme: PresentationTheme + private let backgroundNode: ASImageNode + fileprivate let textInputNode: EditableTextNode + private let placeholderNode: ASTextNode + + var updateHeight: (() -> Void)? + var complete: (() -> Void)? + var textChanged: ((String) -> Void)? + + private let backgroundInsets = UIEdgeInsets(top: 8.0, left: 16.0, bottom: 15.0, right: 16.0) + private let inputInsets = UIEdgeInsets(top: 5.0, left: 12.0, bottom: 5.0, right: 12.0) + + var text: String { + get { + return self.textInputNode.attributedText?.string ?? "" + } + set { + self.textInputNode.attributedText = NSAttributedString(string: newValue, font: Font.regular(17.0), textColor: self.theme.actionSheet.inputTextColor) + self.placeholderNode.isHidden = !newValue.isEmpty + } + } + + var placeholder: String = "" { + didSet { + self.placeholderNode.attributedText = NSAttributedString(string: self.placeholder, font: Font.regular(17.0), textColor: self.theme.actionSheet.inputPlaceholderColor) + } + } + + init(theme: PresentationTheme, placeholder: String) { + self.theme = theme + + self.backgroundNode = ASImageNode() + self.backgroundNode.isLayerBacked = true + self.backgroundNode.displaysAsynchronously = false + self.backgroundNode.displayWithoutProcessing = true + self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 12.0, color: theme.actionSheet.inputHollowBackgroundColor, strokeColor: theme.actionSheet.inputBorderColor, strokeWidth: 1.0) + + self.textInputNode = EditableTextNode() + self.textInputNode.typingAttributes = [NSAttributedString.Key.font.rawValue: Font.regular(17.0), NSAttributedString.Key.foregroundColor.rawValue: theme.actionSheet.inputTextColor] + self.textInputNode.clipsToBounds = true + self.textInputNode.hitTestSlop = UIEdgeInsets(top: -5.0, left: -5.0, bottom: -5.0, right: -5.0) + self.textInputNode.textContainerInset = UIEdgeInsets(top: self.inputInsets.top, left: 0.0, bottom: self.inputInsets.bottom, right: 0.0) + self.textInputNode.keyboardAppearance = theme.rootController.keyboardColor.keyboardAppearance + self.textInputNode.keyboardType = .URL + self.textInputNode.autocapitalizationType = .none + self.textInputNode.returnKeyType = .done + self.textInputNode.autocorrectionType = .no + self.textInputNode.tintColor = theme.actionSheet.controlAccentColor + + self.placeholderNode = ASTextNode() + self.placeholderNode.isUserInteractionEnabled = false + self.placeholderNode.displaysAsynchronously = false + self.placeholderNode.attributedText = NSAttributedString(string: placeholder, font: Font.regular(17.0), textColor: self.theme.actionSheet.inputPlaceholderColor) + + super.init() + + self.textInputNode.delegate = self + + self.addSubnode(self.backgroundNode) + self.addSubnode(self.textInputNode) + self.addSubnode(self.placeholderNode) + } + + func updateTheme(_ theme: PresentationTheme) { + self.theme = theme + + self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 12.0, color: self.theme.actionSheet.inputHollowBackgroundColor, strokeColor: self.theme.actionSheet.inputBorderColor, strokeWidth: 1.0) + self.textInputNode.keyboardAppearance = self.theme.rootController.keyboardColor.keyboardAppearance + self.placeholderNode.attributedText = NSAttributedString(string: self.placeholderNode.attributedText?.string ?? "", font: Font.regular(17.0), textColor: self.theme.actionSheet.inputPlaceholderColor) + self.textInputNode.tintColor = self.theme.actionSheet.controlAccentColor + } + + func updateLayout(width: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat { + let backgroundInsets = self.backgroundInsets + let inputInsets = self.inputInsets + + let textFieldHeight = self.calculateTextFieldMetrics(width: width) + let panelHeight = textFieldHeight + backgroundInsets.top + backgroundInsets.bottom + + let backgroundFrame = CGRect(origin: CGPoint(x: backgroundInsets.left, y: backgroundInsets.top), size: CGSize(width: width - backgroundInsets.left - backgroundInsets.right, height: panelHeight - backgroundInsets.top - backgroundInsets.bottom)) + transition.updateFrame(node: self.backgroundNode, frame: backgroundFrame) + + let placeholderSize = self.placeholderNode.measure(backgroundFrame.size) + transition.updateFrame(node: self.placeholderNode, frame: CGRect(origin: CGPoint(x: backgroundFrame.minX + inputInsets.left, y: backgroundFrame.minY + floor((backgroundFrame.size.height - placeholderSize.height) / 2.0)), size: placeholderSize)) + + transition.updateFrame(node: self.textInputNode, frame: CGRect(origin: CGPoint(x: backgroundFrame.minX + inputInsets.left, y: backgroundFrame.minY), size: CGSize(width: backgroundFrame.size.width - inputInsets.left - inputInsets.right, height: backgroundFrame.size.height))) + + return panelHeight + } + + func activateInput() { + self.textInputNode.becomeFirstResponder() + } + + func deactivateInput() { + self.textInputNode.resignFirstResponder() + } + + @objc func editableTextNodeDidUpdateText(_ editableTextNode: ASEditableTextNode) { + self.updateTextNodeText(animated: true) + self.textChanged?(editableTextNode.textView.text) + self.placeholderNode.isHidden = !(editableTextNode.textView.text ?? "").isEmpty + } + + 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: []) + + func editableTextNode(_ editableTextNode: ASEditableTextNode, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { + if text == "\n" { + self.complete?() + return false + } + + if let domainRegex = self.domainRegex, let pathRegex = self.pathRegex { + let updatedText = (editableTextNode.textView.text as NSString).replacingCharacters(in: range, with: text) + let domainMatches = domainRegex.matches(in: updatedText, options: [], range: NSRange(location: 0, length: updatedText.utf16.count)) + let pathMatches = pathRegex.matches(in: updatedText, options: [], range: NSRange(location: 0, length: updatedText.utf16.count)) + + if domainMatches.count > 0, pathMatches.count == 0 { + return true + } else { + return false + } + } + + return true + } + + private func calculateTextFieldMetrics(width: CGFloat) -> CGFloat { + let backgroundInsets = self.backgroundInsets + let inputInsets = self.inputInsets + + let unboundTextFieldHeight = max(33.0, ceil(self.textInputNode.measure(CGSize(width: width - backgroundInsets.left - backgroundInsets.right - inputInsets.left - inputInsets.right, height: CGFloat.greatestFiniteMagnitude)).height)) + + return min(61.0, max(33.0, unboundTextFieldHeight)) + } + + private func updateTextNodeText(animated: Bool) { + let backgroundInsets = self.backgroundInsets + + let textFieldHeight = self.calculateTextFieldMetrics(width: self.bounds.size.width) + + let panelHeight = textFieldHeight + backgroundInsets.top + backgroundInsets.bottom + if !self.bounds.size.height.isEqual(to: panelHeight) { + self.updateHeight?() + } + } + + @objc func clearPressed() { + self.textInputNode.attributedText = nil + self.deactivateInput() + } +} + +private final class WebBrowserDomainAlertContentNode: AlertContentNode { + private let strings: PresentationStrings + + private let titleNode: ASTextNode + private let textNode: ASTextNode + let inputFieldNode: WebBrowserDomainInputFieldNode + + private let actionNodesSeparator: ASDisplayNode + private let actionNodes: [TextAlertContentActionNode] + private let actionVerticalSeparators: [ASDisplayNode] + + private let disposable = MetaDisposable() + + private var validLayout: CGSize? + + private let hapticFeedback = HapticFeedback() + + var complete: (() -> Void)? { + didSet { + self.inputFieldNode.complete = self.complete + } + } + + override var dismissOnOutsideTap: Bool { + return self.isUserInteractionEnabled + } + + init(theme: AlertControllerTheme, ptheme: PresentationTheme, strings: PresentationStrings, actions: [TextAlertAction]) { + self.strings = strings + + self.titleNode = ASTextNode() + self.titleNode.maximumNumberOfLines = 2 + self.textNode = ASTextNode() + self.textNode.maximumNumberOfLines = 2 + + self.inputFieldNode = WebBrowserDomainInputFieldNode(theme: ptheme, placeholder: strings.WebBrowser_Exceptions_Create_Placeholder) + self.inputFieldNode.text = "" + + self.actionNodesSeparator = ASDisplayNode() + self.actionNodesSeparator.isLayerBacked = true + + self.actionNodes = actions.map { action -> TextAlertContentActionNode in + return TextAlertContentActionNode(theme: theme, action: action) + } + + var actionVerticalSeparators: [ASDisplayNode] = [] + if actions.count > 1 { + for _ in 0 ..< actions.count - 1 { + let separatorNode = ASDisplayNode() + separatorNode.isLayerBacked = true + actionVerticalSeparators.append(separatorNode) + } + } + self.actionVerticalSeparators = actionVerticalSeparators + + super.init() + + self.addSubnode(self.titleNode) + self.addSubnode(self.textNode) + + self.addSubnode(self.inputFieldNode) + + self.addSubnode(self.actionNodesSeparator) + + for actionNode in self.actionNodes { + self.addSubnode(actionNode) + } + self.actionNodes.last?.actionEnabled = false + + for separatorNode in self.actionVerticalSeparators { + self.addSubnode(separatorNode) + } + + self.inputFieldNode.updateHeight = { [weak self] in + if let strongSelf = self { + if let _ = strongSelf.validLayout { + strongSelf.requestLayout?(.animated(duration: 0.15, curve: .spring)) + } + } + } + + self.inputFieldNode.textChanged = { [weak self] text in + if let strongSelf = self, let lastNode = strongSelf.actionNodes.last { + lastNode.actionEnabled = !text.isEmpty + } + } + + self.updateTheme(theme) + } + + deinit { + self.disposable.dispose() + } + + var link: String { + return self.inputFieldNode.text + } + + override func updateTheme(_ theme: AlertControllerTheme) { + self.titleNode.attributedText = NSAttributedString(string: self.strings.WebBrowser_Exceptions_Create_Title, font: Font.bold(17.0), textColor: theme.primaryColor, paragraphAlignment: .center) + self.textNode.attributedText = NSAttributedString(string: self.strings.WebBrowser_Exceptions_Create_Text, font: Font.regular(13.0), textColor: theme.primaryColor, paragraphAlignment: .center) + + self.actionNodesSeparator.backgroundColor = theme.separatorColor + for actionNode in self.actionNodes { + actionNode.updateTheme(theme) + } + for separatorNode in self.actionVerticalSeparators { + separatorNode.backgroundColor = theme.separatorColor + } + + if let size = self.validLayout { + _ = self.updateLayout(size: size, transition: .immediate) + } + } + + override func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { + var size = size + size.width = min(size.width, 270.0) + let measureSize = CGSize(width: size.width - 16.0 * 2.0, height: CGFloat.greatestFiniteMagnitude) + + let hadValidLayout = self.validLayout != nil + + self.validLayout = size + + var origin: CGPoint = CGPoint(x: 0.0, y: 20.0) + let spacing: CGFloat = 5.0 + + let titleSize = self.titleNode.measure(measureSize) + transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - titleSize.width) / 2.0), y: origin.y), size: titleSize)) + origin.y += titleSize.height + 4.0 + + let textSize = self.textNode.measure(measureSize) + transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - textSize.width) / 2.0), y: origin.y), size: textSize)) + origin.y += textSize.height + 6.0 + spacing + + let actionButtonHeight: CGFloat = 44.0 + var minActionsWidth: CGFloat = 0.0 + let maxActionWidth: CGFloat = floor(size.width / CGFloat(self.actionNodes.count)) + let actionTitleInsets: CGFloat = 8.0 + + var effectiveActionLayout = TextAlertContentActionLayout.horizontal + for actionNode in self.actionNodes { + let actionTitleSize = actionNode.titleNode.updateLayout(CGSize(width: maxActionWidth, height: actionButtonHeight)) + if case .horizontal = effectiveActionLayout, actionTitleSize.height > actionButtonHeight * 0.6667 { + effectiveActionLayout = .vertical + } + switch effectiveActionLayout { + case .horizontal: + minActionsWidth += actionTitleSize.width + actionTitleInsets + case .vertical: + minActionsWidth = max(minActionsWidth, actionTitleSize.width + actionTitleInsets) + } + } + + let insets = UIEdgeInsets(top: 18.0, left: 18.0, bottom: 9.0, right: 18.0) + + var contentWidth = max(titleSize.width, minActionsWidth) + contentWidth = max(contentWidth, 234.0) + + var actionsHeight: CGFloat = 0.0 + switch effectiveActionLayout { + case .horizontal: + actionsHeight = actionButtonHeight + case .vertical: + actionsHeight = actionButtonHeight * CGFloat(self.actionNodes.count) + } + + let resultWidth = contentWidth + insets.left + insets.right + + let 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)) + transition.updateAlpha(node: self.inputFieldNode, alpha: inputHeight > 0.0 ? 1.0 : 0.0) + + 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))) + + var actionOffset: CGFloat = 0.0 + let actionWidth: CGFloat = floor(resultSize.width / CGFloat(self.actionNodes.count)) + var separatorIndex = -1 + var nodeIndex = 0 + for actionNode in self.actionNodes { + if separatorIndex >= 0 { + let separatorNode = self.actionVerticalSeparators[separatorIndex] + switch effectiveActionLayout { + case .horizontal: + transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: actionOffset - UIScreenPixel, y: resultSize.height - actionsHeight), size: CGSize(width: UIScreenPixel, height: actionsHeight - UIScreenPixel))) + case .vertical: + transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel))) + } + } + separatorIndex += 1 + + let currentActionWidth: CGFloat + switch effectiveActionLayout { + case .horizontal: + if nodeIndex == self.actionNodes.count - 1 { + currentActionWidth = resultSize.width - actionOffset + } else { + currentActionWidth = actionWidth + } + case .vertical: + currentActionWidth = resultSize.width + } + + let actionNodeFrame: CGRect + switch effectiveActionLayout { + case .horizontal: + actionNodeFrame = CGRect(origin: CGPoint(x: actionOffset, y: resultSize.height - actionsHeight), size: CGSize(width: currentActionWidth, height: actionButtonHeight)) + actionOffset += currentActionWidth + case .vertical: + actionNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset), size: CGSize(width: currentActionWidth, height: actionButtonHeight)) + actionOffset += actionButtonHeight + } + + transition.updateFrame(node: actionNode, frame: actionNodeFrame) + + nodeIndex += 1 + } + + if !hadValidLayout { + self.inputFieldNode.activateInput() + } + + return resultSize + } + + func animateError() { + self.inputFieldNode.layer.addShakeAnimation() + self.hapticFeedback.error() + } +} + +public func webBrowserDomainController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, apply: @escaping (String?) -> Void) -> AlertController { + let presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 } + + var dismissImpl: ((Bool) -> Void)? + var applyImpl: (() -> Void)? + + let actions: [TextAlertAction] = [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: { + dismissImpl?(true) + apply(nil) + }), TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Done, action: { + applyImpl?() + })] + + let contentNode = WebBrowserDomainAlertContentNode(theme: AlertControllerTheme(presentationData: presentationData), ptheme: presentationData.theme, strings: presentationData.strings, actions: actions) + contentNode.complete = { + applyImpl?() + } + applyImpl = { [weak contentNode] in + guard let contentNode = contentNode else { + return + } + let updatedLink = explicitUrl(contentNode.link) + if !updatedLink.isEmpty && isValidUrl(updatedLink, validSchemes: ["http": true, "https": true]) { + dismissImpl?(true) + apply(updatedLink) + } else { + contentNode.animateError() + } + } + + let controller = AlertController(theme: AlertControllerTheme(presentationData: presentationData), contentNode: contentNode) + let presentationDataDisposable = (updatedPresentationData?.signal ?? context.sharedContext.presentationData).start(next: { [weak controller, weak contentNode] presentationData in + controller?.theme = AlertControllerTheme(presentationData: presentationData) + contentNode?.inputFieldNode.updateTheme(presentationData.theme) + }) + controller.dismissed = { _ in + presentationDataDisposable.dispose() + } + dismissImpl = { [weak controller] animated in + contentNode.inputFieldNode.deactivateInput() + if animated { + controller?.dismissAnimated() + } else { + controller?.dismiss() + } + } + return controller +} diff --git a/submodules/SettingsUI/Sources/Data and Storage/WebBrowserDomainExceptionItem.swift b/submodules/SettingsUI/Sources/Data and Storage/WebBrowserDomainExceptionItem.swift new file mode 100644 index 0000000000..572e65ed9f --- /dev/null +++ b/submodules/SettingsUI/Sources/Data and Storage/WebBrowserDomainExceptionItem.swift @@ -0,0 +1,280 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import SwiftSignalKit +import TelegramPresentationData +import TelegramCore +import AccountContext +import ItemListUI + +public class WebBrowserDomainExceptionItem: ListViewItem, ItemListItem { + let presentationData: ItemListPresentationData + let context: AccountContext? + let title: String + let label: String + public let sectionId: ItemListSectionId + let style: ItemListStyle + + public init( + presentationData: ItemListPresentationData, + context: AccountContext? = nil, + title: String, + label: String, + sectionId: ItemListSectionId, + style: ItemListStyle + ) { + self.presentationData = presentationData + self.context = context + self.title = title + self.label = label + self.sectionId = sectionId + self.style = style + } + + public 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)) + + node.contentSize = layout.contentSize + node.insets = layout.insets + + Queue.mainQueue().async { + completion(node, { + return (nil, { _ in apply() }) + }) + } + } + } + + 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) { + Queue.mainQueue().async { + if let nodeValue = node() as? WebBrowserDomainExceptionItemNode { + let makeLayout = nodeValue.asyncLayout() + + async { + let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) + Queue.mainQueue().async { + completion(layout, { _ in + apply() + }) + } + } + } + } + } + + public var selectable: Bool = false + + public func selected(listView: ListView){ + } +} + +public class WebBrowserDomainExceptionItemNode: ListViewItemNode, ItemListItemNode { + private let backgroundNode: ASDisplayNode + private let topStripeNode: ASDisplayNode + private let bottomStripeNode: ASDisplayNode + private let maskNode: ASImageNode + + let iconNode: ASImageNode + let titleNode: TextNode + let labelNode: TextNode + + private let activateArea: AccessibilityAreaNode + + private var item: WebBrowserDomainExceptionItem? + + override public var canBeSelected: Bool { + return false + } + + public var tag: ItemListItemTag? = nil + + public init() { + self.backgroundNode = ASDisplayNode() + self.backgroundNode.isLayerBacked = true + self.backgroundNode.backgroundColor = .white + + self.maskNode = ASImageNode() + self.maskNode.isUserInteractionEnabled = false + + self.topStripeNode = ASDisplayNode() + self.topStripeNode.isLayerBacked = true + + self.bottomStripeNode = ASDisplayNode() + self.bottomStripeNode.isLayerBacked = true + + self.iconNode = ASImageNode() + self.iconNode.isLayerBacked = true + self.iconNode.displaysAsynchronously = false + + self.titleNode = TextNode() + self.titleNode.isUserInteractionEnabled = false + + self.labelNode = TextNode() + self.labelNode.isUserInteractionEnabled = false + + self.activateArea = AccessibilityAreaNode() + + super.init(layerBacked: false, dynamicBounce: false) + + self.addSubnode(self.titleNode) + self.addSubnode(self.labelNode) + + self.addSubnode(self.activateArea) + } + + public func asyncLayout() -> (_ item: WebBrowserDomainExceptionItem, _ params: ListViewItemLayoutParams, _ insets: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { + let makeTitleLayout = TextNode.asyncLayout(self.titleNode) + let makeLabelLayout = TextNode.asyncLayout(self.labelNode) + + let currentItem = self.item + + return { item, params, neighbors in + var updatedTheme: PresentationTheme? + if currentItem?.presentationData.theme !== item.presentationData.theme { + updatedTheme = item.presentationData.theme + } + + let contentSize: CGSize + let insets: UIEdgeInsets + let separatorHeight = UIScreenPixel + let itemBackgroundColor: UIColor + let itemSeparatorColor: UIColor + + let leftInset = 16.0 + params.leftInset + 43.0 + + let titleColor: UIColor = item.presentationData.theme.list.itemPrimaryTextColor + let labelColor: UIColor = item.presentationData.theme.list.itemAccentColor + + let titleFont = Font.medium(item.presentationData.fontSize.itemListBaseFontSize) + let labelFont = Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize * 15.0 / 17.0)) + + let maxTitleWidth: CGFloat = params.width - params.rightInset - 20.0 - leftInset + + let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.title, font: titleFont, textColor: titleColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: maxTitleWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + + let (labelLayout, labelApply) = makeLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.label, font: labelFont, textColor: labelColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: maxTitleWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + + let verticalInset: CGFloat = 11.0 + let titleSpacing: CGFloat = 1.0 + + let height: CGFloat = verticalInset * 2.0 + titleLayout.size.height + titleSpacing + labelLayout.size.height + + switch item.style { + case .plain: + itemBackgroundColor = item.presentationData.theme.list.plainBackgroundColor + itemSeparatorColor = item.presentationData.theme.list.itemPlainSeparatorColor + contentSize = CGSize(width: params.width, height: height) + insets = itemListNeighborsPlainInsets(neighbors) + case .blocks: + itemBackgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor + itemSeparatorColor = item.presentationData.theme.list.itemBlocksSeparatorColor + contentSize = CGSize(width: params.width, height: height) + insets = itemListNeighborsGroupedInsets(neighbors, params) + } + + let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) + + return (ListViewItemNodeLayout(contentSize: contentSize, insets: insets), { [weak self] in + if let strongSelf = self { + strongSelf.item = item + + 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 + strongSelf.activateArea.accessibilityValue = item.label + + if let _ = updatedTheme { + strongSelf.topStripeNode.backgroundColor = itemSeparatorColor + strongSelf.bottomStripeNode.backgroundColor = itemSeparatorColor + strongSelf.backgroundNode.backgroundColor = itemBackgroundColor + } + + let _ = titleApply() + let _ = labelApply() + + switch item.style { + case .plain: + if strongSelf.backgroundNode.supernode != nil { + strongSelf.backgroundNode.removeFromSupernode() + } + if strongSelf.topStripeNode.supernode != nil { + strongSelf.topStripeNode.removeFromSupernode() + } + if strongSelf.bottomStripeNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 0) + } + if strongSelf.maskNode.supernode != nil { + strongSelf.maskNode.removeFromSupernode() + } + strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: leftInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - leftInset, height: separatorHeight)) + case .blocks: + if strongSelf.backgroundNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0) + } + if strongSelf.topStripeNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1) + } + if strongSelf.bottomStripeNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2) + } + if strongSelf.maskNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.maskNode, at: 3) + } + + let hasCorners = itemListHasRoundedBlockLayout(params) + var hasTopCorners = false + var hasBottomCorners = false + switch neighbors.top { + case .sameSection(false): + strongSelf.topStripeNode.isHidden = true + default: + hasTopCorners = true + strongSelf.topStripeNode.isHidden = hasCorners + } + let bottomStripeInset: CGFloat + switch neighbors.bottom { + case .sameSection(false): + bottomStripeInset = leftInset + strongSelf.bottomStripeNode.isHidden = false + default: + bottomStripeInset = 0.0 + hasBottomCorners = true + strongSelf.bottomStripeNode.isHidden = hasCorners + } + + strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil + + strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))) + strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0) + strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: separatorHeight)) + strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - bottomStripeInset, height: separatorHeight)) + } + + var centralContentHeight: CGFloat = titleLayout.size.height + centralContentHeight += titleSpacing + centralContentHeight += labelLayout.size.height + + let titleFrame = CGRect(origin: CGPoint(x: leftInset, 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) + strongSelf.labelNode.frame = labelFrame + } + }) + } + } + + override public 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) { + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + + override public func animateRemoved(_ currentTimestamp: Double, duration: Double) { + self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false) + } +} diff --git a/submodules/SettingsUI/Sources/Data and Storage/WebBrowserSettingsController.swift b/submodules/SettingsUI/Sources/Data and Storage/WebBrowserSettingsController.swift index e5555124ab..48ce499bac 100644 --- a/submodules/SettingsUI/Sources/Data and Storage/WebBrowserSettingsController.swift +++ b/submodules/SettingsUI/Sources/Data and Storage/WebBrowserSettingsController.swift @@ -9,29 +9,69 @@ import TelegramUIPreferences import ItemListUI import AccountContext import OpenInExternalAppUI +import ItemListPeerActionItem +import UndoUI +import WebKit +import LinkPresentation private final class WebBrowserSettingsControllerArguments { let context: AccountContext let updateDefaultBrowser: (String?) -> Void + let updateAutologin: (Bool) -> Void + let clearCookies: () -> Void + let addException: () -> Void + let clearExceptions: () -> Void - init(context: AccountContext, updateDefaultBrowser: @escaping (String?) -> Void) { + init( + context: AccountContext, + updateDefaultBrowser: @escaping (String?) -> Void, + updateAutologin: @escaping (Bool) -> Void, + clearCookies: @escaping () -> Void, + addException: @escaping () -> Void, + clearExceptions: @escaping () -> Void + ) { self.context = context self.updateDefaultBrowser = updateDefaultBrowser + self.updateAutologin = updateAutologin + self.clearCookies = clearCookies + self.addException = addException + self.clearExceptions = clearExceptions } } private enum WebBrowserSettingsSection: Int32 { case browsers + case autologin + case clearCookies + case exceptions } private enum WebBrowserSettingsControllerEntry: ItemListNodeEntry { case browserHeader(PresentationTheme, String) case browser(PresentationTheme, String, OpenInApplication?, String?, Bool, Int32) + case autologin(PresentationTheme, String, Bool) + case autologinInfo(PresentationTheme, String) + + case clearCookies(PresentationTheme, String) + case clearCookiesInfo(PresentationTheme, String) + + case exceptionsHeader(PresentationTheme, String) + case exceptionsAdd(PresentationTheme, String) + case exception(Int32, PresentationTheme, WebBrowserException) + case exceptionsClear(PresentationTheme, String) + case exceptionsInfo(PresentationTheme, String) + var section: ItemListSectionId { switch self { case .browserHeader, .browser: return WebBrowserSettingsSection.browsers.rawValue + case .autologin, .autologinInfo: + return WebBrowserSettingsSection.autologin.rawValue + case .clearCookies, .clearCookiesInfo: + return WebBrowserSettingsSection.clearCookies.rawValue + case .exceptionsHeader, .exceptionsAdd, .exception, .exceptionsClear, .exceptionsInfo: + return WebBrowserSettingsSection.exceptions.rawValue } } @@ -41,6 +81,24 @@ private enum WebBrowserSettingsControllerEntry: ItemListNodeEntry { return 0 case let .browser(_, _, _, _, _, index): return 1 + index + case .autologin: + return 100 + case .autologinInfo: + return 101 + case .clearCookies: + return 102 + case .clearCookiesInfo: + return 103 + case .exceptionsHeader: + return 104 + case .exceptionsAdd: + return 105 + case let .exception(index, _, _): + return 106 + index + case .exceptionsClear: + return 1000 + case .exceptionsInfo: + return 1001 } } @@ -58,6 +116,60 @@ private enum WebBrowserSettingsControllerEntry: ItemListNodeEntry { } else { return false } + case let .autologin(lhsTheme, lhsText, lhsValue): + if case let .autologin(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { + return true + } else { + return false + } + case let .autologinInfo(lhsTheme, lhsText): + if case let .autologinInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .clearCookies(lhsTheme, lhsText): + if case let .clearCookies(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .clearCookiesInfo(lhsTheme, lhsText): + if case let .clearCookiesInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .exceptionsHeader(lhsTheme, lhsText): + if case let .exceptionsHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .exception(lhsIndex, lhsTheme, lhsException): + if case let .exception(rhsIndex, rhsTheme, rhsException) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsException == rhsException { + return true + } else { + return false + } + case let .exceptionsAdd(lhsTheme, lhsText): + if case let .exceptionsAdd(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .exceptionsClear(lhsTheme, lhsText): + if case let .exceptionsClear(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .exceptionsInfo(lhsTheme, lhsText): + if case let .exceptionsInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } } } @@ -74,44 +186,208 @@ private enum WebBrowserSettingsControllerEntry: ItemListNodeEntry { return WebBrowserItem(context: arguments.context, presentationData: presentationData, title: title, application: application, checked: selected, sectionId: self.section) { arguments.updateDefaultBrowser(identifier) } + case let .autologin(_, text, value): + return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, sectionId: self.section, style: .blocks, updated: { updatedValue in + arguments.updateAutologin(updatedValue) + }) + case let .autologinInfo(_, text): + return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) + case let .clearCookies(_, text): + return ItemListPeerActionItem(presentationData: presentationData, icon: PresentationResourcesItemList.accentDeleteIconImage(presentationData.theme), title: text, sectionId: self.section, height: .generic, color: .accent, editing: false, action: { + arguments.clearCookies() + }) + case let .clearCookiesInfo(_, text): + return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) + 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) + 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() + }) + case let .exceptionsClear(_, text): + return ItemListPeerActionItem(presentationData: presentationData, icon: PresentationResourcesItemList.deleteIconImage(presentationData.theme), title: text, sectionId: self.section, height: .generic, color: .destructive, editing: false, action: { + arguments.clearExceptions() + }) + case let .exceptionsInfo(_, text): + return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) } } } -private func webBrowserSettingsControllerEntries(context: AccountContext, presentationData: PresentationData, selectedBrowser: String?) -> [WebBrowserSettingsControllerEntry] { +private func webBrowserSettingsControllerEntries(context: AccountContext, presentationData: PresentationData, settings: WebBrowserSettings) -> [WebBrowserSettingsControllerEntry] { var entries: [WebBrowserSettingsControllerEntry] = [] let options = availableOpenInOptions(context: context, item: .url(url: "http://telegram.org")) - entries.append(.browserHeader(presentationData.theme, presentationData.strings.WebBrowser_DefaultBrowser)) - entries.append(.browser(presentationData.theme, presentationData.strings.WebBrowser_Telegram, nil, nil, selectedBrowser == nil, 0)) - entries.append(.browser(presentationData.theme, presentationData.strings.WebBrowser_InAppSafari, .safari, "inApp", selectedBrowser == "inApp", 1)) + entries.append(.browserHeader(presentationData.theme, presentationData.strings.WebBrowser_OpenLinksIn_Title)) + entries.append(.browser(presentationData.theme, presentationData.strings.WebBrowser_Telegram, nil, nil, settings.defaultWebBrowser == nil, 0)) - var index: Int32 = 2 + var index: Int32 = 1 for option in options { - entries.append(.browser(presentationData.theme, option.title, option.application, option.identifier, option.identifier == selectedBrowser, index)) + entries.append(.browser(presentationData.theme, option.title, option.application, option.identifier, option.identifier == settings.defaultWebBrowser, index)) index += 1 } + if settings.defaultWebBrowser == nil { + entries.append(.autologin(presentationData.theme, presentationData.strings.WebBrowser_AutoLogin, settings.autologin)) + entries.append(.autologinInfo(presentationData.theme, presentationData.strings.WebBrowser_AutoLogin_Info)) + + entries.append(.clearCookies(presentationData.theme, presentationData.strings.WebBrowser_ClearCookies)) + entries.append(.clearCookiesInfo(presentationData.theme, presentationData.strings.WebBrowser_ClearCookies_Info)) + + entries.append(.exceptionsHeader(presentationData.theme, presentationData.strings.WebBrowser_Exceptions_Title)) + entries.append(.exceptionsAdd(presentationData.theme, presentationData.strings.WebBrowser_Exceptions_AddException)) + + var exceptionIndex: Int32 = 0 + for exception in settings.exceptions { + entries.append(.exception(exceptionIndex, presentationData.theme, exception)) + exceptionIndex += 1 + } + + if !settings.exceptions.isEmpty { + entries.append(.exceptionsClear(presentationData.theme, presentationData.strings.WebBrowser_Exceptions_Clear)) + } + + entries.append(.exceptionsInfo(presentationData.theme, presentationData.strings.WebBrowser_Exceptions_Info)) + } + return entries } public func webBrowserSettingsController(context: AccountContext) -> ViewController { - let arguments = WebBrowserSettingsControllerArguments(context: context, updateDefaultBrowser: { identifier in - let _ = updateWebBrowserSettingsInteractively(accountManager: context.sharedContext.accountManager, { $0.withUpdatedDefaultWebBrowser(identifier) }).start() - }) + var clearCookiesImpl: (() -> Void)? + var addExceptionImpl: (() -> Void)? + var clearExceptionsImpl: (() -> Void)? - let signal = combineLatest(context.sharedContext.presentationData, context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.webBrowserSettings])) + let arguments = WebBrowserSettingsControllerArguments( + context: context, + updateDefaultBrowser: { identifier in + let _ = updateWebBrowserSettingsInteractively(accountManager: context.sharedContext.accountManager, { + $0.withUpdatedDefaultWebBrowser(identifier) + }).start() + }, + updateAutologin: { autologin in + let _ = updateWebBrowserSettingsInteractively(accountManager: context.sharedContext.accountManager, { + $0.withUpdatedAutologin(autologin) + }).start() + }, + clearCookies: { + clearCookiesImpl?() + }, + addException: { + addExceptionImpl?() + }, + clearExceptions: { + clearExceptionsImpl?() + } + ) + + let previousSettings = Atomic(value: nil) + + let signal = combineLatest( + context.sharedContext.presentationData, + context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.webBrowserSettings]) + ) |> deliverOnMainQueue |> map { presentationData, sharedData -> (ItemListControllerState, (ItemListNodeState, Any)) in let settings = sharedData.entries[ApplicationSpecificSharedDataKeys.webBrowserSettings]?.get(WebBrowserSettings.self) ?? WebBrowserSettings.defaultSettings + let previousSettings = previousSettings.swap(settings) + + var animateChanges = false + if let previousSettings { + if previousSettings.defaultWebBrowser != settings.defaultWebBrowser { + 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)) - let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: webBrowserSettingsControllerEntries(context: context, presentationData: presentationData, selectedBrowser: settings.defaultWebBrowser), style: .blocks, animateChanges: false) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: webBrowserSettingsControllerEntries(context: context, presentationData: presentationData, settings: settings), style: .blocks, animateChanges: animateChanges) return (controllerState, (listState, arguments)) } let controller = ItemListController(context: context, state: signal) + + clearCookiesImpl = { [weak controller] in + WKWebsiteDataStore.default().removeData(ofTypes: WKWebsiteDataStore.allWebsiteDataTypes(), modifiedSince: Date(timeIntervalSince1970: 0), completionHandler:{}) + + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + controller?.present(UndoOverlayController( + presentationData: presentationData, + content: .info( + title: nil, + text: presentationData.strings.WebBrowser_ClearCookies_Succeed, + timeout: nil, + customUndoText: nil + ), + elevatedLayout: false, + position: .bottom, + action: { _ in return false }), in: .current + ) + } + + addExceptionImpl = { [weak controller] in + let linkController = webBrowserDomainController(context: context, apply: { url in + if let url { + let _ = fetchDomainExceptionInfo(url: url).startStandalone(next: { newException in + let _ = updateWebBrowserSettingsInteractively(accountManager: context.sharedContext.accountManager, { currentSettings in + var currentExceptions = currentSettings.exceptions + for exception in currentExceptions { + if exception.domain == newException.domain { + return currentSettings + } + } + currentExceptions.append(newException) + return currentSettings.withUpdatedExceptions(currentExceptions) + }).start() + }) + } + }) + controller?.present(linkController, in: .window(.root)) + } + + clearExceptionsImpl = { + let _ = updateWebBrowserSettingsInteractively(accountManager: context.sharedContext.accountManager, { currentSettings in + return currentSettings.withUpdatedExceptions([]) + }).start() + } + return controller } + +private func cleanDomain(url: String) -> (domain: String, fullUrl: String) { + if let parsedUrl = URL(string: url) { + let host: String? + let scheme = parsedUrl.scheme ?? "https" + if #available(iOS 16.0, *) { + host = parsedUrl.host(percentEncoded: true)?.lowercased() + } else { + host = parsedUrl.host?.lowercased() + } + return (host ?? url, "\(scheme)://\(host ?? "")") + } else { + return (url, url) + } +} + +private func fetchDomainExceptionInfo(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() + }) + return ActionDisposable { + metadataProvider.cancel() + } + } + } else { + return .single(WebBrowserException(domain: domain, title: domain)) + } +} diff --git a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift index 60e2e1dc3c..373ef8fe9e 100644 --- a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift +++ b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift @@ -45,6 +45,7 @@ public enum PresentationResourceKey: Int32 { case itemListSecondaryCheckIcon case itemListPlusIcon case itemListRoundPlusIcon + case itemListAccentDeleteIcon case itemListDeleteIcon case itemListDeleteIndicatorIcon case itemListReorderIndicatorIcon diff --git a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesItemList.swift b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesItemList.swift index 81e5b19064..081ee55eea 100644 --- a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesItemList.swift +++ b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesItemList.swift @@ -69,6 +69,12 @@ public struct PresentationResourcesItemList { }) } + public static func accentDeleteIconImage(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.itemListAccentDeleteIcon.rawValue, { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionTrash"), color: theme.list.itemAccentColor) + }) + } + public static func deleteIconImage(_ theme: PresentationTheme) -> UIImage? { return theme.image(PresentationResourceKey.itemListDeleteIcon.rawValue, { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionTrash"), color: theme.list.itemDestructiveColor) diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaCoverScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaCoverScreen.swift index 9eca8afc3e..ac4ccd2601 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaCoverScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaCoverScreen.swift @@ -338,6 +338,19 @@ private final class MediaCoverScreenComponent: Component { mediaEditor.seek(position, andPlay: false) } }, + coverPositionUpdated: { [weak mediaEditor] position, tap, commit in + if let mediaEditor { + if tap { + mediaEditor.setOnNextDisplay { + commit() + } + mediaEditor.seek(position, andPlay: false) + } else { + mediaEditor.seek(position, andPlay: false) + commit() + } + } + }, trackTrimUpdated: { _, _, _, _, _ in }, trackOffsetUpdated: { _, _, _ in diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift index 7e6cd65cbd..b5358e21c9 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift @@ -48,6 +48,7 @@ import StickerPackEditTitleController import StickerPickerScreen import UIKitRuntimeUtils import ImageObjectSeparation +import DeviceAccess private let playbackButtonTag = GenericComponentViewTag() private let muteButtonTag = GenericComponentViewTag() @@ -2553,6 +2554,8 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate var recording: MediaEditorScreen.Recording + private let locationManager = LocationManager() + private var presentationData: PresentationData private var validLayout: ContainerViewLayout? @@ -4584,7 +4587,37 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate self.mediaEditor?.play() } - func addWeather(_ weather: StickerPickerScreen.Weather.LoadedWeather) { + func requestWeather() { + + } + + func presentLocationAccessAlert() { + DeviceAccess.authorizeAccess(to: .location(.send), locationManager: self.locationManager, presentationData: self.presentationData, present: { [weak self] c, a in + self?.controller?.present(c, in: .window(.root), with: a) + }, openSettings: { [weak self] in + self?.context.sharedContext.applicationBindings.openSettings() + }, { [weak self] authorized in + guard let self, authorized else { + return + } + let weatherPromise = Promise() + weatherPromise.set(getWeather(context: self.context)) + self.weatherPromise = weatherPromise + + let _ = (weatherPromise.get() + |> deliverOnMainQueue).start(next: { [weak self] result in + if let self, case let .loaded(weather) = result { + self.addWeather(weather) + } + }) + }) + } + + func addWeather(_ weather: StickerPickerScreen.Weather.LoadedWeather?) { + guard let weather else { + + return + } let maxWeatherCount = 3 var currentWeatherCount = 0 self.entitiesView.eachView { entityView in @@ -4948,9 +4981,16 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate if let self { if let weatherPromise = self.weatherPromise { let _ = (weatherPromise.get() - |> take(1)).start(next: { [weak self] weather in - if let self, case let .loaded(loaded) = weather { - self.addWeather(loaded) + |> take(1)).start(next: { [weak self] result in + if let self { + switch result { + case let .loaded(weather): + self.addWeather(weather) + case .notDetermined, .notAllowed: + self.presentLocationAccessAlert() + default: + break + } } }) } diff --git a/submodules/TelegramUI/Components/MediaScrubberComponent/Sources/MediaScrubberComponent.swift b/submodules/TelegramUI/Components/MediaScrubberComponent/Sources/MediaScrubberComponent.swift index e6a063aa26..aa47178efe 100644 --- a/submodules/TelegramUI/Components/MediaScrubberComponent/Sources/MediaScrubberComponent.swift +++ b/submodules/TelegramUI/Components/MediaScrubberComponent/Sources/MediaScrubberComponent.swift @@ -88,6 +88,7 @@ public final class MediaScrubberComponent: Component { let portalView: PortalView? let positionUpdated: (Double, Bool) -> Void + let coverPositionUpdated: (Double, Bool, @escaping () -> Void) -> Void let trackTrimUpdated: (Int32, Double, Double, Bool, Bool) -> Void let trackOffsetUpdated: (Int32, Double, Bool) -> Void let trackLongPressed: (Int32, UIView) -> Void @@ -104,6 +105,7 @@ public final class MediaScrubberComponent: Component { tracks: [Track], portalView: PortalView? = nil, positionUpdated: @escaping (Double, Bool) -> Void, + coverPositionUpdated: @escaping (Double, Bool, @escaping () -> Void) -> Void = { _, _, _ in }, trackTrimUpdated: @escaping (Int32, Double, Double, Bool, Bool) -> Void, trackOffsetUpdated: @escaping (Int32, Double, Bool) -> Void, trackLongPressed: @escaping (Int32, UIView) -> Void @@ -119,6 +121,7 @@ public final class MediaScrubberComponent: Component { self.tracks = tracks self.portalView = portalView self.positionUpdated = positionUpdated + self.coverPositionUpdated = coverPositionUpdated self.trackTrimUpdated = trackTrimUpdated self.trackOffsetUpdated = trackOffsetUpdated self.trackLongPressed = trackLongPressed @@ -164,6 +167,7 @@ public final class MediaScrubberComponent: Component { private var selectedTrackId: Int32 = 0 private var isPanningCursor = false + private var ignoreCursorPositionUpdate = false private var scrubberSize: CGSize? @@ -327,10 +331,18 @@ public final class MediaScrubberComponent: Component { switch gestureRecognizer.state { case .began, .changed: self.isPanningCursor = true - component.positionUpdated(position, false) + if case .cover = component.style { + component.coverPositionUpdated(position, false, {}) + } else { + component.positionUpdated(position, false) + } case .ended, .cancelled: self.isPanningCursor = false - component.positionUpdated(position, true) + if case .cover = component.style { + component.coverPositionUpdated(position, false, {}) + } else { + component.positionUpdated(position, true) + } default: break } @@ -345,7 +357,7 @@ public final class MediaScrubberComponent: Component { var y: CGFloat = -5.0 - UIScreenPixel if let component = self.component, case .cover = component.style { cursorWidth = 30.0 + 12.0 - cursorMargin = 0.0 + cursorMargin = handleWidth height = 50.0 isCover = true y += 1.0 @@ -472,6 +484,23 @@ public final class MediaScrubberComponent: Component { } else { trackTransition = .immediate trackView = TrackView() + trackView.onTap = { [weak self] fraction in + guard let self, let component = self.component else { + return + } + var position = max(self.startPosition, min(self.endPosition, self.trimDuration * fraction)) + if let offset = self.mainAudioTrackOffset { + position += offset + } + self.ignoreCursorPositionUpdate = true + component.coverPositionUpdated(position, true, { [weak self] in + guard let self else { + return + } + self.ignoreCursorPositionUpdate = false + self.state?.updated(transition: .immediate) + }) + } trackView.onSelection = { [weak self] id in guard let self else { return @@ -659,13 +688,15 @@ public final class MediaScrubberComponent: Component { self.cursorPositionAnimation = nil self.cursorDisplayLink?.isPaused = true - var cursorPosition = component.position - if let offset = self.mainAudioTrackOffset { - cursorPosition -= offset + if !self.ignoreCursorPositionUpdate { + var cursorPosition = component.position + if let offset = self.mainAudioTrackOffset { + cursorPosition -= offset + } + let cursorFrame = cursorFrame(size: scrubberSize, height: self.effectiveCursorHeight, position: cursorPosition, duration: trimDuration) + transition.setFrame(view: self.cursorView, frame: cursorFrame) + transition.setFrame(view: self.cursorContentView, frame: cursorFrame.insetBy(dx: 6.0, dy: 2.0).offsetBy(dx: -1.0 - UIScreenPixel, dy: 0.0)) } - let cursorFrame = cursorFrame(size: scrubberSize, height: self.effectiveCursorHeight, position: cursorPosition, duration: trimDuration) - transition.setFrame(view: self.cursorView, frame: cursorFrame) - transition.setFrame(view: self.cursorContentView, frame: cursorFrame.insetBy(dx: 6.0, dy: 2.0).offsetBy(dx: -1.0 - UIScreenPixel, dy: 0.0)) } else { if let (_, _, end, ended) = self.cursorPositionAnimation { if ended, component.position >= self.startPosition && component.position < end - 1.0 { @@ -718,6 +749,7 @@ private class TrackView: UIView, UIScrollViewDelegate, UIGestureRecognizerDelega fileprivate var videoOpaqueFrameLayers: [VideoFrameLayer] = [] var onSelection: (Int32) -> Void = { _ in } + var onTap: (CGFloat) -> Void = { _ in } var offsetUpdated: (Double, Bool) -> Void = { _, _ in } var updated: (ComponentTransition) -> Void = { _ in } @@ -794,10 +826,15 @@ private class TrackView: UIView, UIScrollViewDelegate, UIGestureRecognizerDelega } @objc private func handleTap(_ gestureRecognizer: UITapGestureRecognizer) { - guard let (track, _, _, _) = self.params else { + guard let params = self.params else { return } - self.onSelection(track.id) + if case .cover = params.style { + let location = gestureRecognizer.location(in: self) + self.onTap(location.x / self.frame.width) + } else { + self.onSelection(params.track.id) + } } private func updateTrackOffset(done: Bool) { @@ -841,6 +878,7 @@ private class TrackView: UIView, UIScrollViewDelegate, UIGestureRecognizerDelega } private var params: ( + style: MediaScrubberComponent.Style, track: MediaScrubberComponent.Track, isSelected: Bool, availableSize: CGSize, @@ -889,7 +927,7 @@ private class TrackView: UIView, UIScrollViewDelegate, UIGestureRecognizerDelega transition: ComponentTransition ) -> CGSize { let previousParams = self.params - self.params = (track, isSelected, availableSize, duration) + self.params = (style, track, isSelected, availableSize, duration) let fullTrackHeight: CGFloat let framesCornerRadius: CGFloat diff --git a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift index be7baa83aa..b16bd59352 100644 --- a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift +++ b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift @@ -2014,7 +2014,7 @@ final class ShareWithPeersScreenComponent: Component { hasCategories = true } let sendAsPeersCount = component.stateContext.stateValue?.sendAsPeers.count ?? 1 - if sendAsPeersCount > 1 && !"".isEmpty { + if sendAsPeersCount > 1 { hasChannels = true } if let currentHasChannels = self.currentHasChannels, currentHasChannels != hasChannels { @@ -2618,7 +2618,11 @@ final class ShareWithPeersScreenComponent: Component { inset += 10.0 + environment.safeInsets.bottom + 50.0 + footersTotalHeight } else { if !hasCategories { - inset = 314.0 + if self.selectedOptions.contains(.pin) { + inset = 422.0 + } else { + inset = 314.0 + } inset += 10.0 + environment.safeInsets.bottom + 50.0 + footersTotalHeight } else { if hasChannels { diff --git a/submodules/TelegramUI/Components/VideoMessageCameraScreen/Sources/VideoMessageCameraScreen.swift b/submodules/TelegramUI/Components/VideoMessageCameraScreen/Sources/VideoMessageCameraScreen.swift index cb2dc6c7e9..02de03e48c 100644 --- a/submodules/TelegramUI/Components/VideoMessageCameraScreen/Sources/VideoMessageCameraScreen.swift +++ b/submodules/TelegramUI/Components/VideoMessageCameraScreen/Sources/VideoMessageCameraScreen.swift @@ -81,7 +81,11 @@ struct CameraState: Equatable { } func updatedRecording(_ recording: Recording) -> CameraState { - return CameraState(position: self.position, flashMode: self.flashMode, flashModeDidChange: self.flashModeDidChange, flashTint: self.flashTint, flashTintSize: self.flashTintSize, recording: recording, duration: self.duration, isDualCameraEnabled: self.isDualCameraEnabled, isViewOnceEnabled: self.isViewOnceEnabled) + var flashModeDidChange = self.flashModeDidChange + if case .none = self.recording { + flashModeDidChange = false + } + return CameraState(position: self.position, flashMode: self.flashMode, flashModeDidChange: flashModeDidChange, flashTint: self.flashTint, flashTintSize: self.flashTintSize, recording: recording, duration: self.duration, isDualCameraEnabled: self.isDualCameraEnabled, isViewOnceEnabled: self.isViewOnceEnabled) } func updatedDuration(_ duration: Double) -> CameraState { @@ -408,11 +412,11 @@ private final class VideoMessageCameraScreenComponent: CombinedComponent { let isFirstRecording = initialDuration.isZero controller.node.resumeCameraCapture() - controller.updatePreviewState({ _ in return nil}, transition: .spring(duration: 0.4)) - controller.node.dismissAllTooltips() controller.updateCameraState({ $0.updatedRecording(pressing ? .holding : .handsFree).updatedDuration(initialDuration) }, transition: .spring(duration: 0.4)) + controller.updatePreviewState({ _ in return nil }, transition: .spring(duration: 0.4)) + controller.node.withReadyCamera(isFirstTime: !controller.node.cameraIsActive) { Queue.mainQueue().after(0.15) { self.resultDisposable.set((camera.startRecording() @@ -438,6 +442,10 @@ private final class VideoMessageCameraScreenComponent: CombinedComponent { if initialDuration > 0.0 { controller.onResume() } + + if controller.cameraState.position == .front && controller.cameraState.flashMode == .on { + self.updateScreenBrightness() + } } func stopVideoRecording() { @@ -645,7 +653,9 @@ private final class VideoMessageCameraScreenComponent: CombinedComponent { action: { [weak state] in if let state { state.toggleFlashMode() - flashAction.invoke(Void()) + Queue.mainQueue().justDispatch { + flashAction.invoke(Void()) + } } } ), diff --git a/submodules/TelegramUI/Images.xcassets/Instant View/Bookmark.imageset/Bookmark.pdf b/submodules/TelegramUI/Images.xcassets/Instant View/Bookmark.imageset/Bookmark.pdf new file mode 100644 index 0000000000000000000000000000000000000000..d413418c5dd4d01c2aab27b759af9edee4605a6f GIT binary patch literal 2667 zcmZXWc|26>8^BE=CcBa@=~yO1W|*;WlR?NjmTqO~8q8%fW->F#R^tw3jcUdgvZNyX zY%N5Q8N$62MV7+N9%XKkC0FiDi@W@uKhEbp-_QHJ@AEw8kMo6KtZiTjxFHyf0CWJ7 z-zhKvn3(}66c|Sg;J<>A|K!>>cmfqi2DEJmSSrpM=T8d2@iKU!?T8}=QiA|p-gY-0 z;+;UDl5yA&@EK5SmeWNAL#c!nlLUB%LCPa`hzu~VW-Fy=)RG$4V!`2ZT|FXPB3_=~ zldE2P_T?#FwY|<`yTsU8+CFZVpN}W`aOaUVR|BS66mfi)P4Jt%ruK^wxEi!CSpOI2LtNj2 zB1l~2`@E-b?slG~rKK4pyYHcVK1Ca{ax7=DJFx(~H5l&TwA+ciI7#!!Q<) zGBG7EN?Vc~4fB@d>4eEr!-F8DVZUaG`)%)hW?Ex_NkKt}o*Da@PHz@?3FJ}dr1PwP zFJ`E6K)ZG0qcqVT_d7nRAYOJMdAgMC7E^svL#VKEW813*dezDbbX0|Y;>lt`W?xj| zMP2eAm|H2sDg&AlRl@VOm8a{Wwlg=*J%T_vMg0Rue4euQdcm}>lq(BqYQ7s?87gS9 z3$@8Ss4T-sYDONjcrxbF`|%-bCN;Jpabv@VyC)qXYigBIs0tstzi$m=$+@$r+^6KY z-*4w{(kECodv8@sJvhIkq2fh{hS>$@Y?pG*{Hf@0%MaGiC@c&5JR^dH ziw~4@v`MO>tfEm%Cc4`*U(263V%?9lOg%7}{If+%jX`6?B>Jtl)Y`-C&!5R&PlpL! zmuNd=F8iETeCgmxvv|uUNgo(gL3zT&lde+uR^v`!3~WjzC`jRXcZAhTB?kA@wfns& z@}=wuHB~J;=Q#VpIS@=vCR=`F-tUH(S8)BAWn$j891S78`yo$q*F-i&k|A@p1&qtZ z=D}Ts+BLwOXIMV7WA{6+bVB9*?9wNvfIqG5+t`SRsUwEVyLo+QL@6wwk~ff0YTpxiV>lKOl*S?Frh2aXjfyN|h^jASIsz0+i+#9Ka| zt0-}1TBpo1LBZwv2P9Q01_wb{V7k!*bMaO4W%O|Ef^o-IOX2Vi~Xjv}Zg1t5|f`>Bk2O10PB!h_FQ%c!(g&~N8yEL^*^Vbzb zuy`kj)9^?cEBI@f&+QUaAM-lo6O#e?S}8hFy!R)FQsT(Grq&ojKy}uid>>@iW6^HZ znRZ$7kFpP+a5R}Dk74)Qv>#Ibj19!5-@dTA%i!`3iu|k5^7!A99fgmYGU}ys;_bvL zir$QOc!EF0zkMFnlc+vi6g_iCt*`GX-8(+fgns0YC73<{u%rm6<%WE0a_Cfmt=EGc z=DWgP(4pf+;#WLpS6iJ9V0GqP%LI>43ek*2<||&RU1cp4EoOqIayUjKO#j)w0KXuO zb;9Kuw7-S`NapFJB@2z*-2+G~lt2`x4H$+;ZOQnGxU@vqo!8xCXf^A-Xq~Pf%XWq$ z=cpyt@!Y)^SSwIV%jfUYe?LZ$ND*^Fe`+-YZdqZ1?so4-oi9vu$wXVhu?0p!0-pO) z%{ou{a>UYIPOg?2=K6Heh_Z;Wj^jh(F}bJWItV$3mIu3p?{)gs))doC%OfkyUb#lr z(~pX12B#{>!xdr=J2x@SE{z;6Xd6hna>@Lt%{|-|SkG?dl{bgOwRb1Dd=wcho3D

L zniHuz(P}q#w5j>hm#VS>7UtG*ifC^CzG+~RyiL>SSLv%8V) zbWFNk_UapKddHBGS0o0hj>+?3}F_9*R(aS#oRn3!Rx=EvNizm~_@ zJF2VC5L$I7w^gd1>BZJYfH9$)l=Z0f#pQj^#l>{^UhwV5{@oDxzx=P}fG{>T+7bu7 zEzA=NZ8VWcqEY}apKrS05qTO1Xk)N}-~GV}7l6lFlFk5LfDT;eOZ>gi-9mq_zMtfV zqmaVL{x}N2R~Wu~@O8wBM5N+~yc_xc@*QHZWF7X&wqdV2a`2;{Jf F&0n1gYytoP literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Images.xcassets/Instant View/Settings/Browser.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Instant View/Bookmark.imageset/Contents.json similarity index 75% rename from submodules/TelegramUI/Images.xcassets/Instant View/Settings/Browser.imageset/Contents.json rename to submodules/TelegramUI/Images.xcassets/Instant View/Bookmark.imageset/Contents.json index 0fff16d67e..09b651c240 100644 --- a/submodules/TelegramUI/Images.xcassets/Instant View/Settings/Browser.imageset/Contents.json +++ b/submodules/TelegramUI/Images.xcassets/Instant View/Bookmark.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "ic_lt_safari.pdf", + "filename" : "Bookmark.pdf", "idiom" : "universal" } ], diff --git a/submodules/TelegramUI/Images.xcassets/Instant View/Browser.imageset/Browser.pdf b/submodules/TelegramUI/Images.xcassets/Instant View/Browser.imageset/Browser.pdf new file mode 100644 index 0000000000000000000000000000000000000000..81f7ac317102df77bcaf0b495208ee9ccf7db77b GIT binary patch literal 2160 zcmZXVdpMNo8^@8unEaLua>~A}^P!nx4C6PLF^tnxV+c8A@RHNanwd0{a%gCsjo7Ib z5!oc=5Zkhably%vE4EaFkknu)Np^;4+r9sIujlz*&wW4l_kOSI`52I$i5AGMHc%)M zKmhcx7$^W>v4DdE6r@Fnw@}M}>TqIo91COsaAF*l1v-P_^axOtAqqGNq(!mz09K;o z6pj}tv*jJjGwhkKX$XaAgjMvaO(BAXR+2dLk zqaWGrPmNMg?e-ly{QE=4KmCs1Gd!Kxe4%=$?yq<*EN6JC6w~r(2#-J1U$lr;*9PM^ zG`)|Q94+XTNkFh~Ucv{C^>nmifZtATQ$9Mhr@^?b;d#$ZE=CsfmI+QMWnz!CtPWS*V%lqCO~D z2cr=}R4XfCHWy`Be3U&BaM|$4xY2iL<0}=upnnR>wr6}}_#eJ}9?CN<_vNDjkA(BZ zktJQ(=X#8Y9xaBgFyFTuxE}MqNJa^Bz`-wIYkpzKoVqbvchhfE27c-+{#5rHkSTZQ#ueGh zCUCUMsYlmP|CJ}!YbH#(agq1`iU6*p^F_PteA(?VXFoZae4^qbRq|6;&s4G!gokn( z2uAw^^O}9Ecq18gOEsvQwucok66uzU9yn#Q!)EG^9QVHBf~xa#D&^J7JBx`V8H3I+ z3S{2eUZe54vnpd2<4p^_t0OIli-^19)?9X*a+H_RV))`}QPbcdmo$hoAJo?^3+0;H znco2m^)@B+I?Xz#gD~Aobz9_iPpI(k119Fv-&ZlS%VopT)TCdQW}o`NsU(0+Dhbz4 zneT}mnNO&HeULl<*s=WyNsxQcs>c!~Klm`Bj`8O7mM-GfZM!+jaU^^B(m#|FR4Yib z0jg7(r~C!Yfshb2yI$q49*v)qW`P5a7Kwbt8d~XynwK#5^)18JgcA17)>~=pkE~s- zfD(D4CLF2iCVSca+Y%i?{8(jto0j{-5Y*KP+)qE`Q3&j7@x9@{ld`wsxnT@vao|l_2t#A^=>c!vK*Vz5E z;C7_F)I*nEwp!L6(iA>XCpN%U)y(cyYEa=Zp9%-pT3Dvy3Hzcv-^Y_&bIm&z`OrE6 zEVJ#FUQs};u}s5}dNqFK*z)s>c#WLRrU>aQV2c7=6cb0|dT=qh+ifAF!)d@`AnYUB;SVHYJ{q%qSR`FI&^*wQ!)ko%m=k$E?eI5kR?bC_6bgtn3$iuA)07X zfwLy_Zu0?`@-GmD1QhuP)N1dL@_JlEX0%M zlv?#VIw6v+l6c*AEoJ$Tw)aw8uEf<;bB7nBpOPnJ&OlO@SqJPt*|u*W$dp3FaQOVX zuVF&`?yuqliNYY)>4938MO1;~X*4>E2?PdzW`+;vdk}z=sZnbX*#<^LQ=RB+AP_)o zMXbb^h1I(BWwlm`0y61)8Q~xk5OYinATg5&bQ%k!iEb1_Y)vFn86pj^{w)0ibz*59 zfpE|02vMtm*Z~DtyN5vEE6PRwr+?-u^46w-{;HDLAangfabv5-2~-v}jvn=ODw_dD zLJ@!sbk)B23!ts7t*il&{5g;aU}Xi+)+H-ZpW;}TkZ1(vKa!Pb5MQ6NMTlJgOS1Wr z$YM~V<3I*f{P*0QMdL7L(&IrBz&JX>WOXQT3Z2dZ#D-S}4EK)S4~n{9;XjqZTCsz~ NSlL1i3|w{){|5EXWUT-I literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Images.xcassets/Instant View/Browser.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Instant View/Browser.imageset/Contents.json new file mode 100644 index 0000000000..c45b00a1de --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Instant View/Browser.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Browser.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Instant View/CloseIcon.imageset/Close.pdf b/submodules/TelegramUI/Images.xcassets/Instant View/CloseIcon.imageset/Close.pdf new file mode 100644 index 0000000000..dc18df012d --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Instant View/CloseIcon.imageset/Close.pdf @@ -0,0 +1,83 @@ +%PDF-1.7 + +1 0 obj + << >> +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 6.169983 6.370150 cm +1.000000 1.000000 1.000000 scn +0.359774 1.100077 m +0.100075 0.840378 0.100075 0.419323 0.359774 0.159624 c +0.619473 -0.100075 1.040527 -0.100075 1.300226 0.159624 c +8.830000 7.689398 l +16.359774 0.159624 l +16.619473 -0.100075 17.040527 -0.100075 17.300226 0.159624 c +17.559925 0.419323 17.559925 0.840378 17.300226 1.100077 c +9.770452 8.629850 l +17.300226 16.159624 l +17.559925 16.419323 17.559925 16.840378 17.300226 17.100077 c +17.040527 17.359776 16.619473 17.359776 16.359774 17.100077 c +8.830000 9.570303 l +1.300226 17.100077 l +1.040527 17.359776 0.619473 17.359776 0.359774 17.100077 c +0.100075 16.840378 0.100075 16.419323 0.359774 16.159624 c +7.889547 8.629850 l +0.359774 1.100077 l +h +f* +n +Q + +endstream +endobj + +3 0 obj + 788 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 30.000000 30.000000 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Pages 5 0 R + /Type /Catalog + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000000878 00000 n +0000000900 00000 n +0000001073 00000 n +0000001147 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +1206 +%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Instant View/CloseIcon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Instant View/CloseIcon.imageset/Contents.json index 2553d7c645..2e21e0ab34 100644 --- a/submodules/TelegramUI/Images.xcassets/Instant View/CloseIcon.imageset/Contents.json +++ b/submodules/TelegramUI/Images.xcassets/Instant View/CloseIcon.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "cross.pdf", + "filename" : "Close.pdf", "idiom" : "universal" } ], diff --git a/submodules/TelegramUI/Images.xcassets/Instant View/CloseIcon.imageset/cross.pdf b/submodules/TelegramUI/Images.xcassets/Instant View/CloseIcon.imageset/cross.pdf deleted file mode 100644 index e16f590147..0000000000 --- a/submodules/TelegramUI/Images.xcassets/Instant View/CloseIcon.imageset/cross.pdf +++ /dev/null @@ -1,83 +0,0 @@ -%PDF-1.7 - -1 0 obj - << >> -endobj - -2 0 obj - << /Length 3 0 R >> -stream -/DeviceRGB CS -/DeviceRGB cs -q -1.000000 0.000000 -0.000000 1.000000 5.100098 4.640198 cm -1.000000 1.000000 1.000000 scn -0.970226 14.230053 m -0.710527 14.489752 0.289473 14.489752 0.029774 14.230053 c --0.229925 13.970354 -0.229925 13.549299 0.029774 13.289600 c -5.959549 7.359825 l -0.029774 1.430050 l --0.229925 1.170351 -0.229925 0.749296 0.029774 0.489597 c -0.289473 0.229898 0.710527 0.229898 0.970226 0.489597 c -6.900002 6.419373 l -12.829774 0.489600 l -13.089473 0.229901 13.510528 0.229901 13.770226 0.489600 c -14.029925 0.749299 14.029925 1.170354 13.770226 1.430053 c -7.840454 7.359825 l -13.770226 13.289598 l -14.029925 13.549296 14.029925 13.970351 13.770226 14.230050 c -13.510528 14.489749 13.089473 14.489749 12.829774 14.230050 c -6.900002 8.300278 l -0.970226 14.230053 l -h -f* -n -Q - -endstream -endobj - -3 0 obj - 789 -endobj - -4 0 obj - << /Annots [] - /Type /Page - /MediaBox [ 0.000000 0.000000 24.000000 24.000000 ] - /Resources 1 0 R - /Contents 2 0 R - /Parent 5 0 R - >> -endobj - -5 0 obj - << /Kids [ 4 0 R ] - /Count 1 - /Type /Pages - >> -endobj - -6 0 obj - << /Pages 5 0 R - /Type /Catalog - >> -endobj - -xref -0 7 -0000000000 65535 f -0000000010 00000 n -0000000034 00000 n -0000000879 00000 n -0000000901 00000 n -0000001074 00000 n -0000001148 00000 n -trailer -<< /ID [ (some) (id) ] - /Root 6 0 R - /Size 7 ->> -startxref -1207 -%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Instant View/Settings/Browser.imageset/ic_lt_safari.pdf b/submodules/TelegramUI/Images.xcassets/Instant View/Settings/Browser.imageset/ic_lt_safari.pdf deleted file mode 100644 index ba73353a6d6e002a2b8e4e3cfee270797252fdc1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4345 zcmai&2UHVVx5p__ARwYrq=+L@6-Yv85kY!Dln&A%Kg|PXYKPpd!0FZ@3nq=xYXqp1z>`3Fjw2$*4%Qz z_S3$$cCau21)R<8z&CFKkUMB6Yj+z!glN(PAd0pQ?r040?SOJe%cCuvEzy9K6xhuj zgGM=my-Cs9DrNLAn(;k845=(zpm{l>+0Zp8j!tPiwy(ZP9+44BDlCAjpHZt_=_%jR z4PaQxtG3IZ!^aL!7uUskY(TKF9iGgu5*}`OJgE_~&&T7CV}jH*(}8bNF$#j`M_tE+ zX$O(}nWuQIioCQbHz_|p><)NS$R~1b`&$~wLi6_7RTX+NPZec>yIJrfntR_BH{}C!L z{@OriB~wv94w&zAUJQ)sZ@a;$68>#W?ZTLd#$Bv@FhQrPTlEMH`(yxPxtq#9GX znoBMu;Gsz{Ea$tSOM7?PWj9^*=6yk5YStRnAooQr|$yY>g=(>T*;UAs8=sSt*aM5grXaR|JBhfB{3Ir~KE!EGP^KkA{V79m`)n*Sw} zKJj(lciV21{8_6K##=|Bcs35c85m*NZ{Hw*(Lr@zLn?4z`d!Sx{Q9hhRDZ*2T~E{A zOYFD3@%8FY-^PDjUx_T)G4we+b94{F4n{j!{>nz;=p>0xGWX;yDhxlFJQ)i8mfzYw zE@%LvjO~#n zY@vmA2Mmc$ABBKg-1SIUWG zV>VUJbB@`za);f5`RnyvDaqVlvRU+dm>YyMvGwYcX6a=SlXOU`98XKnO+Qy#2S~O~ zkyK4S*=7%SFo|!Qq2Zy9URiN)Bh9iGy#7_}Fe}9QT~jz>FZKP}p{_(e)2-mE$_#L{ z!d!kqzKVQzHw|rauO(rR?s)bM!uF4@R{kehZ0Tq#7dXa@q)V@68z(YMLOVG&sCj z1xh*@Y)cukLC26IQyk0q>};y+2py{drH`D9x;Q=gTe-+sx_IgRKJLPc*B%sS%c)E5 zTyN5^pp@>UB=1+$qh#HbmyOlqBt3V3oEbTMaTAWgx>lgCV z5!tAei;V5evl05(X>(oFi1}^kaEe8W?j5FiQ7bxl@P0_1`S6Q;J)Y;cJgG#_rQ-zJ zbj@PS3@egfkbgj}NU+g%ykVYUa|x)qXe8t$QU+b& z+lKl5D~;elwm|l&n9J==Jh4;;Ap4NBR~`eL4xHL_ER+kBgwS&>!WzMaYVI82bUtlH z=6o+6eUNETX5&^(pjQs(WYXm1ORWmlR<`3X=9c8fUrFe?VTr_C)R#6-EA1ogBbTRnrqF3fuQ;VH!$1u$?|<7Lf=qxXZzs=ol_r$L z*(WV0`*EoyjwX90n+j&}m|h9I{UiUVhD@Vbx>+kYw?a2tak{ESGE7+^EhowRp?R8l zr1^sxk+&LL4e`P8)#ufUq!%j&xbm*B$PVWD4AfNJt-A3M{SkTzDfaAeFk$iS&gHN8 z9V#>z+Hj6{j%|)*uJWDc=@zKjceo~V9yS46kB!+Dp0Dkl?foQLZfkPfz?Zat&FR|x zB#tDjB+{gq;yXId!}P-$#g4^lLr^Q^9xIfI2)CeWnRwQg-I zuF7acc4^safk401vxySPQj5Y$J?mW6%AO0x@q&h8GSx=aT3)hhDiA%0X}QLFwv|^yb3ssO%xURceABHBi&FXrqdTH$EZMcO?MCv zgK>h>s9M}cWHMy4!I9u6p|6%5qR3Y5AL44^`ZX5=a#=t*Ys>+XDN96cwP)D7ffa7 z_weB*(0VcK9C61a%zcb%?b!^8<%3Sv#YU9yLCJ|)c>EFW~=9mOqpwmG& zc9Tkk#+c#2?V=#R@N@1JpBf$S0BLi_av+@|q&^idorIrL>{zkG6= zaioQEnKFa&ESZ|jtIr-UO}B!-6eY$c=JLUd`iuUs4XyfKH7Lu4imJG74434=Lt2G% zp>lQaY=_r$F62n}$KBy|)3>TmdGzDXHzh1*I@bez>Powplh#Od);o>vbMrf2(!Myo zztpwebr>LGCF12_-RSuv{HR%E&7QV2Y^2QSQ?06(adX}Dx5nidN5D~OY~#J+dmNf| z$|-7cNaLwSMWdNb<@$WPY^|)uG+v;`kk;X?1B1irgjHp!-KS&@lm2h!?`NWB)VRju zm#6go@9j@??}o18^LFzNy*_(VdW}yk)@|#hn(Ld4^_6`#E}iS%%Gs)}zqit;i!yXI zJ8K(b8B--x_!57++zvThu=H#xm?b7{9>3&Qy_)f{`1JGE-sPO7x+d#D(?I!ErOmirWEe89qb0^;SYl{YtWYdbv;`O6{$kd( z!zOn+$9$svix+taS-Y?M45AFON5)4YUq2JI z^}FLYwHP#*ajEB$z^(M-`#ad()U?WX)+)VmJq%CM5m{aVTe9t8o6(U>5jXC+OVw3< z8OSKBD4TpAoLp|QS&J$9gg-peNiMN2Ll{f=XZYh+!wI+%_WI1nrYixqi?7xl@ry2% zgvYa`j~Y+;FB}*jLMC_X{O2AWXRwk-pFUU)$J=zupwBi z%;xZb)vje0-zp^xe;>6f!tbm+9X0E%d}tjLULyxNPTDV5YdzIl)O%gAN^uEUitNSi zV<&uH?7ZE0k4+w&4Qt~2@p><*za%Tb>kw~k-qp5Y-@lxC>-J<}BjR9W*Kxt}>z6?< z+x7f{sL7JXTf4!8>_dy4j=04YweM<4S-4wX_o)u+Cnt+?Lt6qT0ILZY|0Owr=)aiwZ^m{5Aa~K0wkTO=Z@>^rln4nE z@1KzDK_qV&08z2EbR)v^38E9d{RYUe6Zt!bAWgURe)Y2TK5kkP(Fn8Sw!c9w>~vFF+*tpV05^ z&PN1yVvp-iTq94Ua4@g@4IwBD0fPz&K}BIuxVQln%0s+~y1UMnClcWQm;Aj&Zw%TB z3# zjOqW;iNpVczyF{^K#9%HKi9%VVgK?67Zdqy&+ZtMtpghK^M8VttuLDRy#Peh*_jyd zlb{m+D=0fzITNG*=UAQ?|C Bool { - return lhs.defaultWebBrowser == rhs.defaultWebBrowser + if lhs.defaultWebBrowser != rhs.defaultWebBrowser { + return false + } + if lhs.autologin != rhs.autologin { + return false + } + if lhs.exceptions != rhs.exceptions { + return false + } + return true } public func withUpdatedDefaultWebBrowser(_ defaultWebBrowser: String?) -> WebBrowserSettings { - return WebBrowserSettings(defaultWebBrowser: defaultWebBrowser) + return WebBrowserSettings(defaultWebBrowser: defaultWebBrowser, autologin: self.autologin, exceptions: self.exceptions) + } + + public func withUpdatedAutologin(_ autologin: Bool) -> WebBrowserSettings { + return WebBrowserSettings(defaultWebBrowser: self.defaultWebBrowser, autologin: autologin, exceptions: self.exceptions) + } + + public func withUpdatedExceptions(_ exceptions: [WebBrowserException]) -> WebBrowserSettings { + return WebBrowserSettings(defaultWebBrowser: self.defaultWebBrowser, autologin: self.autologin, exceptions: exceptions) } } diff --git a/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift b/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift index 0d68fcff21..59b63dd909 100644 --- a/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift +++ b/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift @@ -713,12 +713,14 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { let body = MarkdownAttributeSet(font: Font.regular(14.0), textColor: .white) let bold: MarkdownAttributeSet + var link = body if savedMessages { bold = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: presentationData.theme.list.itemAccentColor.withMultiplied(hue: 0.933, saturation: 0.61, brightness: 1.0), additionalAttributes: ["URL": ""]) + link = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: .white) } else { bold = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: .white) } - let attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: body, bold: bold, link: body, linkAttribute: { _ in return nil }), textAlignment: .natural) + let attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: body, bold: bold, link: link, linkAttribute: { _ in return nil }), textAlignment: .natural) self.textNode.attributedText = attributedText self.textNode.maximumNumberOfLines = 2