Browser improvements

This commit is contained in:
Ilya Laktyushin 2024-07-22 07:11:46 +04:00
parent af5408d526
commit 345616704a
29 changed files with 1811 additions and 166 deletions

View File

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

View File

@ -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<BrowserScreen.Action>
init(
theme: PresentationTheme,
strings: PresentationStrings,
url: String,
performAction: ActionSlot<BrowserScreen.Action>
) {
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<Empty>
private let cancelButton: HighlightTrackingButton
private var placeholderContent = ComponentView<Empty>()
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<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, transition: transition)
}
}

View File

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

View File

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

View File

@ -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<BrowserScreen.Action>,
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

View File

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

View File

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

View File

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

View File

@ -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<PresentationData, NoError>)? = 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
}

View File

@ -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<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
async {
let node = WebBrowserDomainExceptionItemNode()
let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
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)
}
}

View File

@ -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<WebBrowserSettings?>(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<WebBrowserException, NoError> {
let (domain, domainUrl) = cleanDomain(url: url)
if #available(iOS 13.0, *), let url = URL(string: domainUrl) {
return Signal { subscriber in
let metadataProvider = LPMetadataProvider()
metadataProvider.shouldFetchSubresources = true
metadataProvider.startFetchingMetadata(for: url, completionHandler: { metadata, _ in
let title = metadata?.value(forKey: "_siteName") as? String ?? metadata?.title
subscriber.putNext(WebBrowserException(domain: domain, title: title ?? domain))
subscriber.putCompletion()
})
return ActionDisposable {
metadataProvider.cancel()
}
}
} else {
return .single(WebBrowserException(domain: domain, title: domain))
}
}

View File

@ -45,6 +45,7 @@ public enum PresentationResourceKey: Int32 {
case itemListSecondaryCheckIcon
case itemListPlusIcon
case itemListRoundPlusIcon
case itemListAccentDeleteIcon
case itemListDeleteIcon
case itemListDeleteIndicatorIcon
case itemListReorderIndicatorIcon

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,7 @@
{
"images" : [
{
"filename" : "ic_lt_safari.pdf",
"filename" : "Bookmark.pdf",
"idiom" : "universal"
}
],

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "Browser.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

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

View File

@ -1,7 +1,7 @@
{
"images" : [
{
"filename" : "cross.pdf",
"filename" : "Close.pdf",
"idiom" : "universal"
}
],

View File

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

View File

@ -1033,7 +1033,7 @@ func openExternalUrlImpl(context: AccountContext, urlContext: OpenURLContext, ur
}
if accessChallengeData.data.isLockable {
if passcodeSettings.autolockTimeout != nil && settings.defaultWebBrowser == nil {
settings = WebBrowserSettings(defaultWebBrowser: "safari")
settings = WebBrowserSettings(defaultWebBrowser: "safari", autologin: false, exceptions: [])
}
}
return settings

View File

@ -3,35 +3,84 @@ import Postbox
import TelegramCore
import SwiftSignalKit
public struct WebBrowserSettings: Codable, Equatable {
public let defaultWebBrowser: String?
public struct WebBrowserException: Codable, Equatable {
public let domain: String
public let title: String
public static var defaultSettings: WebBrowserSettings {
return WebBrowserSettings(defaultWebBrowser: nil)
public init(domain: String, title: String) {
self.domain = domain
self.title = title
}
public init(defaultWebBrowser: String?) {
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: StringCodingKey.self)
self.domain = try container.decode(String.self, forKey: "domain")
self.title = try container.decode(String.self, forKey: "title")
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: StringCodingKey.self)
try container.encode(self.domain, forKey: "domain")
try container.encode(self.title, forKey: "title")
}
}
public struct WebBrowserSettings: Codable, Equatable {
public let defaultWebBrowser: String?
public let autologin: Bool
public let exceptions: [WebBrowserException]
public static var defaultSettings: WebBrowserSettings {
return WebBrowserSettings(defaultWebBrowser: nil, autologin: true, exceptions: [])
}
public init(defaultWebBrowser: String?, autologin: Bool, exceptions: [WebBrowserException]) {
self.defaultWebBrowser = defaultWebBrowser
self.autologin = autologin
self.exceptions = exceptions
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: StringCodingKey.self)
self.defaultWebBrowser = try? container.decodeIfPresent(String.self, forKey: "defaultWebBrowser")
self.autologin = (try? container.decodeIfPresent(Bool.self, forKey: "autologin")) ?? true
self.exceptions = (try? container.decodeIfPresent([WebBrowserException].self, forKey: "exceptions")) ?? []
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: StringCodingKey.self)
try container.encodeIfPresent(self.defaultWebBrowser, forKey: "defaultWebBrowser")
try container.encode(self.autologin, forKey: "autologin")
try container.encode(self.exceptions, forKey: "exceptions")
}
public static func ==(lhs: WebBrowserSettings, rhs: WebBrowserSettings) -> 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)
}
}

View File

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