Merge commit '4e46697a66' into beta

This commit is contained in:
Ilya Laktyushin 2024-07-22 20:31:23 +04:00
commit 69f1e7536b
55 changed files with 2559 additions and 615 deletions

View file

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

View file

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

View file

@ -85,6 +85,7 @@ public enum ContactMultiselectionControllerMode {
public enum ContactListFilter {
case excludeWithoutPhoneNumbers
case excludeSelf
case excludeBots
case exclude([EnginePeer.Id])
case disable([EnginePeer.Id])
}

View file

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

View file

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

View file

@ -0,0 +1,362 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import ComponentFlow
import TelegramPresentationData
import AccountContext
import BundleIconComponent
final class AddressBarContentComponent: Component {
let theme: PresentationTheme
let strings: PresentationStrings
let url: String
let performAction: ActionSlot<BrowserScreen.Action>
init(
theme: PresentationTheme,
strings: PresentationStrings,
url: String,
performAction: ActionSlot<BrowserScreen.Action>
) {
self.theme = theme
self.strings = strings
self.url = url
self.performAction = performAction
}
static func ==(lhs: AddressBarContentComponent, rhs: AddressBarContentComponent) -> Bool {
if lhs.theme !== rhs.theme {
return false
}
if lhs.strings !== rhs.strings {
return false
}
if lhs.url != rhs.url {
return false
}
return true
}
final class View: UIView, UITextFieldDelegate {
private final class TextField: UITextField {
override func textRect(forBounds bounds: CGRect) -> CGRect {
return bounds.integral
}
}
private struct Params: Equatable {
var theme: PresentationTheme
var strings: PresentationStrings
var size: CGSize
static func ==(lhs: Params, rhs: Params) -> Bool {
if lhs.theme !== rhs.theme {
return false
}
if lhs.strings !== rhs.strings {
return false
}
if lhs.size != rhs.size {
return false
}
return true
}
}
private let activated: (Bool) -> Void = { _ in }
private let deactivated: (Bool) -> Void = { _ in }
private let updateQuery: (String?) -> Void = { _ in }
private let backgroundLayer: SimpleLayer
private let iconView: UIImageView
private let clearIconView: UIImageView
private let clearIconButton: HighlightTrackingButton
private let cancelButtonTitle: ComponentView<Empty>
private let cancelButton: HighlightTrackingButton
private var placeholderContent = ComponentView<Empty>()
private var textFrame: CGRect?
private var textField: TextField?
private var tapRecognizer: UITapGestureRecognizer?
private var params: Params?
private var component: AddressBarContentComponent?
public var wantsDisplayBelowKeyboard: Bool {
return self.textField != nil
}
init() {
self.backgroundLayer = SimpleLayer()
self.iconView = UIImageView()
self.clearIconView = UIImageView()
self.clearIconButton = HighlightableButton()
self.clearIconView.isHidden = true
self.clearIconButton.isHidden = true
self.cancelButtonTitle = ComponentView()
self.cancelButton = HighlightTrackingButton()
super.init(frame: CGRect())
self.layer.addSublayer(self.backgroundLayer)
self.addSubview(self.iconView)
self.addSubview(self.clearIconView)
self.addSubview(self.clearIconButton)
self.addSubview(self.cancelButton)
self.clipsToBounds = true
let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))
self.tapRecognizer = tapRecognizer
self.addGestureRecognizer(tapRecognizer)
self.cancelButton.highligthedChanged = { [weak self] highlighted in
if let strongSelf = self {
if highlighted {
if let cancelButtonTitleView = strongSelf.cancelButtonTitle.view {
cancelButtonTitleView.layer.removeAnimation(forKey: "opacity")
cancelButtonTitleView.alpha = 0.4
}
} else {
if let cancelButtonTitleView = strongSelf.cancelButtonTitle.view {
cancelButtonTitleView.alpha = 1.0
cancelButtonTitleView.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
}
}
}
}
self.cancelButton.addTarget(self, action: #selector(self.cancelPressed), for: .touchUpInside)
self.clearIconButton.highligthedChanged = { [weak self] highlighted in
if let strongSelf = self {
if highlighted {
strongSelf.clearIconView.layer.removeAnimation(forKey: "opacity")
strongSelf.clearIconView.alpha = 0.4
} else {
strongSelf.clearIconView.alpha = 1.0
strongSelf.clearIconView.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
}
}
}
self.clearIconButton.addTarget(self, action: #selector(self.clearPressed), for: .touchUpInside)
}
required public init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc private func tapGesture(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state {
self.activateTextInput()
}
}
private func activateTextInput() {
if self.textField == nil, let textFrame = self.textFrame {
let backgroundFrame = self.backgroundLayer.frame
let textFieldFrame = CGRect(origin: CGPoint(x: textFrame.minX, y: backgroundFrame.minY), size: CGSize(width: backgroundFrame.maxX - textFrame.minX, height: backgroundFrame.height))
let textField = TextField(frame: textFieldFrame)
textField.autocorrectionType = .no
textField.returnKeyType = .search
self.textField = textField
self.insertSubview(textField, belowSubview: self.clearIconView)
textField.delegate = self
textField.addTarget(self, action: #selector(self.textFieldChanged(_:)), for: .editingChanged)
}
guard !(self.textField?.isFirstResponder ?? false) else {
return
}
self.activated(true)
self.textField?.becomeFirstResponder()
}
@objc private func cancelPressed() {
self.updateQuery(nil)
self.clearIconView.isHidden = true
self.clearIconButton.isHidden = true
let textField = self.textField
self.textField = nil
self.deactivated(textField?.isFirstResponder ?? false)
self.component?.performAction.invoke(.updateSearchActive(false))
if let textField {
textField.resignFirstResponder()
textField.removeFromSuperview()
}
}
@objc private func clearPressed() {
self.updateQuery(nil)
self.textField?.text = ""
self.clearIconView.isHidden = true
self.clearIconButton.isHidden = true
}
func deactivate() {
if let text = self.textField?.text, !text.isEmpty {
self.textField?.endEditing(true)
} else {
self.cancelPressed()
}
}
public func textFieldDidBeginEditing(_ textField: UITextField) {
}
public func textFieldDidEndEditing(_ textField: UITextField) {
}
public func textFieldShouldReturn(_ textField: UITextField) -> Bool {
textField.endEditing(true)
return false
}
@objc private func textFieldChanged(_ textField: UITextField) {
let text = textField.text ?? ""
self.clearIconView.isHidden = text.isEmpty
self.clearIconButton.isHidden = text.isEmpty
self.placeholderContent.view?.isHidden = !text.isEmpty
self.updateQuery(text)
self.component?.performAction.invoke(.updateSearchQuery(text))
if let params = self.params {
self.update(theme: params.theme, strings: params.strings, size: params.size, transition: .immediate)
}
}
func update(component: AddressBarContentComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize {
self.component = component
self.update(theme: component.theme, strings: component.strings, size: availableSize, transition: transition)
return availableSize
}
public func update(theme: PresentationTheme, strings: PresentationStrings, size: CGSize, transition: ComponentTransition) {
let params = Params(
theme: theme,
strings: strings,
size: size
)
if self.params == params {
return
}
let isActiveWithText = true
if self.params?.theme !== theme {
self.iconView.image = generateTintedImage(image: UIImage(bundleImageName: "Components/Search Bar/Loupe"), color: .white)?.withRenderingMode(.alwaysTemplate)
self.iconView.tintColor = theme.rootController.navigationSearchBar.inputIconColor
self.clearIconView.image = generateTintedImage(image: UIImage(bundleImageName: "Components/Search Bar/Clear"), color: .white)?.withRenderingMode(.alwaysTemplate)
self.clearIconView.tintColor = theme.rootController.navigationSearchBar.inputClearButtonColor
}
self.params = params
let sideInset: CGFloat = 10.0
let inputHeight: CGFloat = 36.0
let topInset: CGFloat = (size.height - inputHeight) / 2.0
let sideTextInset: CGFloat = sideInset + 4.0 + 17.0
self.backgroundLayer.backgroundColor = theme.rootController.navigationSearchBar.inputFillColor.cgColor
self.backgroundLayer.cornerRadius = 10.5
let cancelTextSize = self.cancelButtonTitle.update(
transition: .immediate,
component: AnyComponent(Text(
text: strings.Common_Cancel,
font: Font.regular(17.0),
color: theme.rootController.navigationBar.accentTextColor
)),
environment: {},
containerSize: CGSize(width: size.width - 32.0, height: 100.0)
)
let cancelButtonSpacing: CGFloat = 8.0
var backgroundFrame = CGRect(origin: CGPoint(x: sideInset, y: topInset), size: CGSize(width: size.width - sideInset * 2.0, height: inputHeight))
if isActiveWithText {
backgroundFrame.size.width -= cancelTextSize.width + cancelButtonSpacing
}
transition.setFrame(layer: self.backgroundLayer, frame: backgroundFrame)
transition.setFrame(view: self.cancelButton, frame: CGRect(origin: CGPoint(x: backgroundFrame.maxX, y: 0.0), size: CGSize(width: cancelButtonSpacing + cancelTextSize.width, height: size.height)))
let textX: CGFloat = backgroundFrame.minX + sideTextInset
let textFrame = CGRect(origin: CGPoint(x: textX, y: backgroundFrame.minY), size: CGSize(width: backgroundFrame.maxX - textX, height: backgroundFrame.height))
self.textFrame = textFrame
if let image = self.iconView.image {
let iconFrame = CGRect(origin: CGPoint(x: backgroundFrame.minX + 5.0, y: backgroundFrame.minY + floor((backgroundFrame.height - image.size.height) / 2.0)), size: image.size)
transition.setFrame(view: self.iconView, frame: iconFrame)
}
let placeholderSize = self.placeholderContent.update(
transition: transition,
component: AnyComponent(
Text(text: strings.Common_Search, font: Font.regular(17.0), color: theme.rootController.navigationSearchBar.inputPlaceholderTextColor)
),
environment: {},
containerSize: size
)
if let placeholderContentView = self.placeholderContent.view {
if placeholderContentView.superview == nil {
self.addSubview(placeholderContentView)
}
let placeholderContentFrame = CGRect(origin: CGPoint(x: textFrame.minX, y: backgroundFrame.midY - placeholderSize.height / 2.0), size: placeholderSize)
transition.setFrame(view: placeholderContentView, frame: placeholderContentFrame)
}
if let image = self.clearIconView.image {
let iconFrame = CGRect(origin: CGPoint(x: backgroundFrame.maxX - image.size.width - 4.0, y: backgroundFrame.minY + floor((backgroundFrame.height - image.size.height) / 2.0)), size: image.size)
transition.setFrame(view: self.clearIconView, frame: iconFrame)
transition.setFrame(view: self.clearIconButton, frame: iconFrame.insetBy(dx: -8.0, dy: -10.0))
}
if let cancelButtonTitleComponentView = self.cancelButtonTitle.view {
if cancelButtonTitleComponentView.superview == nil {
self.addSubview(cancelButtonTitleComponentView)
cancelButtonTitleComponentView.isUserInteractionEnabled = false
}
transition.setFrame(view: cancelButtonTitleComponentView, frame: CGRect(origin: CGPoint(x: backgroundFrame.maxX + cancelButtonSpacing, y: floor((size.height - cancelTextSize.height) / 2.0)), size: cancelTextSize))
}
if let textField = self.textField {
textField.textColor = theme.rootController.navigationSearchBar.inputTextColor
transition.setFrame(view: textField, frame: CGRect(origin: CGPoint(x: backgroundFrame.minX + sideTextInset, y: backgroundFrame.minY - UIScreenPixel), size: CGSize(width: backgroundFrame.width - sideTextInset - 32.0, height: backgroundFrame.height)))
}
}
}
func makeView() -> View {
return View()
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, transition: transition)
}
}

View file

@ -109,7 +109,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))

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -100,9 +100,9 @@ private enum DataAndStorageEntry: ItemListNodeEntry {
case useLessVoiceData(PresentationTheme, String, Bool)
case useLessVoiceDataInfo(PresentationTheme, String)
case otherHeader(PresentationTheme, String)
case openLinksIn(PresentationTheme, String, String)
case shareSheet(PresentationTheme, String)
case saveEditedPhotos(PresentationTheme, String, Bool)
case openLinksIn(PresentationTheme, String, String)
case pauseMusicOnRecording(PresentationTheme, String, Bool)
case raiseToListen(PresentationTheme, String, Bool)
case raiseToListenInfo(PresentationTheme, String)
@ -123,7 +123,7 @@ private enum DataAndStorageEntry: ItemListNodeEntry {
return DataAndStorageSection.backgroundDownload.rawValue
case .useLessVoiceData, .useLessVoiceDataInfo:
return DataAndStorageSection.voiceCalls.rawValue
case .otherHeader, .shareSheet, .saveEditedPhotos, .openLinksIn, .pauseMusicOnRecording, .raiseToListen, .raiseToListenInfo:
case .otherHeader, .openLinksIn, .shareSheet, .saveEditedPhotos, .pauseMusicOnRecording, .raiseToListen, .raiseToListenInfo:
return DataAndStorageSection.other.rawValue
case .connectionHeader, .connectionProxy:
return DataAndStorageSection.connection.rawValue
@ -162,11 +162,11 @@ private enum DataAndStorageEntry: ItemListNodeEntry {
return 24
case .otherHeader:
return 29
case .shareSheet:
return 30
case .saveEditedPhotos:
return 31
case .openLinksIn:
return 30
case .shareSheet:
return 31
case .saveEditedPhotos:
return 32
case .pauseMusicOnRecording:
return 33
@ -257,6 +257,12 @@ private enum DataAndStorageEntry: ItemListNodeEntry {
} else {
return false
}
case let .openLinksIn(lhsTheme, lhsText, lhsValue):
if case let .openLinksIn(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue {
return true
} else {
return false
}
case let .shareSheet(lhsTheme, lhsText):
if case let .shareSheet(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
@ -269,12 +275,6 @@ private enum DataAndStorageEntry: ItemListNodeEntry {
} else {
return false
}
case let .openLinksIn(lhsTheme, lhsText, lhsValue):
if case let .openLinksIn(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue {
return true
} else {
return false
}
case let .pauseMusicOnRecording(lhsTheme, lhsText, lhsValue):
if case let .pauseMusicOnRecording(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue {
return true
@ -386,6 +386,10 @@ private enum DataAndStorageEntry: ItemListNodeEntry {
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
case let .otherHeader(_, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .openLinksIn(_, text, value):
return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, action: {
arguments.openBrowserSelection()
})
case let .shareSheet(_, text):
return ItemListDisclosureItem(presentationData: presentationData, title: text, label: "", sectionId: self.section, style: .blocks, action: {
arguments.openIntents()
@ -394,10 +398,6 @@ private enum DataAndStorageEntry: ItemListNodeEntry {
return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, sectionId: self.section, style: .blocks, updated: { value in
arguments.toggleSaveEditedPhotos(value)
}, tag: DataAndStorageEntryTag.saveEditedPhotos)
case let .openLinksIn(_, text, value):
return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, action: {
arguments.openBrowserSelection()
})
case let .pauseMusicOnRecording(_, text, value):
return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, sectionId: self.section, style: .blocks, updated: { value in
arguments.togglePauseMusicOnRecording(value)
@ -618,11 +618,11 @@ private func dataAndStorageControllerEntries(state: DataAndStorageControllerStat
entries.append(.useLessVoiceDataInfo(presentationData.theme, presentationData.strings.CallSettings_UseLessDataLongDescription))
entries.append(.otherHeader(presentationData.theme, presentationData.strings.ChatSettings_Other))
entries.append(.openLinksIn(presentationData.theme, presentationData.strings.ChatSettings_OpenLinksIn, defaultWebBrowser))
if #available(iOSApplicationExtension 13.2, iOS 13.2, *) {
entries.append(.shareSheet(presentationData.theme, presentationData.strings.ChatSettings_IntentsSettings))
}
entries.append(.saveEditedPhotos(presentationData.theme, presentationData.strings.Settings_SaveEditedPhotos, data.generatedMediaStoreSettings.storeEditedPhotos))
entries.append(.openLinksIn(presentationData.theme, presentationData.strings.ChatSettings_OpenLinksIn, defaultWebBrowser))
entries.append(.pauseMusicOnRecording(presentationData.theme, presentationData.strings.Settings_PauseMusicOnRecording, data.mediaInputSettings.pauseMusicOnRecording))
entries.append(.raiseToListen(presentationData.theme, presentationData.strings.Settings_RaiseToListen, data.mediaInputSettings.enableRaiseToSpeak))
entries.append(.raiseToListenInfo(presentationData.theme, presentationData.strings.Settings_RaiseToListenInfo))

View file

@ -0,0 +1,448 @@
import Foundation
import UIKit
import SwiftSignalKit
import AsyncDisplayKit
import Display
import TelegramCore
import TelegramPresentationData
import AccountContext
import UrlEscaping
private final class WebBrowserDomainInputFieldNode: ASDisplayNode, ASEditableTextNodeDelegate {
private var theme: PresentationTheme
private let backgroundNode: ASImageNode
fileprivate let textInputNode: EditableTextNode
private let placeholderNode: ASTextNode
var updateHeight: (() -> Void)?
var complete: (() -> Void)?
var textChanged: ((String) -> Void)?
private let backgroundInsets = UIEdgeInsets(top: 8.0, left: 16.0, bottom: 15.0, right: 16.0)
private let inputInsets = UIEdgeInsets(top: 5.0, left: 12.0, bottom: 5.0, right: 12.0)
var text: String {
get {
return self.textInputNode.attributedText?.string ?? ""
}
set {
self.textInputNode.attributedText = NSAttributedString(string: newValue, font: Font.regular(17.0), textColor: self.theme.actionSheet.inputTextColor)
self.placeholderNode.isHidden = !newValue.isEmpty
}
}
var placeholder: String = "" {
didSet {
self.placeholderNode.attributedText = NSAttributedString(string: self.placeholder, font: Font.regular(17.0), textColor: self.theme.actionSheet.inputPlaceholderColor)
}
}
init(theme: PresentationTheme, placeholder: String) {
self.theme = theme
self.backgroundNode = ASImageNode()
self.backgroundNode.isLayerBacked = true
self.backgroundNode.displaysAsynchronously = false
self.backgroundNode.displayWithoutProcessing = true
self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 12.0, color: theme.actionSheet.inputHollowBackgroundColor, strokeColor: theme.actionSheet.inputBorderColor, strokeWidth: 1.0)
self.textInputNode = EditableTextNode()
self.textInputNode.typingAttributes = [NSAttributedString.Key.font.rawValue: Font.regular(17.0), NSAttributedString.Key.foregroundColor.rawValue: theme.actionSheet.inputTextColor]
self.textInputNode.clipsToBounds = true
self.textInputNode.hitTestSlop = UIEdgeInsets(top: -5.0, left: -5.0, bottom: -5.0, right: -5.0)
self.textInputNode.textContainerInset = UIEdgeInsets(top: self.inputInsets.top, left: 0.0, bottom: self.inputInsets.bottom, right: 0.0)
self.textInputNode.keyboardAppearance = theme.rootController.keyboardColor.keyboardAppearance
self.textInputNode.keyboardType = .URL
self.textInputNode.autocapitalizationType = .none
self.textInputNode.returnKeyType = .done
self.textInputNode.autocorrectionType = .no
self.textInputNode.tintColor = theme.actionSheet.controlAccentColor
self.placeholderNode = ASTextNode()
self.placeholderNode.isUserInteractionEnabled = false
self.placeholderNode.displaysAsynchronously = false
self.placeholderNode.attributedText = NSAttributedString(string: placeholder, font: Font.regular(17.0), textColor: self.theme.actionSheet.inputPlaceholderColor)
super.init()
self.textInputNode.delegate = self
self.addSubnode(self.backgroundNode)
self.addSubnode(self.textInputNode)
self.addSubnode(self.placeholderNode)
}
func updateTheme(_ theme: PresentationTheme) {
self.theme = theme
self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 12.0, color: self.theme.actionSheet.inputHollowBackgroundColor, strokeColor: self.theme.actionSheet.inputBorderColor, strokeWidth: 1.0)
self.textInputNode.keyboardAppearance = self.theme.rootController.keyboardColor.keyboardAppearance
self.placeholderNode.attributedText = NSAttributedString(string: self.placeholderNode.attributedText?.string ?? "", font: Font.regular(17.0), textColor: self.theme.actionSheet.inputPlaceholderColor)
self.textInputNode.tintColor = self.theme.actionSheet.controlAccentColor
}
func updateLayout(width: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat {
let backgroundInsets = self.backgroundInsets
let inputInsets = self.inputInsets
let textFieldHeight = self.calculateTextFieldMetrics(width: width)
let panelHeight = textFieldHeight + backgroundInsets.top + backgroundInsets.bottom
let backgroundFrame = CGRect(origin: CGPoint(x: backgroundInsets.left, y: backgroundInsets.top), size: CGSize(width: width - backgroundInsets.left - backgroundInsets.right, height: panelHeight - backgroundInsets.top - backgroundInsets.bottom))
transition.updateFrame(node: self.backgroundNode, frame: backgroundFrame)
let placeholderSize = self.placeholderNode.measure(backgroundFrame.size)
transition.updateFrame(node: self.placeholderNode, frame: CGRect(origin: CGPoint(x: backgroundFrame.minX + inputInsets.left, y: backgroundFrame.minY + floor((backgroundFrame.size.height - placeholderSize.height) / 2.0)), size: placeholderSize))
transition.updateFrame(node: self.textInputNode, frame: CGRect(origin: CGPoint(x: backgroundFrame.minX + inputInsets.left, y: backgroundFrame.minY), size: CGSize(width: backgroundFrame.size.width - inputInsets.left - inputInsets.right, height: backgroundFrame.size.height)))
return panelHeight
}
func activateInput() {
self.textInputNode.becomeFirstResponder()
}
func deactivateInput() {
self.textInputNode.resignFirstResponder()
}
@objc func editableTextNodeDidUpdateText(_ editableTextNode: ASEditableTextNode) {
self.updateTextNodeText(animated: true)
self.textChanged?(editableTextNode.textView.text)
self.placeholderNode.isHidden = !(editableTextNode.textView.text ?? "").isEmpty
}
private let domainRegex = try? NSRegularExpression(pattern: "^(https?://)?([a-zA-Z0-9-]+\\.?)*([a-zA-Z]*)?(:)?(/)?$", options: [])
private let pathRegex = try? NSRegularExpression(pattern: "^(https?://)?([a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,6}/", options: [])
func editableTextNode(_ editableTextNode: ASEditableTextNode, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
if text == "\n" {
self.complete?()
return false
}
if let domainRegex = self.domainRegex, let pathRegex = self.pathRegex {
let updatedText = (editableTextNode.textView.text as NSString).replacingCharacters(in: range, with: text)
let domainMatches = domainRegex.matches(in: updatedText, options: [], range: NSRange(location: 0, length: updatedText.utf16.count))
let pathMatches = pathRegex.matches(in: updatedText, options: [], range: NSRange(location: 0, length: updatedText.utf16.count))
if domainMatches.count > 0, pathMatches.count == 0 {
return true
} else {
return false
}
}
return true
}
private func calculateTextFieldMetrics(width: CGFloat) -> CGFloat {
let backgroundInsets = self.backgroundInsets
let inputInsets = self.inputInsets
let unboundTextFieldHeight = max(33.0, ceil(self.textInputNode.measure(CGSize(width: width - backgroundInsets.left - backgroundInsets.right - inputInsets.left - inputInsets.right, height: CGFloat.greatestFiniteMagnitude)).height))
return min(61.0, max(33.0, unboundTextFieldHeight))
}
private func updateTextNodeText(animated: Bool) {
let backgroundInsets = self.backgroundInsets
let textFieldHeight = self.calculateTextFieldMetrics(width: self.bounds.size.width)
let panelHeight = textFieldHeight + backgroundInsets.top + backgroundInsets.bottom
if !self.bounds.size.height.isEqual(to: panelHeight) {
self.updateHeight?()
}
}
@objc func clearPressed() {
self.textInputNode.attributedText = nil
self.deactivateInput()
}
}
private final class WebBrowserDomainAlertContentNode: AlertContentNode {
private let strings: PresentationStrings
private let titleNode: ASTextNode
private let textNode: ASTextNode
let inputFieldNode: WebBrowserDomainInputFieldNode
private let actionNodesSeparator: ASDisplayNode
private let actionNodes: [TextAlertContentActionNode]
private let actionVerticalSeparators: [ASDisplayNode]
private let disposable = MetaDisposable()
private var validLayout: CGSize?
private let hapticFeedback = HapticFeedback()
var complete: (() -> Void)? {
didSet {
self.inputFieldNode.complete = self.complete
}
}
override var dismissOnOutsideTap: Bool {
return self.isUserInteractionEnabled
}
init(theme: AlertControllerTheme, ptheme: PresentationTheme, strings: PresentationStrings, actions: [TextAlertAction]) {
self.strings = strings
self.titleNode = ASTextNode()
self.titleNode.maximumNumberOfLines = 2
self.textNode = ASTextNode()
self.textNode.maximumNumberOfLines = 2
self.inputFieldNode = WebBrowserDomainInputFieldNode(theme: ptheme, placeholder: strings.WebBrowser_Exceptions_Create_Placeholder)
self.inputFieldNode.text = ""
self.actionNodesSeparator = ASDisplayNode()
self.actionNodesSeparator.isLayerBacked = true
self.actionNodes = actions.map { action -> TextAlertContentActionNode in
return TextAlertContentActionNode(theme: theme, action: action)
}
var actionVerticalSeparators: [ASDisplayNode] = []
if actions.count > 1 {
for _ in 0 ..< actions.count - 1 {
let separatorNode = ASDisplayNode()
separatorNode.isLayerBacked = true
actionVerticalSeparators.append(separatorNode)
}
}
self.actionVerticalSeparators = actionVerticalSeparators
super.init()
self.addSubnode(self.titleNode)
self.addSubnode(self.textNode)
self.addSubnode(self.inputFieldNode)
self.addSubnode(self.actionNodesSeparator)
for actionNode in self.actionNodes {
self.addSubnode(actionNode)
}
self.actionNodes.last?.actionEnabled = false
for separatorNode in self.actionVerticalSeparators {
self.addSubnode(separatorNode)
}
self.inputFieldNode.updateHeight = { [weak self] in
if let strongSelf = self {
if let _ = strongSelf.validLayout {
strongSelf.requestLayout?(.animated(duration: 0.15, curve: .spring))
}
}
}
self.inputFieldNode.textChanged = { [weak self] text in
if let strongSelf = self, let lastNode = strongSelf.actionNodes.last {
lastNode.actionEnabled = !text.isEmpty
}
}
self.updateTheme(theme)
}
deinit {
self.disposable.dispose()
}
var link: String {
return self.inputFieldNode.text
}
override func updateTheme(_ theme: AlertControllerTheme) {
self.titleNode.attributedText = NSAttributedString(string: self.strings.WebBrowser_Exceptions_Create_Title, font: Font.bold(17.0), textColor: theme.primaryColor, paragraphAlignment: .center)
self.textNode.attributedText = NSAttributedString(string: self.strings.WebBrowser_Exceptions_Create_Text, font: Font.regular(13.0), textColor: theme.primaryColor, paragraphAlignment: .center)
self.actionNodesSeparator.backgroundColor = theme.separatorColor
for actionNode in self.actionNodes {
actionNode.updateTheme(theme)
}
for separatorNode in self.actionVerticalSeparators {
separatorNode.backgroundColor = theme.separatorColor
}
if let size = self.validLayout {
_ = self.updateLayout(size: size, transition: .immediate)
}
}
override func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize {
var size = size
size.width = min(size.width, 270.0)
let measureSize = CGSize(width: size.width - 16.0 * 2.0, height: CGFloat.greatestFiniteMagnitude)
let hadValidLayout = self.validLayout != nil
self.validLayout = size
var origin: CGPoint = CGPoint(x: 0.0, y: 20.0)
let spacing: CGFloat = 5.0
let titleSize = self.titleNode.measure(measureSize)
transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - titleSize.width) / 2.0), y: origin.y), size: titleSize))
origin.y += titleSize.height + 4.0
let textSize = self.textNode.measure(measureSize)
transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - textSize.width) / 2.0), y: origin.y), size: textSize))
origin.y += textSize.height + 6.0 + spacing
let actionButtonHeight: CGFloat = 44.0
var minActionsWidth: CGFloat = 0.0
let maxActionWidth: CGFloat = floor(size.width / CGFloat(self.actionNodes.count))
let actionTitleInsets: CGFloat = 8.0
var effectiveActionLayout = TextAlertContentActionLayout.horizontal
for actionNode in self.actionNodes {
let actionTitleSize = actionNode.titleNode.updateLayout(CGSize(width: maxActionWidth, height: actionButtonHeight))
if case .horizontal = effectiveActionLayout, actionTitleSize.height > actionButtonHeight * 0.6667 {
effectiveActionLayout = .vertical
}
switch effectiveActionLayout {
case .horizontal:
minActionsWidth += actionTitleSize.width + actionTitleInsets
case .vertical:
minActionsWidth = max(minActionsWidth, actionTitleSize.width + actionTitleInsets)
}
}
let insets = UIEdgeInsets(top: 18.0, left: 18.0, bottom: 9.0, right: 18.0)
var contentWidth = max(titleSize.width, minActionsWidth)
contentWidth = max(contentWidth, 234.0)
var actionsHeight: CGFloat = 0.0
switch effectiveActionLayout {
case .horizontal:
actionsHeight = actionButtonHeight
case .vertical:
actionsHeight = actionButtonHeight * CGFloat(self.actionNodes.count)
}
let resultWidth = contentWidth + insets.left + insets.right
let inputFieldWidth = resultWidth
let inputFieldHeight = self.inputFieldNode.updateLayout(width: inputFieldWidth, transition: transition)
let inputHeight = inputFieldHeight
transition.updateFrame(node: self.inputFieldNode, frame: CGRect(x: 0.0, y: origin.y, width: resultWidth, height: inputFieldHeight))
transition.updateAlpha(node: self.inputFieldNode, alpha: inputHeight > 0.0 ? 1.0 : 0.0)
let resultSize = CGSize(width: resultWidth, height: titleSize.height + textSize.height + spacing + inputHeight + actionsHeight + insets.top + insets.bottom)
transition.updateFrame(node: self.actionNodesSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel)))
var actionOffset: CGFloat = 0.0
let actionWidth: CGFloat = floor(resultSize.width / CGFloat(self.actionNodes.count))
var separatorIndex = -1
var nodeIndex = 0
for actionNode in self.actionNodes {
if separatorIndex >= 0 {
let separatorNode = self.actionVerticalSeparators[separatorIndex]
switch effectiveActionLayout {
case .horizontal:
transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: actionOffset - UIScreenPixel, y: resultSize.height - actionsHeight), size: CGSize(width: UIScreenPixel, height: actionsHeight - UIScreenPixel)))
case .vertical:
transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel)))
}
}
separatorIndex += 1
let currentActionWidth: CGFloat
switch effectiveActionLayout {
case .horizontal:
if nodeIndex == self.actionNodes.count - 1 {
currentActionWidth = resultSize.width - actionOffset
} else {
currentActionWidth = actionWidth
}
case .vertical:
currentActionWidth = resultSize.width
}
let actionNodeFrame: CGRect
switch effectiveActionLayout {
case .horizontal:
actionNodeFrame = CGRect(origin: CGPoint(x: actionOffset, y: resultSize.height - actionsHeight), size: CGSize(width: currentActionWidth, height: actionButtonHeight))
actionOffset += currentActionWidth
case .vertical:
actionNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset), size: CGSize(width: currentActionWidth, height: actionButtonHeight))
actionOffset += actionButtonHeight
}
transition.updateFrame(node: actionNode, frame: actionNodeFrame)
nodeIndex += 1
}
if !hadValidLayout {
self.inputFieldNode.activateInput()
}
return resultSize
}
func animateError() {
self.inputFieldNode.layer.addShakeAnimation()
self.hapticFeedback.error()
}
}
public func webBrowserDomainController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, apply: @escaping (String?) -> Void) -> AlertController {
let presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 }
var dismissImpl: ((Bool) -> Void)?
var applyImpl: (() -> Void)?
let actions: [TextAlertAction] = [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {
dismissImpl?(true)
apply(nil)
}), TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Done, action: {
applyImpl?()
})]
let contentNode = WebBrowserDomainAlertContentNode(theme: AlertControllerTheme(presentationData: presentationData), ptheme: presentationData.theme, strings: presentationData.strings, actions: actions)
contentNode.complete = {
applyImpl?()
}
applyImpl = { [weak contentNode] in
guard let contentNode = contentNode else {
return
}
let updatedLink = explicitUrl(contentNode.link)
if !updatedLink.isEmpty && isValidUrl(updatedLink, validSchemes: ["http": true, "https": true]) {
dismissImpl?(true)
apply(updatedLink)
} else {
contentNode.animateError()
}
}
let controller = AlertController(theme: AlertControllerTheme(presentationData: presentationData), contentNode: contentNode)
let presentationDataDisposable = (updatedPresentationData?.signal ?? context.sharedContext.presentationData).start(next: { [weak controller, weak contentNode] presentationData in
controller?.theme = AlertControllerTheme(presentationData: presentationData)
contentNode?.inputFieldNode.updateTheme(presentationData.theme)
})
controller.dismissed = { _ in
presentationDataDisposable.dispose()
}
dismissImpl = { [weak controller] animated in
contentNode.inputFieldNode.deactivateInput()
if animated {
controller?.dismissAnimated()
} else {
controller?.dismiss()
}
}
return controller
}

