mirror of
https://github.com/TelegramMessenger/Telegram-iOS.git
synced 2026-07-06 03:33:41 +02:00
Merge commit '4e46697a66' into beta
This commit is contained in:
commit
69f1e7536b
55 changed files with 2559 additions and 615 deletions
|
|
@ -12537,3 +12537,48 @@ Sorry for the inconvenience.";
|
|||
"Conversation.StatusBotSubscribers_any" = "%d users";
|
||||
|
||||
"Story.Editor.Add" = "Add";
|
||||
|
||||
"WebBrowser.LinkForwardTooltip.Chat.One" = "Link forwarded to **%@**";
|
||||
"WebBrowser.LinkForwardTooltip.TwoChats.One" = "Link forwarded to **%@** and **%@**";
|
||||
"WebBrowser.LinkForwardTooltip.ManyChats.One" = "Link forwarded to **%@** and %@ others";
|
||||
"WebBrowser.LinkForwardTooltip.SavedMessages.One" = "Link forwarded to **Saved Messages**";
|
||||
|
||||
"Stars.Intro.StarsSent_1" = "%@ Star sent.";
|
||||
"Stars.Intro.StarsSent_any" = "%@ Stars sent.";
|
||||
"Stars.Intro.StarsSent.ViewChat" = "View Chat";
|
||||
|
||||
"Stars.Gift.Received.Title" = "Received Gift";
|
||||
"Stars.Gift.Received.Text" = "Use Stars to unlock content and services on Telegram. [See Examples >]()";
|
||||
|
||||
"Stars.Gift.Sent.Title" = "Sent Gift";
|
||||
"Stars.Gift.Sent.Text" = "With Stars, %@ will be able to unlock content and services on Telegram. [See Examples >]()";
|
||||
|
||||
"WebBrowser.Reload" = "Reload";
|
||||
"WebBrowser.Share" = "Share";
|
||||
"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";
|
||||
|
||||
"WebBrowser.Done" = "Done";
|
||||
|
|
|
|||
|
|
@ -614,102 +614,6 @@ public enum ContactListActionItemIcon : Equatable {
|
|||
}
|
||||
}
|
||||
|
||||
public struct ContactListAdditionalOption: Equatable {
|
||||
public let title: String
|
||||
public let icon: ContactListActionItemIcon
|
||||
public let action: () -> Void
|
||||
public let clearHighlightAutomatically: Bool
|
||||
|
||||
public init(title: String, icon: ContactListActionItemIcon, action: @escaping () -> Void, clearHighlightAutomatically: Bool = false) {
|
||||
self.title = title
|
||||
self.icon = icon
|
||||
self.action = action
|
||||
self.clearHighlightAutomatically = clearHighlightAutomatically
|
||||
}
|
||||
|
||||
public static func ==(lhs: ContactListAdditionalOption, rhs: ContactListAdditionalOption) -> Bool {
|
||||
return lhs.title == rhs.title && lhs.icon == rhs.icon
|
||||
}
|
||||
}
|
||||
|
||||
public enum ContactListPeerId: Hashable {
|
||||
case peer(PeerId)
|
||||
case deviceContact(DeviceContactStableId)
|
||||
}
|
||||
|
||||
public enum ContactListAction: Equatable {
|
||||
case generic
|
||||
case voiceCall
|
||||
case videoCall
|
||||
case more
|
||||
}
|
||||
|
||||
public enum ContactListPeer: Equatable {
|
||||
case peer(peer: Peer, isGlobal: Bool, participantCount: Int32?)
|
||||
case deviceContact(DeviceContactStableId, DeviceContactBasicData)
|
||||
|
||||
public var id: ContactListPeerId {
|
||||
switch self {
|
||||
case let .peer(peer, _, _):
|
||||
return .peer(peer.id)
|
||||
case let .deviceContact(id, _):
|
||||
return .deviceContact(id)
|
||||
}
|
||||
}
|
||||
|
||||
public var indexName: PeerIndexNameRepresentation {
|
||||
switch self {
|
||||
case let .peer(peer, _, _):
|
||||
return peer.indexName
|
||||
case let .deviceContact(_, contact):
|
||||
return .personName(first: contact.firstName, last: contact.lastName, addressNames: [], phoneNumber: "")
|
||||
}
|
||||
}
|
||||
|
||||
public static func ==(lhs: ContactListPeer, rhs: ContactListPeer) -> Bool {
|
||||
switch lhs {
|
||||
case let .peer(lhsPeer, lhsIsGlobal, lhsParticipantCount):
|
||||
if case let .peer(rhsPeer, rhsIsGlobal, rhsParticipantCount) = rhs, lhsPeer.isEqual(rhsPeer), lhsIsGlobal == rhsIsGlobal, lhsParticipantCount == rhsParticipantCount {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .deviceContact(id, contact):
|
||||
if case .deviceContact(id, contact) = rhs {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public final class ContactSelectionControllerParams {
|
||||
public let context: AccountContext
|
||||
public let updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?
|
||||
public let autoDismiss: Bool
|
||||
public let title: (PresentationStrings) -> String
|
||||
public let options: [ContactListAdditionalOption]
|
||||
public let displayDeviceContacts: Bool
|
||||
public let displayCallIcons: Bool
|
||||
public let multipleSelection: Bool
|
||||
public let requirePhoneNumbers: Bool
|
||||
public let confirmation: (ContactListPeer) -> Signal<Bool, NoError>
|
||||
|
||||
public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, autoDismiss: Bool = true, title: @escaping (PresentationStrings) -> String, options: [ContactListAdditionalOption] = [], displayDeviceContacts: Bool = false, displayCallIcons: Bool = false, multipleSelection: Bool = false, requirePhoneNumbers: Bool = false, confirmation: @escaping (ContactListPeer) -> Signal<Bool, NoError> = { _ in .single(true) }) {
|
||||
self.context = context
|
||||
self.updatedPresentationData = updatedPresentationData
|
||||
self.autoDismiss = autoDismiss
|
||||
self.title = title
|
||||
self.options = options
|
||||
self.displayDeviceContacts = displayDeviceContacts
|
||||
self.displayCallIcons = displayCallIcons
|
||||
self.multipleSelection = multipleSelection
|
||||
self.requirePhoneNumbers = requirePhoneNumbers
|
||||
self.confirmation = confirmation
|
||||
}
|
||||
}
|
||||
|
||||
public enum ChatListSearchFilter: Equatable {
|
||||
case chats
|
||||
case topics
|
||||
|
|
|
|||
|
|
@ -85,6 +85,7 @@ public enum ContactMultiselectionControllerMode {
|
|||
public enum ContactListFilter {
|
||||
case excludeWithoutPhoneNumbers
|
||||
case excludeSelf
|
||||
case excludeBots
|
||||
case exclude([EnginePeer.Id])
|
||||
case disable([EnginePeer.Id])
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
import Foundation
|
||||
import Display
|
||||
import SwiftSignalKit
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import TelegramPresentationData
|
||||
|
||||
public protocol ContactSelectionController: ViewController {
|
||||
var result: Signal<([ContactListPeer], ContactListAction, Bool, Int32?, NSAttributedString?, ChatSendMessageActionSheetController.SendParameters?)?, NoError> { get }
|
||||
|
|
@ -10,3 +13,106 @@ public protocol ContactSelectionController: ViewController {
|
|||
|
||||
func dismissSearch()
|
||||
}
|
||||
|
||||
public enum ContactSelectionControllerMode {
|
||||
case generic
|
||||
case starsGifting(birthdays: [EnginePeer.Id: TelegramBirthday]?, hasActions: Bool)
|
||||
}
|
||||
|
||||
public struct ContactListAdditionalOption: Equatable {
|
||||
public let title: String
|
||||
public let icon: ContactListActionItemIcon
|
||||
public let action: () -> Void
|
||||
public let clearHighlightAutomatically: Bool
|
||||
|
||||
public init(title: String, icon: ContactListActionItemIcon, action: @escaping () -> Void, clearHighlightAutomatically: Bool = false) {
|
||||
self.title = title
|
||||
self.icon = icon
|
||||
self.action = action
|
||||
self.clearHighlightAutomatically = clearHighlightAutomatically
|
||||
}
|
||||
|
||||
public static func ==(lhs: ContactListAdditionalOption, rhs: ContactListAdditionalOption) -> Bool {
|
||||
return lhs.title == rhs.title && lhs.icon == rhs.icon
|
||||
}
|
||||
}
|
||||
|
||||
public enum ContactListPeerId: Hashable {
|
||||
case peer(PeerId)
|
||||
case deviceContact(DeviceContactStableId)
|
||||
}
|
||||
|
||||
public enum ContactListAction: Equatable {
|
||||
case generic
|
||||
case voiceCall
|
||||
case videoCall
|
||||
case more
|
||||
}
|
||||
|
||||
public enum ContactListPeer: Equatable {
|
||||
case peer(peer: Peer, isGlobal: Bool, participantCount: Int32?)
|
||||
case deviceContact(DeviceContactStableId, DeviceContactBasicData)
|
||||
|
||||
public var id: ContactListPeerId {
|
||||
switch self {
|
||||
case let .peer(peer, _, _):
|
||||
return .peer(peer.id)
|
||||
case let .deviceContact(id, _):
|
||||
return .deviceContact(id)
|
||||
}
|
||||
}
|
||||
|
||||
public var indexName: PeerIndexNameRepresentation {
|
||||
switch self {
|
||||
case let .peer(peer, _, _):
|
||||
return peer.indexName
|
||||
case let .deviceContact(_, contact):
|
||||
return .personName(first: contact.firstName, last: contact.lastName, addressNames: [], phoneNumber: "")
|
||||
}
|
||||
}
|
||||
|
||||
public static func ==(lhs: ContactListPeer, rhs: ContactListPeer) -> Bool {
|
||||
switch lhs {
|
||||
case let .peer(lhsPeer, lhsIsGlobal, lhsParticipantCount):
|
||||
if case let .peer(rhsPeer, rhsIsGlobal, rhsParticipantCount) = rhs, lhsPeer.isEqual(rhsPeer), lhsIsGlobal == rhsIsGlobal, lhsParticipantCount == rhsParticipantCount {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .deviceContact(id, contact):
|
||||
if case .deviceContact(id, contact) = rhs {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public final class ContactSelectionControllerParams {
|
||||
public let context: AccountContext
|
||||
public let updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?
|
||||
public let mode: ContactSelectionControllerMode
|
||||
public let autoDismiss: Bool
|
||||
public let title: (PresentationStrings) -> String
|
||||
public let options: Signal<[ContactListAdditionalOption], NoError>
|
||||
public let displayDeviceContacts: Bool
|
||||
public let displayCallIcons: Bool
|
||||
public let multipleSelection: Bool
|
||||
public let requirePhoneNumbers: Bool
|
||||
public let confirmation: (ContactListPeer) -> Signal<Bool, NoError>
|
||||
|
||||
public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, mode: ContactSelectionControllerMode = .generic, autoDismiss: Bool = true, title: @escaping (PresentationStrings) -> String, options: Signal<[ContactListAdditionalOption], NoError> = .single([]), displayDeviceContacts: Bool = false, displayCallIcons: Bool = false, multipleSelection: Bool = false, requirePhoneNumbers: Bool = false, confirmation: @escaping (ContactListPeer) -> Signal<Bool, NoError> = { _ in .single(true) }) {
|
||||
self.context = context
|
||||
self.updatedPresentationData = updatedPresentationData
|
||||
self.mode = mode
|
||||
self.autoDismiss = autoDismiss
|
||||
self.title = title
|
||||
self.options = options
|
||||
self.displayDeviceContacts = displayDeviceContacts
|
||||
self.displayCallIcons = displayCallIcons
|
||||
self.multipleSelection = multipleSelection
|
||||
self.requirePhoneNumbers = requirePhoneNumbers
|
||||
self.confirmation = confirmation
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1183,6 +1183,8 @@ public class AttachmentController: ViewController, MinimizableController {
|
|||
self.blocksBackgroundWhenInOverlay = true
|
||||
self.acceptsFocusWhenInOverlay = true
|
||||
|
||||
self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.context.sharedContext.currentPresentationData.with { $0 }.strings.Common_Back, style: .plain, target: nil, action: nil)
|
||||
|
||||
self.scrollToTop = { [weak self] in
|
||||
if let strongSelf = self {
|
||||
strongSelf.node.scrollToTop()
|
||||
|
|
|
|||
362
submodules/BrowserUI/Sources/BrowserAddressBarComponent.swift
Normal file
362
submodules/BrowserUI/Sources/BrowserAddressBarComponent.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
|
|
@ -109,7 +109,7 @@ private final class BrowserScreenComponent: CombinedComponent {
|
|||
component: AnyComponent(
|
||||
Button(
|
||||
content: AnyComponent(
|
||||
MultilineTextComponent(text: .plain(NSAttributedString(string: environment.strings.Common_Close, font: Font.regular(17.0), textColor: environment.theme.rootController.navigationBar.primaryTextColor, paragraphAlignment: .center)), horizontalAlignment: .left, maximumNumberOfLines: 1)
|
||||
MultilineTextComponent(text: .plain(NSAttributedString(string: environment.strings.WebBrowser_Done, font: Font.regular(17.0), textColor: environment.theme.rootController.navigationBar.accentTextColor, paragraphAlignment: .center)), horizontalAlignment: .left, maximumNumberOfLines: 1)
|
||||
),
|
||||
action: {
|
||||
performAction.invoke(.close)
|
||||
|
|
@ -119,7 +119,6 @@ private final class BrowserScreenComponent: CombinedComponent {
|
|||
)
|
||||
]
|
||||
|
||||
let isLoading = (context.component.contentState?.estimatedProgress ?? 1.0) < 1.0
|
||||
navigationRightItems = [
|
||||
AnyComponentWithIdentity(
|
||||
id: "settings",
|
||||
|
|
@ -130,7 +129,7 @@ private final class BrowserScreenComponent: CombinedComponent {
|
|||
content: LottieComponent.AppBundleContent(
|
||||
name: "anim_moredots"
|
||||
),
|
||||
color: environment.theme.rootController.navigationBar.primaryTextColor,
|
||||
color: environment.theme.rootController.navigationBar.accentTextColor,
|
||||
size: CGSize(width: 30.0, height: 30.0)
|
||||
)
|
||||
),
|
||||
|
|
@ -142,27 +141,6 @@ private final class BrowserScreenComponent: CombinedComponent {
|
|||
)
|
||||
)
|
||||
]
|
||||
if case .webPage = context.component.contentState?.contentType {
|
||||
navigationRightItems.insert(
|
||||
AnyComponentWithIdentity(
|
||||
id: isLoading ? "stop" : "reload",
|
||||
component: AnyComponent(
|
||||
ReferenceButtonComponent(
|
||||
content: AnyComponent(
|
||||
BundleIconComponent(
|
||||
name: isLoading ? "Instant View/CloseIcon" : "Chat/Context Menu/Reload",
|
||||
tintColor: environment.theme.rootController.navigationBar.primaryTextColor
|
||||
)
|
||||
),
|
||||
action: {
|
||||
performAction.invoke(isLoading ? .stop : .reload)
|
||||
}
|
||||
)
|
||||
)
|
||||
),
|
||||
at: 0
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
let collapseFraction = context.component.presentationState.isSearching ? 0.0 : context.component.panelCollapseFraction
|
||||
|
|
@ -211,6 +189,7 @@ private final class BrowserScreenComponent: CombinedComponent {
|
|||
id: "navigation",
|
||||
component: AnyComponent(
|
||||
NavigationToolbarContentComponent(
|
||||
accentColor: environment.theme.rootController.navigationBar.accentTextColor,
|
||||
textColor: environment.theme.rootController.navigationBar.primaryTextColor,
|
||||
canGoBack: context.component.contentState?.canGoBack ?? false,
|
||||
canGoForward: context.component.contentState?.canGoForward ?? false,
|
||||
|
|
@ -281,6 +260,8 @@ public class BrowserScreen: ViewController, MinimizableController {
|
|||
case increaseFontSize
|
||||
case resetFontSize
|
||||
case updateFontIsSerif(Bool)
|
||||
case addBookmark
|
||||
case openBookmarks
|
||||
}
|
||||
|
||||
fileprivate final class Node: ViewControllerTracingNode {
|
||||
|
|
@ -340,6 +321,62 @@ public class BrowserScreen: ViewController, MinimizableController {
|
|||
case .share:
|
||||
let presentationData = self.presentationData
|
||||
let shareController = ShareController(context: self.context, subject: .url(url))
|
||||
shareController.completed = { [weak self] peerIds in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
let _ = (strongSelf.context.engine.data.get(
|
||||
EngineDataList(
|
||||
peerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init)
|
||||
)
|
||||
)
|
||||
|> deliverOnMainQueue).startStandalone(next: { [weak self] peerList in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
|
||||
let peers = peerList.compactMap { $0 }
|
||||
let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }
|
||||
|
||||
let text: String
|
||||
var savedMessages = false
|
||||
if peerIds.count == 1, let peerId = peerIds.first, peerId == strongSelf.context.account.peerId {
|
||||
text = presentationData.strings.WebBrowser_LinkForwardTooltip_SavedMessages_One
|
||||
savedMessages = true
|
||||
} else {
|
||||
if peers.count == 1, let peer = peers.first {
|
||||
let peerName = peer.id == strongSelf.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)
|
||||
text = presentationData.strings.WebBrowser_LinkForwardTooltip_Chat_One(peerName).string
|
||||
} else if peers.count == 2, let firstPeer = peers.first, let secondPeer = peers.last {
|
||||
let firstPeerName = firstPeer.id == strongSelf.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : firstPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)
|
||||
let secondPeerName = secondPeer.id == strongSelf.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : secondPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)
|
||||
text = presentationData.strings.WebBrowser_LinkForwardTooltip_TwoChats_One(firstPeerName, secondPeerName).string
|
||||
} else if let peer = peers.first {
|
||||
let peerName = peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)
|
||||
text = presentationData.strings.WebBrowser_LinkForwardTooltip_ManyChats_One(peerName, "\(peers.count - 1)").string
|
||||
} else {
|
||||
text = ""
|
||||
}
|
||||
}
|
||||
|
||||
strongSelf.controller?.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: savedMessages, text: text), elevatedLayout: false, animateInAsReplacement: true, action: { [weak self] action in
|
||||
if savedMessages, 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)
|
||||
})
|
||||
}
|
||||
shareController.actionCompleted = { [weak self] in
|
||||
self?.controller?.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root))
|
||||
}
|
||||
|
|
@ -446,6 +483,12 @@ public class BrowserScreen: ViewController, MinimizableController {
|
|||
return updatedState
|
||||
})
|
||||
content.updateFontState(self.presentationState.fontState)
|
||||
case .addBookmark:
|
||||
if let content = self.content.last {
|
||||
self.addBookmark(content.currentState.url)
|
||||
}
|
||||
case .openBookmarks:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -553,6 +596,43 @@ public class BrowserScreen: ViewController, MinimizableController {
|
|||
self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(peer), animated: true))
|
||||
}
|
||||
|
||||
func addBookmark(_ url: String) {
|
||||
let _ = enqueueMessages(
|
||||
account: self.context.account,
|
||||
peerId: self.context.account.peerId,
|
||||
messages: [.message(
|
||||
text: url,
|
||||
attributes: [],
|
||||
inlineStickers: [:],
|
||||
mediaReference: nil,
|
||||
threadId: nil,
|
||||
replyToMessageId: nil,
|
||||
replyToStoryId: nil,
|
||||
localGroupingKey: nil,
|
||||
correlationId: nil,
|
||||
bubbleUpEmojiOrStickersets: []
|
||||
)]
|
||||
).start()
|
||||
|
||||
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
|
||||
self.controller?.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: true, text: presentationData.strings.WebBrowser_LinkAddedToBookmarks), elevatedLayout: false, animateInAsReplacement: true, action: { [weak self] action in
|
||||
if let self, action == .info {
|
||||
let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId))
|
||||
|> deliverOnMainQueue).start(next: { [weak self] peer in
|
||||
guard let self, let peer else {
|
||||
return
|
||||
}
|
||||
guard let navigationController = self.controller?.navigationController as? NavigationController else {
|
||||
return
|
||||
}
|
||||
self.minimize()
|
||||
self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(peer), forceOpenChat: true))
|
||||
})
|
||||
}
|
||||
return false
|
||||
}), in: .current)
|
||||
}
|
||||
|
||||
private func setupContentStateUpdates() {
|
||||
for content in self.content {
|
||||
content.onScrollingUpdate = { _ in }
|
||||
|
|
@ -712,16 +792,29 @@ public class BrowserScreen: ViewController, MinimizableController {
|
|||
.action(ContextMenuActionItem(text: self.presentationData.strings.InstantPage_FontSanFrancisco, icon: forceIsSerif ? emptyIcon : checkIcon, action: { (controller, action) in
|
||||
performAction.invoke(.updateFontIsSerif(false))
|
||||
action(.default)
|
||||
})), .action(ContextMenuActionItem(text: self.presentationData.strings.InstantPage_FontNewYork, textFont: .custom(font: Font.with(size: 17.0, design: .serif, traits: []), height: nil, verticalOffset: nil), icon: forceIsSerif ? checkIcon : emptyIcon, action: { (controller, action) in
|
||||
})),
|
||||
.action(ContextMenuActionItem(text: self.presentationData.strings.InstantPage_FontNewYork, textFont: .custom(font: Font.with(size: 17.0, design: .serif, traits: []), height: nil, verticalOffset: nil), icon: forceIsSerif ? checkIcon : emptyIcon, action: { (controller, action) in
|
||||
performAction.invoke(.updateFontIsSerif(true))
|
||||
action(.default)
|
||||
})),
|
||||
.separator,
|
||||
.action(ContextMenuActionItem(text: self.presentationData.strings.WebBrowser_Reload, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Instant View/Settings/Reload"), color: theme.contextMenu.primaryColor) }, action: { (controller, action) in
|
||||
performAction.invoke(.reload)
|
||||
action(.default)
|
||||
})),
|
||||
.action(ContextMenuActionItem(text: self.presentationData.strings.InstantPage_Search, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Instant View/Settings/Search"), color: theme.contextMenu.primaryColor) }, action: { (controller, action) in
|
||||
performAction.invoke(.updateSearchActive(true))
|
||||
action(.default)
|
||||
})),
|
||||
.action(ContextMenuActionItem(text: self.presentationData.strings.InstantPage_OpenInBrowser(openInTitle).string, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Instant View/Settings/Browser"), color: theme.contextMenu.primaryColor) }, action: { [weak self] (controller, action) in
|
||||
.action(ContextMenuActionItem(text: self.presentationData.strings.WebBrowser_Share, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Share"), color: theme.contextMenu.primaryColor) }, action: { (controller, action) in
|
||||
performAction.invoke(.share)
|
||||
action(.default)
|
||||
})),
|
||||
.action(ContextMenuActionItem(text: self.presentationData.strings.WebBrowser_AddBookmark, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Fave"), color: theme.contextMenu.primaryColor) }, action: { (controller, action) in
|
||||
performAction.invoke(.addBookmark)
|
||||
action(.default)
|
||||
})),
|
||||
.action(ContextMenuActionItem(text: self.presentationData.strings.InstantPage_OpenInBrowser(openInTitle).string, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Browser"), color: theme.contextMenu.primaryColor) }, action: { [weak self] (controller, action) in
|
||||
if let self {
|
||||
self.context.sharedContext.applicationBindings.openUrl(openInUrl)
|
||||
}
|
||||
|
|
@ -1018,7 +1111,7 @@ public class BrowserScreen: ViewController, MinimizableController {
|
|||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
public var minimizedProgress: Float? {
|
||||
if let contentState = self.node.contentState {
|
||||
return Float(contentState.readingProgress)
|
||||
|
|
@ -1091,7 +1184,7 @@ private final class BrowserContentComponent: Component {
|
|||
|
||||
let collapsedHeight: CGFloat = 24.0
|
||||
let topInset: CGFloat = component.insets.top + component.navigationBarHeight * (1.0 - component.scrollingPanelOffsetFraction) + collapsedHeight * component.scrollingPanelOffsetFraction
|
||||
let bottomInset = 49.0 + component.insets.bottom
|
||||
let bottomInset = (49.0 + component.insets.bottom) * (1.0 - component.scrollingPanelOffsetFraction)
|
||||
component.content.updateLayout(size: availableSize, insets: UIEdgeInsets(top: topInset, left: component.insets.left, bottom: bottomInset, right: component.insets.right), transition: transition)
|
||||
transition.setFrame(view: component.content, frame: CGRect(origin: .zero, size: availableSize))
|
||||
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ final class SearchBarContentComponent: Component {
|
|||
}
|
||||
|
||||
final class View: UIView, UITextFieldDelegate {
|
||||
private final class EmojiSearchTextField: UITextField {
|
||||
private final class SearchTextField: UITextField {
|
||||
override func textRect(forBounds bounds: CGRect) -> CGRect {
|
||||
return bounds.integral
|
||||
}
|
||||
|
|
@ -75,7 +75,7 @@ final class SearchBarContentComponent: Component {
|
|||
private var placeholderContent = ComponentView<Empty>()
|
||||
|
||||
private var textFrame: CGRect?
|
||||
private var textField: EmojiSearchTextField?
|
||||
private var textField: SearchTextField?
|
||||
|
||||
private var tapRecognizer: UITapGestureRecognizer?
|
||||
|
||||
|
|
@ -160,7 +160,7 @@ final class SearchBarContentComponent: Component {
|
|||
let backgroundFrame = self.backgroundLayer.frame
|
||||
let textFieldFrame = CGRect(origin: CGPoint(x: textFrame.minX, y: backgroundFrame.minY), size: CGSize(width: backgroundFrame.maxX - textFrame.minX, height: backgroundFrame.height))
|
||||
|
||||
let textField = EmojiSearchTextField(frame: textFieldFrame)
|
||||
let textField = SearchTextField(frame: textFieldFrame)
|
||||
textField.autocorrectionType = .no
|
||||
textField.returnKeyType = .search
|
||||
self.textField = textField
|
||||
|
|
@ -285,7 +285,7 @@ final class SearchBarContentComponent: Component {
|
|||
component: AnyComponent(Text(
|
||||
text: strings.Common_Cancel,
|
||||
font: Font.regular(17.0),
|
||||
color: theme.rootController.navigationBar.primaryTextColor
|
||||
color: theme.rootController.navigationBar.accentTextColor
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: size.width - 32.0, height: 100.0)
|
||||
|
|
|
|||
|
|
@ -120,6 +120,7 @@ final class BrowserToolbarComponent: CombinedComponent {
|
|||
}
|
||||
|
||||
final class NavigationToolbarContentComponent: CombinedComponent {
|
||||
let accentColor: UIColor
|
||||
let textColor: UIColor
|
||||
let canGoBack: Bool
|
||||
let canGoForward: Bool
|
||||
|
|
@ -127,12 +128,14 @@ final class NavigationToolbarContentComponent: CombinedComponent {
|
|||
let performHoldAction: (UIView, ContextGesture?, BrowserScreen.Action) -> Void
|
||||
|
||||
init(
|
||||
accentColor: UIColor,
|
||||
textColor: UIColor,
|
||||
canGoBack: Bool,
|
||||
canGoForward: Bool,
|
||||
performAction: ActionSlot<BrowserScreen.Action>,
|
||||
performHoldAction: @escaping (UIView, ContextGesture?, BrowserScreen.Action) -> Void
|
||||
) {
|
||||
self.accentColor = accentColor
|
||||
self.textColor = textColor
|
||||
self.canGoBack = canGoBack
|
||||
self.canGoForward = canGoForward
|
||||
|
|
@ -141,6 +144,9 @@ final class NavigationToolbarContentComponent: CombinedComponent {
|
|||
}
|
||||
|
||||
static func ==(lhs: NavigationToolbarContentComponent, rhs: NavigationToolbarContentComponent) -> Bool {
|
||||
if lhs.accentColor != rhs.accentColor {
|
||||
return false
|
||||
}
|
||||
if lhs.textColor != rhs.textColor {
|
||||
return false
|
||||
}
|
||||
|
|
@ -157,6 +163,7 @@ final class NavigationToolbarContentComponent: CombinedComponent {
|
|||
let back = Child(ContextReferenceButtonComponent.self)
|
||||
let forward = Child(ContextReferenceButtonComponent.self)
|
||||
let share = Child(Button.self)
|
||||
let bookmark = Child(Button.self)
|
||||
let openIn = Child(Button.self)
|
||||
|
||||
return { context in
|
||||
|
|
@ -166,7 +173,7 @@ final class NavigationToolbarContentComponent: CombinedComponent {
|
|||
|
||||
let sideInset: CGFloat = 5.0
|
||||
let buttonSize = CGSize(width: 50.0, height: availableSize.height)
|
||||
let spacing = (availableSize.width - buttonSize.width * 4.0 - sideInset * 2.0) / 3.0
|
||||
let spacing = (availableSize.width - buttonSize.width * 5.0 - sideInset * 2.0) / 4.0
|
||||
|
||||
let canGoBack = context.component.canGoBack
|
||||
let back = back.update(
|
||||
|
|
@ -174,7 +181,7 @@ final class NavigationToolbarContentComponent: CombinedComponent {
|
|||
content: AnyComponent(
|
||||
BundleIconComponent(
|
||||
name: "Instant View/Back",
|
||||
tintColor: canGoBack ? context.component.textColor : context.component.textColor.withAlphaComponent(0.4)
|
||||
tintColor: canGoBack ? context.component.accentColor : context.component.accentColor.withAlphaComponent(0.4)
|
||||
)
|
||||
),
|
||||
minSize: buttonSize,
|
||||
|
|
@ -202,7 +209,7 @@ final class NavigationToolbarContentComponent: CombinedComponent {
|
|||
content: AnyComponent(
|
||||
BundleIconComponent(
|
||||
name: "Instant View/Forward",
|
||||
tintColor: canGoForward ? context.component.textColor : context.component.textColor.withAlphaComponent(0.4)
|
||||
tintColor: canGoForward ? context.component.accentColor : context.component.accentColor.withAlphaComponent(0.4)
|
||||
)
|
||||
),
|
||||
minSize: buttonSize,
|
||||
|
|
@ -229,7 +236,7 @@ final class NavigationToolbarContentComponent: CombinedComponent {
|
|||
content: AnyComponent(
|
||||
BundleIconComponent(
|
||||
name: "Chat List/NavigationShare",
|
||||
tintColor: context.component.textColor
|
||||
tintColor: context.component.accentColor
|
||||
)
|
||||
),
|
||||
action: {
|
||||
|
|
@ -243,23 +250,42 @@ final class NavigationToolbarContentComponent: CombinedComponent {
|
|||
.position(CGPoint(x: sideInset + back.size.width + spacing + forward.size.width + spacing + share.size.width / 2.0, y: availableSize.height / 2.0))
|
||||
)
|
||||
|
||||
let bookmark = bookmark.update(
|
||||
component: Button(
|
||||
content: AnyComponent(
|
||||
BundleIconComponent(
|
||||
name: "Instant View/Bookmark",
|
||||
tintColor: context.component.accentColor
|
||||
)
|
||||
),
|
||||
action: {
|
||||
performAction.invoke(.openBookmarks)
|
||||
}
|
||||
).minSize(buttonSize),
|
||||
availableSize: buttonSize,
|
||||
transition: .easeInOut(duration: 0.2)
|
||||
)
|
||||
context.add(bookmark
|
||||
.position(CGPoint(x: sideInset + back.size.width + spacing + forward.size.width + spacing + share.size.width + spacing + bookmark.size.width / 2.0, y: availableSize.height / 2.0))
|
||||
)
|
||||
|
||||
let openIn = openIn.update(
|
||||
component: Button(
|
||||
content: AnyComponent(
|
||||
BundleIconComponent(
|
||||
name: "Instant View/Minimize",
|
||||
tintColor: context.component.textColor
|
||||
name: "Instant View/Browser",
|
||||
tintColor: context.component.accentColor
|
||||
)
|
||||
),
|
||||
action: {
|
||||
performAction.invoke(.minimize)
|
||||
performAction.invoke(.openIn)
|
||||
}
|
||||
).minSize(buttonSize),
|
||||
availableSize: buttonSize,
|
||||
transition: .easeInOut(duration: 0.2)
|
||||
)
|
||||
context.add(openIn
|
||||
.position(CGPoint(x: sideInset + back.size.width + spacing + forward.size.width + spacing + share.size.width + spacing + openIn.size.width / 2.0, y: availableSize.height / 2.0))
|
||||
.position(CGPoint(x: sideInset + back.size.width + spacing + forward.size.width + spacing + share.size.width + spacing + bookmark.size.width + spacing + openIn.size.width / 2.0, y: availableSize.height / 2.0))
|
||||
)
|
||||
|
||||
return availableSize
|
||||
|
|
|
|||
|
|
@ -152,16 +152,16 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU
|
|||
|
||||
let configuration = WKWebViewConfiguration()
|
||||
|
||||
let bundle = Bundle.main
|
||||
let bundleVersion = bundle.infoDictionary?["CFBundleShortVersionString"] ?? ""
|
||||
|
||||
// let bundle = Bundle.main
|
||||
// let bundleVersion = bundle.infoDictionary?["CFBundleShortVersionString"] ?? ""
|
||||
//
|
||||
var proxyServerHost = "magic.org"
|
||||
if let data = context.currentAppConfiguration.with({ $0 }).data, let hostValue = data["ton_proxy_address"] as? String {
|
||||
proxyServerHost = hostValue
|
||||
}
|
||||
configuration.setURLSchemeHandler(TonSchemeHandler(proxyServerHost: proxyServerHost), forURLScheme: "tonsite")
|
||||
configuration.allowsInlineMediaPlayback = true
|
||||
configuration.applicationNameForUserAgent = "Telegram-iOS/\(bundleVersion)"
|
||||
// configuration.applicationNameForUserAgent = "Telegram-iOS/\(bundleVersion)"
|
||||
if #available(iOSApplicationExtension 10.0, iOS 10.0, *) {
|
||||
configuration.mediaTypesRequiringUserActionForPlayback = []
|
||||
} else {
|
||||
|
|
@ -171,8 +171,6 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU
|
|||
self.webView = WKWebView(frame: CGRect(), configuration: configuration)
|
||||
self.webView.allowsLinkPreview = true
|
||||
|
||||
|
||||
|
||||
if #available(iOS 11.0, *) {
|
||||
self.webView.scrollView.contentInsetAdjustmentBehavior = .never
|
||||
}
|
||||
|
|
@ -194,6 +192,8 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU
|
|||
|
||||
self.webView.allowsBackForwardNavigationGestures = true
|
||||
self.webView.scrollView.delegate = self
|
||||
self.webView.scrollView.clipsToBounds = false
|
||||
// self.webView.translatesAutoresizingMaskIntoConstraints = false
|
||||
self.webView.navigationDelegate = self
|
||||
self.webView.uiDelegate = self
|
||||
self.webView.addObserver(self, forKeyPath: #keyPath(WKWebView.title), options: [], context: nil)
|
||||
|
|
@ -202,6 +202,7 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU
|
|||
self.webView.addObserver(self, forKeyPath: #keyPath(WKWebView.canGoBack), options: [], context: nil)
|
||||
self.webView.addObserver(self, forKeyPath: #keyPath(WKWebView.canGoForward), options: [], context: nil)
|
||||
if #available(iOS 15.0, *) {
|
||||
self.backgroundColor = presentationData.theme.list.plainBackgroundColor
|
||||
self.webView.underPageBackgroundColor = presentationData.theme.list.plainBackgroundColor
|
||||
}
|
||||
if #available(iOS 16.4, *) {
|
||||
|
|
@ -227,6 +228,7 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU
|
|||
func updatePresentationData(_ presentationData: PresentationData) {
|
||||
self.presentationData = presentationData
|
||||
if #available(iOS 15.0, *) {
|
||||
self.backgroundColor = presentationData.theme.list.plainBackgroundColor
|
||||
self.webView.underPageBackgroundColor = presentationData.theme.list.plainBackgroundColor
|
||||
}
|
||||
if let (size, insets) = self.validLayout {
|
||||
|
|
@ -399,17 +401,22 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU
|
|||
private var validLayout: (CGSize, UIEdgeInsets)?
|
||||
func updateLayout(size: CGSize, insets: UIEdgeInsets, transition: ComponentTransition) {
|
||||
self.validLayout = (size, insets)
|
||||
|
||||
var scrollInsets = insets
|
||||
scrollInsets.left = 0.0
|
||||
scrollInsets.right = 0.0
|
||||
scrollInsets.top = 0.0
|
||||
if self.webView.scrollView.contentInset != insets {
|
||||
self.webView.scrollView.contentInset = scrollInsets
|
||||
self.webView.scrollView.scrollIndicatorInsets = scrollInsets
|
||||
}
|
||||
|
||||
self.previousScrollingOffset = ScrollingOffsetState(value: self.webView.scrollView.contentOffset.y, isDraggingOrDecelerating: self.webView.scrollView.isDragging || self.webView.scrollView.isDecelerating)
|
||||
transition.setFrame(view: self.webView, frame: CGRect(origin: CGPoint(x: insets.left, y: insets.top), size: CGSize(width: size.width - insets.left - insets.right, height: size.height - insets.top)))
|
||||
|
||||
let webViewFrame = CGRect(origin: CGPoint(x: insets.left, y: insets.top), size: CGSize(width: size.width - insets.left - insets.right, height: size.height - insets.top - insets.bottom))
|
||||
var refresh = false
|
||||
if self.webView.frame.width > 0 && webViewFrame.width != self.webView.frame.width {
|
||||
refresh = true
|
||||
}
|
||||
transition.setFrame(view: self.webView, frame: webViewFrame)
|
||||
|
||||
if refresh {
|
||||
self.webView.reloadInputViews()
|
||||
}
|
||||
|
||||
self.webView.scrollView.scrollIndicatorInsets = UIEdgeInsets(top: 0.0, left: -insets.left, bottom: 0.0, right: -insets.right)
|
||||
self.webView.scrollView.horizontalScrollIndicatorInsets = UIEdgeInsets(top: 0.0, left: -insets.left, bottom: 0.0, right: -insets.right)
|
||||
|
||||
if let error = self.currentError {
|
||||
let errorSize = self.errorView.update(
|
||||
|
|
@ -749,7 +756,7 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU
|
|||
result.insert(Favicon(url: url.absoluteString, dimensions: nil))
|
||||
}
|
||||
|
||||
var largestIcon = result.first(where: { $0.url.lowercased().contains(".svg") })
|
||||
var largestIcon: Favicon? // = result.first(where: { $0.url.lowercased().contains(".svg") })
|
||||
if largestIcon == nil {
|
||||
largestIcon = result.first
|
||||
for icon in result {
|
||||
|
|
|
|||
|
|
@ -559,7 +559,11 @@ private final class CameraContext {
|
|||
guard let mainDeviceContext = self.mainDeviceContext else {
|
||||
return .complete()
|
||||
}
|
||||
mainDeviceContext.device.setTorchMode(self._flashMode)
|
||||
if self.initialConfiguration.isRoundVideo && self.positionValue == .front {
|
||||
|
||||
} else {
|
||||
mainDeviceContext.device.setTorchMode(self._flashMode)
|
||||
}
|
||||
|
||||
let orientation = self.simplePreviewView?.videoPreviewLayer.connection?.videoOrientation ?? .portrait
|
||||
if self.initialConfiguration.isRoundVideo {
|
||||
|
|
|
|||
|
|
@ -204,9 +204,9 @@ private final class VideoRecorderImpl {
|
|||
|
||||
if let videoInput = self.videoInput {
|
||||
let time = CACurrentMediaTime()
|
||||
if let previousPresentationTime = self.previousPresentationTime, let previousAppendTime = self.previousAppendTime {
|
||||
print("appending \(presentationTime.seconds) (\(presentationTime.seconds - previousPresentationTime) ) on \(time) (\(time - previousAppendTime)")
|
||||
}
|
||||
// if let previousPresentationTime = self.previousPresentationTime, let previousAppendTime = self.previousAppendTime {
|
||||
// print("appending \(presentationTime.seconds) (\(presentationTime.seconds - previousPresentationTime) ) on \(time) (\(time - previousAppendTime)")
|
||||
// }
|
||||
self.previousPresentationTime = presentationTime.seconds
|
||||
self.previousAppendTime = time
|
||||
|
||||
|
|
|
|||
|
|
@ -1499,6 +1499,8 @@ public final class ContactListNode: ASDisplayNode {
|
|||
disabledPeerIds = disabledPeerIds.union(peerIds)
|
||||
case .excludeWithoutPhoneNumbers:
|
||||
requirePhoneNumbers = true
|
||||
case .excludeBots:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1786,6 +1788,8 @@ public final class ContactListNode: ASDisplayNode {
|
|||
disabledPeerIds = disabledPeerIds.union(peerIds)
|
||||
case .excludeWithoutPhoneNumbers:
|
||||
requirePhoneNumbers = true
|
||||
case .excludeBots:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -394,6 +394,7 @@ public final class ContactsSearchContainerNode: SearchDisplayControllerContentNo
|
|||
var existingPeerIds = Set<EnginePeer.Id>()
|
||||
var disabledPeerIds = Set<EnginePeer.Id>()
|
||||
var requirePhoneNumbers = false
|
||||
var excludeBots = false
|
||||
for filter in filters {
|
||||
switch filter {
|
||||
case .excludeSelf:
|
||||
|
|
@ -404,6 +405,8 @@ public final class ContactsSearchContainerNode: SearchDisplayControllerContentNo
|
|||
disabledPeerIds = disabledPeerIds.union(peerIds)
|
||||
case .excludeWithoutPhoneNumbers:
|
||||
requirePhoneNumbers = true
|
||||
case .excludeBots:
|
||||
excludeBots = true
|
||||
}
|
||||
}
|
||||
var existingNormalizedPhoneNumbers = Set<DeviceContactNormalizedPhoneNumber>()
|
||||
|
|
@ -413,10 +416,17 @@ public final class ContactsSearchContainerNode: SearchDisplayControllerContentNo
|
|||
continue
|
||||
}
|
||||
|
||||
if case let .user(user) = peer, requirePhoneNumbers {
|
||||
let phone = user.phone ?? ""
|
||||
if phone.isEmpty {
|
||||
continue
|
||||
if case let .user(user) = peer {
|
||||
if requirePhoneNumbers {
|
||||
let phone = user.phone ?? ""
|
||||
if phone.isEmpty {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if excludeBots {
|
||||
if user.botInfo != nil {
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -442,11 +452,18 @@ public final class ContactsSearchContainerNode: SearchDisplayControllerContentNo
|
|||
continue
|
||||
}
|
||||
|
||||
if let user = peer.peer as? TelegramUser, requirePhoneNumbers {
|
||||
let phone = user.phone ?? ""
|
||||
if phone.isEmpty {
|
||||
continue
|
||||
if let user = peer.peer as? TelegramUser {
|
||||
if requirePhoneNumbers {
|
||||
let phone = user.phone ?? ""
|
||||
if phone.isEmpty {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if excludeBots {
|
||||
if user.botInfo != nil {
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !existingPeerIds.contains(peer.peer.id) {
|
||||
|
|
|
|||
|
|
@ -282,7 +282,7 @@ final class NavigationModalContainer: ASDisplayNode, ASScrollViewDelegate, ASGes
|
|||
let transition: ContainedViewLayoutTransition
|
||||
let dismissProgress: CGFloat
|
||||
if (velocity.y < -0.5 || progress >= 0.5) && self.checkInteractiveDismissWithControllers() {
|
||||
if let controller = self.container.controllers.last as? MinimizableController, self.isDraggingHeader || "".isEmpty {
|
||||
if let controller = self.container.controllers.last as? MinimizableController {
|
||||
dismissProgress = 0.0
|
||||
targetOffset = 0.0
|
||||
transition = .immediate
|
||||
|
|
|
|||
|
|
@ -3089,6 +3089,7 @@ public final class DrawingToolsInteraction {
|
|||
var isAdditional = false
|
||||
var isMessage = false
|
||||
var isLink = false
|
||||
var isWeather = false
|
||||
if let entity = entityView.entity as? DrawingStickerEntity {
|
||||
if case let .dualVideoReference(isAdditionalValue) = entity.content {
|
||||
isVideo = true
|
||||
|
|
@ -3098,6 +3099,8 @@ public final class DrawingToolsInteraction {
|
|||
}
|
||||
} else if entityView.entity is DrawingLinkEntity {
|
||||
isLink = true
|
||||
} else if entityView.entity is DrawingWeatherEntity {
|
||||
isWeather = true
|
||||
}
|
||||
|
||||
guard (!isVideo || isAdditional) && (!isMessage || !isTopmost) else {
|
||||
|
|
@ -3143,7 +3146,7 @@ public final class DrawingToolsInteraction {
|
|||
}
|
||||
}))
|
||||
}
|
||||
if !isVideo && !isMessage && !isLink {
|
||||
if !isVideo && !isMessage && !isLink && !isWeather {
|
||||
if let stickerEntity = entityView.entity as? DrawingStickerEntity, case let .file(_, type) = stickerEntity.content, case .reaction = type {
|
||||
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -80,7 +80,6 @@ public final class LocationViewController: ViewController {
|
|||
private let isStoryLocation: Bool
|
||||
|
||||
private let locationManager = LocationManager()
|
||||
private var permissionDisposable: Disposable?
|
||||
|
||||
private var interaction: LocationViewInteraction?
|
||||
|
||||
|
|
|
|||
|
|
@ -1821,6 +1821,8 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
|
|||
self.moreButtonNode.iconNode.enqueueState(.more, animated: false)
|
||||
|
||||
self.selectedButtonNode = SelectedButtonNode(theme: self.presentationData.theme)
|
||||
self.selectedButtonNode.alpha = 0.0
|
||||
self.selectedButtonNode.transform = CATransform3DMakeScale(0.01, 0.01, 1.0)
|
||||
|
||||
super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationData: presentationData))
|
||||
|
||||
|
|
@ -2239,6 +2241,9 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
|
|||
fileprivate var selectionCount: Int32 = 0
|
||||
fileprivate func updateSelectionState(count: Int32) {
|
||||
self.selectionCount = count
|
||||
guard let layout = self.validLayout else {
|
||||
return
|
||||
}
|
||||
|
||||
let transition = ContainedViewLayoutTransition.animated(duration: 0.25, curve: .easeInOut)
|
||||
var moreIsVisible = false
|
||||
|
|
@ -2262,15 +2267,22 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
|
|||
self.titleView.updateTitle(title: title, isEnabled: isEnabled, animated: true)
|
||||
self.cancelButtonNode.setState(isEnabled ? .cancel : .back, animated: true)
|
||||
|
||||
let selectedSize = self.selectedButtonNode.update(count: count)
|
||||
|
||||
var safeInset: CGFloat = 0.0
|
||||
if layout.safeInsets.right > 0.0 {
|
||||
safeInset += layout.safeInsets.right + 16.0
|
||||
}
|
||||
let navigationHeight = navigationLayout(layout: layout).navigationFrame.height
|
||||
self.selectedButtonNode.frame = CGRect(origin: CGPoint(x: self.view.bounds.width - 54.0 - selectedSize.width - safeInset, y: floorToScreenPixels((navigationHeight - selectedSize.height) / 2.0) + 1.0), size: selectedSize)
|
||||
|
||||
let isSelectionButtonVisible = count > 0 && self.controllerNode.currentDisplayMode == .all
|
||||
transition.updateAlpha(node: self.selectedButtonNode, alpha: isSelectionButtonVisible ? 1.0 : 0.0)
|
||||
transition.updateTransformScale(node: self.selectedButtonNode, scale: isSelectionButtonVisible ? 1.0 : 0.01)
|
||||
|
||||
let selectedSize = self.selectedButtonNode.update(count: count)
|
||||
if self.selectedButtonNode.supernode == nil {
|
||||
self.navigationBar?.addSubnode(self.selectedButtonNode)
|
||||
}
|
||||
self.selectedButtonNode.frame = CGRect(origin: CGPoint(x: self.view.bounds.width - 54.0 - selectedSize.width, y: 18.0 + UIScreenPixel), size: selectedSize)
|
||||
|
||||
self.titleView.segmentsHidden = true
|
||||
moreIsVisible = count > 0
|
||||
|
|
@ -2656,9 +2668,16 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
|
|||
|
||||
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
|
||||
super.containerLayoutUpdated(layout, transition: transition)
|
||||
|
||||
|
||||
self.validLayout = layout
|
||||
self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: navigationLayout(layout: layout).navigationFrame.maxY, transition: transition)
|
||||
|
||||
var safeInset: CGFloat = 0.0
|
||||
if layout.safeInsets.right > 0.0 {
|
||||
safeInset += layout.safeInsets.right + 16.0
|
||||
}
|
||||
let navigationHeight = navigationLayout(layout: layout).navigationFrame.height
|
||||
self.selectedButtonNode.frame = CGRect(origin: CGPoint(x: self.view.bounds.width - 54.0 - self.selectedButtonNode.frame.width - safeInset, y: floorToScreenPixels((navigationHeight - self.selectedButtonNode.frame.height) / 2.0) + 1.0), size: self.selectedButtonNode.frame.size)
|
||||
}
|
||||
|
||||
public var mediaPickerContext: AttachmentMediaPickerContext? {
|
||||
|
|
|
|||
|
|
@ -2720,7 +2720,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
|
|||
if url.hasPrefix("https://apps.apple.com/account/subscriptions") {
|
||||
controller.context.sharedContext.applicationBindings.openSubscriptions()
|
||||
} else if url.hasPrefix("https://") || url.hasPrefix("tg://") {
|
||||
controller.context.sharedContext.openExternalUrl(context: controller.context, urlContext: .generic, url: url, forceExternal: !url.hasPrefix("tg://") && !url.contains("?start="), presentationData: controller.context.sharedContext.currentPresentationData.with({$0}), navigationController: nil, dismissInput: {})
|
||||
controller.context.sharedContext.openExternalUrl(context: controller.context, urlContext: .generic, url: url, forceExternal: false, presentationData: controller.context.sharedContext.currentPresentationData.with({$0}), navigationController: navigationController, dismissInput: {})
|
||||
} else {
|
||||
let context = controller.context
|
||||
let signal: Signal<ResolvedUrl, NoError>?
|
||||
|
|
|
|||
|
|
@ -100,9 +100,9 @@ private enum DataAndStorageEntry: ItemListNodeEntry {
|
|||
case useLessVoiceData(PresentationTheme, String, Bool)
|
||||
case useLessVoiceDataInfo(PresentationTheme, String)
|
||||
case otherHeader(PresentationTheme, String)
|
||||
case openLinksIn(PresentationTheme, String, String)
|
||||
case shareSheet(PresentationTheme, String)
|
||||
case saveEditedPhotos(PresentationTheme, String, Bool)
|
||||
case openLinksIn(PresentationTheme, String, String)
|
||||
case pauseMusicOnRecording(PresentationTheme, String, Bool)
|
||||
case raiseToListen(PresentationTheme, String, Bool)
|
||||
case raiseToListenInfo(PresentationTheme, String)
|
||||
|
|
@ -123,7 +123,7 @@ private enum DataAndStorageEntry: ItemListNodeEntry {
|
|||
return DataAndStorageSection.backgroundDownload.rawValue
|
||||
case .useLessVoiceData, .useLessVoiceDataInfo:
|
||||
return DataAndStorageSection.voiceCalls.rawValue
|
||||
case .otherHeader, .shareSheet, .saveEditedPhotos, .openLinksIn, .pauseMusicOnRecording, .raiseToListen, .raiseToListenInfo:
|
||||
case .otherHeader, .openLinksIn, .shareSheet, .saveEditedPhotos, .pauseMusicOnRecording, .raiseToListen, .raiseToListenInfo:
|
||||
return DataAndStorageSection.other.rawValue
|
||||
case .connectionHeader, .connectionProxy:
|
||||
return DataAndStorageSection.connection.rawValue
|
||||
|
|
@ -162,11 +162,11 @@ private enum DataAndStorageEntry: ItemListNodeEntry {
|
|||
return 24
|
||||
case .otherHeader:
|
||||
return 29
|
||||
case .shareSheet:
|
||||
return 30
|
||||
case .saveEditedPhotos:
|
||||
return 31
|
||||
case .openLinksIn:
|
||||
return 30
|
||||
case .shareSheet:
|
||||
return 31
|
||||
case .saveEditedPhotos:
|
||||
return 32
|
||||
case .pauseMusicOnRecording:
|
||||
return 33
|
||||
|
|
@ -257,6 +257,12 @@ private enum DataAndStorageEntry: ItemListNodeEntry {
|
|||
} else {
|
||||
return false
|
||||
}
|
||||
case let .openLinksIn(lhsTheme, lhsText, lhsValue):
|
||||
if case let .openLinksIn(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .shareSheet(lhsTheme, lhsText):
|
||||
if case let .shareSheet(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
|
||||
return true
|
||||
|
|
@ -269,12 +275,6 @@ private enum DataAndStorageEntry: ItemListNodeEntry {
|
|||
} else {
|
||||
return false
|
||||
}
|
||||
case let .openLinksIn(lhsTheme, lhsText, lhsValue):
|
||||
if case let .openLinksIn(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .pauseMusicOnRecording(lhsTheme, lhsText, lhsValue):
|
||||
if case let .pauseMusicOnRecording(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue {
|
||||
return true
|
||||
|
|
@ -386,6 +386,10 @@ private enum DataAndStorageEntry: ItemListNodeEntry {
|
|||
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
|
||||
case let .otherHeader(_, text):
|
||||
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
|
||||
case let .openLinksIn(_, text, value):
|
||||
return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, action: {
|
||||
arguments.openBrowserSelection()
|
||||
})
|
||||
case let .shareSheet(_, text):
|
||||
return ItemListDisclosureItem(presentationData: presentationData, title: text, label: "", sectionId: self.section, style: .blocks, action: {
|
||||
arguments.openIntents()
|
||||
|
|
@ -394,10 +398,6 @@ private enum DataAndStorageEntry: ItemListNodeEntry {
|
|||
return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, sectionId: self.section, style: .blocks, updated: { value in
|
||||
arguments.toggleSaveEditedPhotos(value)
|
||||
}, tag: DataAndStorageEntryTag.saveEditedPhotos)
|
||||
case let .openLinksIn(_, text, value):
|
||||
return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, action: {
|
||||
arguments.openBrowserSelection()
|
||||
})
|
||||
case let .pauseMusicOnRecording(_, text, value):
|
||||
return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, sectionId: self.section, style: .blocks, updated: { value in
|
||||
arguments.togglePauseMusicOnRecording(value)
|
||||
|
|
@ -618,11 +618,11 @@ private func dataAndStorageControllerEntries(state: DataAndStorageControllerStat
|
|||
entries.append(.useLessVoiceDataInfo(presentationData.theme, presentationData.strings.CallSettings_UseLessDataLongDescription))
|
||||
|
||||
entries.append(.otherHeader(presentationData.theme, presentationData.strings.ChatSettings_Other))
|
||||
entries.append(.openLinksIn(presentationData.theme, presentationData.strings.ChatSettings_OpenLinksIn, defaultWebBrowser))
|
||||
if #available(iOSApplicationExtension 13.2, iOS 13.2, *) {
|
||||
entries.append(.shareSheet(presentationData.theme, presentationData.strings.ChatSettings_IntentsSettings))
|
||||
}
|
||||
entries.append(.saveEditedPhotos(presentationData.theme, presentationData.strings.Settings_SaveEditedPhotos, data.generatedMediaStoreSettings.storeEditedPhotos))
|
||||
entries.append(.openLinksIn(presentationData.theme, presentationData.strings.ChatSettings_OpenLinksIn, defaultWebBrowser))
|
||||
entries.append(.pauseMusicOnRecording(presentationData.theme, presentationData.strings.Settings_PauseMusicOnRecording, data.mediaInputSettings.pauseMusicOnRecording))
|
||||
entries.append(.raiseToListen(presentationData.theme, presentationData.strings.Settings_RaiseToListen, data.mediaInputSettings.enableRaiseToSpeak))
|
||||
entries.append(.raiseToListenInfo(presentationData.theme, presentationData.strings.Settings_RaiseToListenInfo))
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -9,29 +9,60 @@ import TelegramUIPreferences
|
|||
import ItemListUI
|
||||
import AccountContext
|
||||
import OpenInExternalAppUI
|
||||
import ItemListPeerActionItem
|
||||
import UndoUI
|
||||
import WebKit
|
||||
import LinkPresentation
|
||||
|
||||
private final class WebBrowserSettingsControllerArguments {
|
||||
let context: AccountContext
|
||||
let updateDefaultBrowser: (String?) -> Void
|
||||
let clearCookies: () -> Void
|
||||
let addException: () -> Void
|
||||
let clearExceptions: () -> Void
|
||||
|
||||
init(context: AccountContext, updateDefaultBrowser: @escaping (String?) -> Void) {
|
||||
init(
|
||||
context: AccountContext,
|
||||
updateDefaultBrowser: @escaping (String?) -> Void,
|
||||
clearCookies: @escaping () -> Void,
|
||||
addException: @escaping () -> Void,
|
||||
clearExceptions: @escaping () -> Void
|
||||
) {
|
||||
self.context = context
|
||||
self.updateDefaultBrowser = updateDefaultBrowser
|
||||
self.clearCookies = clearCookies
|
||||
self.addException = addException
|
||||
self.clearExceptions = clearExceptions
|
||||
}
|
||||
}
|
||||
|
||||
private enum WebBrowserSettingsSection: Int32 {
|
||||
case browsers
|
||||
case clearCookies
|
||||
case exceptions
|
||||
}
|
||||
|
||||
private enum WebBrowserSettingsControllerEntry: ItemListNodeEntry {
|
||||
case browserHeader(PresentationTheme, String)
|
||||
case browser(PresentationTheme, String, OpenInApplication?, String?, Bool, Int32)
|
||||
|
||||
case clearCookies(PresentationTheme, String)
|
||||
case clearCookiesInfo(PresentationTheme, String)
|
||||
|
||||
case exceptionsHeader(PresentationTheme, String)
|
||||
case exceptionsAdd(PresentationTheme, String)
|
||||
case exception(Int32, PresentationTheme, WebBrowserException)
|
||||
case exceptionsClear(PresentationTheme, String)
|
||||
case exceptionsInfo(PresentationTheme, String)
|
||||
|
||||
var section: ItemListSectionId {
|
||||
switch self {
|
||||
case .browserHeader, .browser:
|
||||
return WebBrowserSettingsSection.browsers.rawValue
|
||||
case .clearCookies, .clearCookiesInfo:
|
||||
return WebBrowserSettingsSection.clearCookies.rawValue
|
||||
case .exceptionsHeader, .exceptionsAdd, .exception, .exceptionsClear, .exceptionsInfo:
|
||||
return WebBrowserSettingsSection.exceptions.rawValue
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -41,6 +72,20 @@ private enum WebBrowserSettingsControllerEntry: ItemListNodeEntry {
|
|||
return 0
|
||||
case let .browser(_, _, _, _, _, index):
|
||||
return 1 + index
|
||||
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 +103,48 @@ private enum WebBrowserSettingsControllerEntry: ItemListNodeEntry {
|
|||
} 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 +161,194 @@ private enum WebBrowserSettingsControllerEntry: ItemListNodeEntry {
|
|||
return WebBrowserItem(context: arguments.context, presentationData: presentationData, title: title, application: application, checked: selected, sectionId: self.section) {
|
||||
arguments.updateDefaultBrowser(identifier)
|
||||
}
|
||||
case let .clearCookies(_, text):
|
||||
return ItemListPeerActionItem(presentationData: presentationData, icon: PresentationResourcesItemList.accentDeleteIconImage(presentationData.theme), title: text, sectionId: self.section, height: .generic, color: .accent, editing: false, action: {
|
||||
arguments.clearCookies()
|
||||
})
|
||||
case let .clearCookiesInfo(_, text):
|
||||
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
|
||||
case let .exceptionsHeader(_, text):
|
||||
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
|
||||
case let .exception(_, _, exception):
|
||||
return WebBrowserDomainExceptionItem(presentationData: presentationData, context: arguments.context, title: exception.title, label: exception.domain, sectionId: self.section, style: .blocks)
|
||||
case let .exceptionsAdd(_, text):
|
||||
return ItemListPeerActionItem(presentationData: presentationData, icon: PresentationResourcesItemList.plusIconImage(presentationData.theme), title: text, sectionId: self.section, height: .generic, color: .accent, editing: false, action: {
|
||||
arguments.addException()
|
||||
})
|
||||
case let .exceptionsClear(_, text):
|
||||
return ItemListPeerActionItem(presentationData: presentationData, icon: PresentationResourcesItemList.deleteIconImage(presentationData.theme), title: text, sectionId: self.section, height: .generic, color: .destructive, editing: false, action: {
|
||||
arguments.clearExceptions()
|
||||
})
|
||||
case let .exceptionsInfo(_, text):
|
||||
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func webBrowserSettingsControllerEntries(context: AccountContext, presentationData: PresentationData, selectedBrowser: String?) -> [WebBrowserSettingsControllerEntry] {
|
||||
private func webBrowserSettingsControllerEntries(context: AccountContext, presentationData: PresentationData, settings: WebBrowserSettings) -> [WebBrowserSettingsControllerEntry] {
|
||||
var entries: [WebBrowserSettingsControllerEntry] = []
|
||||
|
||||
let options = availableOpenInOptions(context: context, item: .url(url: "http://telegram.org"))
|
||||
|
||||
entries.append(.browserHeader(presentationData.theme, presentationData.strings.WebBrowser_DefaultBrowser))
|
||||
entries.append(.browser(presentationData.theme, presentationData.strings.WebBrowser_Telegram, nil, nil, selectedBrowser == nil, 0))
|
||||
entries.append(.browser(presentationData.theme, presentationData.strings.WebBrowser_InAppSafari, .safari, "inApp", selectedBrowser == "inApp", 1))
|
||||
entries.append(.browserHeader(presentationData.theme, presentationData.strings.WebBrowser_OpenLinksIn_Title))
|
||||
entries.append(.browser(presentationData.theme, presentationData.strings.WebBrowser_Telegram, nil, nil, settings.defaultWebBrowser == nil, 0))
|
||||
|
||||
var index: Int32 = 2
|
||||
var index: Int32 = 1
|
||||
for option in options {
|
||||
entries.append(.browser(presentationData.theme, option.title, option.application, option.identifier, option.identifier == selectedBrowser, index))
|
||||
entries.append(.browser(presentationData.theme, option.title, option.application, option.identifier, option.identifier == settings.defaultWebBrowser, index))
|
||||
index += 1
|
||||
}
|
||||
|
||||
if settings.defaultWebBrowser == nil {
|
||||
entries.append(.clearCookies(presentationData.theme, presentationData.strings.WebBrowser_ClearCookies))
|
||||
entries.append(.clearCookiesInfo(presentationData.theme, presentationData.strings.WebBrowser_ClearCookies_Info))
|
||||
|
||||
entries.append(.exceptionsHeader(presentationData.theme, presentationData.strings.WebBrowser_Exceptions_Title))
|
||||
entries.append(.exceptionsAdd(presentationData.theme, presentationData.strings.WebBrowser_Exceptions_AddException))
|
||||
|
||||
var exceptionIndex: Int32 = 0
|
||||
for exception in settings.exceptions {
|
||||
entries.append(.exception(exceptionIndex, presentationData.theme, exception))
|
||||
exceptionIndex += 1
|
||||
}
|
||||
|
||||
if !settings.exceptions.isEmpty {
|
||||
entries.append(.exceptionsClear(presentationData.theme, presentationData.strings.WebBrowser_Exceptions_Clear))
|
||||
}
|
||||
|
||||
entries.append(.exceptionsInfo(presentationData.theme, presentationData.strings.WebBrowser_Exceptions_Info))
|
||||
}
|
||||
|
||||
return entries
|
||||
}
|
||||
|
||||
public func webBrowserSettingsController(context: AccountContext) -> ViewController {
|
||||
let arguments = WebBrowserSettingsControllerArguments(context: context, updateDefaultBrowser: { identifier in
|
||||
let _ = updateWebBrowserSettingsInteractively(accountManager: context.sharedContext.accountManager, { $0.withUpdatedDefaultWebBrowser(identifier) }).start()
|
||||
})
|
||||
var clearCookiesImpl: (() -> Void)?
|
||||
var addExceptionImpl: (() -> Void)?
|
||||
var clearExceptionsImpl: (() -> Void)?
|
||||
|
||||
let signal = combineLatest(context.sharedContext.presentationData, context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.webBrowserSettings]))
|
||||
let arguments = WebBrowserSettingsControllerArguments(
|
||||
context: context,
|
||||
updateDefaultBrowser: { identifier in
|
||||
let _ = updateWebBrowserSettingsInteractively(accountManager: context.sharedContext.accountManager, {
|
||||
$0.withUpdatedDefaultWebBrowser(identifier)
|
||||
}).start()
|
||||
},
|
||||
clearCookies: {
|
||||
clearCookiesImpl?()
|
||||
},
|
||||
addException: {
|
||||
addExceptionImpl?()
|
||||
},
|
||||
clearExceptions: {
|
||||
clearExceptionsImpl?()
|
||||
}
|
||||
)
|
||||
|
||||
let previousSettings = Atomic<WebBrowserSettings?>(value: nil)
|
||||
|
||||
let signal = combineLatest(
|
||||
context.sharedContext.presentationData,
|
||||
context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.webBrowserSettings])
|
||||
)
|
||||
|> deliverOnMainQueue
|
||||
|> map { presentationData, sharedData -> (ItemListControllerState, (ItemListNodeState, Any)) in
|
||||
let settings = sharedData.entries[ApplicationSpecificSharedDataKeys.webBrowserSettings]?.get(WebBrowserSettings.self) ?? WebBrowserSettings.defaultSettings
|
||||
let previousSettings = previousSettings.swap(settings)
|
||||
|
||||
var animateChanges = false
|
||||
if let previousSettings {
|
||||
if previousSettings.defaultWebBrowser != settings.defaultWebBrowser {
|
||||
animateChanges = true
|
||||
}
|
||||
}
|
||||
|
||||
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.WebBrowser_Title), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back))
|
||||
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: webBrowserSettingsControllerEntries(context: context, presentationData: presentationData, selectedBrowser: settings.defaultWebBrowser), style: .blocks, animateChanges: false)
|
||||
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: webBrowserSettingsControllerEntries(context: context, presentationData: presentationData, settings: settings), style: .blocks, animateChanges: animateChanges)
|
||||
|
||||
return (controllerState, (listState, arguments))
|
||||
}
|
||||
|
||||
let controller = ItemListController(context: context, state: signal)
|
||||
|
||||
clearCookiesImpl = { [weak controller] in
|
||||
WKWebsiteDataStore.default().removeData(ofTypes: WKWebsiteDataStore.allWebsiteDataTypes(), modifiedSince: Date(timeIntervalSince1970: 0), completionHandler:{})
|
||||
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
controller?.present(UndoOverlayController(
|
||||
presentationData: presentationData,
|
||||
content: .info(
|
||||
title: nil,
|
||||
text: presentationData.strings.WebBrowser_ClearCookies_Succeed,
|
||||
timeout: nil,
|
||||
customUndoText: nil
|
||||
),
|
||||
elevatedLayout: false,
|
||||
position: .bottom,
|
||||
action: { _ in return false }), in: .current
|
||||
)
|
||||
}
|
||||
|
||||
addExceptionImpl = { [weak controller] in
|
||||
let linkController = webBrowserDomainController(context: context, apply: { url in
|
||||
if let url {
|
||||
let _ = fetchDomainExceptionInfo(url: url).startStandalone(next: { newException in
|
||||
let _ = updateWebBrowserSettingsInteractively(accountManager: context.sharedContext.accountManager, { currentSettings in
|
||||
var currentExceptions = currentSettings.exceptions
|
||||
for exception in currentExceptions {
|
||||
if exception.domain == newException.domain {
|
||||
return currentSettings
|
||||
}
|
||||
}
|
||||
currentExceptions.append(newException)
|
||||
return currentSettings.withUpdatedExceptions(currentExceptions)
|
||||
}).start()
|
||||
})
|
||||
}
|
||||
})
|
||||
controller?.present(linkController, in: .window(.root))
|
||||
}
|
||||
|
||||
clearExceptionsImpl = {
|
||||
let _ = updateWebBrowserSettingsInteractively(accountManager: context.sharedContext.accountManager, { currentSettings in
|
||||
return currentSettings.withUpdatedExceptions([])
|
||||
}).start()
|
||||
}
|
||||
|
||||
return controller
|
||||
}
|
||||
|
||||
private func cleanDomain(url: String) -> (domain: String, fullUrl: String) {
|
||||
if let parsedUrl = URL(string: url) {
|
||||
let host: String?
|
||||
let scheme = parsedUrl.scheme ?? "https"
|
||||
if #available(iOS 16.0, *) {
|
||||
host = parsedUrl.host(percentEncoded: true)?.lowercased()
|
||||
} else {
|
||||
host = parsedUrl.host?.lowercased()
|
||||
}
|
||||
return (host ?? url, "\(scheme)://\(host ?? "")")
|
||||
} else {
|
||||
return (url, url)
|
||||
}
|
||||
}
|
||||
|
||||
private func fetchDomainExceptionInfo(url: String) -> Signal<WebBrowserException, NoError> {
|
||||
let (domain, domainUrl) = cleanDomain(url: url)
|
||||
if #available(iOS 13.0, *), let url = URL(string: domainUrl) {
|
||||
return Signal { subscriber in
|
||||
let metadataProvider = LPMetadataProvider()
|
||||
metadataProvider.shouldFetchSubresources = true
|
||||
metadataProvider.startFetchingMetadata(for: url, completionHandler: { metadata, _ in
|
||||
let title = metadata?.value(forKey: "_siteName") as? String ?? metadata?.title
|
||||
subscriber.putNext(WebBrowserException(domain: domain, title: title ?? domain))
|
||||
subscriber.putCompletion()
|
||||
})
|
||||
return ActionDisposable {
|
||||
metadataProvider.cancel()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return .single(WebBrowserException(domain: domain, title: domain))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ public enum PresentationResourceKey: Int32 {
|
|||
case itemListSecondaryCheckIcon
|
||||
case itemListPlusIcon
|
||||
case itemListRoundPlusIcon
|
||||
case itemListAccentDeleteIcon
|
||||
case itemListDeleteIcon
|
||||
case itemListDeleteIndicatorIcon
|
||||
case itemListReorderIndicatorIcon
|
||||
|
|
|
|||
|
|
@ -69,6 +69,12 @@ public struct PresentationResourcesItemList {
|
|||
})
|
||||
}
|
||||
|
||||
public static func accentDeleteIconImage(_ theme: PresentationTheme) -> UIImage? {
|
||||
return theme.image(PresentationResourceKey.itemListAccentDeleteIcon.rawValue, { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionTrash"), color: theme.list.itemAccentColor)
|
||||
})
|
||||
}
|
||||
|
||||
public static func deleteIconImage(_ theme: PresentationTheme) -> UIImage? {
|
||||
return theme.image(PresentationResourceKey.itemListDeleteIcon.rawValue, { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionTrash"), color: theme.list.itemDestructiveColor)
|
||||
|
|
|
|||
|
|
@ -338,6 +338,19 @@ private final class MediaCoverScreenComponent: Component {
|
|||
mediaEditor.seek(position, andPlay: false)
|
||||
}
|
||||
},
|
||||
coverPositionUpdated: { [weak mediaEditor] position, tap, commit in
|
||||
if let mediaEditor {
|
||||
if tap {
|
||||
mediaEditor.setOnNextDisplay {
|
||||
commit()
|
||||
}
|
||||
mediaEditor.seek(position, andPlay: false)
|
||||
} else {
|
||||
mediaEditor.seek(position, andPlay: false)
|
||||
commit()
|
||||
}
|
||||
}
|
||||
},
|
||||
trackTrimUpdated: { _, _, _, _, _ in
|
||||
},
|
||||
trackOffsetUpdated: { _, _, _ in
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ import StickerPackEditTitleController
|
|||
import StickerPickerScreen
|
||||
import UIKitRuntimeUtils
|
||||
import ImageObjectSeparation
|
||||
import DeviceAccess
|
||||
|
||||
private let playbackButtonTag = GenericComponentViewTag()
|
||||
private let muteButtonTag = GenericComponentViewTag()
|
||||
|
|
@ -1198,6 +1199,7 @@ final class MediaEditorScreenComponent: Component {
|
|||
|
||||
let displayTopButtons = !(self.inputPanelExternalState.isEditing || isEditingTextEntity || component.isDisplayingTool != nil)
|
||||
|
||||
var inputPanelSize: CGSize = .zero
|
||||
if case .storyEditor = controller.mode {
|
||||
let nextInputMode: MessageInputPanelComponent.InputMode
|
||||
switch self.currentInputMode {
|
||||
|
|
@ -1217,7 +1219,7 @@ final class MediaEditorScreenComponent: Component {
|
|||
}
|
||||
|
||||
self.inputPanel.parentState = state
|
||||
let inputPanelSize = self.inputPanel.update(
|
||||
inputPanelSize = self.inputPanel.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(MessageInputPanelComponent(
|
||||
externalState: self.inputPanelExternalState,
|
||||
|
|
@ -1429,192 +1431,7 @@ final class MediaEditorScreenComponent: Component {
|
|||
transition.setFrame(view: inputPanelView, frame: inputPanelFrame)
|
||||
transition.setAlpha(view: inputPanelView, alpha: isEditingTextEntity || component.isDisplayingTool != nil || component.isDismissing || component.isInteractingWithEntities ? 0.0 : 1.0)
|
||||
}
|
||||
|
||||
if let playerState = state.playerState {
|
||||
let scrubberInset: CGFloat = 9.0
|
||||
|
||||
let minDuration: Double
|
||||
let maxDuration: Double
|
||||
if playerState.isAudioOnly {
|
||||
minDuration = 5.0
|
||||
maxDuration = 15.0
|
||||
} else {
|
||||
minDuration = 1.0
|
||||
maxDuration = storyMaxVideoDuration
|
||||
}
|
||||
|
||||
let previousTrackCount = self.currentVisibleTracks?.count
|
||||
let visibleTracks = playerState.tracks.filter { $0.visibleInTimeline }.map { MediaScrubberComponent.Track($0) }
|
||||
self.currentVisibleTracks = visibleTracks
|
||||
|
||||
var scrubberTransition = transition
|
||||
if let previousTrackCount, previousTrackCount != visibleTracks.count {
|
||||
scrubberTransition = .easeInOut(duration: 0.2)
|
||||
}
|
||||
|
||||
let isAudioOnly = playerState.isAudioOnly
|
||||
let hasMainVideoTrack = playerState.tracks.contains(where: { $0.id == 0 })
|
||||
|
||||
let scrubber: ComponentView<Empty>
|
||||
if let current = self.scrubber {
|
||||
scrubber = current
|
||||
} else {
|
||||
scrubber = ComponentView<Empty>()
|
||||
self.scrubber = scrubber
|
||||
}
|
||||
|
||||
let scrubberSize = scrubber.update(
|
||||
transition: scrubberTransition,
|
||||
component: AnyComponent(MediaScrubberComponent(
|
||||
context: component.context,
|
||||
style: .editor,
|
||||
theme: environment.theme,
|
||||
generationTimestamp: playerState.generationTimestamp,
|
||||
position: playerState.position,
|
||||
minDuration: minDuration,
|
||||
maxDuration: maxDuration,
|
||||
isPlaying: playerState.isPlaying,
|
||||
tracks: visibleTracks,
|
||||
positionUpdated: { [weak mediaEditor] position, apply in
|
||||
if let mediaEditor {
|
||||
mediaEditor.seek(position, andPlay: apply)
|
||||
}
|
||||
},
|
||||
trackTrimUpdated: { [weak mediaEditor] trackId, start, end, updatedEnd, apply in
|
||||
guard let mediaEditor else {
|
||||
return
|
||||
}
|
||||
let trimRange = start..<end
|
||||
if trackId == 2 {
|
||||
mediaEditor.setAudioTrackTrimRange(trimRange, apply: apply)
|
||||
if isAudioOnly {
|
||||
let offset = (mediaEditor.values.audioTrackOffset ?? 0.0)
|
||||
if apply {
|
||||
mediaEditor.seek(offset + start, andPlay: true)
|
||||
} else {
|
||||
mediaEditor.seek(offset + start, andPlay: false)
|
||||
mediaEditor.stop()
|
||||
}
|
||||
} else {
|
||||
if apply {
|
||||
mediaEditor.play()
|
||||
} else {
|
||||
mediaEditor.stop()
|
||||
}
|
||||
}
|
||||
} else if trackId == 1 {
|
||||
mediaEditor.setAdditionalVideoTrimRange(trimRange, apply: apply)
|
||||
if hasMainVideoTrack {
|
||||
if apply {
|
||||
mediaEditor.play()
|
||||
} else {
|
||||
mediaEditor.stop()
|
||||
}
|
||||
} else {
|
||||
if apply {
|
||||
mediaEditor.seek(start, andPlay: true)
|
||||
} else {
|
||||
mediaEditor.seek(updatedEnd ? end : start, andPlay: false)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
mediaEditor.setVideoTrimRange(trimRange, apply: apply)
|
||||
if apply {
|
||||
mediaEditor.seek(start, andPlay: true)
|
||||
} else {
|
||||
mediaEditor.seek(updatedEnd ? end : start, andPlay: false)
|
||||
}
|
||||
}
|
||||
},
|
||||
trackOffsetUpdated: { trackId, offset, apply in
|
||||
guard let mediaEditor else {
|
||||
return
|
||||
}
|
||||
if trackId == 2 {
|
||||
mediaEditor.setAudioTrackOffset(offset, apply: apply)
|
||||
if isAudioOnly {
|
||||
let offset = (mediaEditor.values.audioTrackOffset ?? 0.0)
|
||||
let start = (mediaEditor.values.audioTrackTrimRange?.lowerBound ?? 0.0)
|
||||
if apply {
|
||||
mediaEditor.seek(offset + start, andPlay: true)
|
||||
} else {
|
||||
mediaEditor.seek(offset + start, andPlay: false)
|
||||
mediaEditor.stop()
|
||||
}
|
||||
} else {
|
||||
if apply {
|
||||
let audioStart = mediaEditor.values.audioTrackTrimRange?.lowerBound ?? 0.0
|
||||
let audioOffset = min(0.0, mediaEditor.values.audioTrackOffset ?? 0.0)
|
||||
|
||||
var start = -audioOffset + audioStart
|
||||
if let duration = mediaEditor.duration {
|
||||
let lowerBound = mediaEditor.values.videoTrimRange?.lowerBound ?? 0.0
|
||||
let upperBound = mediaEditor.values.videoTrimRange?.upperBound ?? duration
|
||||
if start >= upperBound {
|
||||
start = lowerBound
|
||||
} else if start < lowerBound {
|
||||
start = lowerBound
|
||||
}
|
||||
}
|
||||
|
||||
mediaEditor.seek(start, andPlay: true)
|
||||
mediaEditor.play()
|
||||
} else {
|
||||
mediaEditor.stop()
|
||||
}
|
||||
}
|
||||
} else if trackId == 1 {
|
||||
mediaEditor.setAdditionalVideoOffset(offset, apply: apply)
|
||||
}
|
||||
},
|
||||
trackLongPressed: { [weak controller] trackId, sourceView in
|
||||
guard let controller else {
|
||||
return
|
||||
}
|
||||
controller.node.presentTrackOptions(trackId: trackId, sourceView: sourceView)
|
||||
}
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: previewSize.width - scrubberInset * 2.0, height: availableSize.height)
|
||||
)
|
||||
|
||||
let scrubberFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - scrubberSize.width) / 2.0), y: availableSize.height - environment.safeInsets.bottom - scrubberSize.height + controlsBottomInset - inputPanelSize.height + 3.0), size: scrubberSize)
|
||||
if let scrubberView = scrubber.view {
|
||||
var animateIn = false
|
||||
if scrubberView.superview == nil {
|
||||
animateIn = true
|
||||
if let inputPanelBackgroundView = self.inputPanelBackground.view, inputPanelBackgroundView.superview != nil {
|
||||
self.insertSubview(scrubberView, belowSubview: inputPanelBackgroundView)
|
||||
} else {
|
||||
self.addSubview(scrubberView)
|
||||
}
|
||||
}
|
||||
if animateIn {
|
||||
scrubberView.frame = scrubberFrame
|
||||
} else {
|
||||
scrubberTransition.setFrame(view: scrubberView, frame: scrubberFrame)
|
||||
}
|
||||
if !self.animatingButtons && !(!hasMainVideoTrack && animateIn) {
|
||||
transition.setAlpha(view: scrubberView, alpha: component.isDisplayingTool != nil || component.isDismissing || component.isInteractingWithEntities || isEditingCaption || isRecordingAdditionalVideo || isEditingTextEntity ? 0.0 : 1.0)
|
||||
} else if animateIn {
|
||||
scrubberView.layer.animatePosition(from: CGPoint(x: 0.0, y: 44.0), to: .zero, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
|
||||
scrubberView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
scrubberView.layer.animateScale(from: 0.6, to: 1.0, duration: 0.2)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if let scrubber = self.scrubber {
|
||||
self.scrubber = nil
|
||||
if let scrubberView = scrubber.view {
|
||||
scrubberView.layer.animatePosition(from: .zero, to: CGPoint(x: 0.0, y: 44.0), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true)
|
||||
scrubberView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in
|
||||
scrubberView.removeFromSuperview()
|
||||
})
|
||||
scrubberView.layer.animateScale(from: 1.0, to: 0.6, duration: 0.2, removeOnCompletion: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
let saveContentComponent: AnyComponentWithIdentity<Empty>
|
||||
if component.hasAppeared {
|
||||
saveContentComponent = AnyComponentWithIdentity(
|
||||
|
|
@ -1971,8 +1788,199 @@ final class MediaEditorScreenComponent: Component {
|
|||
transition.setScale(view: switchCameraButtonView, scale: isRecordingAdditionalVideo ? 1.0 : 0.01)
|
||||
transition.setAlpha(view: switchCameraButtonView, alpha: isRecordingAdditionalVideo ? 1.0 : 0.0)
|
||||
}
|
||||
|
||||
} else {
|
||||
inputPanelSize = CGSize(width: 0.0, height: 12.0)
|
||||
}
|
||||
|
||||
if case .stickerEditor = controller.mode {
|
||||
|
||||
} else {
|
||||
if let playerState = state.playerState {
|
||||
let scrubberInset: CGFloat = 9.0
|
||||
|
||||
let minDuration: Double
|
||||
let maxDuration: Double
|
||||
if playerState.isAudioOnly {
|
||||
minDuration = 5.0
|
||||
maxDuration = 15.0
|
||||
} else {
|
||||
minDuration = 1.0
|
||||
maxDuration = storyMaxVideoDuration
|
||||
}
|
||||
|
||||
let previousTrackCount = self.currentVisibleTracks?.count
|
||||
let visibleTracks = playerState.tracks.filter { $0.visibleInTimeline }.map { MediaScrubberComponent.Track($0) }
|
||||
self.currentVisibleTracks = visibleTracks
|
||||
|
||||
var scrubberTransition = transition
|
||||
if let previousTrackCount, previousTrackCount != visibleTracks.count {
|
||||
scrubberTransition = .easeInOut(duration: 0.2)
|
||||
}
|
||||
|
||||
let isAudioOnly = playerState.isAudioOnly
|
||||
let hasMainVideoTrack = playerState.tracks.contains(where: { $0.id == 0 })
|
||||
|
||||
let scrubber: ComponentView<Empty>
|
||||
if let current = self.scrubber {
|
||||
scrubber = current
|
||||
} else {
|
||||
scrubber = ComponentView<Empty>()
|
||||
self.scrubber = scrubber
|
||||
}
|
||||
|
||||
let scrubberSize = scrubber.update(
|
||||
transition: scrubberTransition,
|
||||
component: AnyComponent(MediaScrubberComponent(
|
||||
context: component.context,
|
||||
style: .editor,
|
||||
theme: environment.theme,
|
||||
generationTimestamp: playerState.generationTimestamp,
|
||||
position: playerState.position,
|
||||
minDuration: minDuration,
|
||||
maxDuration: maxDuration,
|
||||
isPlaying: playerState.isPlaying,
|
||||
tracks: visibleTracks,
|
||||
positionUpdated: { [weak mediaEditor] position, apply in
|
||||
if let mediaEditor {
|
||||
mediaEditor.seek(position, andPlay: apply)
|
||||
}
|
||||
},
|
||||
trackTrimUpdated: { [weak mediaEditor] trackId, start, end, updatedEnd, apply in
|
||||
guard let mediaEditor else {
|
||||
return
|
||||
}
|
||||
let trimRange = start..<end
|
||||
if trackId == 2 {
|
||||
mediaEditor.setAudioTrackTrimRange(trimRange, apply: apply)
|
||||
if isAudioOnly {
|
||||
let offset = (mediaEditor.values.audioTrackOffset ?? 0.0)
|
||||
if apply {
|
||||
mediaEditor.seek(offset + start, andPlay: true)
|
||||
} else {
|
||||
mediaEditor.seek(offset + start, andPlay: false)
|
||||
mediaEditor.stop()
|
||||
}
|
||||
} else {
|
||||
if apply {
|
||||
mediaEditor.play()
|
||||
} else {
|
||||
mediaEditor.stop()
|
||||
}
|
||||
}
|
||||
} else if trackId == 1 {
|
||||
mediaEditor.setAdditionalVideoTrimRange(trimRange, apply: apply)
|
||||
if hasMainVideoTrack {
|
||||
if apply {
|
||||
mediaEditor.play()
|
||||
} else {
|
||||
mediaEditor.stop()
|
||||
}
|
||||
} else {
|
||||
if apply {
|
||||
mediaEditor.seek(start, andPlay: true)
|
||||
} else {
|
||||
mediaEditor.seek(updatedEnd ? end : start, andPlay: false)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
mediaEditor.setVideoTrimRange(trimRange, apply: apply)
|
||||
if apply {
|
||||
mediaEditor.seek(start, andPlay: true)
|
||||
} else {
|
||||
mediaEditor.seek(updatedEnd ? end : start, andPlay: false)
|
||||
}
|
||||
}
|
||||
},
|
||||
trackOffsetUpdated: { trackId, offset, apply in
|
||||
guard let mediaEditor else {
|
||||
return
|
||||
}
|
||||
if trackId == 2 {
|
||||
mediaEditor.setAudioTrackOffset(offset, apply: apply)
|
||||
if isAudioOnly {
|
||||
let offset = (mediaEditor.values.audioTrackOffset ?? 0.0)
|
||||
let start = (mediaEditor.values.audioTrackTrimRange?.lowerBound ?? 0.0)
|
||||
if apply {
|
||||
mediaEditor.seek(offset + start, andPlay: true)
|
||||
} else {
|
||||
mediaEditor.seek(offset + start, andPlay: false)
|
||||
mediaEditor.stop()
|
||||
}
|
||||
} else {
|
||||
if apply {
|
||||
let audioStart = mediaEditor.values.audioTrackTrimRange?.lowerBound ?? 0.0
|
||||
let audioOffset = min(0.0, mediaEditor.values.audioTrackOffset ?? 0.0)
|
||||
|
||||
var start = -audioOffset + audioStart
|
||||
if let duration = mediaEditor.duration {
|
||||
let lowerBound = mediaEditor.values.videoTrimRange?.lowerBound ?? 0.0
|
||||
let upperBound = mediaEditor.values.videoTrimRange?.upperBound ?? duration
|
||||
if start >= upperBound {
|
||||
start = lowerBound
|
||||
} else if start < lowerBound {
|
||||
start = lowerBound
|
||||
}
|
||||
}
|
||||
|
||||
mediaEditor.seek(start, andPlay: true)
|
||||
mediaEditor.play()
|
||||
} else {
|
||||
mediaEditor.stop()
|
||||
}
|
||||
}
|
||||
} else if trackId == 1 {
|
||||
mediaEditor.setAdditionalVideoOffset(offset, apply: apply)
|
||||
}
|
||||
},
|
||||
trackLongPressed: { [weak controller] trackId, sourceView in
|
||||
guard let controller else {
|
||||
return
|
||||
}
|
||||
controller.node.presentTrackOptions(trackId: trackId, sourceView: sourceView)
|
||||
}
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: previewSize.width - scrubberInset * 2.0, height: availableSize.height)
|
||||
)
|
||||
|
||||
let scrubberFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - scrubberSize.width) / 2.0), y: availableSize.height - environment.safeInsets.bottom - scrubberSize.height + controlsBottomInset - inputPanelSize.height + 3.0), size: scrubberSize)
|
||||
if let scrubberView = scrubber.view {
|
||||
var animateIn = false
|
||||
if scrubberView.superview == nil {
|
||||
animateIn = true
|
||||
if let inputPanelBackgroundView = self.inputPanelBackground.view, inputPanelBackgroundView.superview != nil {
|
||||
self.insertSubview(scrubberView, belowSubview: inputPanelBackgroundView)
|
||||
} else {
|
||||
self.addSubview(scrubberView)
|
||||
}
|
||||
}
|
||||
if animateIn {
|
||||
scrubberView.frame = scrubberFrame
|
||||
} else {
|
||||
scrubberTransition.setFrame(view: scrubberView, frame: scrubberFrame)
|
||||
}
|
||||
if !self.animatingButtons && !(!hasMainVideoTrack && animateIn) {
|
||||
transition.setAlpha(view: scrubberView, alpha: component.isDisplayingTool != nil || component.isDismissing || component.isInteractingWithEntities || isEditingCaption || isRecordingAdditionalVideo || isEditingTextEntity ? 0.0 : 1.0)
|
||||
} else if animateIn {
|
||||
scrubberView.layer.animatePosition(from: CGPoint(x: 0.0, y: 44.0), to: .zero, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
|
||||
scrubberView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
scrubberView.layer.animateScale(from: 0.6, to: 1.0, duration: 0.2)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if let scrubber = self.scrubber {
|
||||
self.scrubber = nil
|
||||
if let scrubberView = scrubber.view {
|
||||
scrubberView.layer.animatePosition(from: .zero, to: CGPoint(x: 0.0, y: 44.0), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true)
|
||||
scrubberView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in
|
||||
scrubberView.removeFromSuperview()
|
||||
})
|
||||
scrubberView.layer.animateScale(from: 1.0, to: 0.6, duration: 0.2, removeOnCompletion: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if case .stickerEditor = controller.mode {
|
||||
var stickerButtonsHidden = buttonsAreHidden
|
||||
if let displayingTool = component.isDisplayingTool, [.cutoutErase, .cutoutRestore].contains(displayingTool) {
|
||||
|
|
@ -2546,6 +2554,8 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
|||
|
||||
var recording: MediaEditorScreen.Recording
|
||||
|
||||
private let locationManager = LocationManager()
|
||||
|
||||
private var presentationData: PresentationData
|
||||
private var validLayout: ContainerViewLayout?
|
||||
|
||||
|
|
@ -2878,6 +2888,12 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
|||
if isFromCamera && mediaDimensions.width > mediaDimensions.height {
|
||||
mediaEntity.scale = storyDimensions.height / fittedSize.height
|
||||
}
|
||||
|
||||
if case .botPreview = controller.mode {
|
||||
if fittedSize.width / fittedSize.height < storyDimensions.width / storyDimensions.height {
|
||||
mediaEntity.scale = storyDimensions.height / fittedSize.height
|
||||
}
|
||||
}
|
||||
|
||||
let initialValues: MediaEditorValues?
|
||||
if case let .draft(draft, _) = subject {
|
||||
|
|
@ -4571,7 +4587,37 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
|||
self.mediaEditor?.play()
|
||||
}
|
||||
|
||||
func addWeather(_ weather: StickerPickerScreen.Weather.LoadedWeather) {
|
||||
func requestWeather() {
|
||||
|
||||
}
|
||||
|
||||
func presentLocationAccessAlert() {
|
||||
DeviceAccess.authorizeAccess(to: .location(.send), locationManager: self.locationManager, presentationData: self.presentationData, present: { [weak self] c, a in
|
||||
self?.controller?.present(c, in: .window(.root), with: a)
|
||||
}, openSettings: { [weak self] in
|
||||
self?.context.sharedContext.applicationBindings.openSettings()
|
||||
}, { [weak self] authorized in
|
||||
guard let self, authorized else {
|
||||
return
|
||||
}
|
||||
let weatherPromise = Promise<StickerPickerScreen.Weather>()
|
||||
weatherPromise.set(getWeather(context: self.context))
|
||||
self.weatherPromise = weatherPromise
|
||||
|
||||
let _ = (weatherPromise.get()
|
||||
|> deliverOnMainQueue).start(next: { [weak self] result in
|
||||
if let self, case let .loaded(weather) = result {
|
||||
self.addWeather(weather)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func addWeather(_ weather: StickerPickerScreen.Weather.LoadedWeather?) {
|
||||
guard let weather else {
|
||||
|
||||
return
|
||||
}
|
||||
let maxWeatherCount = 3
|
||||
var currentWeatherCount = 0
|
||||
self.entitiesView.eachView { entityView in
|
||||
|
|
@ -4935,9 +4981,16 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
|||
if let self {
|
||||
if let weatherPromise = self.weatherPromise {
|
||||
let _ = (weatherPromise.get()
|
||||
|> take(1)).start(next: { [weak self] weather in
|
||||
if let self, case let .loaded(loaded) = weather {
|
||||
self.addWeather(loaded)
|
||||
|> take(1)).start(next: { [weak self] result in
|
||||
if let self {
|
||||
switch result {
|
||||
case let .loaded(weather):
|
||||
self.addWeather(weather)
|
||||
case .notDetermined, .notAllowed:
|
||||
self.presentLocationAccessAlert()
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import TelegramCore
|
|||
import StickerPickerScreen
|
||||
import AccountContext
|
||||
import DeviceLocationManager
|
||||
import DeviceAccess
|
||||
|
||||
struct StoryWeather {
|
||||
let emoji: String
|
||||
|
|
@ -50,33 +51,44 @@ func getWeather(context: AccountContext) -> Signal<StickerPickerScreen.Weather,
|
|||
guard let locationManager = context.sharedContext.locationManager else {
|
||||
return .single(.none)
|
||||
}
|
||||
return .single(.fetching)
|
||||
|> then(
|
||||
currentLocationManagerCoordinate(manager: locationManager, timeout: 5.0)
|
||||
|> mapToSignal { location in
|
||||
if let location {
|
||||
return getWeatherData(context: context, location: location)
|
||||
|> mapToSignal { weather in
|
||||
if let weather {
|
||||
let effectiveEmoji = emojiFor(for: weather.emoji.strippedEmoji, date: Date(), location: location)
|
||||
if let match = context.animatedEmojiStickersValue[effectiveEmoji]?.first {
|
||||
return .single(.loaded(StickerPickerScreen.Weather.LoadedWeather(
|
||||
emoji: effectiveEmoji,
|
||||
emojiFile: match.file,
|
||||
temperature: weather.temperature
|
||||
)))
|
||||
} else {
|
||||
return .single(.none)
|
||||
|
||||
return DeviceAccess.authorizationStatus(subject: .location(.send))
|
||||
|> mapToSignal { status in
|
||||
switch status {
|
||||
case .notDetermined:
|
||||
return .single(.notDetermined)
|
||||
case .denied, .restricted, .unreachable:
|
||||
return .single(.notAllowed)
|
||||
case .allowed:
|
||||
return .single(.fetching)
|
||||
|> then(
|
||||
currentLocationManagerCoordinate(manager: locationManager, timeout: 5.0)
|
||||
|> mapToSignal { location in
|
||||
if let location {
|
||||
return getWeatherData(context: context, location: location)
|
||||
|> mapToSignal { weather in
|
||||
if let weather {
|
||||
let effectiveEmoji = emojiFor(for: weather.emoji.strippedEmoji, date: Date(), location: location)
|
||||
if let match = context.animatedEmojiStickersValue[effectiveEmoji]?.first {
|
||||
return .single(.loaded(StickerPickerScreen.Weather.LoadedWeather(
|
||||
emoji: effectiveEmoji,
|
||||
emojiFile: match.file,
|
||||
temperature: weather.temperature
|
||||
)))
|
||||
} else {
|
||||
return .single(.none)
|
||||
}
|
||||
} else {
|
||||
return .single(.none)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return .single(.none)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return .single(.none)
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private struct WeatherBotConfiguration {
|
||||
|
|
|
|||
|
|
@ -88,6 +88,7 @@ public final class MediaScrubberComponent: Component {
|
|||
let portalView: PortalView?
|
||||
|
||||
let positionUpdated: (Double, Bool) -> Void
|
||||
let coverPositionUpdated: (Double, Bool, @escaping () -> Void) -> Void
|
||||
let trackTrimUpdated: (Int32, Double, Double, Bool, Bool) -> Void
|
||||
let trackOffsetUpdated: (Int32, Double, Bool) -> Void
|
||||
let trackLongPressed: (Int32, UIView) -> Void
|
||||
|
|
@ -104,6 +105,7 @@ public final class MediaScrubberComponent: Component {
|
|||
tracks: [Track],
|
||||
portalView: PortalView? = nil,
|
||||
positionUpdated: @escaping (Double, Bool) -> Void,
|
||||
coverPositionUpdated: @escaping (Double, Bool, @escaping () -> Void) -> Void = { _, _, _ in },
|
||||
trackTrimUpdated: @escaping (Int32, Double, Double, Bool, Bool) -> Void,
|
||||
trackOffsetUpdated: @escaping (Int32, Double, Bool) -> Void,
|
||||
trackLongPressed: @escaping (Int32, UIView) -> Void
|
||||
|
|
@ -119,6 +121,7 @@ public final class MediaScrubberComponent: Component {
|
|||
self.tracks = tracks
|
||||
self.portalView = portalView
|
||||
self.positionUpdated = positionUpdated
|
||||
self.coverPositionUpdated = coverPositionUpdated
|
||||
self.trackTrimUpdated = trackTrimUpdated
|
||||
self.trackOffsetUpdated = trackOffsetUpdated
|
||||
self.trackLongPressed = trackLongPressed
|
||||
|
|
@ -164,6 +167,7 @@ public final class MediaScrubberComponent: Component {
|
|||
|
||||
private var selectedTrackId: Int32 = 0
|
||||
private var isPanningCursor = false
|
||||
private var ignoreCursorPositionUpdate = false
|
||||
|
||||
private var scrubberSize: CGSize?
|
||||
|
||||
|
|
@ -327,10 +331,18 @@ public final class MediaScrubberComponent: Component {
|
|||
switch gestureRecognizer.state {
|
||||
case .began, .changed:
|
||||
self.isPanningCursor = true
|
||||
component.positionUpdated(position, false)
|
||||
if case .cover = component.style {
|
||||
component.coverPositionUpdated(position, false, {})
|
||||
} else {
|
||||
component.positionUpdated(position, false)
|
||||
}
|
||||
case .ended, .cancelled:
|
||||
self.isPanningCursor = false
|
||||
component.positionUpdated(position, true)
|
||||
if case .cover = component.style {
|
||||
component.coverPositionUpdated(position, false, {})
|
||||
} else {
|
||||
component.positionUpdated(position, true)
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
|
@ -345,7 +357,7 @@ public final class MediaScrubberComponent: Component {
|
|||
var y: CGFloat = -5.0 - UIScreenPixel
|
||||
if let component = self.component, case .cover = component.style {
|
||||
cursorWidth = 30.0 + 12.0
|
||||
cursorMargin = 0.0
|
||||
cursorMargin = handleWidth
|
||||
height = 50.0
|
||||
isCover = true
|
||||
y += 1.0
|
||||
|
|
@ -472,6 +484,23 @@ public final class MediaScrubberComponent: Component {
|
|||
} else {
|
||||
trackTransition = .immediate
|
||||
trackView = TrackView()
|
||||
trackView.onTap = { [weak self] fraction in
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
}
|
||||
var position = max(self.startPosition, min(self.endPosition, self.trimDuration * fraction))
|
||||
if let offset = self.mainAudioTrackOffset {
|
||||
position += offset
|
||||
}
|
||||
self.ignoreCursorPositionUpdate = true
|
||||
component.coverPositionUpdated(position, true, { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.ignoreCursorPositionUpdate = false
|
||||
self.state?.updated(transition: .immediate)
|
||||
})
|
||||
}
|
||||
trackView.onSelection = { [weak self] id in
|
||||
guard let self else {
|
||||
return
|
||||
|
|
@ -659,13 +688,15 @@ public final class MediaScrubberComponent: Component {
|
|||
self.cursorPositionAnimation = nil
|
||||
self.cursorDisplayLink?.isPaused = true
|
||||
|
||||
var cursorPosition = component.position
|
||||
if let offset = self.mainAudioTrackOffset {
|
||||
cursorPosition -= offset
|
||||
if !self.ignoreCursorPositionUpdate {
|
||||
var cursorPosition = component.position
|
||||
if let offset = self.mainAudioTrackOffset {
|
||||
cursorPosition -= offset
|
||||
}
|
||||
let cursorFrame = cursorFrame(size: scrubberSize, height: self.effectiveCursorHeight, position: cursorPosition, duration: trimDuration)
|
||||
transition.setFrame(view: self.cursorView, frame: cursorFrame)
|
||||
transition.setFrame(view: self.cursorContentView, frame: cursorFrame.insetBy(dx: 6.0, dy: 2.0).offsetBy(dx: -1.0 - UIScreenPixel, dy: 0.0))
|
||||
}
|
||||
let cursorFrame = cursorFrame(size: scrubberSize, height: self.effectiveCursorHeight, position: cursorPosition, duration: trimDuration)
|
||||
transition.setFrame(view: self.cursorView, frame: cursorFrame)
|
||||
transition.setFrame(view: self.cursorContentView, frame: cursorFrame.insetBy(dx: 6.0, dy: 2.0).offsetBy(dx: -1.0 - UIScreenPixel, dy: 0.0))
|
||||
} else {
|
||||
if let (_, _, end, ended) = self.cursorPositionAnimation {
|
||||
if ended, component.position >= self.startPosition && component.position < end - 1.0 {
|
||||
|
|
@ -718,6 +749,7 @@ private class TrackView: UIView, UIScrollViewDelegate, UIGestureRecognizerDelega
|
|||
fileprivate var videoOpaqueFrameLayers: [VideoFrameLayer] = []
|
||||
|
||||
var onSelection: (Int32) -> Void = { _ in }
|
||||
var onTap: (CGFloat) -> Void = { _ in }
|
||||
var offsetUpdated: (Double, Bool) -> Void = { _, _ in }
|
||||
var updated: (ComponentTransition) -> Void = { _ in }
|
||||
|
||||
|
|
@ -794,10 +826,15 @@ private class TrackView: UIView, UIScrollViewDelegate, UIGestureRecognizerDelega
|
|||
}
|
||||
|
||||
@objc private func handleTap(_ gestureRecognizer: UITapGestureRecognizer) {
|
||||
guard let (track, _, _, _) = self.params else {
|
||||
guard let params = self.params else {
|
||||
return
|
||||
}
|
||||
self.onSelection(track.id)
|
||||
if case .cover = params.style {
|
||||
let location = gestureRecognizer.location(in: self)
|
||||
self.onTap(location.x / self.frame.width)
|
||||
} else {
|
||||
self.onSelection(params.track.id)
|
||||
}
|
||||
}
|
||||
|
||||
private func updateTrackOffset(done: Bool) {
|
||||
|
|
@ -841,6 +878,7 @@ private class TrackView: UIView, UIScrollViewDelegate, UIGestureRecognizerDelega
|
|||
}
|
||||
|
||||
private var params: (
|
||||
style: MediaScrubberComponent.Style,
|
||||
track: MediaScrubberComponent.Track,
|
||||
isSelected: Bool,
|
||||
availableSize: CGSize,
|
||||
|
|
@ -889,7 +927,7 @@ private class TrackView: UIView, UIScrollViewDelegate, UIGestureRecognizerDelega
|
|||
transition: ComponentTransition
|
||||
) -> CGSize {
|
||||
let previousParams = self.params
|
||||
self.params = (track, isSelected, availableSize, duration)
|
||||
self.params = (style, track, isSelected, availableSize, duration)
|
||||
|
||||
let fullTrackHeight: CGFloat
|
||||
let framesCornerRadius: CGFloat
|
||||
|
|
|
|||
|
|
@ -649,7 +649,7 @@ public class MinimizedContainerImpl: ASDisplayNode, MinimizedContainer, ASScroll
|
|||
scrollView.isScrollEnabled = false
|
||||
scrollView.panGestureRecognizer.isEnabled = false
|
||||
scrollView.panGestureRecognizer.isEnabled = true
|
||||
scrollView.contentOffset = contentOffset
|
||||
scrollView.setContentOffset(contentOffset, animated: false)
|
||||
self.currentTransition = .collapse
|
||||
self.requestUpdate(transition: .animated(duration: 0.4, curve: .customSpring(damping: 180.0, initialVelocity: initialVelocity)))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2014,7 +2014,7 @@ final class ShareWithPeersScreenComponent: Component {
|
|||
hasCategories = true
|
||||
}
|
||||
let sendAsPeersCount = component.stateContext.stateValue?.sendAsPeers.count ?? 1
|
||||
if sendAsPeersCount > 1 && !"".isEmpty {
|
||||
if sendAsPeersCount > 1 {
|
||||
hasChannels = true
|
||||
}
|
||||
if let currentHasChannels = self.currentHasChannels, currentHasChannels != hasChannels {
|
||||
|
|
@ -2618,7 +2618,11 @@ final class ShareWithPeersScreenComponent: Component {
|
|||
inset += 10.0 + environment.safeInsets.bottom + 50.0 + footersTotalHeight
|
||||
} else {
|
||||
if !hasCategories {
|
||||
inset = 314.0
|
||||
if self.selectedOptions.contains(.pin) {
|
||||
inset = 422.0
|
||||
} else {
|
||||
inset = 314.0
|
||||
}
|
||||
inset += 10.0 + environment.safeInsets.bottom + 50.0 + footersTotalHeight
|
||||
} else {
|
||||
if hasChannels {
|
||||
|
|
|
|||
|
|
@ -200,8 +200,8 @@ private final class StarsTransactionSheetContent: CombinedComponent {
|
|||
switch subject {
|
||||
case let .transaction(transaction, parentPeer):
|
||||
if transaction.flags.contains(.isGift) {
|
||||
titleText = "Received Gift"
|
||||
descriptionText = "Use Stars to unlock content and services on Telegram. [See Examples >]()"
|
||||
titleText = strings.Stars_Gift_Received_Title
|
||||
descriptionText = strings.Stars_Gift_Received_Text
|
||||
count = transaction.count
|
||||
countOnTop = true
|
||||
transactionId = transaction.id
|
||||
|
|
@ -218,7 +218,6 @@ private final class StarsTransactionSheetContent: CombinedComponent {
|
|||
photo = nil
|
||||
isRefund = false
|
||||
isGift = true
|
||||
delayedCloseOnOpenPeer = false
|
||||
} else {
|
||||
switch transaction.peer {
|
||||
case let .peer(peer):
|
||||
|
|
@ -320,9 +319,9 @@ private final class StarsTransactionSheetContent: CombinedComponent {
|
|||
delayedCloseOnOpenPeer = false
|
||||
case let .gift(message):
|
||||
let incoming = message.flags.contains(.Incoming)
|
||||
titleText = incoming ? "Received Gift" : "Sent Gift"
|
||||
titleText = incoming ? strings.Stars_Gift_Received_Title : strings.Stars_Gift_Sent_Title
|
||||
let peerName = state.peerMap[message.id.peerId]?.compactDisplayTitle ?? ""
|
||||
descriptionText = incoming ? "Use Stars to unlock content and services on Telegram. [See Examples >]()" : "With Stars, \(peerName) will be able to unlock content and services on Telegram. [See Examples >]()"
|
||||
descriptionText = incoming ? strings.Stars_Gift_Received_Text : strings.Stars_Gift_Sent_Text(peerName).string
|
||||
if let action = message.media.first(where: { $0 is TelegramMediaAction }) as? TelegramMediaAction, case let .giftStars(_, _, countValue, _, _, _) = action.action {
|
||||
count = countValue
|
||||
if !incoming {
|
||||
|
|
@ -346,7 +345,6 @@ private final class StarsTransactionSheetContent: CombinedComponent {
|
|||
photo = nil
|
||||
isRefund = false
|
||||
isGift = true
|
||||
delayedCloseOnOpenPeer = false
|
||||
}
|
||||
if let spaceRegex {
|
||||
let nsRange = NSRange(descriptionText.startIndex..., in: descriptionText)
|
||||
|
|
@ -484,10 +482,7 @@ private final class StarsTransactionSheetContent: CombinedComponent {
|
|||
)
|
||||
),
|
||||
action: {
|
||||
if toPeer.id.namespace == Namespaces.Peer.CloudUser && toPeer.id.id._internalGetInt64Value() == 777000 {
|
||||
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
|
||||
component.context.sharedContext.openExternalUrl(context: component.context, urlContext: .generic, url: strings.Stars_Transaction_FragmentUnknown_URL, forceExternal: true, presentationData: presentationData, navigationController: nil, dismissInput: {})
|
||||
} else if delayedCloseOnOpenPeer {
|
||||
if delayedCloseOnOpenPeer {
|
||||
component.openPeer(toPeer)
|
||||
Queue.mainQueue().after(1.0, {
|
||||
component.cancel(false)
|
||||
|
|
@ -607,8 +602,11 @@ private final class StarsTransactionSheetContent: CombinedComponent {
|
|||
}
|
||||
},
|
||||
tapAction: { attributes, _ in
|
||||
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
|
||||
component.context.sharedContext.openExternalUrl(context: component.context, urlContext: .generic, url: strings.Stars_Transaction_Terms_URL, forceExternal: true, presentationData: presentationData, navigationController: nil, dismissInput: {})
|
||||
if let controller = controller() as? StarsTransactionScreen, let navigationController = controller.navigationController as? NavigationController {
|
||||
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
|
||||
component.context.sharedContext.openExternalUrl(context: component.context, urlContext: .generic, url: strings.Stars_Transaction_Terms_URL, forceExternal: false, presentationData: presentationData, navigationController: navigationController, dismissInput: {})
|
||||
component.cancel(true)
|
||||
}
|
||||
}
|
||||
),
|
||||
availableSize: CGSize(width: context.availableSize.width - textSideInset * 2.0, height: context.availableSize.height),
|
||||
|
|
|
|||
|
|
@ -466,8 +466,10 @@ final class StarsStatisticsScreenComponent: Component {
|
|||
return nil
|
||||
}
|
||||
},
|
||||
tapAction: { attributes, _ in
|
||||
component.context.sharedContext.openExternalUrl(context: component.context, urlContext: .generic, url: strings.Stars_BotRevenue_Withdraw_Info_URL, forceExternal: true, presentationData: presentationData, navigationController: nil, dismissInput: {})
|
||||
tapAction: { [weak self] attributes, _ in
|
||||
if let controller = self?.controller?() as? StarsStatisticsScreen, let navigationController = controller.navigationController as? NavigationController {
|
||||
component.context.sharedContext.openExternalUrl(context: component.context, urlContext: .generic, url: strings.Stars_BotRevenue_Withdraw_Info_URL, forceExternal: false, presentationData: presentationData, navigationController: navigationController, dismissInput: {})
|
||||
}
|
||||
}
|
||||
)),
|
||||
items: [AnyComponentWithIdentity(id: 0, component: AnyComponent(
|
||||
|
|
|
|||
|
|
@ -857,8 +857,13 @@ public final class StarsTransactionsScreen: ViewControllerComponentContainer {
|
|||
return
|
||||
}
|
||||
|
||||
if let navigationController = self.navigationController as? NavigationController {
|
||||
var controllers = navigationController.viewControllers
|
||||
controllers = controllers.filter { !($0 is ContactSelectionController) }
|
||||
navigationController.setViewControllers(controllers, animated: true)
|
||||
}
|
||||
|
||||
Queue.mainQueue().after(2.0) {
|
||||
//TODO:localize
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
let resultController = UndoOverlayController(
|
||||
presentationData: presentationData,
|
||||
|
|
@ -867,8 +872,8 @@ public final class StarsTransactionsScreen: ViewControllerComponentContainer {
|
|||
scale: 0.066,
|
||||
colors: [:],
|
||||
title: nil,
|
||||
text: "\(stars) Stars sent.",
|
||||
customUndoText: "View Chat",
|
||||
text: presentationData.strings.Stars_Intro_StarsSent(Int32(stars)),
|
||||
customUndoText: presentationData.strings.Stars_Intro_StarsSent_ViewChat,
|
||||
timeout: nil
|
||||
),
|
||||
elevatedLayout: false,
|
||||
|
|
|
|||
|
|
@ -66,6 +66,8 @@ private final class SheetContent: CombinedComponent {
|
|||
let component = context.component
|
||||
let state = context.state
|
||||
|
||||
let controller = environment.controller
|
||||
|
||||
let theme = environment.theme.withModalBlocksBackground()
|
||||
let strings = environment.strings
|
||||
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
|
||||
|
|
@ -229,7 +231,9 @@ private final class SheetContent: CombinedComponent {
|
|||
}
|
||||
},
|
||||
tapAction: { attributes, _ in
|
||||
component.context.sharedContext.openExternalUrl(context: component.context, urlContext: .generic, url: strings.Stars_PaidContent_AmountInfo_URL, forceExternal: true, presentationData: presentationData, navigationController: nil, dismissInput: {})
|
||||
if let controller = controller() as? StarsWithdrawScreen, let navigationController = controller.navigationController as? NavigationController {
|
||||
component.context.sharedContext.openExternalUrl(context: component.context, urlContext: .generic, url: strings.Stars_PaidContent_AmountInfo_URL, forceExternal: false, presentationData: presentationData, navigationController: navigationController, dismissInput: {})
|
||||
}
|
||||
}
|
||||
))
|
||||
case let .reaction(starsToTop):
|
||||
|
|
@ -307,7 +311,6 @@ private final class SheetContent: CombinedComponent {
|
|||
buttonAttributedString.addAttribute(.baselineOffset, value: 1.0, range: NSRange(range, in: buttonAttributedString.string))
|
||||
}
|
||||
|
||||
let controller = environment.controller
|
||||
let button = button.update(
|
||||
component: ButtonComponent(
|
||||
background: ButtonComponent.Background(
|
||||
|
|
|
|||
|
|
@ -2065,6 +2065,8 @@ public class StickerPickerScreen: ViewController {
|
|||
}
|
||||
|
||||
case none
|
||||
case notDetermined
|
||||
case notAllowed
|
||||
case fetching
|
||||
case loaded(StickerPickerScreen.Weather.LoadedWeather)
|
||||
}
|
||||
|
|
@ -2722,66 +2724,64 @@ final class StoryStickersContentView: UIView, EmojiCustomContentView {
|
|||
|
||||
} else {
|
||||
maxHorizontalItems = 3
|
||||
|
||||
|
||||
let weatherButtonContent: AnyComponent<Empty>
|
||||
switch self.weather {
|
||||
case .notAllowed, .notDetermined:
|
||||
weatherButtonContent = AnyComponent(
|
||||
InteractiveStickerButtonContent(
|
||||
context: self.context,
|
||||
theme: theme,
|
||||
title: stringForTemperature(24),
|
||||
iconName: "☀️",
|
||||
iconFile: self.context.animatedEmojiStickersValue["☀️"]?.first?.file,
|
||||
useOpaqueTheme: useOpaqueTheme,
|
||||
tintContainerView: self.tintContainerView
|
||||
)
|
||||
)
|
||||
case let .loaded(weather):
|
||||
items.append(
|
||||
AnyComponentWithIdentity(
|
||||
id: "weather",
|
||||
component: AnyComponent(
|
||||
CameraButton(
|
||||
content: AnyComponentWithIdentity(
|
||||
id: "weather",
|
||||
component: AnyComponent(
|
||||
InteractiveStickerButtonContent(
|
||||
context: self.context,
|
||||
theme: theme,
|
||||
title: stringForTemperature(weather.temperature),
|
||||
iconName: weather.emoji,
|
||||
iconFile: weather.emojiFile,
|
||||
useOpaqueTheme: useOpaqueTheme,
|
||||
tintContainerView: self.tintContainerView
|
||||
)
|
||||
)
|
||||
),
|
||||
action: { [weak self] in
|
||||
if let self {
|
||||
self.weatherAction()
|
||||
}
|
||||
})
|
||||
)
|
||||
weatherButtonContent = AnyComponent(
|
||||
InteractiveStickerButtonContent(
|
||||
context: self.context,
|
||||
theme: theme,
|
||||
title: stringForTemperature(weather.temperature),
|
||||
iconName: weather.emoji,
|
||||
iconFile: weather.emojiFile,
|
||||
useOpaqueTheme: useOpaqueTheme,
|
||||
tintContainerView: self.tintContainerView
|
||||
)
|
||||
)
|
||||
case .fetching:
|
||||
items.append(
|
||||
AnyComponentWithIdentity(
|
||||
id: "weather",
|
||||
component: AnyComponent(
|
||||
CameraButton(
|
||||
content: AnyComponentWithIdentity(
|
||||
id: "weather",
|
||||
component: AnyComponent(
|
||||
InteractiveStickerButtonContent(
|
||||
context: self.context,
|
||||
theme: theme,
|
||||
title: nil,
|
||||
iconName: nil,
|
||||
useOpaqueTheme: useOpaqueTheme,
|
||||
tintContainerView: self.tintContainerView
|
||||
)
|
||||
)
|
||||
),
|
||||
action: { [weak self] in
|
||||
if let self {
|
||||
self.weatherAction()
|
||||
}
|
||||
})
|
||||
)
|
||||
weatherButtonContent = AnyComponent(
|
||||
InteractiveStickerButtonContent(
|
||||
context: self.context,
|
||||
theme: theme,
|
||||
title: nil,
|
||||
iconName: nil,
|
||||
useOpaqueTheme: useOpaqueTheme,
|
||||
tintContainerView: self.tintContainerView
|
||||
)
|
||||
)
|
||||
default:
|
||||
fatalError()
|
||||
}
|
||||
items.append(
|
||||
AnyComponentWithIdentity(
|
||||
id: "weather",
|
||||
component: AnyComponent(
|
||||
CameraButton(
|
||||
content: AnyComponentWithIdentity(
|
||||
id: "weather",
|
||||
component: weatherButtonContent
|
||||
),
|
||||
action: { [weak self] in
|
||||
if let self {
|
||||
self.weatherAction()
|
||||
}
|
||||
})
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
items.append(
|
||||
|
|
|
|||
|
|
@ -81,7 +81,11 @@ struct CameraState: Equatable {
|
|||
}
|
||||
|
||||
func updatedRecording(_ recording: Recording) -> CameraState {
|
||||
return CameraState(position: self.position, flashMode: self.flashMode, flashModeDidChange: self.flashModeDidChange, flashTint: self.flashTint, flashTintSize: self.flashTintSize, recording: recording, duration: self.duration, isDualCameraEnabled: self.isDualCameraEnabled, isViewOnceEnabled: self.isViewOnceEnabled)
|
||||
var flashModeDidChange = self.flashModeDidChange
|
||||
if case .none = self.recording {
|
||||
flashModeDidChange = false
|
||||
}
|
||||
return CameraState(position: self.position, flashMode: self.flashMode, flashModeDidChange: flashModeDidChange, flashTint: self.flashTint, flashTintSize: self.flashTintSize, recording: recording, duration: self.duration, isDualCameraEnabled: self.isDualCameraEnabled, isViewOnceEnabled: self.isViewOnceEnabled)
|
||||
}
|
||||
|
||||
func updatedDuration(_ duration: Double) -> CameraState {
|
||||
|
|
@ -121,6 +125,7 @@ private final class VideoMessageCameraScreenComponent: CombinedComponent {
|
|||
let push: (ViewController) -> Void
|
||||
let startRecording: ActionSlot<Void>
|
||||
let stopRecording: ActionSlot<Void>
|
||||
let cancelRecording: ActionSlot<Void>
|
||||
let completion: ActionSlot<VideoMessageCameraScreen.CaptureResult>
|
||||
|
||||
init(
|
||||
|
|
@ -135,6 +140,7 @@ private final class VideoMessageCameraScreenComponent: CombinedComponent {
|
|||
push: @escaping (ViewController) -> Void,
|
||||
startRecording: ActionSlot<Void>,
|
||||
stopRecording: ActionSlot<Void>,
|
||||
cancelRecording: ActionSlot<Void>,
|
||||
completion: ActionSlot<VideoMessageCameraScreen.CaptureResult>
|
||||
) {
|
||||
self.context = context
|
||||
|
|
@ -148,6 +154,7 @@ private final class VideoMessageCameraScreenComponent: CombinedComponent {
|
|||
self.push = push
|
||||
self.startRecording = startRecording
|
||||
self.stopRecording = stopRecording
|
||||
self.cancelRecording = cancelRecording
|
||||
self.completion = completion
|
||||
}
|
||||
|
||||
|
|
@ -216,6 +223,7 @@ private final class VideoMessageCameraScreenComponent: CombinedComponent {
|
|||
private let present: (ViewController) -> Void
|
||||
private let startRecording: ActionSlot<Void>
|
||||
private let stopRecording: ActionSlot<Void>
|
||||
private let cancelRecording: ActionSlot<Void>
|
||||
private let completion: ActionSlot<VideoMessageCameraScreen.CaptureResult>
|
||||
private let getController: () -> VideoMessageCameraScreen?
|
||||
|
||||
|
|
@ -234,6 +242,7 @@ private final class VideoMessageCameraScreenComponent: CombinedComponent {
|
|||
present: @escaping (ViewController) -> Void,
|
||||
startRecording: ActionSlot<Void>,
|
||||
stopRecording: ActionSlot<Void>,
|
||||
cancelRecording: ActionSlot<Void>,
|
||||
completion: ActionSlot<VideoMessageCameraScreen.CaptureResult>,
|
||||
getController: @escaping () -> VideoMessageCameraScreen? = {
|
||||
return nil
|
||||
|
|
@ -243,6 +252,7 @@ private final class VideoMessageCameraScreenComponent: CombinedComponent {
|
|||
self.present = present
|
||||
self.startRecording = startRecording
|
||||
self.stopRecording = stopRecording
|
||||
self.cancelRecording = cancelRecording
|
||||
self.completion = completion
|
||||
self.getController = getController
|
||||
|
||||
|
|
@ -260,6 +270,10 @@ private final class VideoMessageCameraScreenComponent: CombinedComponent {
|
|||
self.stopRecording.connect({ [weak self] _ in
|
||||
self?.stopVideoRecording()
|
||||
})
|
||||
|
||||
self.cancelRecording.connect({ [weak self] _ in
|
||||
self?.cancelVideoRecording()
|
||||
})
|
||||
}
|
||||
|
||||
deinit {
|
||||
|
|
@ -284,19 +298,28 @@ private final class VideoMessageCameraScreenComponent: CombinedComponent {
|
|||
}
|
||||
self.lastFlipTimestamp = currentTimestamp
|
||||
|
||||
let isFrontCamera = controller.cameraState.position == .back
|
||||
camera.togglePosition()
|
||||
|
||||
|
||||
self.hapticFeedback.impact(.veryLight)
|
||||
|
||||
self.updateScreenBrightness(isFrontCamera: isFrontCamera)
|
||||
|
||||
if isFrontCamera {
|
||||
camera.setTorchActive(false)
|
||||
} else {
|
||||
camera.setTorchActive(controller.cameraState.flashMode == .on)
|
||||
}
|
||||
}
|
||||
|
||||
func toggleFlashMode() {
|
||||
guard let controller = self.getController(), let camera = controller.camera else {
|
||||
return
|
||||
}
|
||||
var flashOn = false
|
||||
var isFlashOn = false
|
||||
switch controller.cameraState.flashMode {
|
||||
case .off:
|
||||
flashOn = true
|
||||
isFlashOn = true
|
||||
camera.setFlashMode(.on)
|
||||
case .on:
|
||||
camera.setFlashMode(.off)
|
||||
|
|
@ -305,22 +328,29 @@ private final class VideoMessageCameraScreenComponent: CombinedComponent {
|
|||
}
|
||||
self.hapticFeedback.impact(.light)
|
||||
|
||||
self.updateScreenBrightness(flashOn: flashOn)
|
||||
self.updateScreenBrightness(isFlashOn: isFlashOn)
|
||||
|
||||
if controller.cameraState.position == .back {
|
||||
if isFlashOn {
|
||||
camera.setTorchActive(true)
|
||||
} else {
|
||||
camera.setTorchActive(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var initialBrightness: CGFloat?
|
||||
private var brightnessArguments: (Double, Double, CGFloat, CGFloat)?
|
||||
private var brightnessAnimator: ConstantDisplayLinkAnimator?
|
||||
|
||||
func updateScreenBrightness(flashOn: Bool?) {
|
||||
func updateScreenBrightness(isFrontCamera: Bool? = nil, isFlashOn: Bool? = nil) {
|
||||
guard let controller = self.getController() else {
|
||||
return
|
||||
}
|
||||
let isFrontCamera = controller.cameraState.position == .front
|
||||
let isVideo = true
|
||||
let isFlashOn = flashOn ?? (controller.cameraState.flashMode == .on)
|
||||
let isFrontCamera = isFrontCamera ?? (controller.cameraState.position == .front)
|
||||
let isFlashOn = isFlashOn ?? (controller.cameraState.flashMode == .on)
|
||||
|
||||
if isFrontCamera && isVideo && isFlashOn {
|
||||
if isFrontCamera && isFlashOn {
|
||||
if self.initialBrightness == nil {
|
||||
self.initialBrightness = UIScreen.main.brightness
|
||||
self.brightnessArguments = (CACurrentMediaTime(), 0.2, UIScreen.main.brightness, 1.0)
|
||||
|
|
@ -382,11 +412,11 @@ private final class VideoMessageCameraScreenComponent: CombinedComponent {
|
|||
let isFirstRecording = initialDuration.isZero
|
||||
controller.node.resumeCameraCapture()
|
||||
|
||||
controller.updatePreviewState({ _ in return nil}, transition: .spring(duration: 0.4))
|
||||
|
||||
controller.node.dismissAllTooltips()
|
||||
controller.updateCameraState({ $0.updatedRecording(pressing ? .holding : .handsFree).updatedDuration(initialDuration) }, transition: .spring(duration: 0.4))
|
||||
|
||||
controller.updatePreviewState({ _ in return nil }, transition: .spring(duration: 0.4))
|
||||
|
||||
controller.node.withReadyCamera(isFirstTime: !controller.node.cameraIsActive) {
|
||||
Queue.mainQueue().after(0.15) {
|
||||
self.resultDisposable.set((camera.startRecording()
|
||||
|
|
@ -412,6 +442,10 @@ private final class VideoMessageCameraScreenComponent: CombinedComponent {
|
|||
if initialDuration > 0.0 {
|
||||
controller.onResume()
|
||||
}
|
||||
|
||||
if controller.cameraState.position == .front && controller.cameraState.flashMode == .on {
|
||||
self.updateScreenBrightness()
|
||||
}
|
||||
}
|
||||
|
||||
func stopVideoRecording() {
|
||||
|
|
@ -439,7 +473,7 @@ private final class VideoMessageCameraScreenComponent: CombinedComponent {
|
|||
}
|
||||
}))
|
||||
|
||||
if case .front = controller.cameraState.position, let initialBrightness = self.initialBrightness {
|
||||
if let initialBrightness = self.initialBrightness {
|
||||
self.initialBrightness = nil
|
||||
self.brightnessArguments = (CACurrentMediaTime(), 0.2, UIScreen.main.brightness, initialBrightness)
|
||||
self.animateBrightnessChange()
|
||||
|
|
@ -453,6 +487,14 @@ private final class VideoMessageCameraScreenComponent: CombinedComponent {
|
|||
controller.updateCameraState({ $0.updatedRecording(.handsFree) }, transition: .spring(duration: 0.4))
|
||||
}
|
||||
|
||||
func cancelVideoRecording() {
|
||||
if let initialBrightness = self.initialBrightness {
|
||||
self.initialBrightness = nil
|
||||
self.brightnessArguments = (CACurrentMediaTime(), 0.2, UIScreen.main.brightness, initialBrightness)
|
||||
self.animateBrightnessChange()
|
||||
}
|
||||
}
|
||||
|
||||
func updateZoom(fraction: CGFloat) {
|
||||
guard let camera = self.getController()?.camera else {
|
||||
return
|
||||
|
|
@ -462,7 +504,7 @@ private final class VideoMessageCameraScreenComponent: CombinedComponent {
|
|||
}
|
||||
|
||||
func makeState() -> State {
|
||||
return State(context: self.context, present: self.present, startRecording: self.startRecording, stopRecording: self.stopRecording, completion: self.completion, getController: self.getController)
|
||||
return State(context: self.context, present: self.present, startRecording: self.startRecording, stopRecording: self.stopRecording, cancelRecording: self.cancelRecording, completion: self.completion, getController: self.getController)
|
||||
}
|
||||
|
||||
static var body: Body {
|
||||
|
|
@ -517,7 +559,7 @@ private final class VideoMessageCameraScreenComponent: CombinedComponent {
|
|||
}
|
||||
|
||||
if !component.isPreviewing {
|
||||
if case .on = component.cameraState.flashMode {
|
||||
if case .on = component.cameraState.flashMode, case .front = component.cameraState.position {
|
||||
let frontFlash = frontFlash.update(
|
||||
component: Image(image: state.image(.flashImage, theme: environment.theme), tintColor: component.cameraState.flashTint.color),
|
||||
availableSize: availableSize,
|
||||
|
|
@ -611,7 +653,9 @@ private final class VideoMessageCameraScreenComponent: CombinedComponent {
|
|||
action: { [weak state] in
|
||||
if let state {
|
||||
state.toggleFlashMode()
|
||||
flashAction.invoke(Void())
|
||||
Queue.mainQueue().justDispatch {
|
||||
flashAction.invoke(Void())
|
||||
}
|
||||
}
|
||||
}
|
||||
),
|
||||
|
|
@ -801,6 +845,7 @@ public class VideoMessageCameraScreen: ViewController {
|
|||
|
||||
fileprivate let startRecording = ActionSlot<Void>()
|
||||
fileprivate let stopRecording = ActionSlot<Void>()
|
||||
fileprivate let cancelRecording = ActionSlot<Void>()
|
||||
private let completion = ActionSlot<VideoMessageCameraScreen.CaptureResult>()
|
||||
|
||||
var cameraState: CameraState {
|
||||
|
|
@ -1432,6 +1477,7 @@ public class VideoMessageCameraScreen: ViewController {
|
|||
},
|
||||
startRecording: self.startRecording,
|
||||
stopRecording: self.stopRecording,
|
||||
cancelRecording: self.cancelRecording,
|
||||
completion: self.completion
|
||||
)
|
||||
),
|
||||
|
|
@ -1894,6 +1940,8 @@ public class VideoMessageCameraScreen: ViewController {
|
|||
}
|
||||
|
||||
public func discardVideo() {
|
||||
self.node.cancelRecording.invoke(Void())
|
||||
|
||||
self.requestDismiss(animated: true)
|
||||
}
|
||||
|
||||
|
|
|
|||
BIN
submodules/TelegramUI/Images.xcassets/Instant View/Bookmark.imageset/Bookmark.pdf
vendored
Normal file
BIN
submodules/TelegramUI/Images.xcassets/Instant View/Bookmark.imageset/Bookmark.pdf
vendored
Normal file
Binary file not shown.
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "ic_lt_safari.pdf",
|
||||
"filename" : "Bookmark.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
BIN
submodules/TelegramUI/Images.xcassets/Instant View/Browser.imageset/Browser.pdf
vendored
Normal file
BIN
submodules/TelegramUI/Images.xcassets/Instant View/Browser.imageset/Browser.pdf
vendored
Normal file
Binary file not shown.
12
submodules/TelegramUI/Images.xcassets/Instant View/Browser.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Instant View/Browser.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "Browser.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
83
submodules/TelegramUI/Images.xcassets/Instant View/CloseIcon.imageset/Close.pdf
vendored
Normal file
83
submodules/TelegramUI/Images.xcassets/Instant View/CloseIcon.imageset/Close.pdf
vendored
Normal 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
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "cross.pdf",
|
||||
"filename" : "Close.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
|
|
|
|||
|
|
@ -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
|
||||
Binary file not shown.
12
submodules/TelegramUI/Images.xcassets/Instant View/Settings/Reload.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Instant View/Settings/Reload.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "scheduled.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
86
submodules/TelegramUI/Images.xcassets/Instant View/Settings/Reload.imageset/scheduled.pdf
vendored
Normal file
86
submodules/TelegramUI/Images.xcassets/Instant View/Settings/Reload.imageset/scheduled.pdf
vendored
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
%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 3.334991 3.334991 cm
|
||||
0.000000 0.000000 0.000000 scn
|
||||
8.665000 17.330002 m
|
||||
8.297730 17.330002 8.000000 17.032270 8.000000 16.665001 c
|
||||
8.000000 16.297733 8.297730 16.000002 8.665000 16.000002 c
|
||||
12.716009 16.000002 16.000000 12.716011 16.000000 8.665002 c
|
||||
16.000000 4.613993 12.716009 1.330002 8.665000 1.330002 c
|
||||
4.613991 1.330002 1.330000 4.613993 1.330000 8.665002 c
|
||||
1.330000 11.380140 2.805339 13.751918 5.000000 15.020325 c
|
||||
5.000000 13.165002 l
|
||||
5.000000 12.797732 5.297730 12.500002 5.665000 12.500002 c
|
||||
6.032269 12.500002 6.330000 12.797732 6.330000 13.165002 c
|
||||
6.330000 16.665001 l
|
||||
6.330000 17.032270 6.032269 17.330002 5.665000 17.330002 c
|
||||
2.165000 17.330002 l
|
||||
1.797731 17.330002 1.500000 17.032270 1.500000 16.665001 c
|
||||
1.500000 16.297733 1.797731 16.000002 2.165000 16.000002 c
|
||||
4.050117 16.000002 l
|
||||
1.617408 14.466265 0.000000 11.755083 0.000000 8.665002 c
|
||||
0.000000 3.879455 3.879452 0.000000 8.665000 0.000000 c
|
||||
13.450547 0.000000 17.330002 3.879455 17.330002 8.665002 c
|
||||
17.330002 13.450549 13.450547 17.330002 8.665000 17.330002 c
|
||||
h
|
||||
f*
|
||||
n
|
||||
Q
|
||||
|
||||
endstream
|
||||
endobj
|
||||
|
||||
3 0 obj
|
||||
1114
|
||||
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
|
||||
0000001204 00000 n
|
||||
0000001227 00000 n
|
||||
0000001400 00000 n
|
||||
0000001474 00000 n
|
||||
trailer
|
||||
<< /ID [ (some) (id) ]
|
||||
/Root 6 0 R
|
||||
/Size 7
|
||||
>>
|
||||
startxref
|
||||
1533
|
||||
%%EOF
|
||||
|
|
@ -7129,7 +7129,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
|||
self.didAppear = true
|
||||
|
||||
self.chatDisplayNode.historyNode.experimentalSnapScrollToItem = false
|
||||
self.chatDisplayNode.historyNode.canReadHistory.set(combineLatest(context.sharedContext.applicationBindings.applicationInForeground, self.canReadHistory.get()) |> map { a, b in
|
||||
self.chatDisplayNode.historyNode.canReadHistory.set(combineLatest(self.context.sharedContext.applicationBindings.applicationInForeground, self.canReadHistory.get()) |> map { a, b in
|
||||
return a && b
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import ChatSendMessageActionUI
|
|||
|
||||
class ContactSelectionControllerImpl: ViewController, ContactSelectionController, PresentableController, AttachmentContainable {
|
||||
private let context: AccountContext
|
||||
private let mode: ContactSelectionControllerMode
|
||||
private let autoDismiss: Bool
|
||||
|
||||
fileprivate var contactsNode: ContactSelectionControllerNode {
|
||||
|
|
@ -35,7 +36,7 @@ class ContactSelectionControllerImpl: ViewController, ContactSelectionController
|
|||
|
||||
private let index: PeerNameIndex = .lastNameFirst
|
||||
private let titleProducer: (PresentationStrings) -> String
|
||||
private let options: [ContactListAdditionalOption]
|
||||
private let options: Signal<[ContactListAdditionalOption], NoError>
|
||||
private let displayDeviceContacts: Bool
|
||||
private let displayCallIcons: Bool
|
||||
private let multipleSelection: Bool
|
||||
|
|
@ -94,6 +95,7 @@ class ContactSelectionControllerImpl: ViewController, ContactSelectionController
|
|||
|
||||
init(_ params: ContactSelectionControllerParams) {
|
||||
self.context = params.context
|
||||
self.mode = params.mode
|
||||
self.autoDismiss = params.autoDismiss
|
||||
self.titleProducer = params.title
|
||||
self.options = params.options
|
||||
|
|
@ -207,7 +209,7 @@ class ContactSelectionControllerImpl: ViewController, ContactSelectionController
|
|||
}
|
||||
|
||||
override func loadDisplayNode() {
|
||||
self.displayNode = ContactSelectionControllerNode(context: self.context, presentationData: self.presentationData, options: self.options, displayDeviceContacts: self.displayDeviceContacts, displayCallIcons: self.displayCallIcons, multipleSelection: self.multipleSelection, requirePhoneNumbers: self.requirePhoneNumbers)
|
||||
self.displayNode = ContactSelectionControllerNode(context: self.context, mode: self.mode, presentationData: self.presentationData, options: self.options, displayDeviceContacts: self.displayDeviceContacts, displayCallIcons: self.displayCallIcons, multipleSelection: self.multipleSelection, requirePhoneNumbers: self.requirePhoneNumbers)
|
||||
self._ready.set(self.contactsNode.contactListNode.ready)
|
||||
|
||||
self.contactsNode.navigationBar = self.navigationBar
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ final class ContactSelectionControllerNode: ASDisplayNode {
|
|||
|
||||
var searchContainerNode: ContactsSearchContainerNode?
|
||||
|
||||
init(context: AccountContext, presentationData: PresentationData, options: [ContactListAdditionalOption], displayDeviceContacts: Bool, displayCallIcons: Bool, multipleSelection: Bool, requirePhoneNumbers: Bool) {
|
||||
init(context: AccountContext, mode: ContactSelectionControllerMode, presentationData: PresentationData, options: Signal<[ContactListAdditionalOption], NoError>, displayDeviceContacts: Bool, displayCallIcons: Bool, multipleSelection: Bool, requirePhoneNumbers: Bool) {
|
||||
self.context = context
|
||||
self.presentationData = presentationData
|
||||
self.displayDeviceContacts = displayDeviceContacts
|
||||
|
|
@ -65,10 +65,55 @@ final class ContactSelectionControllerNode: ASDisplayNode {
|
|||
if requirePhoneNumbers {
|
||||
filters.append(.excludeWithoutPhoneNumbers)
|
||||
}
|
||||
if case .starsGifting = mode {
|
||||
filters.append(.excludeBots)
|
||||
}
|
||||
self.filters = filters
|
||||
|
||||
let displayTopPeers: ContactListPresentation.TopPeers
|
||||
if case let .starsGifting(birthdays, hasActions) = mode {
|
||||
if let birthdays {
|
||||
let today = Calendar(identifier: .gregorian).component(.day, from: Date())
|
||||
var sections: [(String, [EnginePeer.Id], Bool)] = []
|
||||
var todayPeers: [EnginePeer.Id] = []
|
||||
var yesterdayPeers: [EnginePeer.Id] = []
|
||||
var tomorrowPeers: [EnginePeer.Id] = []
|
||||
|
||||
for (peerId, birthday) in birthdays {
|
||||
if birthday.day == today {
|
||||
todayPeers.append(peerId)
|
||||
} else if birthday.day == today - 1 || birthday.day > today + 5 {
|
||||
yesterdayPeers.append(peerId)
|
||||
} else if birthday.day == today + 1 || birthday.day < today + 5 {
|
||||
tomorrowPeers.append(peerId)
|
||||
}
|
||||
}
|
||||
|
||||
if !todayPeers.isEmpty {
|
||||
sections.append((presentationData.strings.Premium_Gift_ContactSelection_BirthdayToday, todayPeers, hasActions))
|
||||
}
|
||||
if !yesterdayPeers.isEmpty {
|
||||
sections.append((presentationData.strings.Premium_Gift_ContactSelection_BirthdayYesterday, yesterdayPeers, hasActions))
|
||||
}
|
||||
if !tomorrowPeers.isEmpty {
|
||||
sections.append((presentationData.strings.Premium_Gift_ContactSelection_BirthdayTomorrow, tomorrowPeers, hasActions))
|
||||
}
|
||||
|
||||
displayTopPeers = .custom(sections)
|
||||
} else {
|
||||
displayTopPeers = .recent
|
||||
}
|
||||
} else {
|
||||
displayTopPeers = .none
|
||||
}
|
||||
|
||||
let presentation: Signal<ContactListPresentation, NoError> = options
|
||||
|> map { options in
|
||||
return .natural(options: options, includeChatList: false, topPeers: displayTopPeers)
|
||||
}
|
||||
|
||||
var contextActionImpl: ((EnginePeer, ASDisplayNode, ContextGesture?, CGPoint?) -> Void)?
|
||||
self.contactListNode = ContactListNode(context: context, updatedPresentationData: (presentationData, self.presentationDataPromise.get()), presentation: .single(.natural(options: options, includeChatList: false, topPeers: .none)), filters: filters, onlyWriteable: false, isGroupInvitation: false, displayCallIcons: displayCallIcons, contextAction: multipleSelection ? { peer, node, gesture, _, _ in
|
||||
self.contactListNode = ContactListNode(context: context, updatedPresentationData: (presentationData, self.presentationDataPromise.get()), presentation: presentation, filters: filters, onlyWriteable: false, isGroupInvitation: false, displayCallIcons: displayCallIcons, contextAction: multipleSelection ? { peer, node, gesture, _, _ in
|
||||
contextActionImpl?(peer, node, gesture, nil)
|
||||
} : nil, multipleSelection: multipleSelection)
|
||||
|
||||
|
|
@ -262,7 +307,7 @@ final class ContactSelectionControllerNode: ASDisplayNode {
|
|||
} else {
|
||||
categories.insert(.global)
|
||||
}
|
||||
self.searchDisplayController = SearchDisplayController(presentationData: self.presentationData, contentNode: ContactsSearchContainerNode(context: self.context, updatedPresentationData: (self.presentationData, self.presentationDataPromise.get()), onlyWriteable: false, categories: categories, addContact: nil, openPeer: { [weak self] peer in
|
||||
self.searchDisplayController = SearchDisplayController(presentationData: self.presentationData, contentNode: ContactsSearchContainerNode(context: self.context, updatedPresentationData: (self.presentationData, self.presentationDataPromise.get()), onlyWriteable: false, categories: categories, filters: self.filters, addContact: nil, openPeer: { [weak self] peer in
|
||||
if let strongSelf = self {
|
||||
var updated = false
|
||||
strongSelf.contactListNode.updateSelectionState { state -> ContactListNodeGroupSelectionState? in
|
||||
|
|
|
|||
|
|
@ -1033,7 +1033,7 @@ func openExternalUrlImpl(context: AccountContext, urlContext: OpenURLContext, ur
|
|||
}
|
||||
if accessChallengeData.data.isLockable {
|
||||
if passcodeSettings.autolockTimeout != nil && settings.defaultWebBrowser == nil {
|
||||
settings = WebBrowserSettings(defaultWebBrowser: "safari")
|
||||
settings = WebBrowserSettings(defaultWebBrowser: "safari", exceptions: [])
|
||||
}
|
||||
}
|
||||
return settings
|
||||
|
|
|
|||
|
|
@ -2192,6 +2192,7 @@ public final class SharedAccountContextImpl: SharedAccountContext {
|
|||
var reachedLimitImpl: ((Int32) -> Void)?
|
||||
var presentBirthdayPickerImpl: (() -> Void)?
|
||||
let mode: ContactMultiselectionControllerMode
|
||||
var starsMode: ContactSelectionControllerMode = .generic
|
||||
var currentBirthdays: [EnginePeer.Id: TelegramBirthday]?
|
||||
if case let .chatList(birthdays) = source, let birthdays, !birthdays.isEmpty {
|
||||
mode = .premiumGifting(birthdays: birthdays, selectToday: true, hasActions: true)
|
||||
|
|
@ -2201,6 +2202,7 @@ public final class SharedAccountContextImpl: SharedAccountContext {
|
|||
currentBirthdays = birthdays
|
||||
} else if case let .stars(birthdays) = source {
|
||||
mode = .premiumGifting(birthdays: birthdays, selectToday: false, hasActions: false)
|
||||
starsMode = .starsGifting(birthdays: birthdays, hasActions: false)
|
||||
currentBirthdays = birthdays
|
||||
} else {
|
||||
mode = .premiumGifting(birthdays: nil, selectToday: false, hasActions: true)
|
||||
|
|
@ -2237,7 +2239,10 @@ public final class SharedAccountContextImpl: SharedAccountContext {
|
|||
options.set(context.engine.payments.starsGiftOptions(peerId: nil))
|
||||
let contactsController = context.sharedContext.makeContactSelectionController(ContactSelectionControllerParams(
|
||||
context: context,
|
||||
title: { strings in return strings.Stars_Purchase_GiftStars }
|
||||
mode: starsMode,
|
||||
autoDismiss: false,
|
||||
title: { strings in return strings.Stars_Purchase_GiftStars },
|
||||
options: contactOptions
|
||||
))
|
||||
let _ = (contactsController.result
|
||||
|> deliverOnMainQueue).start(next: { result in
|
||||
|
|
|
|||
|
|
@ -3,35 +3,73 @@ import Postbox
|
|||
import TelegramCore
|
||||
import SwiftSignalKit
|
||||
|
||||
public struct WebBrowserSettings: Codable, Equatable {
|
||||
public let defaultWebBrowser: String?
|
||||
public struct WebBrowserException: Codable, Equatable {
|
||||
public let domain: String
|
||||
public let title: String
|
||||
|
||||
public static var defaultSettings: WebBrowserSettings {
|
||||
return WebBrowserSettings(defaultWebBrowser: nil)
|
||||
public init(domain: String, title: String) {
|
||||
self.domain = domain
|
||||
self.title = title
|
||||
}
|
||||
|
||||
public init(defaultWebBrowser: String?) {
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: StringCodingKey.self)
|
||||
|
||||
self.domain = try container.decode(String.self, forKey: "domain")
|
||||
self.title = try container.decode(String.self, forKey: "title")
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: StringCodingKey.self)
|
||||
|
||||
try container.encode(self.domain, forKey: "domain")
|
||||
try container.encode(self.title, forKey: "title")
|
||||
}
|
||||
}
|
||||
|
||||
public struct WebBrowserSettings: Codable, Equatable {
|
||||
public let defaultWebBrowser: String?
|
||||
public let exceptions: [WebBrowserException]
|
||||
|
||||
public static var defaultSettings: WebBrowserSettings {
|
||||
return WebBrowserSettings(defaultWebBrowser: nil, exceptions: [])
|
||||
}
|
||||
|
||||
public init(defaultWebBrowser: String?, exceptions: [WebBrowserException]) {
|
||||
self.defaultWebBrowser = defaultWebBrowser
|
||||
self.exceptions = exceptions
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: StringCodingKey.self)
|
||||
|
||||
self.defaultWebBrowser = try? container.decodeIfPresent(String.self, forKey: "defaultWebBrowser")
|
||||
self.exceptions = (try? container.decodeIfPresent([WebBrowserException].self, forKey: "exceptions")) ?? []
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: StringCodingKey.self)
|
||||
|
||||
try container.encodeIfPresent(self.defaultWebBrowser, forKey: "defaultWebBrowser")
|
||||
try container.encode(self.exceptions, forKey: "exceptions")
|
||||
}
|
||||
|
||||
public static func ==(lhs: WebBrowserSettings, rhs: WebBrowserSettings) -> Bool {
|
||||
return lhs.defaultWebBrowser == rhs.defaultWebBrowser
|
||||
if lhs.defaultWebBrowser != rhs.defaultWebBrowser {
|
||||
return false
|
||||
}
|
||||
if lhs.exceptions != rhs.exceptions {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
public func withUpdatedDefaultWebBrowser(_ defaultWebBrowser: String?) -> WebBrowserSettings {
|
||||
return WebBrowserSettings(defaultWebBrowser: defaultWebBrowser)
|
||||
return WebBrowserSettings(defaultWebBrowser: defaultWebBrowser, exceptions: self.exceptions)
|
||||
}
|
||||
|
||||
public func withUpdatedExceptions(_ exceptions: [WebBrowserException]) -> WebBrowserSettings {
|
||||
return WebBrowserSettings(defaultWebBrowser: self.defaultWebBrowser, exceptions: exceptions)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -713,12 +713,14 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
|
|||
|
||||
let body = MarkdownAttributeSet(font: Font.regular(14.0), textColor: .white)
|
||||
let bold: MarkdownAttributeSet
|
||||
var link = body
|
||||
if savedMessages {
|
||||
bold = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: presentationData.theme.list.itemAccentColor.withMultiplied(hue: 0.933, saturation: 0.61, brightness: 1.0), additionalAttributes: ["URL": ""])
|
||||
link = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: .white)
|
||||
} else {
|
||||
bold = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: .white)
|
||||
}
|
||||
let attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: body, bold: bold, link: body, linkAttribute: { _ in return nil }), textAlignment: .natural)
|
||||
let attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: body, bold: bold, link: link, linkAttribute: { _ in return nil }), textAlignment: .natural)
|
||||
self.textNode.attributedText = attributedText
|
||||
self.textNode.maximumNumberOfLines = 2
|
||||
|
||||
|
|
|
|||
|
|
@ -1796,6 +1796,8 @@ public final class WebAppController: ViewController, AttachmentContainable {
|
|||
self.navigationItem.rightBarButtonItem?.action = #selector(self.moreButtonPressed)
|
||||
self.navigationItem.rightBarButtonItem?.target = self
|
||||
|
||||
self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Back, style: .plain, target: nil, action: nil)
|
||||
|
||||
let titleView = WebAppTitleView(context: self.context, theme: self.presentationData.theme)
|
||||
titleView.title = WebAppTitle(title: params.botName, counter: self.presentationData.strings.WebApp_Miniapp, isVerified: params.botVerified)
|
||||
self.navigationItem.titleView = titleView
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue