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.Text.Stars_any" = "**%@** Stars";
"Gift.Buy.Confirm.BuyFor_1" = "Buy for %@ Star"; "Gift.Buy.Confirm.BuyFor_1" = "Buy for %@ Star";
"Gift.Buy.Confirm.BuyFor_any" = "Buy for %@ Stars"; "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 InviteLinksUI
import UndoUI import UndoUI
import TelegramCallsUI import TelegramCallsUI
import TelegramUIPreferences
public enum CallListControllerMode { public enum CallListControllerMode {
case tab case tab
@ -734,10 +735,22 @@ public final class CallListController: TelegramBaseController {
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/AddUser"), color: theme.contextMenu.primaryColor) return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/AddUser"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] c, f in }, action: { [weak self] c, f in
c?.dismiss(completion: { [weak self] in c?.dismiss(completion: { [weak self] in
guard let strongSelf = self else { guard let self else {
return 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) itemNode.updateLayout(size: layout.size, insets: insets, visualNavigationHeight: visualNavigationHeight, originalNavigationHeight: originalNavigationHeight, inlineNavigationLocation: inlineNavigationLocation, inlineNavigationTransitionFraction: itemInlineNavigationTransitionFraction, storiesInset: storiesInset, transition: nodeTransition)
if let scrollingOffset = self.scrollingOffset { if let scrollingOffset = self.scrollingOffset {
itemNode.updateScrollingOffset(navigationHeight: scrollingOffset.navigationHeight, offset: scrollingOffset.offset, transition: nodeTransition) 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)) return .single(.setupPhoto(accountPeer))
} else if suggestions.contains(.gracePremium) { } else if suggestions.contains(.gracePremium) {
return .single(.premiumGrace) return .single(.premiumGrace)
} else if suggestions.contains(.setupBirthday) && birthday == nil {
return .single(.setupBirthday)
} else if suggestions.contains(.xmasPremiumGift) { } else if suggestions.contains(.xmasPremiumGift) {
return .single(.xmasPremiumGift) return .single(.xmasPremiumGift)
} else if suggestions.contains(.annualPremium) || suggestions.contains(.upgradePremium) || suggestions.contains(.restorePremium), let inAppPurchaseManager = context.inAppPurchaseManager { } 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) 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} }) { } 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)) return .single(.link(id: id, url: url, title: title, subtitle: subtitle))
} else { } 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 = max(0.0, min(1.0, (CACurrentMediaTime() - animationState.startTime) / animationState.duration))
animationFraction = animationState.curve.solve(at: animationFraction) animationFraction = animationState.curve.solve(at: animationFraction)
if animationState.fromExtracted != isExtracted { if animationState.fromExtracted != isExtracted {
fixedTransitionDirection = isExtracted ? true : false fixedTransitionDirection = isExtracted
} }
} else { } else {
animationFraction = 1.0 animationFraction = 1.0

View File

@ -527,7 +527,12 @@ final class ContextControllerNode: ViewControllerTracingNode, ASScrollViewDelega
guard let strongSelf = self, let _ = gesture else { guard let strongSelf = self, let _ = gesture else {
return 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 let initialPoint: CGPoint
if let current = strongSelf.initialContinueGesturePoint { if let current = strongSelf.initialContinueGesturePoint {
initialPoint = current initialPoint = current

View File

@ -697,7 +697,7 @@ final class ColorGridComponent: Component {
bottomRightRadius = largeCornerRadius 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) var selectionKnobImage = ColorSelectionImage(size: CGSize(width: squareSize, height: squareSize), topLeftRadius: topLeftRadius, topRightRadius: topRightRadius, bottomLeftRadius: bottomLeftRadius, bottomRightRadius: bottomRightRadius, isLight: isLight)
if selectionKnobImage != self.selectionKnobImage { if selectionKnobImage != self.selectionKnobImage {

View File

@ -203,6 +203,7 @@ private enum ApplicationSpecificGlobalNotice: Int32 {
case channelSendGiftTooltip = 76 case channelSendGiftTooltip = 76
case starGiftWearTips = 77 case starGiftWearTips = 77
case channelSuggestTooltip = 78 case channelSuggestTooltip = 78
case multipleStoriesTooltip = 79
var key: ValueBoxKey { var key: ValueBoxKey {
let v = ValueBoxKey(length: 4) let v = ValueBoxKey(length: 4)
@ -564,6 +565,10 @@ private struct ApplicationSpecificNoticeKeys {
static func channelSuggestTooltip() -> NoticeEntryKey { static func channelSuggestTooltip() -> NoticeEntryKey {
return NoticeEntryKey(namespace: noticeNamespace(namespace: globalNamespace), key: ApplicationSpecificGlobalNotice.channelSuggestTooltip.key) 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 { public struct ApplicationSpecificNotice {
@ -2426,4 +2431,31 @@ public struct ApplicationSpecificNotice {
return Int(previousValue) 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) self.node.resumeCameraCapture(fromGallery: true)
} }
var dismissControllerImpl: (() -> Void)? class DismissArgs {
var resumeOnDismiss = true
}
var dismissControllerImpl: ((Bool) -> Void)?
let dismissArgs = DismissArgs()
let controller: ViewController let controller: ViewController
if let current = self.galleryController { if let current = self.galleryController {
controller = current controller = current
@ -3686,7 +3692,7 @@ public class CameraScreenImpl: ViewController, CameraScreen {
} }
} }
dismissControllerImpl?() dismissControllerImpl?(true)
} else { } else {
stopCameraCapture() stopCameraCapture()
@ -3759,17 +3765,19 @@ public class CameraScreenImpl: ViewController, CameraScreen {
self.node.collage?.addResults(signals: results) self.node.collage?.addResults(signals: results)
} }
} else { } else {
self.node.animateOutToEditor()
if let assets = results as? [PHAsset] { if let assets = results as? [PHAsset] {
self.completion(.single(.assets(assets)), nil, self.remainingStoryCount, { self.completion(.single(.assets(assets)), nil, self.remainingStoryCount, {
}) })
} }
} }
self.galleryController = nil self.galleryController = nil
dismissControllerImpl?() dismissControllerImpl?(false)
}, dismissed: { [weak self] in }, dismissed: { [weak self] in
resumeCameraCapture() if dismissArgs.resumeOnDismiss {
resumeCameraCapture()
}
if let self { if let self {
self.node.hasGallery = false self.node.hasGallery = false
self.node.requestUpdateLayout(transition: .immediate) self.node.requestUpdateLayout(transition: .immediate)
@ -3780,7 +3788,8 @@ public class CameraScreenImpl: ViewController, CameraScreen {
) )
self.galleryController = controller self.galleryController = controller
dismissControllerImpl = { [weak controller] in dismissControllerImpl = { [weak controller] resume in
dismissArgs.resumeOnDismiss = resume
controller?.dismiss(animated: true) controller?.dismiss(animated: true)
} }
} }

View File

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

View File

@ -109,6 +109,15 @@ public final class FilterSelectorComponent: Component {
return true 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 { func update(component: FilterSelectorComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
self.component = component self.component = component
self.state = state self.state = state

View File

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

View File

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

View File

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

View File

@ -49,6 +49,7 @@ import StickerPickerScreen
import UIKitRuntimeUtils import UIKitRuntimeUtils
import ImageObjectSeparation import ImageObjectSeparation
import SaveProgressScreen import SaveProgressScreen
import TelegramNotices
private let playbackButtonTag = GenericComponentViewTag() private let playbackButtonTag = GenericComponentViewTag()
private let muteButtonTag = GenericComponentViewTag() private let muteButtonTag = GenericComponentViewTag()
@ -58,6 +59,7 @@ private let drawButtonTag = GenericComponentViewTag()
private let textButtonTag = GenericComponentViewTag() private let textButtonTag = GenericComponentViewTag()
private let stickerButtonTag = GenericComponentViewTag() private let stickerButtonTag = GenericComponentViewTag()
private let dayNightButtonTag = GenericComponentViewTag() private let dayNightButtonTag = GenericComponentViewTag()
private let selectionButtonTag = GenericComponentViewTag()
final class MediaEditorScreenComponent: Component { final class MediaEditorScreenComponent: Component {
typealias EnvironmentType = ViewControllerComponentContainer.Environment typealias EnvironmentType = ViewControllerComponentContainer.Environment
@ -2320,7 +2322,8 @@ final class MediaEditorScreenComponent: Component {
controller.hapticFeedback.impact(.light) controller.hapticFeedback.impact(.light)
} }
}, },
animateAlpha: false animateAlpha: false,
tag: selectionButtonTag
)), )),
environment: {}, environment: {},
containerSize: CGSize(width: 33.0, height: 33.0) 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) 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? fileprivate weak var saveTooltip: SaveProgressScreen?
func presentSaveTooltip() { func presentSaveTooltip() {
guard let controller = self.controller else { guard let controller = self.controller else {
@ -5725,6 +5755,8 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
if hasAppeared && !self.hasAppeared { if hasAppeared && !self.hasAppeared {
self.hasAppeared = hasAppeared self.hasAppeared = hasAppeared
self.presentSelectionTooltip()
} }
let componentSize = self.componentHost.update( let componentSize = self.componentHost.update(

View File

@ -347,7 +347,7 @@ extension PeerInfoScreenImpl {
let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max))
self.context.account.postbox.mediaBox.storeResourceData(resource.id, data: data) 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) { if [.suggest, .fallback].contains(mode) {
} else { } else {

View File

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