View file

@ -0,0 +1,280 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramPresentationData
import TelegramCore
import AccountContext
import ItemListUI
public class WebBrowserDomainExceptionItem: ListViewItem, ItemListItem {
let presentationData: ItemListPresentationData
let context: AccountContext?
let title: String
let label: String
public let sectionId: ItemListSectionId
let style: ItemListStyle
public init(
presentationData: ItemListPresentationData,
context: AccountContext? = nil,
title: String,
label: String,
sectionId: ItemListSectionId,
style: ItemListStyle
) {
self.presentationData = presentationData
self.context = context
self.title = title
self.label = label
self.sectionId = sectionId
self.style = style
}
public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
async {
let node = WebBrowserDomainExceptionItemNode()
let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
node.contentSize = layout.contentSize
node.insets = layout.insets
Queue.mainQueue().async {
completion(node, {
return (nil, { _ in apply() })
})
}
}
}
public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
Queue.mainQueue().async {
if let nodeValue = node() as? WebBrowserDomainExceptionItemNode {
let makeLayout = nodeValue.asyncLayout()
async {
let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
Queue.mainQueue().async {
completion(layout, { _ in
apply()
})
}
}
}
}
}
public var selectable: Bool = false
public func selected(listView: ListView){
}
}
public class WebBrowserDomainExceptionItemNode: ListViewItemNode, ItemListItemNode {
private let backgroundNode: ASDisplayNode
private let topStripeNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode
private let maskNode: ASImageNode
let iconNode: ASImageNode
let titleNode: TextNode
let labelNode: TextNode
private let activateArea: AccessibilityAreaNode
private var item: WebBrowserDomainExceptionItem?
override public var canBeSelected: Bool {
return false
}
public var tag: ItemListItemTag? = nil
public init() {
self.backgroundNode = ASDisplayNode()
self.backgroundNode.isLayerBacked = true
self.backgroundNode.backgroundColor = .white
self.maskNode = ASImageNode()
self.maskNode.isUserInteractionEnabled = false
self.topStripeNode = ASDisplayNode()
self.topStripeNode.isLayerBacked = true
self.bottomStripeNode = ASDisplayNode()
self.bottomStripeNode.isLayerBacked = true
self.iconNode = ASImageNode()
self.iconNode.isLayerBacked = true
self.iconNode.displaysAsynchronously = false
self.titleNode = TextNode()
self.titleNode.isUserInteractionEnabled = false
self.labelNode = TextNode()
self.labelNode.isUserInteractionEnabled = false
self.activateArea = AccessibilityAreaNode()
super.init(layerBacked: false, dynamicBounce: false)
self.addSubnode(self.titleNode)
self.addSubnode(self.labelNode)
self.addSubnode(self.activateArea)
}
public func asyncLayout() -> (_ item: WebBrowserDomainExceptionItem, _ params: ListViewItemLayoutParams, _ insets: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
let makeLabelLayout = TextNode.asyncLayout(self.labelNode)
let currentItem = self.item
return { item, params, neighbors in
var updatedTheme: PresentationTheme?
if currentItem?.presentationData.theme !== item.presentationData.theme {
updatedTheme = item.presentationData.theme
}
let contentSize: CGSize
let insets: UIEdgeInsets
let separatorHeight = UIScreenPixel
let itemBackgroundColor: UIColor
let itemSeparatorColor: UIColor
let leftInset = 16.0 + params.leftInset + 43.0
let titleColor: UIColor = item.presentationData.theme.list.itemPrimaryTextColor
let labelColor: UIColor = item.presentationData.theme.list.itemAccentColor
let titleFont = Font.medium(item.presentationData.fontSize.itemListBaseFontSize)
let labelFont = Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize * 15.0 / 17.0))
let maxTitleWidth: CGFloat = params.width - params.rightInset - 20.0 - leftInset
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.title, font: titleFont, textColor: titleColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: maxTitleWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let (labelLayout, labelApply) = makeLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.label, font: labelFont, textColor: labelColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: maxTitleWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let verticalInset: CGFloat = 11.0
let titleSpacing: CGFloat = 1.0
let height: CGFloat = verticalInset * 2.0 + titleLayout.size.height + titleSpacing + labelLayout.size.height
switch item.style {
case .plain:
itemBackgroundColor = item.presentationData.theme.list.plainBackgroundColor
itemSeparatorColor = item.presentationData.theme.list.itemPlainSeparatorColor
contentSize = CGSize(width: params.width, height: height)
insets = itemListNeighborsPlainInsets(neighbors)
case .blocks:
itemBackgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor
itemSeparatorColor = item.presentationData.theme.list.itemBlocksSeparatorColor
contentSize = CGSize(width: params.width, height: height)
insets = itemListNeighborsGroupedInsets(neighbors, params)
}
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
return (ListViewItemNodeLayout(contentSize: contentSize, insets: insets), { [weak self] in
if let strongSelf = self {
strongSelf.item = item
strongSelf.activateArea.frame = CGRect(origin: CGPoint(x: params.leftInset, y: 0.0), size: CGSize(width: params.width - params.leftInset - params.rightInset, height: layout.contentSize.height))
strongSelf.activateArea.accessibilityLabel = item.title
strongSelf.activateArea.accessibilityValue = item.label
if let _ = updatedTheme {
strongSelf.topStripeNode.backgroundColor = itemSeparatorColor
strongSelf.bottomStripeNode.backgroundColor = itemSeparatorColor
strongSelf.backgroundNode.backgroundColor = itemBackgroundColor
}
let _ = titleApply()
let _ = labelApply()
switch item.style {
case .plain:
if strongSelf.backgroundNode.supernode != nil {
strongSelf.backgroundNode.removeFromSupernode()
}
if strongSelf.topStripeNode.supernode != nil {
strongSelf.topStripeNode.removeFromSupernode()
}
if strongSelf.bottomStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 0)
}
if strongSelf.maskNode.supernode != nil {
strongSelf.maskNode.removeFromSupernode()
}
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: leftInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - leftInset, height: separatorHeight))
case .blocks:
if strongSelf.backgroundNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
}
if strongSelf.topStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1)
}
if strongSelf.bottomStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2)
}
if strongSelf.maskNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.maskNode, at: 3)
}
let hasCorners = itemListHasRoundedBlockLayout(params)
var hasTopCorners = false
var hasBottomCorners = false
switch neighbors.top {
case .sameSection(false):
strongSelf.topStripeNode.isHidden = true
default:
hasTopCorners = true
strongSelf.topStripeNode.isHidden = hasCorners
}
let bottomStripeInset: CGFloat
switch neighbors.bottom {
case .sameSection(false):
bottomStripeInset = leftInset
strongSelf.bottomStripeNode.isHidden = false
default:
bottomStripeInset = 0.0
hasBottomCorners = true
strongSelf.bottomStripeNode.isHidden = hasCorners
}
strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0)
strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: separatorHeight))
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - bottomStripeInset, height: separatorHeight))
}
var centralContentHeight: CGFloat = titleLayout.size.height
centralContentHeight += titleSpacing
centralContentHeight += labelLayout.size.height
let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: floor((height - centralContentHeight) / 2.0)), size: titleLayout.size)
strongSelf.titleNode.frame = titleFrame
let labelFrame = CGRect(origin: CGPoint(x: leftInset, y: titleFrame.maxY + titleSpacing), size: labelLayout.size)
strongSelf.labelNode.frame = labelFrame
}
})
}
}
override public func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
}
override public func animateAdded(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
override public func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
}
}

