Various fixes

This commit is contained in:
Ilya Laktyushin 2025-05-05 18:42:51 +04:00
parent fa46338010
commit ee38ee55d4
18 changed files with 389 additions and 167 deletions

View File

@ -14315,3 +14315,8 @@ Sorry for the inconvenience.";
"Gift.Buy.Confirm.Text.Stars_any" = "**%@** Stars";
"Gift.Buy.Confirm.BuyFor_1" = "Buy for %@ Star";
"Gift.Buy.Confirm.BuyFor_any" = "Buy for %@ Stars";
"Calls.HideCallsTab" = "Hide Calls Tab";
"Story.Editor.TooltipSelection_1" = "Tap here to view your %@ story";
"Story.Editor.TooltipSelection_any" = "Tap here to view your %@ stories";

View File

@ -16,6 +16,7 @@ import TelegramBaseController
import InviteLinksUI
import UndoUI
import TelegramCallsUI
import TelegramUIPreferences
public enum CallListControllerMode {
case tab
@ -734,10 +735,22 @@ public final class CallListController: TelegramBaseController {
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/AddUser"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] c, f in
c?.dismiss(completion: { [weak self] in
guard let strongSelf = self else {
guard let self else {
return
}
strongSelf.callPressed()
self.callPressed()
})
})))
items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.Calls_HideCallsTab, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Peer Info/HideIcon"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] c, f in
c?.dismiss(completion: { [weak self] in
guard let self else {
return
}
let _ = updateCallListSettingsInteractively(accountManager: self.context.sharedContext.accountManager, {
$0.withUpdatedShowTab(false)
}).start()
})
})))

View File

