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.Title" = "Sent Gift";
"Stars.Gift.Sent.Text" = "With Stars, %@ will be able to unlock content and services on Telegram. [See Examples >]()"; "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( component: AnyComponent(
Button( Button(
content: AnyComponent( 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: { action: {
performAction.invoke(.close) performAction.invoke(.close)
@ -130,7 +133,7 @@ private final class BrowserScreenComponent: CombinedComponent {
content: LottieComponent.AppBundleContent( content: LottieComponent.AppBundleContent(
name: "anim_moredots" name: "anim_moredots"
), ),
color: environment.theme.rootController.navigationBar.primaryTextColor, color: environment.theme.rootController.navigationBar.accentTextColor,
size: CGSize(width: 30.0, height: 30.0) size: CGSize(width: 30.0, height: 30.0)
) )
), ),
@ -150,7 +153,7 @@ private final class BrowserScreenComponent: CombinedComponent {
ReferenceButtonComponent( ReferenceButtonComponent(
content: AnyComponent( content: AnyComponent(
BundleIconComponent( 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 tintColor: environment.theme.rootController.navigationBar.primaryTextColor
) )
), ),
@ -211,6 +214,7 @@ private final class BrowserScreenComponent: CombinedComponent {
id: "navigation", id: "navigation",
component: AnyComponent( component: AnyComponent(
NavigationToolbarContentComponent( NavigationToolbarContentComponent(
accentColor: environment.theme.rootController.navigationBar.accentTextColor,
textColor: environment.theme.rootController.navigationBar.primaryTextColor, textColor: environment.theme.rootController.navigationBar.primaryTextColor,
canGoBack: context.component.contentState?.canGoBack ?? false, canGoBack: context.component.contentState?.canGoBack ?? false,
canGoForward: context.component.contentState?.canGoForward ?? false, canGoForward: context.component.contentState?.canGoForward ?? false,
@ -281,6 +285,8 @@ public class BrowserScreen: ViewController, MinimizableController {
case increaseFontSize case increaseFontSize
case resetFontSize case resetFontSize
case updateFontIsSerif(Bool) case updateFontIsSerif(Bool)
case addBookmark
case openBookmarks
} }
fileprivate final class Node: ViewControllerTracingNode { fileprivate final class Node: ViewControllerTracingNode {
@ -502,6 +508,12 @@ public class BrowserScreen: ViewController, MinimizableController {
return updatedState return updatedState
}) })
content.updateFontState(self.presentationState.fontState) 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)) 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() { private func setupContentStateUpdates() {
for content in self.content { for content in self.content {
content.onScrollingUpdate = { _ in } content.onScrollingUpdate = { _ in }
@ -777,7 +826,11 @@ public class BrowserScreen: ViewController, MinimizableController {
performAction.invoke(.updateSearchActive(true)) performAction.invoke(.updateSearchActive(true))
action(.default) 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 { if let self {
self.context.sharedContext.applicationBindings.openUrl(openInUrl) self.context.sharedContext.applicationBindings.openUrl(openInUrl)
} }

View File

@ -33,7 +33,7 @@ final class SearchBarContentComponent: Component {
} }
final class View: UIView, UITextFieldDelegate { final class View: UIView, UITextFieldDelegate {
private final class EmojiSearchTextField: UITextField { private final class SearchTextField: UITextField {
override func textRect(forBounds bounds: CGRect) -> CGRect { override func textRect(forBounds bounds: CGRect) -> CGRect {
return bounds.integral return bounds.integral
} }
@ -75,7 +75,7 @@ final class SearchBarContentComponent: Component {
private var placeholderContent = ComponentView<Empty>() private var placeholderContent = ComponentView<Empty>()
private var textFrame: CGRect? private var textFrame: CGRect?
private var textField: EmojiSearchTextField? private var textField: SearchTextField?
private var tapRecognizer: UITapGestureRecognizer? private var tapRecognizer: UITapGestureRecognizer?
@ -160,7 +160,7 @@ final class SearchBarContentComponent: Component {
let backgroundFrame = self.backgroundLayer.frame 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 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.autocorrectionType = .no
textField.returnKeyType = .search textField.returnKeyType = .search
self.textField = textField self.textField = textField
@ -285,7 +285,7 @@ final class SearchBarContentComponent: Component {
component: AnyComponent(Text( component: AnyComponent(Text(
text: strings.Common_Cancel, text: strings.Common_Cancel,
font: Font.regular(17.0), font: Font.regular(17.0),
color: theme.rootController.navigationBar.primaryTextColor color: theme.rootController.navigationBar.accentTextColor
)), )),
environment: {}, environment: {},
containerSize: CGSize(width: size.width - 32.0, height: 100.0) containerSize: CGSize(width: size.width - 32.0, height: 100.0)

View File

@ -120,6 +120,7 @@ final class BrowserToolbarComponent: CombinedComponent {
} }
final class NavigationToolbarContentComponent: CombinedComponent { final class NavigationToolbarContentComponent: CombinedComponent {
let accentColor: UIColor
let textColor: UIColor let textColor: UIColor
let canGoBack: Bool let canGoBack: Bool
let canGoForward: Bool let canGoForward: Bool
@ -127,12 +128,14 @@ final class NavigationToolbarContentComponent: CombinedComponent {
let performHoldAction: (UIView, ContextGesture?, BrowserScreen.Action) -> Void let performHoldAction: (UIView, ContextGesture?, BrowserScreen.Action) -> Void
init( init(
accentColor: UIColor,
textColor: UIColor, textColor: UIColor,
canGoBack: Bool, canGoBack: Bool,
canGoForward: Bool, canGoForward: Bool,
performAction: ActionSlot<BrowserScreen.Action>, performAction: ActionSlot<BrowserScreen.Action>,
performHoldAction: @escaping (UIView, ContextGesture?, BrowserScreen.Action) -> Void performHoldAction: @escaping (UIView, ContextGesture?, BrowserScreen.Action) -> Void
) { ) {
self.accentColor = accentColor
self.textColor = textColor self.textColor = textColor
self.canGoBack = canGoBack self.canGoBack = canGoBack
self.canGoForward = canGoForward self.canGoForward = canGoForward
@ -141,6 +144,9 @@ final class NavigationToolbarContentComponent: CombinedComponent {
} }
static func ==(lhs: NavigationToolbarContentComponent, rhs: NavigationToolbarContentComponent) -> Bool { static func ==(lhs: NavigationToolbarContentComponent, rhs: NavigationToolbarContentComponent) -> Bool {
if lhs.accentColor != rhs.accentColor {
return false
}
if lhs.textColor != rhs.textColor { if lhs.textColor != rhs.textColor {
return false return false
} }
@ -157,6 +163,7 @@ final class NavigationToolbarContentComponent: CombinedComponent {
let back = Child(ContextReferenceButtonComponent.self) let back = Child(ContextReferenceButtonComponent.self)
let forward = Child(ContextReferenceButtonComponent.self) let forward = Child(ContextReferenceButtonComponent.self)
let share = Child(Button.self) let share = Child(Button.self)
let bookmark = Child(Button.self)
let openIn = Child(Button.self) let openIn = Child(Button.self)
return { context in return { context in
@ -166,7 +173,7 @@ final class NavigationToolbarContentComponent: CombinedComponent {
let sideInset: CGFloat = 5.0 let sideInset: CGFloat = 5.0
let buttonSize = CGSize(width: 50.0, height: availableSize.height) 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 canGoBack = context.component.canGoBack
let back = back.update( let back = back.update(
@ -174,7 +181,7 @@ final class NavigationToolbarContentComponent: CombinedComponent {
content: AnyComponent( content: AnyComponent(
BundleIconComponent( BundleIconComponent(
name: "Instant View/Back", 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, minSize: buttonSize,
@ -202,7 +209,7 @@ final class NavigationToolbarContentComponent: CombinedComponent {
content: AnyComponent( content: AnyComponent(
BundleIconComponent( BundleIconComponent(
name: "Instant View/Forward", 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, minSize: buttonSize,
@ -229,7 +236,7 @@ final class NavigationToolbarContentComponent: CombinedComponent {
content: AnyComponent( content: AnyComponent(
BundleIconComponent( BundleIconComponent(
name: "Chat List/NavigationShare", name: "Chat List/NavigationShare",
tintColor: context.component.textColor tintColor: context.component.accentColor
) )
), ),
action: { 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)) .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( let openIn = openIn.update(
component: Button( component: Button(
content: AnyComponent( content: AnyComponent(
BundleIconComponent( BundleIconComponent(
name: "Instant View/Minimize", name: "Instant View/Browser",
tintColor: context.component.textColor tintColor: context.component.accentColor
) )
), ),
action: { action: {
performAction.invoke(.minimize) performAction.invoke(.openIn)
} }
).minSize(buttonSize), ).minSize(buttonSize),
availableSize: buttonSize, availableSize: buttonSize,
transition: .easeInOut(duration: 0.2) transition: .easeInOut(duration: 0.2)
) )
context.add(openIn 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 return availableSize

View File

@ -204,9 +204,9 @@ private final class VideoRecorderImpl {
if let videoInput = self.videoInput { if let videoInput = self.videoInput {
let time = CACurrentMediaTime() let time = CACurrentMediaTime()
if let previousPresentationTime = self.previousPresentationTime, let previousAppendTime = self.previousAppendTime { // if let previousPresentationTime = self.previousPresentationTime, let previousAppendTime = self.previousAppendTime {
print("appending \(presentationTime.seconds) (\(presentationTime.seconds - previousPresentationTime) ) on \(time) (\(time - previousAppendTime)") // print("appending \(presentationTime.seconds) (\(presentationTime.seconds - previousPresentationTime) ) on \(time) (\(time - previousAppendTime)")
} // }
self.previousPresentationTime = presentationTime.seconds self.previousPresentationTime = presentationTime.seconds
self.previousAppendTime = time self.previousAppendTime = time

View File

@ -80,7 +80,6 @@ public final class LocationViewController: ViewController {
private let isStoryLocation: Bool private let isStoryLocation: Bool
private let locationManager = LocationManager() private let locationManager = LocationManager()
private var permissionDisposable: Disposable?
private var interaction: LocationViewInteraction? private var interaction: LocationViewInteraction?

View File

@ -100,9 +100,9 @@ private enum DataAndStorageEntry: ItemListNodeEntry {
case useLessVoiceData(PresentationTheme, String, Bool) case useLessVoiceData(PresentationTheme, String, Bool)
case useLessVoiceDataInfo(PresentationTheme, String) case useLessVoiceDataInfo(PresentationTheme, String)
case otherHeader(PresentationTheme, String) case otherHeader(PresentationTheme, String)
case openLinksIn(PresentationTheme, String, String)
case shareSheet(PresentationTheme, String) case shareSheet(PresentationTheme, String)
case saveEditedPhotos(PresentationTheme, String, Bool) case saveEditedPhotos(PresentationTheme, String, Bool)
case openLinksIn(PresentationTheme, String, String)
case pauseMusicOnRecording(PresentationTheme, String, Bool) case pauseMusicOnRecording(PresentationTheme, String, Bool)
case raiseToListen(PresentationTheme, String, Bool) case raiseToListen(PresentationTheme, String, Bool)
case raiseToListenInfo(PresentationTheme, String) case raiseToListenInfo(PresentationTheme, String)
@ -123,7 +123,7 @@ private enum DataAndStorageEntry: ItemListNodeEntry {
return DataAndStorageSection.backgroundDownload.rawValue return DataAndStorageSection.backgroundDownload.rawValue
case .useLessVoiceData, .useLessVoiceDataInfo: case .useLessVoiceData, .useLessVoiceDataInfo:
return DataAndStorageSection.voiceCalls.rawValue return DataAndStorageSection.voiceCalls.rawValue
case .otherHeader, .shareSheet, .saveEditedPhotos, .openLinksIn, .pauseMusicOnRecording, .raiseToListen, .raiseToListenInfo: case .otherHeader, .openLinksIn, .shareSheet, .saveEditedPhotos, .pauseMusicOnRecording, .raiseToListen, .raiseToListenInfo:
return DataAndStorageSection.other.rawValue return DataAndStorageSection.other.rawValue
case .connectionHeader, .connectionProxy: case .connectionHeader, .connectionProxy:
return DataAndStorageSection.connection.rawValue return DataAndStorageSection.connection.rawValue
@ -162,11 +162,11 @@ private enum DataAndStorageEntry: ItemListNodeEntry {
return 24 return 24
case .otherHeader: case .otherHeader:
return 29 return 29
case .shareSheet:
return 30
case .saveEditedPhotos:
return 31
case .openLinksIn: case .openLinksIn:
return 30
case .shareSheet:
return 31
case .saveEditedPhotos:
return 32 return 32
case .pauseMusicOnRecording: case .pauseMusicOnRecording:
return 33 return 33
@ -257,6 +257,12 @@ private enum DataAndStorageEntry: ItemListNodeEntry {
} else { } else {
return false 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): case let .shareSheet(lhsTheme, lhsText):
if case let .shareSheet(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { if case let .shareSheet(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true return true
@ -269,12 +275,6 @@ private enum DataAndStorageEntry: ItemListNodeEntry {
} else { } else {
return false 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): case let .pauseMusicOnRecording(lhsTheme, lhsText, lhsValue):
if case let .pauseMusicOnRecording(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { if case let .pauseMusicOnRecording(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue {
return true return true
@ -386,6 +386,10 @@ private enum DataAndStorageEntry: ItemListNodeEntry {
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
case let .otherHeader(_, text): case let .otherHeader(_, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) 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): case let .shareSheet(_, text):
return ItemListDisclosureItem(presentationData: presentationData, title: text, label: "", sectionId: self.section, style: .blocks, action: { return ItemListDisclosureItem(presentationData: presentationData, title: text, label: "", sectionId: self.section, style: .blocks, action: {
arguments.openIntents() 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 return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, sectionId: self.section, style: .blocks, updated: { value in
arguments.toggleSaveEditedPhotos(value) arguments.toggleSaveEditedPhotos(value)
}, tag: DataAndStorageEntryTag.saveEditedPhotos) }, 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): case let .pauseMusicOnRecording(_, text, value):
return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, sectionId: self.section, style: .blocks, updated: { value in return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, sectionId: self.section, style: .blocks, updated: { value in
arguments.togglePauseMusicOnRecording(value) arguments.togglePauseMusicOnRecording(value)
@ -618,11 +618,11 @@ private func dataAndStorageControllerEntries(state: DataAndStorageControllerStat
entries.append(.useLessVoiceDataInfo(presentationData.theme, presentationData.strings.CallSettings_UseLessDataLongDescription)) entries.append(.useLessVoiceDataInfo(presentationData.theme, presentationData.strings.CallSettings_UseLessDataLongDescription))
entries.append(.otherHeader(presentationData.theme, presentationData.strings.ChatSettings_Other)) 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, *) { if #available(iOSApplicationExtension 13.2, iOS 13.2, *) {
entries.append(.shareSheet(presentationData.theme, presentationData.strings.ChatSettings_IntentsSettings)) entries.append(.shareSheet(presentationData.theme, presentationData.strings.ChatSettings_IntentsSettings))
} }
entries.append(.saveEditedPhotos(presentationData.theme, presentationData.strings.Settings_SaveEditedPhotos, data.generatedMediaStoreSettings.storeEditedPhotos)) 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(.pauseMusicOnRecording(presentationData.theme, presentationData.strings.Settings_PauseMusicOnRecording, data.mediaInputSettings.pauseMusicOnRecording))
entries.append(.raiseToListen(presentationData.theme, presentationData.strings.Settings_RaiseToListen, data.mediaInputSettings.enableRaiseToSpeak)) entries.append(.raiseToListen(presentationData.theme, presentationData.strings.Settings_RaiseToListen, data.mediaInputSettings.enableRaiseToSpeak))
entries.append(.raiseToListenInfo(presentationData.theme, presentationData.strings.Settings_RaiseToListenInfo)) 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 ItemListUI
import AccountContext import AccountContext
import OpenInExternalAppUI import OpenInExternalAppUI
import ItemListPeerActionItem
import UndoUI
import WebKit
import LinkPresentation
private final class WebBrowserSettingsControllerArguments { private final class WebBrowserSettingsControllerArguments {
let context: AccountContext let context: AccountContext
let updateDefaultBrowser: (String?) -> Void 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.context = context
self.updateDefaultBrowser = updateDefaultBrowser self.updateDefaultBrowser = updateDefaultBrowser
self.updateAutologin = updateAutologin
self.clearCookies = clearCookies
self.addException = addException
self.clearExceptions = clearExceptions
} }
} }
private enum WebBrowserSettingsSection: Int32 { private enum WebBrowserSettingsSection: Int32 {
case browsers case browsers
case autologin
case clearCookies
case exceptions
} }
private enum WebBrowserSettingsControllerEntry: ItemListNodeEntry { private enum WebBrowserSettingsControllerEntry: ItemListNodeEntry {
case browserHeader(PresentationTheme, String) case browserHeader(PresentationTheme, String)
case browser(PresentationTheme, String, OpenInApplication?, String?, Bool, Int32) 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 { var section: ItemListSectionId {
switch self { switch self {
case .browserHeader, .browser: case .browserHeader, .browser:
return WebBrowserSettingsSection.browsers.rawValue 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 return 0
case let .browser(_, _, _, _, _, index): case let .browser(_, _, _, _, _, index):
return 1 + 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 { } else {
return false 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) { return WebBrowserItem(context: arguments.context, presentationData: presentationData, title: title, application: application, checked: selected, sectionId: self.section) {
arguments.updateDefaultBrowser(identifier) 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] = [] var entries: [WebBrowserSettingsControllerEntry] = []
let options = availableOpenInOptions(context: context, item: .url(url: "http://telegram.org")) let options = availableOpenInOptions(context: context, item: .url(url: "http://telegram.org"))
entries.append(.browserHeader(presentationData.theme, presentationData.strings.WebBrowser_DefaultBrowser)) entries.append(.browserHeader(presentationData.theme, presentationData.strings.WebBrowser_OpenLinksIn_Title))
entries.append(.browser(presentationData.theme, presentationData.strings.WebBrowser_Telegram, nil, nil, selectedBrowser == nil, 0)) entries.append(.browser(presentationData.theme, presentationData.strings.WebBrowser_Telegram, nil, nil, settings.defaultWebBrowser == nil, 0))
entries.append(.browser(presentationData.theme, presentationData.strings.WebBrowser_InAppSafari, .safari, "inApp", selectedBrowser == "inApp", 1))
var index: Int32 = 2 var index: Int32 = 1
for option in options { 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 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 return entries
} }
public func webBrowserSettingsController(context: AccountContext) -> ViewController { public func webBrowserSettingsController(context: AccountContext) -> ViewController {
let arguments = WebBrowserSettingsControllerArguments(context: context, updateDefaultBrowser: { identifier in var clearCookiesImpl: (() -> Void)?
let _ = updateWebBrowserSettingsInteractively(accountManager: context.sharedContext.accountManager, { $0.withUpdatedDefaultWebBrowser(identifier) }).start() 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 |> deliverOnMainQueue
|> map { presentationData, sharedData -> (ItemListControllerState, (ItemListNodeState, Any)) in |> map { presentationData, sharedData -> (ItemListControllerState, (ItemListNodeState, Any)) in
let settings = sharedData.entries[ApplicationSpecificSharedDataKeys.webBrowserSettings]?.get(WebBrowserSettings.self) ?? WebBrowserSettings.defaultSettings 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 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)) return (controllerState, (listState, arguments))
} }
let controller = ItemListController(context: context, state: signal) 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 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 itemListSecondaryCheckIcon
case itemListPlusIcon case itemListPlusIcon
case itemListRoundPlusIcon case itemListRoundPlusIcon
case itemListAccentDeleteIcon
case itemListDeleteIcon case itemListDeleteIcon
case itemListDeleteIndicatorIcon case itemListDeleteIndicatorIcon
case itemListReorderIndicatorIcon 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? { public static func deleteIconImage(_ theme: PresentationTheme) -> UIImage? {
return theme.image(PresentationResourceKey.itemListDeleteIcon.rawValue, { theme in return theme.image(PresentationResourceKey.itemListDeleteIcon.rawValue, { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionTrash"), color: theme.list.itemDestructiveColor) 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) 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 trackTrimUpdated: { _, _, _, _, _ in
}, },
trackOffsetUpdated: { _, _, _ in trackOffsetUpdated: { _, _, _ in

View File

@ -48,6 +48,7 @@ import StickerPackEditTitleController
import StickerPickerScreen import StickerPickerScreen
import UIKitRuntimeUtils import UIKitRuntimeUtils
import ImageObjectSeparation import ImageObjectSeparation
import DeviceAccess
private let playbackButtonTag = GenericComponentViewTag() private let playbackButtonTag = GenericComponentViewTag()
private let muteButtonTag = GenericComponentViewTag() private let muteButtonTag = GenericComponentViewTag()
@ -2553,6 +2554,8 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
var recording: MediaEditorScreen.Recording var recording: MediaEditorScreen.Recording
private let locationManager = LocationManager()
private var presentationData: PresentationData private var presentationData: PresentationData
private var validLayout: ContainerViewLayout? private var validLayout: ContainerViewLayout?
@ -4584,7 +4587,37 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
self.mediaEditor?.play() 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 let maxWeatherCount = 3
var currentWeatherCount = 0 var currentWeatherCount = 0
self.entitiesView.eachView { entityView in self.entitiesView.eachView { entityView in
@ -4948,9 +4981,16 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
if let self { if let self {
if let weatherPromise = self.weatherPromise { if let weatherPromise = self.weatherPromise {
let _ = (weatherPromise.get() let _ = (weatherPromise.get()
|> take(1)).start(next: { [weak self] weather in |> take(1)).start(next: { [weak self] result in
if let self, case let .loaded(loaded) = weather { if let self {
self.addWeather(loaded) 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 portalView: PortalView?
let positionUpdated: (Double, Bool) -> Void let positionUpdated: (Double, Bool) -> Void
let coverPositionUpdated: (Double, Bool, @escaping () -> Void) -> Void
let trackTrimUpdated: (Int32, Double, Double, Bool, Bool) -> Void let trackTrimUpdated: (Int32, Double, Double, Bool, Bool) -> Void
let trackOffsetUpdated: (Int32, Double, Bool) -> Void let trackOffsetUpdated: (Int32, Double, Bool) -> Void
let trackLongPressed: (Int32, UIView) -> Void let trackLongPressed: (Int32, UIView) -> Void
@ -104,6 +105,7 @@ public final class MediaScrubberComponent: Component {
tracks: [Track], tracks: [Track],
portalView: PortalView? = nil, portalView: PortalView? = nil,
positionUpdated: @escaping (Double, Bool) -> Void, positionUpdated: @escaping (Double, Bool) -> Void,
coverPositionUpdated: @escaping (Double, Bool, @escaping () -> Void) -> Void = { _, _, _ in },
trackTrimUpdated: @escaping (Int32, Double, Double, Bool, Bool) -> Void, trackTrimUpdated: @escaping (Int32, Double, Double, Bool, Bool) -> Void,
trackOffsetUpdated: @escaping (Int32, Double, Bool) -> Void, trackOffsetUpdated: @escaping (Int32, Double, Bool) -> Void,
trackLongPressed: @escaping (Int32, UIView) -> Void trackLongPressed: @escaping (Int32, UIView) -> Void
@ -119,6 +121,7 @@ public final class MediaScrubberComponent: Component {
self.tracks = tracks self.tracks = tracks
self.portalView = portalView self.portalView = portalView
self.positionUpdated = positionUpdated self.positionUpdated = positionUpdated
self.coverPositionUpdated = coverPositionUpdated
self.trackTrimUpdated = trackTrimUpdated self.trackTrimUpdated = trackTrimUpdated
self.trackOffsetUpdated = trackOffsetUpdated self.trackOffsetUpdated = trackOffsetUpdated
self.trackLongPressed = trackLongPressed self.trackLongPressed = trackLongPressed
@ -164,6 +167,7 @@ public final class MediaScrubberComponent: Component {
private var selectedTrackId: Int32 = 0 private var selectedTrackId: Int32 = 0
private var isPanningCursor = false private var isPanningCursor = false
private var ignoreCursorPositionUpdate = false
private var scrubberSize: CGSize? private var scrubberSize: CGSize?
@ -327,10 +331,18 @@ public final class MediaScrubberComponent: Component {
switch gestureRecognizer.state { switch gestureRecognizer.state {
case .began, .changed: case .began, .changed:
self.isPanningCursor = true self.isPanningCursor = true
if case .cover = component.style {
component.coverPositionUpdated(position, false, {})
} else {
component.positionUpdated(position, false) component.positionUpdated(position, false)
}
case .ended, .cancelled: case .ended, .cancelled:
self.isPanningCursor = false self.isPanningCursor = false
if case .cover = component.style {
component.coverPositionUpdated(position, false, {})
} else {
component.positionUpdated(position, true) component.positionUpdated(position, true)
}
default: default:
break break
} }
@ -345,7 +357,7 @@ public final class MediaScrubberComponent: Component {
var y: CGFloat = -5.0 - UIScreenPixel var y: CGFloat = -5.0 - UIScreenPixel
if let component = self.component, case .cover = component.style { if let component = self.component, case .cover = component.style {
cursorWidth = 30.0 + 12.0 cursorWidth = 30.0 + 12.0
cursorMargin = 0.0 cursorMargin = handleWidth
height = 50.0 height = 50.0
isCover = true isCover = true
y += 1.0 y += 1.0
@ -472,6 +484,23 @@ public final class MediaScrubberComponent: Component {
} else { } else {
trackTransition = .immediate trackTransition = .immediate
trackView = TrackView() 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 trackView.onSelection = { [weak self] id in
guard let self else { guard let self else {
return return
@ -659,6 +688,7 @@ public final class MediaScrubberComponent: Component {
self.cursorPositionAnimation = nil self.cursorPositionAnimation = nil
self.cursorDisplayLink?.isPaused = true self.cursorDisplayLink?.isPaused = true
if !self.ignoreCursorPositionUpdate {
var cursorPosition = component.position var cursorPosition = component.position
if let offset = self.mainAudioTrackOffset { if let offset = self.mainAudioTrackOffset {
cursorPosition -= offset cursorPosition -= offset
@ -666,6 +696,7 @@ public final class MediaScrubberComponent: Component {
let cursorFrame = cursorFrame(size: scrubberSize, height: self.effectiveCursorHeight, position: cursorPosition, duration: trimDuration) let cursorFrame = cursorFrame(size: scrubberSize, height: self.effectiveCursorHeight, position: cursorPosition, duration: trimDuration)
transition.setFrame(view: self.cursorView, frame: cursorFrame) 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)) transition.setFrame(view: self.cursorContentView, frame: cursorFrame.insetBy(dx: 6.0, dy: 2.0).offsetBy(dx: -1.0 - UIScreenPixel, dy: 0.0))
}
} else { } else {
if let (_, _, end, ended) = self.cursorPositionAnimation { if let (_, _, end, ended) = self.cursorPositionAnimation {
if ended, component.position >= self.startPosition && component.position < end - 1.0 { 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] = [] fileprivate var videoOpaqueFrameLayers: [VideoFrameLayer] = []
var onSelection: (Int32) -> Void = { _ in } var onSelection: (Int32) -> Void = { _ in }
var onTap: (CGFloat) -> Void = { _ in }
var offsetUpdated: (Double, Bool) -> Void = { _, _ in } var offsetUpdated: (Double, Bool) -> Void = { _, _ in }
var updated: (ComponentTransition) -> Void = { _ in } var updated: (ComponentTransition) -> Void = { _ in }
@ -794,10 +826,15 @@ private class TrackView: UIView, UIScrollViewDelegate, UIGestureRecognizerDelega
} }
@objc private func handleTap(_ gestureRecognizer: UITapGestureRecognizer) { @objc private func handleTap(_ gestureRecognizer: UITapGestureRecognizer) {
guard let (track, _, _, _) = self.params else { guard let params = self.params else {
return 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) { private func updateTrackOffset(done: Bool) {
@ -841,6 +878,7 @@ private class TrackView: UIView, UIScrollViewDelegate, UIGestureRecognizerDelega
} }
private var params: ( private var params: (
style: MediaScrubberComponent.Style,
track: MediaScrubberComponent.Track, track: MediaScrubberComponent.Track,
isSelected: Bool, isSelected: Bool,
availableSize: CGSize, availableSize: CGSize,
@ -889,7 +927,7 @@ private class TrackView: UIView, UIScrollViewDelegate, UIGestureRecognizerDelega
transition: ComponentTransition transition: ComponentTransition
) -> CGSize { ) -> CGSize {
let previousParams = self.params let previousParams = self.params
self.params = (track, isSelected, availableSize, duration) self.params = (style, track, isSelected, availableSize, duration)
let fullTrackHeight: CGFloat let fullTrackHeight: CGFloat
let framesCornerRadius: CGFloat let framesCornerRadius: CGFloat

View File

@ -2014,7 +2014,7 @@ final class ShareWithPeersScreenComponent: Component {
hasCategories = true hasCategories = true
} }
let sendAsPeersCount = component.stateContext.stateValue?.sendAsPeers.count ?? 1 let sendAsPeersCount = component.stateContext.stateValue?.sendAsPeers.count ?? 1
if sendAsPeersCount > 1 && !"".isEmpty { if sendAsPeersCount > 1 {
hasChannels = true hasChannels = true
} }
if let currentHasChannels = self.currentHasChannels, currentHasChannels != hasChannels { if let currentHasChannels = self.currentHasChannels, currentHasChannels != hasChannels {
@ -2618,7 +2618,11 @@ final class ShareWithPeersScreenComponent: Component {
inset += 10.0 + environment.safeInsets.bottom + 50.0 + footersTotalHeight inset += 10.0 + environment.safeInsets.bottom + 50.0 + footersTotalHeight
} else { } else {
if !hasCategories { if !hasCategories {
if self.selectedOptions.contains(.pin) {
inset = 422.0
} else {
inset = 314.0 inset = 314.0
}
inset += 10.0 + environment.safeInsets.bottom + 50.0 + footersTotalHeight inset += 10.0 + environment.safeInsets.bottom + 50.0 + footersTotalHeight
} else { } else {
if hasChannels { if hasChannels {

View File

@ -81,7 +81,11 @@ struct CameraState: Equatable {
} }
func updatedRecording(_ recording: Recording) -> CameraState { 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 { func updatedDuration(_ duration: Double) -> CameraState {
@ -408,11 +412,11 @@ private final class VideoMessageCameraScreenComponent: CombinedComponent {
let isFirstRecording = initialDuration.isZero let isFirstRecording = initialDuration.isZero
controller.node.resumeCameraCapture() controller.node.resumeCameraCapture()
controller.updatePreviewState({ _ in return nil}, transition: .spring(duration: 0.4))
controller.node.dismissAllTooltips() controller.node.dismissAllTooltips()
controller.updateCameraState({ $0.updatedRecording(pressing ? .holding : .handsFree).updatedDuration(initialDuration) }, transition: .spring(duration: 0.4)) 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) { controller.node.withReadyCamera(isFirstTime: !controller.node.cameraIsActive) {
Queue.mainQueue().after(0.15) { Queue.mainQueue().after(0.15) {
self.resultDisposable.set((camera.startRecording() self.resultDisposable.set((camera.startRecording()
@ -438,6 +442,10 @@ private final class VideoMessageCameraScreenComponent: CombinedComponent {
if initialDuration > 0.0 { if initialDuration > 0.0 {
controller.onResume() controller.onResume()
} }
if controller.cameraState.position == .front && controller.cameraState.flashMode == .on {
self.updateScreenBrightness()
}
} }
func stopVideoRecording() { func stopVideoRecording() {
@ -645,9 +653,11 @@ private final class VideoMessageCameraScreenComponent: CombinedComponent {
action: { [weak state] in action: { [weak state] in
if let state { if let state {
state.toggleFlashMode() state.toggleFlashMode()
Queue.mainQueue().justDispatch {
flashAction.invoke(Void()) flashAction.invoke(Void())
} }
} }
}
), ),
availableSize: availableSize, availableSize: availableSize,
transition: context.transition transition: context.transition

View File

@ -1,7 +1,7 @@
{ {
"images" : [ "images" : [
{ {
"filename" : "ic_lt_safari.pdf", "filename" : "Bookmark.pdf",
"idiom" : "universal" "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" : [ "images" : [
{ {
"filename" : "cross.pdf", "filename" : "Close.pdf",
"idiom" : "universal" "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 accessChallengeData.data.isLockable {
if passcodeSettings.autolockTimeout != nil && settings.defaultWebBrowser == nil { if passcodeSettings.autolockTimeout != nil && settings.defaultWebBrowser == nil {
settings = WebBrowserSettings(defaultWebBrowser: "safari") settings = WebBrowserSettings(defaultWebBrowser: "safari", autologin: false, exceptions: [])
} }
} }
return settings return settings

View File

@ -3,35 +3,84 @@ import Postbox
import TelegramCore import TelegramCore
import SwiftSignalKit import SwiftSignalKit
public struct WebBrowserSettings: Codable, Equatable { public struct WebBrowserException: Codable, Equatable {
public let defaultWebBrowser: String? public let domain: String
public let title: String
public static var defaultSettings: WebBrowserSettings { public init(domain: String, title: String) {
return WebBrowserSettings(defaultWebBrowser: nil) 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.defaultWebBrowser = defaultWebBrowser
self.autologin = autologin
self.exceptions = exceptions
} }
public init(from decoder: Decoder) throws { public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: StringCodingKey.self) let container = try decoder.container(keyedBy: StringCodingKey.self)
self.defaultWebBrowser = try? container.decodeIfPresent(String.self, forKey: "defaultWebBrowser") 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 { public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: StringCodingKey.self) var container = encoder.container(keyedBy: StringCodingKey.self)
try container.encodeIfPresent(self.defaultWebBrowser, forKey: "defaultWebBrowser") 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 { 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 { 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 body = MarkdownAttributeSet(font: Font.regular(14.0), textColor: .white)
let bold: MarkdownAttributeSet let bold: MarkdownAttributeSet
var link = body
if savedMessages { 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": ""]) 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 { } else {
bold = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: .white) 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.attributedText = attributedText
self.textNode.maximumNumberOfLines = 2 self.textNode.maximumNumberOfLines = 2