View file

@ -9,29 +9,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))
}
}

View file

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

View file

@ -69,6 +69,12 @@ public struct PresentationResourcesItemList {
})
}
public static func accentDeleteIconImage(_ theme: PresentationTheme) -> UIImage? {
return theme.image(PresentationResourceKey.itemListAccentDeleteIcon.rawValue, { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionTrash"), color: theme.list.itemAccentColor)
})
}
public static func deleteIconImage(_ theme: PresentationTheme) -> UIImage? {
return theme.image(PresentationResourceKey.itemListDeleteIcon.rawValue, { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionTrash"), color: theme.list.itemDestructiveColor)

View file

@ -338,6 +338,19 @@ private final class MediaCoverScreenComponent: Component {
mediaEditor.seek(position, andPlay: false)
}
},
coverPositionUpdated: { [weak mediaEditor] position, tap, commit in
if let mediaEditor {
if tap {
mediaEditor.setOnNextDisplay {
commit()
}
mediaEditor.seek(position, andPlay: false)
} else {
mediaEditor.seek(position, andPlay: false)
commit()
}
}
},
trackTrimUpdated: { _, _, _, _, _ in
},
trackOffsetUpdated: { _, _, _ in

View file

@ -48,6 +48,7 @@ import StickerPackEditTitleController
import StickerPickerScreen
import UIKitRuntimeUtils
import ImageObjectSeparation
import DeviceAccess
private let playbackButtonTag = GenericComponentViewTag()
private let muteButtonTag = GenericComponentViewTag()
@ -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
}
}
})
}

