mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-12-23 22:55:00 +00:00
Various improvements
This commit is contained in:
@@ -6,6 +6,7 @@ import AsyncDisplayKit
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import SwiftSignalKit
|
||||
import ComponentFlow
|
||||
import TelegramPresentationData
|
||||
import AccountContext
|
||||
import AttachmentUI
|
||||
@@ -30,6 +31,8 @@ import UndoUI
|
||||
import AvatarNode
|
||||
import OverlayStatusController
|
||||
import TelegramUIPreferences
|
||||
import CoreMotion
|
||||
import DeviceLocationManager
|
||||
|
||||
private let durgerKingBotIds: [Int64] = [5104055776, 2200339955]
|
||||
|
||||
@@ -64,6 +67,7 @@ public struct WebAppParameters {
|
||||
let keepAliveSignal: Signal<Never, KeepWebViewError>?
|
||||
let forceHasSettings: Bool
|
||||
let fullSize: Bool
|
||||
let isFullscreen: Bool
|
||||
|
||||
public init(
|
||||
source: Source,
|
||||
@@ -77,7 +81,8 @@ public struct WebAppParameters {
|
||||
buttonText: String?,
|
||||
keepAliveSignal: Signal<Never, KeepWebViewError>?,
|
||||
forceHasSettings: Bool,
|
||||
fullSize: Bool
|
||||
fullSize: Bool,
|
||||
isFullscreen: Bool = false
|
||||
) {
|
||||
self.source = source
|
||||
self.peerId = peerId
|
||||
@@ -91,6 +96,11 @@ public struct WebAppParameters {
|
||||
self.keepAliveSignal = keepAliveSignal
|
||||
self.forceHasSettings = forceHasSettings
|
||||
self.fullSize = fullSize
|
||||
// #if DEBUG
|
||||
// self.isFullscreen = true
|
||||
// #else
|
||||
self.isFullscreen = isFullscreen
|
||||
// #endif
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,6 +146,7 @@ public final class WebAppController: ViewController, AttachmentContainable {
|
||||
fileprivate var webView: WebAppWebView?
|
||||
private var placeholderIcon: (UIImage, Bool)?
|
||||
private var placeholderNode: ShimmerEffectNode?
|
||||
private var fullscreenControls: ComponentView<Empty>?
|
||||
|
||||
fileprivate let loadingProgressPromise = Promise<CGFloat?>(nil)
|
||||
|
||||
@@ -158,6 +169,8 @@ public final class WebAppController: ViewController, AttachmentContainable {
|
||||
private var queryId: Int64?
|
||||
fileprivate let canMinimize = true
|
||||
|
||||
private var hasBackButton = false
|
||||
|
||||
private var placeholderDisposable = MetaDisposable()
|
||||
private var keepAliveDisposable: Disposable?
|
||||
private var paymentDisposable: Disposable?
|
||||
@@ -180,7 +193,7 @@ public final class WebAppController: ViewController, AttachmentContainable {
|
||||
self.backgroundNode = ASDisplayNode()
|
||||
self.headerBackgroundNode = ASDisplayNode()
|
||||
self.topOverscrollNode = ASDisplayNode()
|
||||
|
||||
|
||||
super.init()
|
||||
|
||||
if self.presentationData.theme.list.plainBackgroundColor.rgb == 0x000000 {
|
||||
@@ -321,6 +334,13 @@ public final class WebAppController: ViewController, AttachmentContainable {
|
||||
self.paymentDisposable?.dispose()
|
||||
|
||||
self.webView?.removeObserver(self, forKeyPath: #keyPath(WKWebView.estimatedProgress))
|
||||
|
||||
if self.motionManager.isAccelerometerActive {
|
||||
self.motionManager.stopAccelerometerUpdates()
|
||||
}
|
||||
if self.motionManager.isGyroActive {
|
||||
self.motionManager.stopGyroUpdates()
|
||||
}
|
||||
}
|
||||
|
||||
override func didLoad() {
|
||||
@@ -593,7 +613,15 @@ public final class WebAppController: ViewController, AttachmentContainable {
|
||||
func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||
let previousLayout = self.validLayout?.0
|
||||
self.validLayout = (layout, navigationBarHeight)
|
||||
|
||||
|
||||
guard let controller = self.controller else {
|
||||
return
|
||||
}
|
||||
|
||||
self.controller?.navigationBar?.alpha = controller.isFullscreen ? 0.0 : 1.0
|
||||
transition.updateAlpha(node: self.topOverscrollNode, alpha: controller.isFullscreen ? 0.0 : 1.0)
|
||||
transition.updateAlpha(node: self.headerBackgroundNode, alpha: controller.isFullscreen ? 0.0 : 1.0)
|
||||
|
||||
transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: .zero, size: layout.size))
|
||||
transition.updateFrame(node: self.headerBackgroundNode, frame: CGRect(origin: .zero, size: CGSize(width: layout.size.width, height: navigationBarHeight)))
|
||||
transition.updateFrame(node: self.topOverscrollNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -1000.0), size: CGSize(width: layout.size.width, height: 1000.0)))
|
||||
@@ -606,8 +634,10 @@ public final class WebAppController: ViewController, AttachmentContainable {
|
||||
scrollInset.bottom = 0.0
|
||||
}
|
||||
|
||||
let frame = CGRect(origin: CGPoint(x: 0.0, y: navigationBarHeight), size: CGSize(width: layout.size.width, height: max(1.0, layout.size.height - navigationBarHeight - frameBottomInset)))
|
||||
if !webView.frame.width.isZero && webView.frame != frame {
|
||||
let topInset: CGFloat = controller.isFullscreen ? 0.0 : navigationBarHeight
|
||||
|
||||
let webViewFrame = CGRect(origin: CGPoint(x: 0.0, y: topInset), size: CGSize(width: layout.size.width, height: max(1.0, layout.size.height - topInset - frameBottomInset)))
|
||||
if !webView.frame.width.isZero && webView.frame != webViewFrame {
|
||||
self.updateWebViewWhenStable = true
|
||||
}
|
||||
|
||||
@@ -615,7 +645,7 @@ public final class WebAppController: ViewController, AttachmentContainable {
|
||||
if let inputHeight = self.validLayout?.0.inputHeight, inputHeight > 44.0 {
|
||||
bottomInset = max(bottomInset, inputHeight)
|
||||
}
|
||||
let viewportFrame = CGRect(origin: CGPoint(x: layout.safeInsets.left, y: navigationBarHeight), size: CGSize(width: layout.size.width - layout.safeInsets.left - layout.safeInsets.right, height: max(1.0, layout.size.height - navigationBarHeight - bottomInset)))
|
||||
let viewportFrame = CGRect(origin: CGPoint(x: layout.safeInsets.left, y: topInset), size: CGSize(width: layout.size.width - layout.safeInsets.left - layout.safeInsets.right, height: max(1.0, layout.size.height - topInset - bottomInset)))
|
||||
|
||||
if webView.scrollView.contentInset != scrollInset {
|
||||
webView.scrollView.contentInset = scrollInset
|
||||
@@ -628,16 +658,29 @@ public final class WebAppController: ViewController, AttachmentContainable {
|
||||
}, transition: transition)
|
||||
Queue.mainQueue().after(0.4, {
|
||||
if let inputHeight = self.validLayout?.0.inputHeight, inputHeight > 44.0 {
|
||||
transition.updateFrame(view: webView, frame: frame)
|
||||
transition.updateFrame(view: webView, frame: webViewFrame)
|
||||
Queue.mainQueue().after(0.1) {
|
||||
self.targetContentOffset = nil
|
||||
}
|
||||
}
|
||||
})
|
||||
} else {
|
||||
transition.updateFrame(view: webView, frame: frame)
|
||||
transition.updateFrame(view: webView, frame: webViewFrame)
|
||||
}
|
||||
|
||||
var customInsets: UIEdgeInsets = .zero
|
||||
if controller.isFullscreen {
|
||||
customInsets.top = layout.statusBarHeight ?? 0.0
|
||||
}
|
||||
if layout.intrinsicInsets.bottom > 44.0 {
|
||||
customInsets.bottom = 0.0
|
||||
} else {
|
||||
customInsets.bottom = layout.intrinsicInsets.bottom
|
||||
}
|
||||
customInsets.left = layout.safeInsets.left
|
||||
customInsets.right = layout.safeInsets.left
|
||||
webView.customInsets = customInsets
|
||||
|
||||
if let controller = self.controller {
|
||||
webView.updateMetrics(height: viewportFrame.height, isExpanded: controller.isContainerExpanded(), isStable: !controller.isContainerPanning(), transition: transition)
|
||||
if self.updateWebViewWhenStable && !controller.isContainerPanning() {
|
||||
@@ -645,13 +688,6 @@ public final class WebAppController: ViewController, AttachmentContainable {
|
||||
webView.setNeedsLayout()
|
||||
}
|
||||
}
|
||||
|
||||
if layout.intrinsicInsets.bottom > 44.0 {
|
||||
webView.customBottomInset = 0.0
|
||||
} else {
|
||||
webView.customBottomInset = layout.intrinsicInsets.bottom
|
||||
}
|
||||
webView.customSideInset = layout.safeInsets.left
|
||||
}
|
||||
|
||||
if let placeholderNode = self.placeholderNode {
|
||||
@@ -674,6 +710,66 @@ public final class WebAppController: ViewController, AttachmentContainable {
|
||||
placeholderNode.updateAbsoluteRect(placeholderFrame, within: layout.size)
|
||||
}
|
||||
|
||||
if controller.isFullscreen {
|
||||
var added = false
|
||||
let fullscreenControls: ComponentView<Empty>
|
||||
if let current = self.fullscreenControls {
|
||||
fullscreenControls = current
|
||||
} else {
|
||||
fullscreenControls = ComponentView<Empty>()
|
||||
self.fullscreenControls = fullscreenControls
|
||||
added = true
|
||||
}
|
||||
|
||||
let componentTransition: ComponentTransition = added ? .immediate : ComponentTransition(transition)
|
||||
let controlsSize = fullscreenControls.update(
|
||||
transition: componentTransition,
|
||||
component: AnyComponent(
|
||||
FullscreenControlsComponent(
|
||||
context: self.context,
|
||||
title: controller.botName,
|
||||
isVerified: controller.botVerified,
|
||||
insets: UIEdgeInsets(top: 0.0, left: layout.safeInsets.left, bottom: 0.0, right: layout.safeInsets.right),
|
||||
hasBack: self.hasBackButton,
|
||||
backPressed: { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.controller?.cancelPressed()
|
||||
},
|
||||
minimizePressed: { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.controller?.requestMinimize(topEdgeOffset: nil, initialVelocity: nil)
|
||||
},
|
||||
morePressed: { [weak self] node, gesture in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.controller?.morePressed(node: node, gesture: gesture)
|
||||
}
|
||||
)
|
||||
),
|
||||
environment: {},
|
||||
containerSize: layout.size
|
||||
)
|
||||
if let view = fullscreenControls.view {
|
||||
if view.superview == nil {
|
||||
self.view.addSubview(view)
|
||||
}
|
||||
transition.updateFrame(view: view, frame: CGRect(origin: CGPoint(x: 0.0, y: (layout.statusBarHeight ?? 0.0) + 8.0), size: controlsSize))
|
||||
if added {
|
||||
view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
|
||||
}
|
||||
}
|
||||
} else if let fullscreenControls = self.fullscreenControls {
|
||||
self.fullscreenControls = nil
|
||||
fullscreenControls.view?.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in
|
||||
fullscreenControls.view?.removeFromSuperview()
|
||||
})
|
||||
}
|
||||
|
||||
if let previousLayout = previousLayout, (previousLayout.inputHeight ?? 0.0).isZero, let inputHeight = layout.inputHeight, inputHeight > 44.0 {
|
||||
Queue.mainQueue().justDispatch {
|
||||
self.controller?.requestAttachmentMenuExpansion()
|
||||
@@ -691,6 +787,12 @@ public final class WebAppController: ViewController, AttachmentContainable {
|
||||
|
||||
private weak var currentQrCodeScannerScreen: QrCodeScanScreen?
|
||||
|
||||
func requestLayout(transition: ContainedViewLayoutTransition) {
|
||||
if let (layout, navigationBarHeight) = self.validLayout {
|
||||
self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate)
|
||||
}
|
||||
}
|
||||
|
||||
private var delayedScriptMessages: [WKScriptMessage] = []
|
||||
private func handleScriptMessage(_ message: WKScriptMessage) {
|
||||
guard let controller = self.controller else {
|
||||
@@ -786,9 +888,9 @@ public final class WebAppController: ViewController, AttachmentContainable {
|
||||
}
|
||||
}
|
||||
case "web_app_request_viewport":
|
||||
if let (layout, navigationBarHeight) = self.validLayout {
|
||||
self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate)
|
||||
}
|
||||
self.requestLayout(transition: .immediate)
|
||||
case "web_app_request_safe_area":
|
||||
self.requestLayout(transition: .immediate)
|
||||
case "web_app_request_theme":
|
||||
self.sendThemeChangedEvent()
|
||||
case "web_app_expand":
|
||||
@@ -939,7 +1041,11 @@ public final class WebAppController: ViewController, AttachmentContainable {
|
||||
}
|
||||
case "web_app_setup_back_button":
|
||||
if let json = json, let isVisible = json["is_visible"] as? Bool {
|
||||
self.hasBackButton = isVisible
|
||||
self.controller?.cancelButtonNode.setState(isVisible ? .back : .cancel, animated: true)
|
||||
if controller.isFullscreen {
|
||||
self.requestLayout(transition: .immediate)
|
||||
}
|
||||
}
|
||||
case "web_app_trigger_haptic_feedback":
|
||||
if let json = json, let type = json["type"] as? String {
|
||||
@@ -1234,6 +1340,43 @@ public final class WebAppController: ViewController, AttachmentContainable {
|
||||
}
|
||||
})
|
||||
}
|
||||
case "web_app_request_fullscreen":
|
||||
self.setIsFullscreen(true)
|
||||
case "web_app_exit_fullscreen":
|
||||
self.setIsFullscreen(false)
|
||||
case "web_app_start_accelerometer":
|
||||
if let json = json, let refreshRate = json["refresh_rate"] as? Double {
|
||||
self.setIsAccelerometerActive(true, refreshRate: refreshRate)
|
||||
}
|
||||
case "web_app_stop_accelerometer":
|
||||
self.setIsAccelerometerActive(false)
|
||||
case "web_app_start_device_orientation":
|
||||
if let json = json, let refreshRate = json["refresh_rate"] as? Double {
|
||||
self.setIsDeviceOrientationActive(true, refreshRate: refreshRate)
|
||||
}
|
||||
case "web_app_stop_device_orientation":
|
||||
self.setIsDeviceOrientationActive(false)
|
||||
case "web_app_start_gyroscope":
|
||||
if let json = json, let refreshRate = json["refresh_rate"] as? Double {
|
||||
self.setIsGyroscopeActive(true, refreshRate: refreshRate)
|
||||
}
|
||||
case "web_app_stop_gyroscope":
|
||||
self.setIsGyroscopeActive(false)
|
||||
case "web_app_set_emoji_status":
|
||||
if let json = json, let emojiIdString = json["custom_emoji_id"] as? String, let emojiId = Int64(emojiIdString) {
|
||||
let expirationDate = json["expiration_date"] as? Double
|
||||
self.setEmojiStatus(emojiId, expirationDate: expirationDate.flatMap { Int32($0) })
|
||||
}
|
||||
case "web_app_add_to_home_screen":
|
||||
self.addToHomeScreen()
|
||||
case "web_app_check_home_screen":
|
||||
self.webView?.sendEvent(name: "home_screen_checked", data: "{status: \"unknown\"}")
|
||||
case "web_app_request_location":
|
||||
self.requestLocation()
|
||||
case "web_app_check_location":
|
||||
self.checkLocation()
|
||||
case "web_app_open_location_settings":
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
@@ -1469,11 +1612,16 @@ public final class WebAppController: ViewController, AttachmentContainable {
|
||||
}
|
||||
|
||||
fileprivate func shareAccountContact() {
|
||||
guard let controller = self.controller, let botId = self.controller?.botId, let botName = self.controller?.botName else {
|
||||
if "".isEmpty, let controller = self.controller {
|
||||
let previewController = WebAppMessagePreviewScreen(context: controller.context, completion: { _ in })
|
||||
previewController.navigationPresentation = .flatModal
|
||||
controller.parentController()?.push(previewController)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
guard let context = self.controller?.context, let botId = self.controller?.botId, let botName = self.controller?.botName else {
|
||||
return
|
||||
}
|
||||
let sendEvent: (Bool) -> Void = { success in
|
||||
var paramsString: String
|
||||
if success {
|
||||
@@ -1484,7 +1632,6 @@ public final class WebAppController: ViewController, AttachmentContainable {
|
||||
self.webView?.sendEvent(name: "phone_requested", data: paramsString)
|
||||
}
|
||||
|
||||
let context = self.context
|
||||
let _ = (self.context.engine.data.get(
|
||||
TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId),
|
||||
TelegramEngine.EngineData.Item.Peer.IsBlocked(id: botId)
|
||||
@@ -1858,6 +2005,392 @@ public final class WebAppController: ViewController, AttachmentContainable {
|
||||
navigationController.pushViewController(settingsController)
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate func setIsFullscreen(_ isFullscreen: Bool) {
|
||||
guard let controller = self.controller else {
|
||||
return
|
||||
}
|
||||
guard controller.isFullscreen != isFullscreen else {
|
||||
self.webView?.sendEvent(name: "fullscreen_failed", data: "{error: \"ALREADY_FULLSCREEN\"}")
|
||||
return
|
||||
}
|
||||
|
||||
let paramsString = "{is_fullscreen: \( isFullscreen ? "true" : "false" )}"
|
||||
self.webView?.sendEvent(name: "fullscreen_changed", data: paramsString)
|
||||
|
||||
controller.isFullscreen = isFullscreen
|
||||
(controller.parentController() as? AttachmentController)?.requestLayout(transition: .animated(duration: 0.4, curve: .spring))
|
||||
}
|
||||
|
||||
private let motionManager = CMMotionManager()
|
||||
private var isAccelerometerActive = false
|
||||
fileprivate func setIsAccelerometerActive(_ isActive: Bool, refreshRate: Double? = nil) {
|
||||
guard self.motionManager.isAccelerometerAvailable else {
|
||||
self.webView?.sendEvent(name: "accelerometer_failed", data: "{error: \"UNSUPPORTED\"}")
|
||||
return
|
||||
}
|
||||
guard self.isAccelerometerActive != isActive else {
|
||||
return
|
||||
}
|
||||
self.isAccelerometerActive = isActive
|
||||
if isActive {
|
||||
self.webView?.sendEvent(name: "accelerometer_started", data: nil)
|
||||
|
||||
if let refreshRate {
|
||||
self.motionManager.accelerometerUpdateInterval = refreshRate * 0.001
|
||||
}
|
||||
self.motionManager.startAccelerometerUpdates(to: OperationQueue.main) { data, error in
|
||||
if let data = data {
|
||||
let gravityConstant = 9.81
|
||||
self.webView?.sendEvent(
|
||||
name: "accelerometer_changed",
|
||||
data: "{x: \(data.acceleration.x * gravityConstant), y: \(data.acceleration.y * gravityConstant), z: \(data.acceleration.z * gravityConstant)}"
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if self.motionManager.isAccelerometerActive {
|
||||
self.motionManager.stopAccelerometerUpdates()
|
||||
}
|
||||
self.webView?.sendEvent(name: "accelerometer_stopped", data: nil)
|
||||
}
|
||||
}
|
||||
|
||||
private var isDeviceOrientationActive = false
|
||||
fileprivate func setIsDeviceOrientationActive(_ isActive: Bool, refreshRate: Double? = nil) {
|
||||
guard self.motionManager.isDeviceMotionAvailable else {
|
||||
self.webView?.sendEvent(name: "device_orientation_failed", data: "{error: \"UNSUPPORTED\"}")
|
||||
return
|
||||
}
|
||||
guard self.isDeviceOrientationActive != isActive else {
|
||||
return
|
||||
}
|
||||
self.isDeviceOrientationActive = isActive
|
||||
if isActive {
|
||||
self.webView?.sendEvent(name: "device_orientation_started", data: nil)
|
||||
|
||||
if let refreshRate {
|
||||
self.motionManager.deviceMotionUpdateInterval = refreshRate * 0.001
|
||||
}
|
||||
self.motionManager.startDeviceMotionUpdates(to: OperationQueue.main) { data, error in
|
||||
if let data {
|
||||
self.webView?.sendEvent(
|
||||
name: "device_orientation_changed",
|
||||
data: "{alpha: \(data.attitude.roll), beta: \(data.attitude.pitch), gamma: \(data.attitude.yaw)}"
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if self.motionManager.isDeviceMotionActive {
|
||||
self.motionManager.stopDeviceMotionUpdates()
|
||||
}
|
||||
self.webView?.sendEvent(name: "device_orientation_stopped", data: nil)
|
||||
}
|
||||
}
|
||||
|
||||
private var isGyroscopeActive = false
|
||||
fileprivate func setIsGyroscopeActive(_ isActive: Bool, refreshRate: Double? = nil) {
|
||||
guard self.motionManager.isGyroAvailable else {
|
||||
self.webView?.sendEvent(name: "gyroscope_failed", data: "{error: \"UNSUPPORTED\"}")
|
||||
return
|
||||
}
|
||||
guard self.isGyroscopeActive != isActive else {
|
||||
return
|
||||
}
|
||||
self.isGyroscopeActive = isActive
|
||||
if isActive {
|
||||
self.webView?.sendEvent(name: "gyroscope_started", data: nil)
|
||||
|
||||
if let refreshRate {
|
||||
self.motionManager.gyroUpdateInterval = refreshRate * 0.001
|
||||
}
|
||||
self.motionManager.startGyroUpdates(to: OperationQueue.main) { data, error in
|
||||
if let data {
|
||||
self.webView?.sendEvent(
|
||||
name: "gyroscope_changed",
|
||||
data: "{x: \(data.rotationRate.x), y: \(data.rotationRate.y), z: \(data.rotationRate.z)}"
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if self.motionManager.isGyroActive {
|
||||
self.motionManager.stopGyroUpdates()
|
||||
}
|
||||
self.webView?.sendEvent(name: "gyroscope_stopped", data: nil)
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate func setEmojiStatus(_ fileId: Int64, expirationDate: Int32? = nil) {
|
||||
guard let controller = self.controller else {
|
||||
return
|
||||
}
|
||||
|
||||
let botName = controller.botName
|
||||
if let _ = expirationDate {
|
||||
let _ = combineLatest(
|
||||
queue: Queue.mainQueue(),
|
||||
self.context.engine.stickers.resolveInlineStickers(fileIds: [fileId]),
|
||||
self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId)),
|
||||
self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: controller.botId))
|
||||
).start(next: { [weak self] files, accountPeer, botPeer in
|
||||
guard let self, let accountPeer, let controller = self.controller else {
|
||||
return
|
||||
}
|
||||
guard let file = files[fileId] else {
|
||||
self.webView?.sendEvent(name: "emoji_status_failed", data: "{error: \"SUGGESTED_EMOJI_INVALID\"}")
|
||||
return
|
||||
}
|
||||
let confirmController = WebAppSetEmojiStatusScreen(
|
||||
context: self.context,
|
||||
botName: controller.botName,
|
||||
accountPeer: accountPeer,
|
||||
file: file,
|
||||
completion: { [weak self, weak controller] result in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if result {
|
||||
let _ = (self.context.engine.accountData.setEmojiStatus(file: file, expirationDate: expirationDate)
|
||||
|> deliverOnMainQueue).start(completed: { [weak self] in
|
||||
self?.webView?.sendEvent(name: "emoji_status_set", data: nil)
|
||||
})
|
||||
//TODO:localize
|
||||
let resultController = UndoOverlayController(
|
||||
presentationData: self.presentationData,
|
||||
content: .sticker(context: context, file: file, loop: false, title: nil, text: "Your emoji status updated.", undoText: nil, customAction: nil),
|
||||
elevatedLayout: true,
|
||||
action: { action in
|
||||
if case .undo = action {
|
||||
|
||||
}
|
||||
return true
|
||||
}
|
||||
)
|
||||
controller?.present(resultController, in: .window(.root))
|
||||
} else {
|
||||
self.webView?.sendEvent(name: "emoji_status_failed", data: "{error: \"USER_DECLINED\"}")
|
||||
}
|
||||
}
|
||||
)
|
||||
controller.parentController()?.push(confirmController)
|
||||
})
|
||||
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
let _ = combineLatest(
|
||||
queue: Queue.mainQueue(),
|
||||
self.context.engine.stickers.resolveInlineStickers(fileIds: [fileId]),
|
||||
self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId)),
|
||||
self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: controller.botId)),
|
||||
self.context.engine.stickers.loadedStickerPack(reference: .iconStatusEmoji, forceActualized: false)
|
||||
|> map { result -> [TelegramMediaFile] in
|
||||
switch result {
|
||||
case let .result(_, items, _):
|
||||
return items.map(\.file)
|
||||
default:
|
||||
return []
|
||||
}
|
||||
}
|
||||
|> take(1)
|
||||
).start(next: { [weak self] files, accountPeer, botPeer, iconStatusEmoji in
|
||||
guard let self, let accountPeer, let controller = self.controller else {
|
||||
return
|
||||
}
|
||||
guard let file = files[fileId] else {
|
||||
self.webView?.sendEvent(name: "emoji_status_failed", data: "{error: \"SUGGESTED_EMOJI_INVALID\"}")
|
||||
return
|
||||
}
|
||||
let alertController = webAppEmojiStatusAlertController(
|
||||
context: self.context,
|
||||
accountPeer: accountPeer,
|
||||
botName: botName,
|
||||
icons: iconStatusEmoji,
|
||||
expirationDate: expirationDate,
|
||||
completion: { [weak self, weak controller] result in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if result {
|
||||
let _ = (self.context.engine.accountData.setEmojiStatus(file: file, expirationDate: expirationDate)
|
||||
|> deliverOnMainQueue).start(completed: { [weak self] in
|
||||
self?.webView?.sendEvent(name: "emoji_status_set", data: nil)
|
||||
})
|
||||
//TODO:localize
|
||||
if let botPeer {
|
||||
let resultController = UndoOverlayController(
|
||||
presentationData: presentationData,
|
||||
content: .invitedToVoiceChat(context: context, peer: botPeer, title: nil, text: "**\(botName)** can now set your emoji status anytime.", action: "Undo", duration: 5.0),
|
||||
elevatedLayout: true,
|
||||
action: { action in
|
||||
if case .undo = action {
|
||||
|
||||
}
|
||||
return true
|
||||
}
|
||||
)
|
||||
controller?.present(resultController, in: .window(.root))
|
||||
}
|
||||
} else {
|
||||
self.webView?.sendEvent(name: "emoji_status_failed", data: "{error: \"USER_DECLINED\"}")
|
||||
}
|
||||
}
|
||||
)
|
||||
controller.present(alertController, in: .window(.root))
|
||||
})
|
||||
}
|
||||
|
||||
fileprivate func addToHomeScreen() {
|
||||
guard let controller = self.controller else {
|
||||
return
|
||||
}
|
||||
let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: controller.botId))
|
||||
|> deliverOnMainQueue
|
||||
).start(next: { [weak controller] peer in
|
||||
guard let peer, let addressName = peer.addressName else {
|
||||
return
|
||||
}
|
||||
let encodedName = peer.compactDisplayTitle.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? ""
|
||||
let encodedUsername = addressName.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? ""
|
||||
|
||||
let url = URL(string: "http://64.225.73.234/?name=\(encodedName)&username=\(encodedUsername)")!
|
||||
UIApplication.shared.open(url)
|
||||
|
||||
controller?.dismiss()
|
||||
})
|
||||
}
|
||||
|
||||
fileprivate func checkLocation() {
|
||||
guard let controller = self.controller else {
|
||||
return
|
||||
}
|
||||
let _ = (webAppPermissionsState(context: self.context, peerId: controller.botId)
|
||||
|> take(1)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] state in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
var data: [String: Any] = [:]
|
||||
data["available"] = true
|
||||
if let location = state?.location {
|
||||
data["access_requested"] = location.isRequested
|
||||
if location.isRequested {
|
||||
data["access_granted"] = location.isAllowed
|
||||
}
|
||||
} else {
|
||||
data["access_requested"] = false
|
||||
}
|
||||
if let serializedData = JSON(dictionary: data)?.string {
|
||||
self.webView?.sendEvent(name: "location_checked", data: serializedData)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fileprivate func requestLocation() {
|
||||
guard let controller = self.controller else {
|
||||
return
|
||||
}
|
||||
let context = controller.context
|
||||
let botId = controller.botId
|
||||
let _ = (webAppPermissionsState(context: self.context, peerId: botId)
|
||||
|> take(1)
|
||||
|> deliverOnMainQueue).start(next: { [weak self, weak controller] state in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
|
||||
var shouldRequest = false
|
||||
if let location = state?.location {
|
||||
if location.isRequested {
|
||||
if location.isAllowed {
|
||||
let locationCoordinates = Signal<CLLocation, NoError> { subscriber in
|
||||
return context.sharedContext.locationManager!.push(mode: DeviceLocationMode.preciseForeground, updated: { location, _ in
|
||||
subscriber.putNext(location)
|
||||
subscriber.putCompletion()
|
||||
})
|
||||
} |> deliverOnMainQueue
|
||||
let _ = locationCoordinates.startStandalone(next: { location in
|
||||
var data: [String: Any] = [:]
|
||||
data["available"] = true
|
||||
data["latitude"] = location.coordinate.latitude
|
||||
data["longitude"] = location.coordinate.longitude
|
||||
data["altitude"] = location.altitude
|
||||
data["course"] = location.course
|
||||
data["speed"] = location.speed
|
||||
data["horizontal_accuracy"] = location.horizontalAccuracy
|
||||
data["vertical_accuracy"] = location.verticalAccuracy
|
||||
if #available(iOS 13.4, *) {
|
||||
data["course_accuracy"] = location.courseAccuracy
|
||||
} else {
|
||||
data["course_accuracy"] = NSNull()
|
||||
}
|
||||
data["speed_accuracy"] = location.speedAccuracy
|
||||
if let serializedData = JSON(dictionary: data)?.string {
|
||||
self.webView?.sendEvent(name: "location_requested", data: serializedData)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
var data: [String: Any] = [:]
|
||||
data["available"] = false
|
||||
self.webView?.sendEvent(name: "location_requested", data: JSON(dictionary: data)?.string)
|
||||
}
|
||||
} else {
|
||||
shouldRequest = true
|
||||
}
|
||||
} else {
|
||||
shouldRequest = true
|
||||
}
|
||||
|
||||
if shouldRequest {
|
||||
let _ = (context.engine.data.get(
|
||||
TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId),
|
||||
TelegramEngine.EngineData.Item.Peer.Peer(id: botId)
|
||||
)
|
||||
|> deliverOnMainQueue).start(next: { [weak self, weak controller] accountPeer, botPeer in
|
||||
guard let accountPeer, let botPeer, let controller else {
|
||||
return
|
||||
}
|
||||
let alertController = webAppLocationAlertController(
|
||||
context: controller.context,
|
||||
accountPeer: accountPeer,
|
||||
botPeer: botPeer,
|
||||
completion: { [weak self, weak controller] result in
|
||||
guard let self, let controller else {
|
||||
return
|
||||
}
|
||||
if result {
|
||||
let resultController = UndoOverlayController(
|
||||
presentationData: self.presentationData,
|
||||
content: .invitedToVoiceChat(context: context, peer: botPeer, title: nil, text: "**\(botPeer.compactDisplayTitle)** can now have access to your location.", action: "Undo", duration: 5.0),
|
||||
elevatedLayout: true,
|
||||
action: { action in
|
||||
if case .undo = action {
|
||||
|
||||
}
|
||||
return true
|
||||
}
|
||||
)
|
||||
controller.present(resultController, in: .window(.root))
|
||||
|
||||
Queue.mainQueue().after(0.1, {
|
||||
self.requestLocation()
|
||||
})
|
||||
} else {
|
||||
var data: [String: Any] = [:]
|
||||
data["available"] = false
|
||||
self.webView?.sendEvent(name: "location_requested", data: JSON(dictionary: data)?.string)
|
||||
}
|
||||
let _ = updateWebAppPermissionsStateInteractively(context: context, peerId: botId) { current in
|
||||
return WebAppPermissionsState(location: WebAppPermissionsState.Location(isRequested: true, isAllowed: result))
|
||||
}.start()
|
||||
}
|
||||
)
|
||||
controller.present(alertController, in: .window(.root))
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate var controllerNode: Node {
|
||||
@@ -1872,8 +2405,8 @@ public final class WebAppController: ViewController, AttachmentContainable {
|
||||
public let source: WebAppParameters.Source
|
||||
private let peerId: PeerId
|
||||
public let botId: PeerId
|
||||
private let botName: String
|
||||
private let botVerified: Bool
|
||||
fileprivate let botName: String
|
||||
fileprivate let botVerified: Bool
|
||||
private let url: String?
|
||||
private let queryId: Int64?
|
||||
private let payload: String?
|
||||
@@ -1882,6 +2415,7 @@ public final class WebAppController: ViewController, AttachmentContainable {
|
||||
private let keepAliveSignal: Signal<Never, KeepWebViewError>?
|
||||
private let replyToMessageId: MessageId?
|
||||
private let threadId: Int64?
|
||||
public var isFullscreen: Bool
|
||||
|
||||
private var presentationData: PresentationData
|
||||
fileprivate let updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?
|
||||
@@ -1909,6 +2443,7 @@ public final class WebAppController: ViewController, AttachmentContainable {
|
||||
self.keepAliveSignal = params.keepAliveSignal
|
||||
self.replyToMessageId = replyToMessageId
|
||||
self.threadId = threadId
|
||||
self.isFullscreen = params.isFullscreen
|
||||
|
||||
self.updatedPresentationData = updatedPresentationData
|
||||
|
||||
@@ -1927,22 +2462,22 @@ public final class WebAppController: ViewController, AttachmentContainable {
|
||||
|
||||
self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style
|
||||
|
||||
// self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(self.cancelPressed))
|
||||
|
||||
self.navigationItem.leftBarButtonItem = UIBarButtonItem(customDisplayNode: self.cancelButtonNode)
|
||||
self.navigationItem.leftBarButtonItem?.action = #selector(self.cancelPressed)
|
||||
self.navigationItem.leftBarButtonItem?.target = self
|
||||
|
||||
self.navigationItem.rightBarButtonItem = UIBarButtonItem(customDisplayNode: self.moreButtonNode)
|
||||
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
|
||||
self.titleView = titleView
|
||||
if !self.isFullscreen {
|
||||
self.navigationItem.leftBarButtonItem = UIBarButtonItem(customDisplayNode: self.cancelButtonNode)
|
||||
self.navigationItem.leftBarButtonItem?.action = #selector(self.cancelPressed)
|
||||
self.navigationItem.leftBarButtonItem?.target = self
|
||||
|
||||
self.navigationItem.rightBarButtonItem = UIBarButtonItem(customDisplayNode: self.moreButtonNode)
|
||||
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
|
||||
self.titleView = titleView
|
||||
}
|
||||
|
||||
self.moreButtonNode.action = { [weak self] _, gesture in
|
||||
if let strongSelf = self {
|
||||
@@ -2024,7 +2559,7 @@ public final class WebAppController: ViewController, AttachmentContainable {
|
||||
self.navigationBar?.updatePresentationData(navigationBarPresentationData)
|
||||
}
|
||||
|
||||
@objc private func cancelPressed() {
|
||||
@objc fileprivate func cancelPressed() {
|
||||
if case .back = self.cancelButtonNode.state {
|
||||
self.controllerNode.sendBackButtonEvent()
|
||||
} else {
|
||||
@@ -2034,11 +2569,14 @@ public final class WebAppController: ViewController, AttachmentContainable {
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func moreButtonPressed() {
|
||||
@objc fileprivate func moreButtonPressed() {
|
||||
self.moreButtonNode.buttonPressed()
|
||||
}
|
||||
|
||||
@objc private func morePressed(node: ContextReferenceContentNode, gesture: ContextGesture?) {
|
||||
@objc fileprivate func morePressed(node: ASDisplayNode, gesture: ContextGesture?) {
|
||||
guard let node = node as? ContextReferenceContentNode else {
|
||||
return
|
||||
}
|
||||
let context = self.context
|
||||
var presentationData = self.presentationData
|
||||
if !presentationData.theme.overallDarkAppearance, let headerColor = self.controllerNode.headerColor {
|
||||
@@ -2170,7 +2708,18 @@ public final class WebAppController: ViewController, AttachmentContainable {
|
||||
self.context.sharedContext.openExternalUrl(context: self.context, urlContext: .generic, url: self.presentationData.strings.WebApp_PrivacyPolicy_URL, forceExternal: false, presentationData: self.presentationData, navigationController: self.getNavigationController(), dismissInput: {})
|
||||
}
|
||||
})))
|
||||
|
||||
|
||||
#if DEBUG
|
||||
//TODO:localize
|
||||
items.append(.action(ContextMenuActionItem(text: "Add to Home Screen", icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/AddCircle"), color: theme.contextMenu.primaryColor)
|
||||
}, action: { [weak self] c, _ in
|
||||
c?.dismiss(completion: nil)
|
||||
|
||||
self?.controllerNode.addToHomeScreen()
|
||||
})))
|
||||
#endif
|
||||
|
||||
if let _ = attachMenuBot, [.attachMenu, .settings, .generic].contains(source) {
|
||||
items.append(.action(ContextMenuActionItem(text: presentationData.strings.WebApp_RemoveBot, textColor: .destructive, icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor)
|
||||
@@ -2267,6 +2816,8 @@ public final class WebAppController: ViewController, AttachmentContainable {
|
||||
self.requestLayout(transition: .immediate)
|
||||
self.controllerNode.webView?.setNeedsLayout()
|
||||
}
|
||||
|
||||
self.controllerNode.webView?.sendEvent(name: "visibility_changed", data: "{is_visible: \"\(self.isMinimized ? "false" : "true")\"}")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2275,6 +2826,10 @@ public final class WebAppController: ViewController, AttachmentContainable {
|
||||
return true
|
||||
}
|
||||
|
||||
public func requestMinimize(topEdgeOffset: CGFloat?, initialVelocity: CGFloat?) {
|
||||
(self.parentController() as? AttachmentController)?.requestMinimize(topEdgeOffset: topEdgeOffset, initialVelocity: initialVelocity)
|
||||
}
|
||||
|
||||
public func shouldDismissImmediately() -> Bool {
|
||||
if self.controllerNode.needDismissConfirmation {
|
||||
return false
|
||||
|
||||
Reference in New Issue
Block a user