@ -1015,7 +1015,7 @@ public final class ChatListContainerNode: ASDisplayNode, ASGestureRecognizerDele
}
}
itemNode.listNode.isMainTab.set(self.availableFilters.firstIndex(where: { $0.id == id }) == 0 ? true : false)
itemNode.listNode.isMainTab.set(self.availableFilters.firstIndex(where: { $0.id == id }) == 0)
itemNode.updateLayout(size: layout.size, insets: insets, visualNavigationHeight: visualNavigationHeight, originalNavigationHeight: originalNavigationHeight, inlineNavigationLocation: inlineNavigationLocation, inlineNavigationTransitionFraction: itemInlineNavigationTransitionFraction, storiesInset: storiesInset, transition: nodeTransition)
if let scrollingOffset = self.scrollingOffset {
itemNode.updateScrollingOffset(navigationHeight: scrollingOffset.navigationHeight, offset: scrollingOffset.offset, transition: nodeTransition)

View File

@ -2092,8 +2092,6 @@ public final class ChatListNode: ListView {
return .single(.setupPhoto(accountPeer))
} else if suggestions.contains(.gracePremium) {
return .single(.premiumGrace)
} else if suggestions.contains(.setupBirthday) && birthday == nil {
return .single(.setupBirthday)
} else if suggestions.contains(.xmasPremiumGift) {
return .single(.xmasPremiumGift)
} else if suggestions.contains(.annualPremium) || suggestions.contains(.upgradePremium) || suggestions.contains(.restorePremium), let inAppPurchaseManager = context.inAppPurchaseManager {
@ -2149,6 +2147,8 @@ public final class ChatListNode: ListView {
}
return .birthdayPremiumGift(peers: todayBirthdayPeers, birthdays: birthdays)
}
} else if suggestions.contains(.setupBirthday) && birthday == nil {
return .single(.setupBirthday)
} else if case let .link(id, url, title, subtitle) = suggestions.first(where: { if case .link = $0 { return true } else { return false} }) {
return .single(.link(id: id, url: url, title: title, subtitle: subtitle))
} else {

View File

@ -501,7 +501,7 @@ public final class ReactionButtonAsyncNode: ContextControllerSourceView {
animationFraction = max(0.0, min(1.0, (CACurrentMediaTime() - animationState.startTime) / animationState.duration))
animationFraction = animationState.curve.solve(at: animationFraction)
if animationState.fromExtracted != isExtracted {
fixedTransitionDirection = isExtracted ? true : false
fixedTransitionDirection = isExtracted
}
} else {
animationFraction = 1.0

View File

@ -527,7 +527,12 @@ final class ContextControllerNode: ViewControllerTracingNode, ASScrollViewDelega
guard let strongSelf = self, let _ = gesture else {
return
}
let localPoint = strongSelf.view.convert(point, from: view)
let localPoint: CGPoint
if let layout = strongSelf.validLayout, layout.metrics.isTablet, layout.size.width > layout.size.height, let view {
localPoint = view.convert(point, to: nil)
} else {
localPoint = strongSelf.view.convert(point, from: view)
}
let initialPoint: CGPoint
if let current = strongSelf.initialContinueGesturePoint {
initialPoint = current

View File

@ -697,7 +697,7 @@ final class ColorGridComponent: Component {
bottomRightRadius = largeCornerRadius
}
let isLight = (selectedColor?.toUIColor().lightness ?? 1.0) < 0.5 ? true : false
let isLight = (selectedColor?.toUIColor().lightness ?? 1.0) < 0.5
var selectionKnobImage = ColorSelectionImage(size: CGSize(width: squareSize, height: squareSize), topLeftRadius: topLeftRadius, topRightRadius: topRightRadius, bottomLeftRadius: bottomLeftRadius, bottomRightRadius: bottomRightRadius, isLight: isLight)
if selectionKnobImage != self.selectionKnobImage {

View File

@ -203,6 +203,7 @@ private enum ApplicationSpecificGlobalNotice: Int32 {
case channelSendGiftTooltip = 76
case starGiftWearTips = 77
case channelSuggestTooltip = 78
case multipleStoriesTooltip = 79
var key: ValueBoxKey {
let v = ValueBoxKey(length: 4)
@ -564,6 +565,10 @@ private struct ApplicationSpecificNoticeKeys {
static func channelSuggestTooltip() -> NoticeEntryKey {
return NoticeEntryKey(namespace: noticeNamespace(namespace: globalNamespace), key: ApplicationSpecificGlobalNotice.channelSuggestTooltip.key)
}
static func multipleStoriesTooltip() -> NoticeEntryKey {
return NoticeEntryKey(namespace: noticeNamespace(namespace: globalNamespace), key: ApplicationSpecificGlobalNotice.multipleStoriesTooltip.key)
}
}
public struct ApplicationSpecificNotice {
@ -2426,4 +2431,31 @@ public struct ApplicationSpecificNotice {
return Int(previousValue)
}
}
public static func getMultipleStoriesTooltip(accountManager: AccountManager<TelegramAccountManagerTypes>) -> Signal<Int32, NoError> {
return accountManager.transaction { transaction -> Int32 in
if let value = transaction.getNotice(ApplicationSpecificNoticeKeys.multipleStoriesTooltip())?.get(ApplicationSpecificCounterNotice.self) {
return value.value
} else {
return 0
}
}
}
public static func incrementMultipleStoriesTooltip(accountManager: AccountManager<TelegramAccountManagerTypes>, count: Int = 1) -> Signal<Int, NoError> {
return accountManager.transaction { transaction -> Int in
var currentValue: Int32 = 0
if let value = transaction.getNotice(ApplicationSpecificNoticeKeys.multipleStoriesTooltip())?.get(ApplicationSpecificCounterNotice.self) {
currentValue = value.value
}
let previousValue = currentValue
currentValue += Int32(count)
if let entry = CodableEntry(ApplicationSpecificCounterNotice(value: currentValue)) {
transaction.setNotice(ApplicationSpecificNoticeKeys.multipleStoriesTooltip(), entry)
}
return Int(previousValue)
}
}
}

View File

@ -3630,7 +3630,13 @@ public class CameraScreenImpl: ViewController, CameraScreen {
self.node.resumeCameraCapture(fromGallery: true)
}
var dismissControllerImpl: (() -> Void)?
class DismissArgs {
var resumeOnDismiss = true
}
var dismissControllerImpl: ((Bool) -> Void)?
let dismissArgs = DismissArgs()
let controller: ViewController
if let current = self.galleryController {
controller = current
@ -3686,7 +3692,7 @@ public class CameraScreenImpl: ViewController, CameraScreen {
}
}
dismissControllerImpl?()
dismissControllerImpl?(true)
} else {
stopCameraCapture()
@ -3759,17 +3765,19 @@ public class CameraScreenImpl: ViewController, CameraScreen {
self.node.collage?.addResults(signals: results)
}
} else {
self.node.animateOutToEditor()
if let assets = results as? [PHAsset] {
self.completion(.single(.assets(assets)), nil, self.remainingStoryCount, {
})
}
}
self.galleryController = nil
dismissControllerImpl?()
dismissControllerImpl?(false)
}, dismissed: { [weak self] in
resumeCameraCapture()
if dismissArgs.resumeOnDismiss {
resumeCameraCapture()
}
if let self {
self.node.hasGallery = false
self.node.requestUpdateLayout(transition: .immediate)
@ -3780,7 +3788,8 @@ public class CameraScreenImpl: ViewController, CameraScreen {
)
self.galleryController = controller
dismissControllerImpl = { [weak controller] in
dismissControllerImpl = { [weak controller] resume in
dismissArgs.resumeOnDismiss = resume
controller?.dismiss(animated: true)
}
}

View File

@ -235,6 +235,8 @@ final class GiftOptionsScreenComponent: Component {
private var chevronImage: (UIImage, PresentationTheme)?
private var resaleConfiguration: StarsSubscriptionConfiguration?
override init(frame: CGRect) {
self.scrollView = ScrollView()
self.scrollView.showsVerticalScrollIndicator = true
@ -408,9 +410,14 @@ final class GiftOptionsScreenComponent: Component {
switch gift {
case let .generic(gift):
if let availability = gift.availability, availability.remains == 0, let minResaleStars = availability.minResaleStars {
subject = .starGift(gift: gift, price: "⭐️ \(minResaleStars)+")
let priceString = presentationStringsFormattedNumber(Int32(minResaleStars), environment.dateTimeFormat.groupingSeparator)
if let resaleConfiguration = self.resaleConfiguration, minResaleStars == resaleConfiguration.starGiftResaleMaxAmount || availability.resale == 1 {
subject = .starGift(gift: gift, price: "⭐️ \(priceString)")
} else {
subject = .starGift(gift: gift, price: "⭐️ \(priceString)+")
}
} else {
subject = .starGift(gift: gift, price: "⭐️ \(gift.price)")
subject = .starGift(gift: gift, price: "⭐️ \(presentationStringsFormattedNumber(Int32(gift.price), environment.dateTimeFormat.groupingSeparator))")
}
case let .unique(gift):
subject = .uniqueGift(gift: gift, price: nil)
@ -773,6 +780,8 @@ final class GiftOptionsScreenComponent: Component {
self.optionsPromise.set(component.context.engine.payments.starsTopUpOptions()
|> map(Optional.init))
}
self.resaleConfiguration = StarsSubscriptionConfiguration.with(appConfiguration: component.context.currentAppConfiguration.with { $0 })
}
self.component = component

View File

@ -109,6 +109,15 @@ public final class FilterSelectorComponent: Component {
return true
}
func animateIn() {
for (_, item) in self.visibleItems {
if let itemView = item.title.view {
itemView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
itemView.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2)
}
}
}
func update(component: FilterSelectorComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
self.component = component
self.state = state

View File

@ -27,6 +27,8 @@ import UndoUI
import ContextUI
import LottieComponent
private let minimumCountToDisplayFilters = 18
final class GiftStoreScreenComponent: Component {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
@ -93,7 +95,8 @@ final class GiftStoreScreenComponent: Component {
private var starsStateDisposable: Disposable?
private var starsState: StarsContext.State?
private var initialCount: Int?
private var component: GiftStoreScreenComponent?
private(set) weak var state: State?
private var environment: EnvironmentType?
@ -148,6 +151,13 @@ final class GiftStoreScreenComponent: Component {
}
}
private var effectiveIsLoading: Bool {
if self.state?.starGiftsState?.gifts == nil || self.state?.starGiftsState?.dataState == .loading {
return true
}
return false
}
private func updateScrolling(interactive: Bool = false, transition: ComponentTransition) {
guard let environment = self.environment, let component = self.component, self.state?.starGiftsState?.dataState != .loading else {
return
@ -163,6 +173,11 @@ final class GiftStoreScreenComponent: Component {
transition.setAlpha(view: topSeparator, alpha: topPanelAlpha)
}
var topInset = environment.navigationHeight + 39.0
if let initialCount = self.initialCount, initialCount < minimumCountToDisplayFilters {
topInset = environment.navigationHeight
}
let visibleBounds = self.scrollView.bounds.insetBy(dx: 0.0, dy: -10.0)
if let starGifts = self.effectiveGifts {
let sideInset: CGFloat = 16.0 + environment.safeInsets.left
@ -172,7 +187,7 @@ final class GiftStoreScreenComponent: Component {
let starsOptionSize = CGSize(width: optionWidth, height: 154.0)
var validIds: [AnyHashable] = []
var itemFrame = CGRect(origin: CGPoint(x: sideInset, y: environment.navigationHeight + 39.0 + 9.0), size: starsOptionSize)
var itemFrame = CGRect(origin: CGPoint(x: sideInset, y: topInset + 9.0), size: starsOptionSize)
let controller = environment.controller
@ -337,7 +352,6 @@ final class GiftStoreScreenComponent: Component {
showClearFilters = true
}
let topInset: CGFloat = environment.navigationHeight + 39.0
let bottomInset: CGFloat = environment.safeInsets.bottom
var emptyResultsActionFrame = CGRect(
@ -443,7 +457,7 @@ final class GiftStoreScreenComponent: Component {
}
func openSortContextMenu(sourceView: UIView) {
guard let component = self.component, let controller = self.environment?.controller() else {
guard let component = self.component, let controller = self.environment?.controller(), !self.effectiveIsLoading else {
return
}
@ -486,10 +500,10 @@ final class GiftStoreScreenComponent: Component {
}
func openModelContextMenu(sourceView: UIView) {
guard let component = self.component, let controller = self.environment?.controller() else {
guard let component = self.component, let controller = self.environment?.controller(), !self.effectiveIsLoading else {
return
}
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
let searchQueryPromise = ValuePromise<String>("")
@ -579,7 +593,7 @@ final class GiftStoreScreenComponent: Component {
}
func openBackdropContextMenu(sourceView: UIView) {
guard let component = self.component, let controller = self.environment?.controller() else {
guard let component = self.component, let controller = self.environment?.controller(), !self.effectiveIsLoading else {
return
}
@ -672,7 +686,7 @@ final class GiftStoreScreenComponent: Component {
}
func openSymbolContextMenu(sourceView: UIView) {
guard let component = self.component, let controller = self.environment?.controller() else {
guard let component = self.component, let controller = self.environment?.controller(), !self.effectiveIsLoading else {
return
}
@ -789,10 +803,7 @@ final class GiftStoreScreenComponent: Component {
}
self.component = component
var isLoading = false
if self.state?.starGiftsState?.gifts == nil || self.state?.starGiftsState?.dataState == .loading {
isLoading = true
}
let isLoading = self.effectiveIsLoading
let theme = environment.theme
let strings = environment.strings
@ -808,7 +819,10 @@ final class GiftStoreScreenComponent: Component {
var contentHeight: CGFloat = 0.0
contentHeight += environment.navigationHeight
let topPanelHeight = environment.navigationHeight + 39.0
var topPanelHeight = environment.navigationHeight + 39.0
if let initialCount = self.initialCount, initialCount < minimumCountToDisplayFilters {
topPanelHeight = environment.navigationHeight
}
let topPanelSize = self.topPanel.update(
transition: transition,
@ -913,7 +927,10 @@ final class GiftStoreScreenComponent: Component {
}
let effectiveCount: Int32
if let count = self.effectiveGifts?.count {
if let count = self.effectiveGifts?.count, count > 0 || self.initialCount != nil {
if self.initialCount == nil {
self.initialCount = count
}
effectiveCount = Int32(count)
} else if let resale = component.gift.availability?.resale {
effectiveCount = Int32(resale)
@ -1028,13 +1045,15 @@ final class GiftStoreScreenComponent: Component {
}
))
let loadingTransition: ComponentTransition = .easeInOut(duration: 0.25)
let filterSize = self.filterSelector.update(
transition: transition,
component: AnyComponent(FilterSelectorComponent(
context: component.context,
colors: FilterSelectorComponent.Colors(
foreground: theme.list.itemPrimaryTextColor.withMultipliedAlpha(0.65),
background: theme.list.itemSecondaryTextColor.withMultipliedAlpha(0.15)
background: theme.list.itemSecondaryTextColor.mixedWith(theme.list.blocksBackgroundColor, alpha: 0.85)
),
items: filterItems
)),
@ -1043,9 +1062,14 @@ final class GiftStoreScreenComponent: Component {
)
if let filterSelectorView = self.filterSelector.view {
if filterSelectorView.superview == nil {
filterSelectorView.alpha = 0.0
self.addSubview(filterSelectorView)
}
transition.setFrame(view: filterSelectorView, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - filterSize.width) / 2.0), y: topInset + 56.0), size: filterSize))
if let initialCount = self.initialCount, initialCount >= minimumCountToDisplayFilters {
loadingTransition.setAlpha(view: filterSelectorView, alpha: 1.0)
}
}
if let starGifts = self.state?.starGiftsState?.gifts {
@ -1088,14 +1112,13 @@ final class GiftStoreScreenComponent: Component {
self.updateScrolling(transition: transition)
let loadingTransition: ComponentTransition = .easeInOut(duration: 0.25)
if isLoading {
self.loadingNode.update(size: availableSize, theme: environment.theme, transition: .immediate)
loadingTransition.setAlpha(view: self.loadingNode.view, alpha: 1.0)
} else {
loadingTransition.setAlpha(view: self.loadingNode.view, alpha: 0.0)
}
transition.setFrame(view: self.loadingNode.view, frame: CGRect(origin: CGPoint(x: 0.0, y: environment.navigationHeight + 39.0 + 7.0), size: availableSize))
transition.setFrame(view: self.loadingNode.view, frame: CGRect(origin: CGPoint(x: 0.0, y: environment.navigationHeight), size: availableSize))
return availableSize
}
@ -1108,19 +1131,22 @@ final class GiftStoreScreenComponent: Component {
final class State: ComponentState {
private let context: AccountContext
var peerId: EnginePeer.Id
private let gift: StarGift.Gift
private var disposable: Disposable?
fileprivate let starGiftsContext: ResaleGiftsContext
fileprivate var starGiftsState: ResaleGiftsContext.State?
init(
context: AccountContext,
peerId: EnginePeer.Id,
giftId: Int64
gift: StarGift.Gift
) {
self.context = context
self.peerId = peerId
self.starGiftsContext = ResaleGiftsContext(account: context.account, giftId: giftId)
self.gift = gift
self.starGiftsContext = ResaleGiftsContext(account: context.account, giftId: gift.id)
super.init()
@ -1140,7 +1166,7 @@ final class GiftStoreScreenComponent: Component {
}
func makeState() -> State {
return State(context: self.context, peerId: self.peerId, giftId: self.gift.id)
return State(context: self.context, peerId: self.peerId, gift: self.gift)
}
func update(view: View, availableSize: CGSize, state: State, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {

View File

@ -154,10 +154,17 @@ final class LoadingShimmerNode: ASDisplayNode {
context.setFillColor(theme.list.blocksBackgroundColor.cgColor)
context.fill(CGRect(origin: CGPoint(), size: size))
var currentY: CGFloat = 0.0
let sideInset: CGFloat = 16.0
let filterSpacing: CGFloat = 6.0
let filterWidth = (size.width - sideInset * 2.0 - filterSpacing * 3.0) / 4.0
for i in 0 ..< 4 {
context.addPath(CGPath(roundedRect: CGRect(origin: CGPoint(x: sideInset + (filterWidth + filterSpacing) * CGFloat(i), y: 0.0), size: CGSize(width: filterWidth, height: 28.0)), cornerWidth: 14.0, cornerHeight: 14.0, transform: nil))
}
var currentY: CGFloat = 39.0 + 7.0
var rowIndex: Int = 0
let sideInset: CGFloat = 16.0// + environment.safeInsets.left
let optionSpacing: CGFloat = 10.0
let optionWidth = (size.width - sideInset * 2.0 - optionSpacing * 2.0) / 3.0
let itemSize = CGSize(width: optionWidth, height: 154.0)
@ -167,7 +174,7 @@ final class LoadingShimmerNode: ASDisplayNode {
while currentY < size.height {
for i in 0 ..< 3 {
let itemOrigin = CGPoint(x: sideInset + CGFloat(i) * (itemSize.width + optionSpacing), y: 2.0 + CGFloat(rowIndex) * (itemSize.height + optionSpacing))
let itemOrigin = CGPoint(x: sideInset + CGFloat(i) * (itemSize.width + optionSpacing), y: 39.0 + 9.0 + CGFloat(rowIndex) * (itemSize.height + optionSpacing))
context.addPath(CGPath(roundedRect: CGRect(origin: itemOrigin, size: itemSize), cornerWidth: 10.0, cornerHeight: 10.0, transform: nil))
}
currentY += itemSize.height

View File

@ -496,7 +496,7 @@ private final class GiftViewSheetContent: CombinedComponent {
if currentTime > starsConvertMaxDate {
let days: Int32 = Int32(ceil(Float(configuration.convertToStarsPeriod) / 86400.0))
let controller = textAlertController(
let alertController = textAlertController(
context: self.context,
title: presentationData.strings.Gift_Convert_Title,
text: presentationData.strings.Gift_Convert_Period_Unavailable_Text(presentationData.strings.Gift_Convert_Period_Unavailable_Days(days)).string,
@ -505,7 +505,7 @@ private final class GiftViewSheetContent: CombinedComponent {
],
parseMarkdown: true
)
controller.present(controller, in: .window(.root))
controller.present(alertController, in: .window(.root))
} else {
let delta = starsConvertMaxDate - currentTime
let days: Int32 = Int32(ceil(Float(delta) / 86400.0))

View File

@ -67,6 +67,7 @@ swift_library(
"//submodules/TelegramUI/Components/SaveProgressScreen",
"//submodules/TelegramUI/Components/MediaAssetsContext",
"//submodules/CheckNode",
"//submodules/TelegramNotices",
],
visibility = [
"//visibility:public",

View File

@ -49,6 +49,7 @@ import StickerPickerScreen
import UIKitRuntimeUtils
import ImageObjectSeparation
import SaveProgressScreen
import TelegramNotices
private let playbackButtonTag = GenericComponentViewTag()
private let muteButtonTag = GenericComponentViewTag()
@ -58,6 +59,7 @@ private let drawButtonTag = GenericComponentViewTag()
private let textButtonTag = GenericComponentViewTag()
private let stickerButtonTag = GenericComponentViewTag()
private let dayNightButtonTag = GenericComponentViewTag()
private let selectionButtonTag = GenericComponentViewTag()
final class MediaEditorScreenComponent: Component {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
@ -2320,7 +2322,8 @@ final class MediaEditorScreenComponent: Component {
controller.hapticFeedback.impact(.light)
}
},
animateAlpha: false
animateAlpha: false,
tag: selectionButtonTag
)),
environment: {},
containerSize: CGSize(width: 33.0, height: 33.0)
@ -4744,6 +4747,33 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
self.controller?.present(tooltipController, in: .current)
}
private var displayedSelectionTooltip = false
func presentSelectionTooltip() {
guard let sourceView = self.componentHost.findTaggedView(tag: selectionButtonTag), !self.displayedSelectionTooltip, self.items.count > 1 else {
return
}
self.displayedSelectionTooltip = true
let _ = (ApplicationSpecificNotice.getMultipleStoriesTooltip(accountManager: self.context.sharedContext.accountManager)
|> deliverOnMainQueue).start(next: { [weak self] count in
guard let self, count < 3 else {
return
}
let parentFrame = self.view.convert(self.bounds, to: nil)
let absoluteFrame = sourceView.convert(sourceView.bounds, to: nil).offsetBy(dx: -parentFrame.minX, dy: 0.0)
let location = CGRect(origin: CGPoint(x: absoluteFrame.midX, y: absoluteFrame.minY - 3.0), size: CGSize())
let text = self.presentationData.strings.Story_Editor_TooltipSelection(Int32(self.items.count))
let tooltipController = TooltipScreen(account: self.context.account, sharedContext: self.context.sharedContext, text: .plain(text: text), location: .point(location, .bottom), displayDuration: .default, inset: 8.0, shouldDismissOnTouch: { _, _ in
return .dismiss(consume: false)
})
self.controller?.present(tooltipController, in: .current)
let _ = ApplicationSpecificNotice.incrementMultipleStoriesTooltip(accountManager: self.context.sharedContext.accountManager).start()
})
}
fileprivate weak var saveTooltip: SaveProgressScreen?
func presentSaveTooltip() {
guard let controller = self.controller else {
@ -5725,6 +5755,8 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
if hasAppeared && !self.hasAppeared {
self.hasAppeared = hasAppeared
self.presentSelectionTooltip()
}
let componentSize = self.componentHost.update(

View File

@ -347,7 +347,7 @@ extension PeerInfoScreenImpl {
let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max))
self.context.account.postbox.mediaBox.storeResourceData(resource.id, data: data)
let representation = TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 640, height: 640), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: mode == .custom ? true : false)
let representation = TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 640, height: 640), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: mode == .custom)
if [.suggest, .fallback].contains(mode) {
} else {

View File

@ -895,8 +895,13 @@ public final class WebAppController: ViewController, AttachmentContainable {
if let controller = self.controller {
webView.updateMetrics(height: viewportFrame.height, isExpanded: controller.isContainerExpanded(), isStable: !controller.isContainerPanning(), transition: transition)
let contentInsetsData = "{top:\(contentTopInset), bottom:0.0, left:0.0, right:0.0}"
webView.sendEvent(name: "content_safe_area_changed", data: contentInsetsData)
let data: JSON = [
"top": Double(contentTopInset),
"bottom": 0.0,
"left": 0.0,
"right": 0.0
]
webView.sendEvent(name: "content_safe_area_changed", data: data.string)
if self.updateWebViewWhenStable && !controller.isContainerPanning() {
self.updateWebViewWhenStable = false
@ -1333,7 +1338,7 @@ public final class WebAppController: ViewController, AttachmentContainable {
controller.completion = { [weak self] result in
if let strongSelf = self {
if let result = result {
strongSelf.sendQrCodeScannedEvent(data: result)
strongSelf.sendQrCodeScannedEvent(dataString: result)
} else {
strongSelf.sendQrCodeScannerClosedEvent()
}
@ -1923,8 +1928,11 @@ public final class WebAppController: ViewController, AttachmentContainable {
}
private func sendInvoiceClosedEvent(slug: String, result: InvoiceCloseResult) {
let paramsString = "{slug: \"\(slug)\", status: \"\(result.string)\"}"
self.webView?.sendEvent(name: "invoice_closed", data: paramsString)
let data: JSON = [
"slug": slug,
"status": result.string
]
self.webView?.sendEvent(name: "invoice_closed", data: data.string)
}
fileprivate func sendBackButtonEvent() {
@ -1936,24 +1944,23 @@ public final class WebAppController: ViewController, AttachmentContainable {
}
fileprivate func sendAlertButtonEvent(id: String?) {
var paramsString: String?
if let id = id {
paramsString = "{button_id: \"\(id)\"}"
var data: [String: Any] = [:]
if let id {
data["button_id"] = id
}
self.webView?.sendEvent(name: "popup_closed", data: paramsString ?? "{}")
}
fileprivate func sendPhoneRequestedEvent(phone: String?) {
var paramsString: String?
if let phone = phone {
paramsString = "{phone_number: \"\(phone)\"}"
if let serializedData = JSON(dictionary: data)?.string {
self.webView?.sendEvent(name: "popup_closed", data: serializedData)
}
self.webView?.sendEvent(name: "phone_requested", data: paramsString)
}
fileprivate func sendQrCodeScannedEvent(data: String?) {
let paramsString = data.flatMap { "{data: \"\($0)\"}" } ?? "{}"
self.webView?.sendEvent(name: "qr_text_received", data: paramsString)
fileprivate func sendQrCodeScannedEvent(dataString: String?) {
var data: [String: Any] = [:]
if let dataString {
data["data"] = dataString
}
if let serializedData = JSON(dictionary: data)?.string {
self.webView?.sendEvent(name: "qr_text_received", data: serializedData)
}
}
fileprivate func sendQrCodeScannerClosedEvent() {
@ -1961,14 +1968,15 @@ public final class WebAppController: ViewController, AttachmentContainable {
}
fileprivate func sendClipboardTextEvent(requestId: String, fillData: Bool) {
var paramsString: String
var data: [String: Any] = [:]
data["req_id"] = requestId
if fillData {
let data = UIPasteboard.general.string ?? ""
paramsString = "{req_id: \"\(requestId)\", data: \"\(data)\"}"
} else {
paramsString = "{req_id: \"\(requestId)\"}"
let pasteboardData = UIPasteboard.general.string ?? ""
data["data"] = pasteboardData
}
if let serializedData = JSON(dictionary: data)?.string {
self.webView?.sendEvent(name: "clipboard_text_received", data: serializedData)
}
self.webView?.sendEvent(name: "clipboard_text_received", data: paramsString)
}
fileprivate func requestWriteAccess() {
@ -1977,13 +1985,10 @@ public final class WebAppController: ViewController, AttachmentContainable {
}
let sendEvent: (Bool) -> Void = { success in
var paramsString: String
if success {
paramsString = "{status: \"allowed\"}"
} else {
paramsString = "{status: \"cancelled\"}"
}
self.webView?.sendEvent(name: "write_access_requested", data: paramsString)
let data: JSON = [
"status": success ? "allowed" : "cancelled"
]
self.webView?.sendEvent(name: "write_access_requested", data: data.string)
}
let _ = (self.context.engine.messages.canBotSendMessages(botId: controller.botId)
@ -2021,13 +2026,10 @@ public final class WebAppController: ViewController, AttachmentContainable {
return
}
let sendEvent: (Bool) -> Void = { success in
var paramsString: String
if success {
paramsString = "{status: \"sent\"}"
} else {
paramsString = "{status: \"cancelled\"}"
}
self.webView?.sendEvent(name: "phone_requested", data: paramsString)
let data: JSON = [
"status": success ? "sent" : "cancelled"
]
self.webView?.sendEvent(name: "phone_requested", data: data.string)
}
let _ = (self.context.engine.data.get(
@ -2348,28 +2350,15 @@ public final class WebAppController: ViewController, AttachmentContainable {
state.opaqueToken = encryptedData
return state
})
var data: [String: Any] = [:]
data["status"] = "updated"
guard let jsonData = try? JSONSerialization.data(withJSONObject: data) else {
return
}
guard let jsonDataString = String(data: jsonData, encoding: .utf8) else {
return
}
self.webView?.sendEvent(name: "biometry_token_updated", data: jsonDataString)
let data: JSON = [
"status": "updated"
]
self.webView?.sendEvent(name: "biometry_token_updated", data: data.string)
} else {
var data: [String: Any] = [:]
data["status"] = "failed"
guard let jsonData = try? JSONSerialization.data(withJSONObject: data) else {
return
}
guard let jsonDataString = String(data: jsonData, encoding: .utf8) else {
return
}
self.webView?.sendEvent(name: "biometry_token_updated", data: jsonDataString)
let data: JSON = [
"status": "failed"
]
self.webView?.sendEvent(name: "biometry_token_updated", data: data.string)
}
}
}.start()
@ -2379,17 +2368,10 @@ public final class WebAppController: ViewController, AttachmentContainable {
state.opaqueToken = nil
return state
})
var data: [String: Any] = [:]
data["status"] = "removed"
guard let jsonData = try? JSONSerialization.data(withJSONObject: data) else {
return
}
guard let jsonDataString = String(data: jsonData, encoding: .utf8) else {
return
}
self.webView?.sendEvent(name: "biometry_token_updated", data: jsonDataString)
let data: JSON = [
"status": "removed"
]
self.webView?.sendEvent(name: "biometry_token_updated", data: data.string)
}
}
@ -2410,13 +2392,18 @@ public final class WebAppController: ViewController, AttachmentContainable {
return
}
guard controller.isFullscreen != isFullscreen else {
self.webView?.sendEvent(name: "fullscreen_failed", data: "{error: \"ALREADY_FULLSCREEN\"}")
let data: JSON = [
"error": "ALREADY_FULLSCREEN"
]
self.webView?.sendEvent(name: "fullscreen_failed", data: data.string)
return
}
let paramsString = "{is_fullscreen: \( isFullscreen ? "true" : "false" )}"
self.webView?.sendEvent(name: "fullscreen_changed", data: paramsString)
let data: JSON = [
"is_fullscreen": isFullscreen
]
self.webView?.sendEvent(name: "fullscreen_changed", data: data.string)
controller.isFullscreen = isFullscreen
if isFullscreen {
controller.requestAttachmentMenuExpansion()
@ -2436,7 +2423,10 @@ public final class WebAppController: ViewController, AttachmentContainable {
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\"}")
let data: JSON = [
"error": "UNSUPPORTED"
]
self.webView?.sendEvent(name: "accelerometer_failed", data: data.string)
return
}
guard self.isAccelerometerActive != isActive else {
@ -2451,15 +2441,17 @@ public final class WebAppController: ViewController, AttachmentContainable {
} else {
self.motionManager.accelerometerUpdateInterval = 1.0
}
self.motionManager.startAccelerometerUpdates(to: OperationQueue.main) { [weak self] data, error in
guard let self, let data else {
self.motionManager.startAccelerometerUpdates(to: OperationQueue.main) { [weak self] accelerometerData, error in
guard let self, let accelerometerData else {
return
}
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)}"
)
let gravityConstant: Double = 9.81
let data: JSON = [
"x": Double(accelerometerData.acceleration.x * gravityConstant),
"y": Double(accelerometerData.acceleration.y * gravityConstant),
"z": Double(accelerometerData.acceleration.z * gravityConstant)
]
self.webView?.sendEvent(name: "accelerometer_changed", data: data.string)
}
} else {
if self.motionManager.isAccelerometerActive {
@ -2472,7 +2464,10 @@ public final class WebAppController: ViewController, AttachmentContainable {
private var isDeviceOrientationActive = false
fileprivate func setIsDeviceOrientationActive(_ isActive: Bool, refreshRate: Double? = nil, absolute: Bool = false) {
guard self.motionManager.isDeviceMotionAvailable else {
self.webView?.sendEvent(name: "device_orientation_failed", data: "{error: \"UNSUPPORTED\"}")
let data: JSON = [
"error": "UNSUPPORTED"
]
self.webView?.sendEvent(name: "device_orientation_failed", data: data.string)
return
}
guard self.isDeviceOrientationActive != isActive else {
@ -2505,25 +2500,29 @@ public final class WebAppController: ViewController, AttachmentContainable {
}
effectiveIsAbsolute = false
}
self.motionManager.startDeviceMotionUpdates(using: referenceFrame, to: OperationQueue.main) { [weak self] data, error in
guard let self, let data else {
self.motionManager.startDeviceMotionUpdates(using: referenceFrame, to: OperationQueue.main) { [weak self] motionData, error in
guard let self, let motionData else {
return
}
var alpha: Double
if effectiveIsAbsolute {
alpha = data.heading * .pi / 180.0
alpha = motionData.heading * .pi / 180.0
if alpha > .pi {
alpha -= 2.0 * .pi
} else if alpha < -.pi {
alpha += 2.0 * .pi
}
} else {
alpha = data.attitude.yaw
alpha = motionData.attitude.yaw
}
self.webView?.sendEvent(
name: "device_orientation_changed",
data: "{absolute: \(effectiveIsAbsolute ? "true" : "false"), alpha: \(alpha), beta: \(data.attitude.pitch), gamma: \(data.attitude.roll)}"
)
let data: JSON = [
"absolute": effectiveIsAbsolute,
"alpha": Double(alpha),
"beta": Double(motionData.attitude.pitch),
"gamma": Double(motionData.attitude.roll)
]
self.webView?.sendEvent(name: "device_orientation_changed", data: data.string)
}
} else {
if self.motionManager.isDeviceMotionActive {
@ -2536,7 +2535,10 @@ public final class WebAppController: ViewController, AttachmentContainable {
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\"}")
let data: JSON = [
"error": "UNSUPPORTED"
]
self.webView?.sendEvent(name: "gyroscope_failed", data: data.string)
return
}
guard self.isGyroscopeActive != isActive else {
@ -2551,14 +2553,16 @@ public final class WebAppController: ViewController, AttachmentContainable {
} else {
self.motionManager.gyroUpdateInterval = 1.0
}
self.motionManager.startGyroUpdates(to: OperationQueue.main) { [weak self] data, error in
guard let self, let data else {
self.motionManager.startGyroUpdates(to: OperationQueue.main) { [weak self] gyroData, error in
guard let self, let gyroData else {
return
}
self.webView?.sendEvent(
name: "gyroscope_changed",
data: "{x: \(data.rotationRate.x), y: \(data.rotationRate.y), z: \(data.rotationRate.z)}"
)
let data: JSON = [
"x": Double(gyroData.rotationRate.x),
"y": Double(gyroData.rotationRate.y),
"z": Double(gyroData.rotationRate.z)
]
self.webView?.sendEvent(name: "gyroscope_changed", data: data.string)
}
} else {
if self.motionManager.isGyroActive {
@ -2575,7 +2579,10 @@ public final class WebAppController: ViewController, AttachmentContainable {
let _ = (self.context.engine.messages.getPreparedInlineMessage(botId: controller.botId, id: id)
|> deliverOnMainQueue).start(next: { [weak self, weak controller] preparedMessage in
guard let self, let controller, let preparedMessage else {
self?.webView?.sendEvent(name: "prepared_message_failed", data: "{error: \"MESSAGE_EXPIRED\"}")
let data: JSON = [
"error": "MESSAGE_EXPIRED"
]
self?.webView?.sendEvent(name: "prepared_message_failed", data: data.string)
return
}
let previewController = WebAppMessagePreviewScreen(context: controller.context, botName: controller.botName, botAddress: controller.botAddress, preparedMessage: preparedMessage, completion: { [weak self] result in
@ -2585,7 +2592,10 @@ public final class WebAppController: ViewController, AttachmentContainable {
if result {
self.webView?.sendEvent(name: "prepared_message_sent", data: nil)
} else {
self.webView?.sendEvent(name: "prepared_message_failed", data: "{error: \"USER_DECLINED\"}")
let data: JSON = [
"error": "USER_DECLINED"
]
self.webView?.sendEvent(name: "prepared_message_failed", data: data.string)
}
})
previewController.navigationPresentation = .flatModal
@ -2599,7 +2609,10 @@ public final class WebAppController: ViewController, AttachmentContainable {
}
guard !fileName.contains("/") && fileName.lengthOfBytes(using: .utf8) < 256 && url.lengthOfBytes(using: .utf8) < 32768 else {
self.webView?.sendEvent(name: "file_download_requested", data: "{status: \"cancelled\"}")
let data: JSON = [
"status": "cancelled"
]
self.webView?.sendEvent(name: "file_download_requested", data: data.string)
return
}
@ -2635,7 +2648,10 @@ public final class WebAppController: ViewController, AttachmentContainable {
return
}
guard canDownload else {
self.webView?.sendEvent(name: "file_download_requested", data: "{status: \"cancelled\"}")
let data: JSON = [
"status": "cancelled"
]
self.webView?.sendEvent(name: "file_download_requested", data: data.string)
return
}
var fileSizeString = ""
@ -2646,14 +2662,20 @@ public final class WebAppController: ViewController, AttachmentContainable {
let text: String = self.presentationData.strings.WebApp_Download_Text(controller.botName, fileName, fileSizeString).string
let alertController = standardTextAlertController(theme: AlertControllerTheme(presentationData: self.presentationData), title: title, text: text, actions: [
TextAlertAction(type: .genericAction, title: self.presentationData.strings.Common_Cancel, action: { [weak self] in
self?.webView?.sendEvent(name: "file_download_requested", data: "{status: \"cancelled\"}")
let data: JSON = [
"status": "cancelled"
]
self?.webView?.sendEvent(name: "file_download_requested", data: data.string)
}),
TextAlertAction(type: .defaultAction, title: self.presentationData.strings.WebApp_Download_Download, action: { [weak self] in
self?.startDownload(url: url, fileName: fileName, fileSize: fileSize, isMedia: isMedia)
})
], parseMarkdown: true)
alertController.dismissed = { [weak self] byOutsideTap in
self?.webView?.sendEvent(name: "file_download_requested", data: "{status: \"cancelled\"}")
let data: JSON = [
"status": "cancelled"
]
self?.webView?.sendEvent(name: "file_download_requested", data: data.string)
}
controller.present(alertController, in: .window(.root))
})
@ -2664,7 +2686,10 @@ public final class WebAppController: ViewController, AttachmentContainable {
guard let controller = self.controller else {
return
}
self.webView?.sendEvent(name: "file_download_requested", data: "{status: \"downloading\"}")
let data: JSON = [
"status": "downloading"
]
self.webView?.sendEvent(name: "file_download_requested", data: data.string)
var removeImpl: (() -> Void)?
let fileDownload = FileDownload(
@ -2840,13 +2865,20 @@ public final class WebAppController: ViewController, AttachmentContainable {
demoController?.replace(with: c)
}
controller.parentController()?.push(demoController)
self.webView?.sendEvent(name: "emoji_status_access_requested", data: "{status: \"cancelled\"}")
let data: JSON = [
"status": "cancelled"
]
self.webView?.sendEvent(name: "emoji_status_access_requested", data: data.string)
return
}
let _ = (context.engine.peers.toggleBotEmojiStatusAccess(peerId: botId, enabled: true)
|> deliverOnMainQueue).startStandalone(completed: { [weak self] in
self?.webView?.sendEvent(name: "emoji_status_access_requested", data: "{status: \"allowed\"}")
let data: JSON = [
"status": "allowed"
]
self?.webView?.sendEvent(name: "emoji_status_access_requested", data: data.string)
})
if let botPeer {
@ -2865,7 +2897,10 @@ public final class WebAppController: ViewController, AttachmentContainable {
controller.present(resultController, in: .window(.root))
}
} else {
self.webView?.sendEvent(name: "emoji_status_access_requested", data: "{status: \"cancelled\"}")
let data: JSON = [
"status": "cancelled"
]
self.webView?.sendEvent(name: "emoji_status_access_requested", data: data.string)
}
let _ = updateWebAppPermissionsStateInteractively(context: context, peerId: botId) { current in
@ -2874,7 +2909,10 @@ public final class WebAppController: ViewController, AttachmentContainable {
}
)
alertController.dismissed = { [weak self] byOutsideTap in
self?.webView?.sendEvent(name: "emoji_status_access_requested", data: "{status: \"cancelled\"}")
let data: JSON = [
"status": "cancelled"
]
self?.webView?.sendEvent(name: "emoji_status_access_requested", data: data.string)
}
controller.present(alertController, in: .window(.root))
})
@ -2894,7 +2932,10 @@ public final class WebAppController: ViewController, AttachmentContainable {
return
}
guard let file = files[fileId] else {
self.webView?.sendEvent(name: "emoji_status_failed", data: "{error: \"SUGGESTED_EMOJI_INVALID\"}")
let data: JSON = [
"error": "SUGGESTED_EMOJI_INVALID"
]
self.webView?.sendEvent(name: "emoji_status_failed", data: data.string)
return
}
let confirmController = WebAppSetEmojiStatusScreen(
@ -2919,7 +2960,11 @@ public final class WebAppController: ViewController, AttachmentContainable {
demoController?.replace(with: c)
}
controller.parentController()?.push(demoController)
self.webView?.sendEvent(name: "emoji_status_failed", data: "{error: \"USER_DECLINED\"}")
let data: JSON = [
"error": "USER_DECLINED"
]
self.webView?.sendEvent(name: "emoji_status_failed", data: data.string)
return
}
@ -2951,7 +2996,10 @@ public final class WebAppController: ViewController, AttachmentContainable {
)
controller.present(resultController, in: .window(.root))
} else {
self.webView?.sendEvent(name: "emoji_status_failed", data: "{error: \"USER_DECLINED\"}")
let data: JSON = [
"error": "USER_DECLINED"
]
self.webView?.sendEvent(name: "emoji_status_failed", data: data.string)
}
}
)
@ -3302,6 +3350,24 @@ public final class WebAppController: ViewController, AttachmentContainable {
}
}
})
self.longTapWithTabBar = { [weak self] in
guard let self else {
return
}
let _ = (context.engine.messages.attachMenuBots()
|> take(1)
|> deliverOnMainQueue).start(next: { [weak self] attachMenuBots in
guard let self else {
return
}
let attachMenuBot = attachMenuBots.first(where: { $0.peer.id == self.botId && !$0.flags.contains(.notActivated) })
if let _ = attachMenuBot, [.attachMenu, .settings, .generic].contains(self.source) {
self.removeAttachBot()
}
})
}
}
required public init(coder aDecoder: NSCoder) {
@ -3561,14 +3627,8 @@ public final class WebAppController: ViewController, AttachmentContainable {
}, action: { [weak self] c, _ in
c?.dismiss(completion: nil)
if let strongSelf = self {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
strongSelf.present(textAlertController(context: context, title: presentationData.strings.WebApp_RemoveConfirmationTitle, text: presentationData.strings.WebApp_RemoveAllConfirmationText(strongSelf.botName).string, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: { [weak self] in
if let strongSelf = self {
let _ = context.engine.messages.removeBotFromAttachMenu(botId: strongSelf.botId).start()
strongSelf.dismiss()
}
})], parseMarkdown: true), in: .window(.root))
if let self {
self.removeAttachBot()
}
})))
}
@ -3580,6 +3640,17 @@ public final class WebAppController: ViewController, AttachmentContainable {
self.presentInGlobalOverlay(contextController)
}
private func removeAttachBot() {
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
self.present(textAlertController(context: context, title: presentationData.strings.WebApp_RemoveConfirmationTitle, text: presentationData.strings.WebApp_RemoveAllConfirmationText(self.botName).string, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: { [weak self] in
guard let self else {
return
}
let _ = self.context.engine.messages.removeBotFromAttachMenu(botId: self.botId).start()
self.dismiss()
})], parseMarkdown: true), in: .window(.root))
}
override public func loadDisplayNode() {
self.displayNode = Node(context: self.context, controller: self)
@ -3660,7 +3731,10 @@ public final class WebAppController: ViewController, AttachmentContainable {
self.controllerNode.webView?.setNeedsLayout()
}
self.controllerNode.webView?.sendEvent(name: "visibility_changed", data: "{is_visible: \(self.isMinimized ? "false" : "true")}")
let data: JSON = [
"is_visible": !self.isMinimized,
]
self.controllerNode.webView?.sendEvent(name: "visibility_changed", data: data.string)
}
}
}