View file

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

View file

@ -88,6 +88,7 @@ public final class MediaScrubberComponent: Component {
let portalView: PortalView?
let positionUpdated: (Double, Bool) -> Void
let coverPositionUpdated: (Double, Bool, @escaping () -> Void) -> Void
let trackTrimUpdated: (Int32, Double, Double, Bool, Bool) -> Void
let trackOffsetUpdated: (Int32, Double, Bool) -> Void
let trackLongPressed: (Int32, UIView) -> Void
@ -104,6 +105,7 @@ public final class MediaScrubberComponent: Component {
tracks: [Track],
portalView: PortalView? = nil,
positionUpdated: @escaping (Double, Bool) -> Void,
coverPositionUpdated: @escaping (Double, Bool, @escaping () -> Void) -> Void = { _, _, _ in },
trackTrimUpdated: @escaping (Int32, Double, Double, Bool, Bool) -> Void,
trackOffsetUpdated: @escaping (Int32, Double, Bool) -> Void,
trackLongPressed: @escaping (Int32, UIView) -> Void
@ -119,6 +121,7 @@ public final class MediaScrubberComponent: Component {
self.tracks = tracks
self.portalView = portalView
self.positionUpdated = positionUpdated
self.coverPositionUpdated = coverPositionUpdated
self.trackTrimUpdated = trackTrimUpdated
self.trackOffsetUpdated = trackOffsetUpdated
self.trackLongPressed = trackLongPressed
@ -164,6 +167,7 @@ public final class MediaScrubberComponent: Component {
private var selectedTrackId: Int32 = 0
private var isPanningCursor = false
private var ignoreCursorPositionUpdate = false
private var scrubberSize: CGSize?
@ -327,10 +331,18 @@ public final class MediaScrubberComponent: Component {
switch gestureRecognizer.state {
case .began, .changed:
self.isPanningCursor = true
component.positionUpdated(position, false)
if case .cover = component.style {
component.coverPositionUpdated(position, false, {})
} else {
component.positionUpdated(position, false)
}
case .ended, .cancelled:
self.isPanningCursor = false
component.positionUpdated(position, true)
if case .cover = component.style {
component.coverPositionUpdated(position, false, {})
} else {
component.positionUpdated(position, true)
}
default:
break
}
@ -345,7 +357,7 @@ public final class MediaScrubberComponent: Component {
var y: CGFloat = -5.0 - UIScreenPixel
if let component = self.component, case .cover = component.style {
cursorWidth = 30.0 + 12.0
cursorMargin = 0.0
cursorMargin = handleWidth
height = 50.0
isCover = true
y += 1.0
@ -472,6 +484,23 @@ public final class MediaScrubberComponent: Component {
} else {
trackTransition = .immediate
trackView = TrackView()
trackView.onTap = { [weak self] fraction in
guard let self, let component = self.component else {
return
}
var position = max(self.startPosition, min(self.endPosition, self.trimDuration * fraction))
if let offset = self.mainAudioTrackOffset {
position += offset
}
self.ignoreCursorPositionUpdate = true
component.coverPositionUpdated(position, true, { [weak self] in
guard let self else {
return
}
self.ignoreCursorPositionUpdate = false
self.state?.updated(transition: .immediate)
})
}
trackView.onSelection = { [weak self] id in
guard let self else {
return
@ -659,13 +688,15 @@ public final class MediaScrubberComponent: Component {
self.cursorPositionAnimation = nil
self.cursorDisplayLink?.isPaused = true
var cursorPosition = component.position
if let offset = self.mainAudioTrackOffset {
cursorPosition -= offset
if !self.ignoreCursorPositionUpdate {
var cursorPosition = component.position
if let offset = self.mainAudioTrackOffset {
cursorPosition -= offset
}
let cursorFrame = cursorFrame(size: scrubberSize, height: self.effectiveCursorHeight, position: cursorPosition, duration: trimDuration)
transition.setFrame(view: self.cursorView, frame: cursorFrame)
transition.setFrame(view: self.cursorContentView, frame: cursorFrame.insetBy(dx: 6.0, dy: 2.0).offsetBy(dx: -1.0 - UIScreenPixel, dy: 0.0))
}
let cursorFrame = cursorFrame(size: scrubberSize, height: self.effectiveCursorHeight, position: cursorPosition, duration: trimDuration)
transition.setFrame(view: self.cursorView, frame: cursorFrame)
transition.setFrame(view: self.cursorContentView, frame: cursorFrame.insetBy(dx: 6.0, dy: 2.0).offsetBy(dx: -1.0 - UIScreenPixel, dy: 0.0))
} else {
if let (_, _, end, ended) = self.cursorPositionAnimation {
if ended, component.position >= self.startPosition && component.position < end - 1.0 {
@ -718,6 +749,7 @@ private class TrackView: UIView, UIScrollViewDelegate, UIGestureRecognizerDelega
fileprivate var videoOpaqueFrameLayers: [VideoFrameLayer] = []
var onSelection: (Int32) -> Void = { _ in }
var onTap: (CGFloat) -> Void = { _ in }
var offsetUpdated: (Double, Bool) -> Void = { _, _ in }
var updated: (ComponentTransition) -> Void = { _ in }
@ -794,10 +826,15 @@ private class TrackView: UIView, UIScrollViewDelegate, UIGestureRecognizerDelega
}
@objc private func handleTap(_ gestureRecognizer: UITapGestureRecognizer) {
guard let (track, _, _, _) = self.params else {
guard let params = self.params else {
return
}
self.onSelection(track.id)
if case .cover = params.style {
let location = gestureRecognizer.location(in: self)
self.onTap(location.x / self.frame.width)
} else {
self.onSelection(params.track.id)
}
}
private func updateTrackOffset(done: Bool) {
@ -841,6 +878,7 @@ private class TrackView: UIView, UIScrollViewDelegate, UIGestureRecognizerDelega
}
private var params: (
style: MediaScrubberComponent.Style,
track: MediaScrubberComponent.Track,
isSelected: Bool,
availableSize: CGSize,
@ -889,7 +927,7 @@ private class TrackView: UIView, UIScrollViewDelegate, UIGestureRecognizerDelega
transition: ComponentTransition
) -> CGSize {
let previousParams = self.params
self.params = (track, isSelected, availableSize, duration)
self.params = (style, track, isSelected, availableSize, duration)
let fullTrackHeight: CGFloat
let framesCornerRadius: CGFloat

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -81,7 +81,11 @@ struct CameraState: Equatable {
}
func updatedRecording(_ recording: Recording) -> CameraState {
return CameraState(position: self.position, flashMode: self.flashMode, flashModeDidChange: self.flashModeDidChange, flashTint: self.flashTint, flashTintSize: self.flashTintSize, recording: recording, duration: self.duration, isDualCameraEnabled: self.isDualCameraEnabled, isViewOnceEnabled: self.isViewOnceEnabled)
var flashModeDidChange = self.flashModeDidChange
if case .none = self.recording {
flashModeDidChange = false
}
return CameraState(position: self.position, flashMode: self.flashMode, flashModeDidChange: flashModeDidChange, flashTint: self.flashTint, flashTintSize: self.flashTintSize, recording: recording, duration: self.duration, isDualCameraEnabled: self.isDualCameraEnabled, isViewOnceEnabled: self.isViewOnceEnabled)
}
func updatedDuration(_ duration: Double) -> CameraState {
@ -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)
}

View file

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

View file

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

View file

@ -0,0 +1,83 @@
%PDF-1.7
1 0 obj
<< >>
endobj
2 0 obj
<< /Length 3 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 6.169983 6.370150 cm
1.000000 1.000000 1.000000 scn
0.359774 1.100077 m
0.100075 0.840378 0.100075 0.419323 0.359774 0.159624 c
0.619473 -0.100075 1.040527 -0.100075 1.300226 0.159624 c
8.830000 7.689398 l
16.359774 0.159624 l
16.619473 -0.100075 17.040527 -0.100075 17.300226 0.159624 c
17.559925 0.419323 17.559925 0.840378 17.300226 1.100077 c
9.770452 8.629850 l
17.300226 16.159624 l
17.559925 16.419323 17.559925 16.840378 17.300226 17.100077 c
17.040527 17.359776 16.619473 17.359776 16.359774 17.100077 c
8.830000 9.570303 l
1.300226 17.100077 l
1.040527 17.359776 0.619473 17.359776 0.359774 17.100077 c
0.100075 16.840378 0.100075 16.419323 0.359774 16.159624 c
7.889547 8.629850 l
0.359774 1.100077 l
h
f*
n
Q
endstream
endobj
3 0 obj
788
endobj
4 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 30.000000 30.000000 ]
/Resources 1 0 R
/Contents 2 0 R
/Parent 5 0 R
>>
endobj
5 0 obj
<< /Kids [ 4 0 R ]
/Count 1
/Type /Pages
>>
endobj
6 0 obj
<< /Pages 5 0 R
/Type /Catalog
>>
endobj
xref
0 7
0000000000 65535 f
0000000010 00000 n
0000000034 00000 n
0000000878 00000 n
0000000900 00000 n
0000001073 00000 n
0000001147 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 6 0 R
/Size 7
>>
startxref
1206
%%EOF

View file

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

View file

@ -1,83 +0,0 @@
%PDF-1.7
1 0 obj
<< >>
endobj
2 0 obj
<< /Length 3 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 5.100098 4.640198 cm
1.000000 1.000000 1.000000 scn
0.970226 14.230053 m
0.710527 14.489752 0.289473 14.489752 0.029774 14.230053 c
-0.229925 13.970354 -0.229925 13.549299 0.029774 13.289600 c
5.959549 7.359825 l
0.029774 1.430050 l
-0.229925 1.170351 -0.229925 0.749296 0.029774 0.489597 c
0.289473 0.229898 0.710527 0.229898 0.970226 0.489597 c
6.900002 6.419373 l
12.829774 0.489600 l
13.089473 0.229901 13.510528 0.229901 13.770226 0.489600 c
14.029925 0.749299 14.029925 1.170354 13.770226 1.430053 c
7.840454 7.359825 l
13.770226 13.289598 l
14.029925 13.549296 14.029925 13.970351 13.770226 14.230050 c
13.510528 14.489749 13.089473 14.489749 12.829774 14.230050 c
6.900002 8.300278 l
0.970226 14.230053 l
h
f*
n
Q
endstream
endobj
3 0 obj
789
endobj
4 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 24.000000 24.000000 ]
/Resources 1 0 R
/Contents 2 0 R
/Parent 5 0 R
>>
endobj
5 0 obj
<< /Kids [ 4 0 R ]
/Count 1
/Type /Pages
>>
endobj
6 0 obj
<< /Pages 5 0 R
/Type /Catalog
>>
endobj
xref
0 7
0000000000 65535 f
0000000010 00000 n
0000000034 00000 n
0000000879 00000 n
0000000901 00000 n
0000001074 00000 n
0000001148 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 6 0 R
/Size 7
>>
startxref
1207
%%EOF

View file

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

View 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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -713,12 +713,14 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
let body = MarkdownAttributeSet(font: Font.regular(14.0), textColor: .white)
let bold: MarkdownAttributeSet
var link = body
if savedMessages {
bold = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: presentationData.theme.list.itemAccentColor.withMultiplied(hue: 0.933, saturation: 0.61, brightness: 1.0), additionalAttributes: ["URL": ""])
link = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: .white)
} else {
bold = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: .white)
}
let attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: body, bold: bold, link: body, linkAttribute: { _ in return nil }), textAlignment: .natural)
let attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: body, bold: bold, link: link, linkAttribute: { _ in return nil }), textAlignment: .natural)
self.textNode.attributedText = attributedText
self.textNode.maximumNumberOfLines = 2

View file

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