Merge commit '2af0b2a8b7' into story-folders

# Conflicts:
#	MODULE.bazel.lock
#	submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift
#	submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift
#	submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift
This commit is contained in:
Isaac 2025-07-22 18:28:44 +02:00
commit cb1961ff72
28 changed files with 415 additions and 145 deletions

View file

@ -14700,6 +14700,9 @@ Sorry for the inconvenience.";
"AccessDenied.AgeVerificationCamera" = "Telegram needs access to your camera for age verification.\n\nOpen your device's Settings > Privacy > Camera and set Telegram to ON.";
"PeerInfo.Gifts.Collections.All" = "All Gifts";
"PeerInfo.Gifts.Collections.Add" = "Add Collection";
"PeerInfo.Gifts.AddGiftsButton" = "Add Gifts";
"PeerInfo.Gifts.AddCollection" = "Add Collection";
@ -14728,6 +14731,12 @@ Sorry for the inconvenience.";
"PeerInfo.Gifts.EmptyCollection.Text" = "Add some gifts to this collection.";
"PeerInfo.Gifts.EmptyCollection.Action" = "Add to Collection";
"PeerInfo.Gifts.AddedToCollection" = "The gift has been added to **%@**.";
"PeerInfo.Gifts.RemovedFromCollection" = "The gift has been removed from **%@**.";
"PeerInfo.Gifts.AddedToCollectionUnique" = "**%1$@** has been added to **%2$@**.";
"PeerInfo.Gifts.RemovedFromCollectionUnique" = "**%1$@** has been removed from **%2$@**.";
"AddGifts.Title" = "Add Gifts";
"AddGifts.AddGifts_1" = "Add %@ Gift";
"AddGifts.AddGifts_any" = "Add %@ Gifts";
@ -14736,8 +14745,8 @@ Sorry for the inconvenience.";
"Gift.Options.Gift.BuyLimitReached_any" = "You've already sent %@ of these gifts, and it's the limit.";
"AgeVerification.Title" = "Age Verification";
"AgeVerification.Text" = "To access this content, you must confirm you are at least **18** years old as required by UK law.\n\nThis is a one-time process using your phone's camera. Your selfie will not be stored by Telegram.";
"AgeVerification.Text.gb" = "To access this content, you must confirm you are at least **18** years old as required by UK law.\n\nThis is a one-time process using your phone's camera. Your selfie will not be stored by Telegram.";
"AgeVerification.Text" = "To access this content, you must confirm you are at least **18** years old.\n\nThis is a one-time process using your phone's camera. Your selfie will not be stored by Telegram.";
"AgeVerification.Text.GB" = "To access this content, you must confirm you are at least **18** years old as required by UK law.\n\nThis is a one-time process using your phone's camera. Your selfie will not be stored by Telegram.";
"AgeVerification.Verify" = "Verify My Age";
"AgeVerification.Success.Title" = "Age check passed!";

View file

@ -1298,7 +1298,7 @@ public protocol SharedAccountContext: AnyObject {
func makeIncomingMessagePrivacyScreen(context: AccountContext, value: GlobalPrivacySettings.NonContactChatsPrivacy, exceptions: SelectivePrivacySettings, update: @escaping (GlobalPrivacySettings.NonContactChatsPrivacy) -> Void) -> ViewController
func openWebApp(context: AccountContext, parentController: ViewController, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?, botPeer: EnginePeer, chatPeer: EnginePeer?, threadId: Int64?, buttonText: String, url: String, simple: Bool, source: ChatOpenWebViewSource, skipTermsOfService: Bool, payload: String?)
func openWebApp(context: AccountContext, parentController: ViewController, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?, botPeer: EnginePeer, chatPeer: EnginePeer?, threadId: Int64?, buttonText: String, url: String, simple: Bool, source: ChatOpenWebViewSource, skipTermsOfService: Bool, payload: String?, verifyAgeCompletion: ((Int) -> Void)?)
func makeAffiliateProgramSetupScreenInitialData(context: AccountContext, peerId: EnginePeer.Id, mode: AffiliateProgramSetupScreenMode) -> Signal<AffiliateProgramSetupScreenInitialData, NoError>
func makeAffiliateProgramSetupScreen(context: AccountContext, initialData: AffiliateProgramSetupScreenInitialData) -> ViewController

View file

@ -12,6 +12,7 @@ public final class BotCheckoutController: ViewController {
public enum FetchError {
case generic
case disallowedStarGifts
case starGiftsUserLimit
}
public let form: BotPaymentForm
@ -58,6 +59,8 @@ public final class BotCheckoutController: ViewController {
switch error {
case .disallowedStarGift:
return .disallowedStarGifts
case .starGiftUserLimit:
return .starGiftsUserLimit
default:
return .generic
}

View file

@ -1250,7 +1250,8 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
simple: true,
source: .generic,
skipTermsOfService: true,
payload: nil
payload: nil,
verifyAgeCompletion: nil
)
}

View file

@ -3282,7 +3282,8 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
simple: true,
source: .generic,
skipTermsOfService: true,
payload: nil
payload: nil,
verifyAgeCompletion: nil
)
interaction.dismissSearch()
}
@ -4268,7 +4269,8 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
simple: true,
source: .generic,
skipTermsOfService: true,
payload: nil
payload: nil,
verifyAgeCompletion: nil
)
}
}
@ -4287,7 +4289,8 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
simple: true,
source: .generic,
skipTermsOfService: true,
payload: nil
payload: nil,
verifyAgeCompletion: nil
)
interaction.dismissSearch()
} else {

View file

@ -871,7 +871,7 @@
if (_intent == TGMediaAssetsControllerSendMediaIntent && _selectionContext.allowGrouping)
[[NSUserDefaults standardUserDefaults] setObject:@(!_selectionContext.grouping) forKey:@"TG_mediaGroupingDisabled_v0"];
return [TGMediaAssetsController resultSignalsForSelectionContext:_selectionContext editingContext:_editingContext intent:_intent currentItem:currentItem storeAssets:storeAssets convertToJpeg:false descriptionGenerator:descriptionGenerator saveEditedPhotos:_saveEditedPhotos];
}
@ -889,6 +889,9 @@
if (selectedItems.count == 0 && currentItem != nil)
[selectedItems addObject:currentItem];
if (intent == TGMediaAssetsControllerSendMediaIntent)
[[NSUserDefaults standardUserDefaults] setObject:@(editingContext.isHighQualityPhoto) forKey:@"TG_photoHighQuality_v0"];
if (saveEditedPhotos && storeAssets && editingContext != nil)
{
NSMutableArray *fullSizeSignals = [[NSMutableArray alloc] init];

View file

@ -2159,6 +2159,12 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att
strongSelf.controllerNode.dismissInput()
}
}, selectionState: selectionContext, editingState: editingContext ?? TGMediaEditingContext())
let highQualityPhoto = UserDefaults.standard.bool(forKey: "TG_photoHighQuality_v0")
if highQualityPhoto {
self.interaction?.editingState.setHighQualityPhoto(highQualityPhoto)
}
self.interaction?.selectionState?.grouping = true
self.interaction?.editingState.sendPaidMessageStars = sendPaidMessageStars ?? 0

View file

@ -3096,13 +3096,7 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
products.append(PremiumProduct(option: option, storeProduct: product))
}
}
//TODO:release
if let product = availableProducts.first(where: { $0.id.hasSuffix(".annual") }) {
let (currency, price) = product.priceCurrencyAndAmount
products.insert(PremiumProduct(option: PremiumPromoConfiguration.PremiumProductOption(isCurrent: false, months: 24, currency: currency, amount: price * 2, botUrl: "", transactionId: nil, availableForUpgrade: true, storeProductId: "org.telegram.telegramPremium.biannual"), storeProduct: product), at: 0)
}
strongSelf.products = products
strongSelf.isPremium = forceHasPremium || isPremium
strongSelf.otherPeerName = otherPeerName

View file

@ -180,6 +180,7 @@ public enum BotPaymentFormRequestError {
case noPaymentNeeded
case disallowedStarGift
case starGiftResellTooEarly(Int32)
case starGiftUserLimit
}
extension BotPaymentInvoice {
@ -488,6 +489,8 @@ func _internal_fetchBotPaymentForm(accountPeerId: PeerId, postbox: Postbox, netw
if let value = Int32(timeout) {
return .fail(.starGiftResellTooEarly(value))
}
} else if error.errorDescription == "STARGIFT_USER_USAGE_LIMITED" {
return .fail(.starGiftUserLimit)
}
return .fail(.generic)
}
@ -651,6 +654,7 @@ public enum SendBotPaymentFormError {
case alreadyPaid
case starGiftOutOfStock
case disallowedStarGift
case starGiftUserLimit
}
public enum SendBotPaymentResult {

View file

@ -405,26 +405,81 @@ public func presentAgeVerification(context: AccountContext, parentController: Vi
if state.verificationPassed {
completion()
} else {
let miniappPromise = Promise<EnginePeer?>(nil)
var useVerifyAgeBot = false
if let value = context.currentAppConfiguration.with({ $0 }).data?["force_verify_age_bot"] as? Bool, value {
useVerifyAgeBot = value
}
if useVerifyAgeBot, let verifyAgeBotUsername = context.currentAppConfiguration.with({ $0 }).data?["verify_age_bot_username"] as? String {
miniappPromise.set(context.engine.peers.resolvePeerByName(name: verifyAgeBotUsername, referrer: nil)
|> mapToSignal { result in
if case let .result(peer) = result {
return .single(peer)
}
return .complete()
})
}
let infoScreen = AgeVerificationScreen(context: context, completion: { [weak parentController] check, availability in
if check {
let scanScreen = FaceScanScreen(context: context, availability: availability, completion: { [weak parentController] passed in
if passed {
let _ = updateAgeVerificationState(engine: context.engine, { _ in
return AgeVerificationState(verificationPassed: passed)
}).start()
completion()
let navigationController = parentController?.navigationController
Queue.mainQueue().after(2.0) {
let controller = UndoOverlayController(presentationData: presentationData, content: .actionSucceeded(title: presentationData.strings.AgeVerification_Success_Title, text: presentationData.strings.AgeVerification_Success_Text, cancel: nil, destructive: false), action: { _ in return true })
(navigationController?.viewControllers.last as? ViewController)?.present(controller, in: .window(.root))
}
var requiredAge = 18
if let value = context.currentAppConfiguration.with({ $0 }).data?["verify_age_min"] as? Double {
requiredAge = Int(value)
}
let success = { [weak parentController] in
let _ = updateAgeVerificationState(engine: context.engine, { _ in
return AgeVerificationState(verificationPassed: true)
}).start()
completion()
let navigationController = parentController?.navigationController
Queue.mainQueue().after(2.0) {
let controller = UndoOverlayController(presentationData: presentationData, content: .actionSucceeded(title: presentationData.strings.AgeVerification_Success_Title, text: presentationData.strings.AgeVerification_Success_Text, cancel: nil, destructive: false), action: { _ in return true })
(navigationController?.viewControllers.last as? ViewController)?.present(controller, in: .current)
}
}
let failure = { [weak parentController] in
let controller = UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_banned", scale: 0.066, colors: [:], title: presentationData.strings.AgeVerification_Fail_Title, text: presentationData.strings.AgeVerification_Fail_Text, customUndoText: nil, timeout: nil), action: { _ in return true })
parentController?.present(controller, in: .current)
}
let _ = (miniappPromise.get()
|> take(1)
|> deliverOnMainQueue).start(next: { peer in
if let peer, let parentController {
context.sharedContext.openWebApp(
context: context,
parentController: parentController,
updatedPresentationData: nil,
botPeer: peer,
chatPeer: nil,
threadId: nil,
buttonText: "",
url: "",
simple: true,
source: .generic,
skipTermsOfService: true,
payload: nil,
verifyAgeCompletion: { age in
if age >= requiredAge {
success()
} else {
failure()
}
}
)
} else {
let controller = UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_banned", scale: 0.066, colors: [:], title: presentationData.strings.AgeVerification_Fail_Title, text: presentationData.strings.AgeVerification_Fail_Text, customUndoText: nil, timeout: nil), action: { _ in return true })
parentController?.present(controller, in: .window(.root))
let scanScreen = FaceScanScreen(context: context, availability: availability, completion: { age in
if age >= requiredAge {
success()
} else {
failure()
}
})
parentController?.push(scanScreen)
}
})
parentController?.push(scanScreen)
}
})
parentController?.push(infoScreen)

View file

@ -20,8 +20,6 @@ import ZipArchive
import PlainButtonComponent
import MultilineTextComponent
private let requiredAge = 18
final class FaceScanScreenComponent: Component {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
@ -109,7 +107,7 @@ final class FaceScanScreenComponent: Component {
self.backgroundColor = .black
self.previewLayer.backgroundColor = UIColor.red.cgColor
//self.previewLayer.backgroundColor = UIColor.red.cgColor
self.previewLayer.videoGravity = .resizeAspectFill
self.layer.addSublayer(previewLayer)
@ -240,7 +238,7 @@ final class FaceScanScreenComponent: Component {
let targetCenter = CGPoint(x: 0.5, y: 0.5)
let distance = sqrt(pow(faceCenter.x - targetCenter.x, 2) + pow(faceCenter.y - targetCenter.y, 2))
if distance < 0.35 {
if distance < 0.24 {
switch processState {
case .waitingForFace:
self.processState = .positioning
@ -320,7 +318,7 @@ final class FaceScanScreenComponent: Component {
if !self.ages.isEmpty {
let averageAge = self.ages.reduce(0, +) / Double(self.ages.count)
if let environment = self.environment, let controller = environment.controller() as? FaceScanScreen {
controller.completion(averageAge >= Double(requiredAge))
controller.completion(Int(averageAge))
controller.dismiss(animated: true)
}
} else {
@ -436,7 +434,7 @@ final class FaceScanScreenComponent: Component {
let center = CGPoint(x: availableSize.width / 2, y: environment.statusBarHeight + 10.0 + widthRadius * 1.3)
var previewScale = 1.0
var previewScale = 0.85
if self.processState == .tracking || self.processState == .readyToStart || self.processState == .completed || self.transitioningToViewFinder {
let circlePath = CGPath(roundedRect: CGRect(x: center.x - radius, y: center.y - radius, width: radius * 2, height: radius * 2), cornerWidth: radius, cornerHeight: radius, transform: nil)
path.addPath(circlePath)
@ -457,7 +455,6 @@ final class FaceScanScreenComponent: Component {
self.frameView.frame = frameViewFrame
self.frameView.update(size: frameViewFrame.size)
//TODO:localize
var instructionString = environment.strings.FaceScan_Instruction_Position
switch self.processState {
case .waitingForFace, .positioning:
@ -545,12 +542,12 @@ final class FaceScanScreenComponent: Component {
public final class FaceScanScreen: ViewControllerComponentContainer {
private let context: AccountContext
fileprivate let completion: (Bool) -> Void
fileprivate let completion: (Int) -> Void
public init(
context: AccountContext,
availability: Signal<AgeVerificationAvailability, NoError>,
completion: @escaping (Bool) -> Void
completion: @escaping (Int) -> Void
) {
self.context = context
self.completion = completion

View file

@ -363,6 +363,8 @@ final class GiftSetupScreenComponent: Component {
let entities = generateChatInputTextEntities(self.textInputState.text)
var finalPrice: Int64
var perUserLimit: Int32?
var giftFile: TelegramMediaFile?
let source: BotPaymentInvoiceSource
switch component.subject {
case let .premium(product):
@ -377,6 +379,8 @@ final class GiftSetupScreenComponent: Component {
if self.includeUpgrade, let upgradeStars = starGift.upgradeStars {
finalPrice += upgradeStars
}
perUserLimit = starGift.perUserLimit?.total
giftFile = starGift.file
source = .starGift(hideName: self.hideName, includeUpgrade: self.includeUpgrade, peerId: peerId, giftId: starGift.id, text: self.textInputState.text.string, entities: entities)
}
@ -395,6 +399,8 @@ final class GiftSetupScreenComponent: Component {
switch error {
case .disallowedStarGifts:
return .fail(.disallowedStarGift)
case .starGiftsUserLimit:
return .fail(.starGiftUserLimit)
default:
return .fail(.generic)
}
@ -468,6 +474,14 @@ final class GiftSetupScreenComponent: Component {
var errorText: String?
switch error {
case .starGiftUserLimit:
if let perUserLimit, let giftFile {
let text = presentationData.strings.Gift_Options_Gift_BuyLimitReached(perUserLimit)
let undoController = UndoOverlayController(presentationData: presentationData, content: .sticker(context: component.context, file: giftFile, loop: true, title: nil, text: text, undoText: nil, customAction: nil), action: { _ in return false })
controller.present(undoController, in: .current)
return
}
return
case .starGiftOutOfStock:
errorText = presentationData.strings.Gift_Send_ErrorOutOfStock
case .disallowedStarGift:

View file

@ -288,11 +288,14 @@ private final class GiftViewSheetContent: CombinedComponent {
}
var minRequiredAmount = StarsAmount(value: 100, nanos: 0)
var canUpgrade = false
if let resellStars = self.subject.arguments?.resellStars {
minRequiredAmount = StarsAmount(value: resellStars, nanos: 0)
} else if let arguments = self.subject.arguments, arguments.canUpgrade && arguments.upgradeStars == nil {
canUpgrade = true
}
if let starsContext = context.starsContext, let state = starsContext.currentState, state.balance < minRequiredAmount {
if let starsContext = context.starsContext, let state = starsContext.currentState, state.balance < minRequiredAmount || canUpgrade {
self.optionsDisposable = (context.engine.payments.starsTopUpOptions()
|> deliverOnMainQueue).start(next: { [weak self] options in
guard let self else {

View file

@ -1257,7 +1257,8 @@ final class AffiliateProgramSetupScreenComponent: Component {
simple: true,
source: .generic,
skipTermsOfService: true,
payload: nil
payload: nil,
verifyAgeCompletion: nil
)
} else if let navigationController = controller.navigationController as? NavigationController {
component.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: component.context, chatLocation: .peer(item.peer), subject: nil, keepStack: .always, animated: true, pushController: { [weak navigationController] chatController, animated, completion in

View file

@ -1917,7 +1917,7 @@ final class PeerInfoHeaderNode: ASDisplayNode {
let apparentBackgroundHeight = (1.0 - transitionFraction) * backgroundHeight + transitionFraction * transitionSourceHeight
var subtitleRatingSize: CGSize?
if let cachedData = cachedData as? CachedUserData, let starRating = cachedData.starRating {
if !"".isEmpty, let cachedData = cachedData as? CachedUserData, let starRating = cachedData.starRating {
let subtitleRating: ComponentView<Empty>
var subtitleRatingTransition = ComponentTransition(transition)
if let current = self.subtitleRating {

View file

@ -1451,7 +1451,8 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese
simple: true,
source: .generic,
skipTermsOfService: true,
payload: nil
payload: nil,
verifyAgeCompletion: nil
)
})
}
@ -11365,7 +11366,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
let canReorderEquals = lhs.2 == rhs.2
return filterEquals && sortingEquals && canReorderEquals
})
|> map { [weak self, weak pane, weak giftsContext] filter, sorting, canReorder -> ContextController.Items in
|> map { [weak pane, weak giftsContext] filter, sorting, canReorder -> ContextController.Items in
var items: [ContextMenuItem] = []
if hasVisibility {
@ -11390,13 +11391,12 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
}
})))
items.append(.action(ContextMenuActionItem(text: strings.PeerInfo_Gifts_ShareCollection, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Forward"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] _, f in
f(.default)
//TODO:release
self?.openShareLink(url: "https://t.me/")
})))
// items.append(.action(ContextMenuActionItem(text: strings.PeerInfo_Gifts_ShareCollection, icon: { theme in
// return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Forward"), color: theme.contextMenu.primaryColor)
// }, action: { [weak self] _, f in
// f(.default)
// self?.openShareLink(url: "https://t.me/")
// })))
}
if canReorder {
@ -11428,15 +11428,17 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
items.append(.separator)
}
items.append(.action(ContextMenuActionItem(text: sorting == .date ? strings.PeerInfo_Gifts_SortByValue : strings.PeerInfo_Gifts_SortByDate, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: sorting == .date ? "Peer Info/SortValue" : "Peer Info/SortDate"), color: theme.contextMenu.primaryColor)
}, action: { [weak giftsContext] _, f in
f(.default)
giftsContext?.updateSorting(sorting == .date ? .value : .date)
})))
if let pane, case .all = pane.currentCollection {
items.append(.action(ContextMenuActionItem(text: sorting == .date ? strings.PeerInfo_Gifts_SortByValue : strings.PeerInfo_Gifts_SortByDate, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: sorting == .date ? "Peer Info/SortValue" : "Peer Info/SortDate"), color: theme.contextMenu.primaryColor)
}, action: { [weak giftsContext] _, f in
f(.default)
giftsContext?.updateSorting(sorting == .date ? .value : .date)
})))
items.append(.separator)
items.append(.separator)
}
let toggleFilter: (ProfileGiftsContext.Filters) -> Void = { [weak giftsContext] value in
var updatedFilter = filter

View file

@ -23,17 +23,20 @@ final class AddGiftsScreenComponent: Component {
let context: AccountContext
let peerId: EnginePeer.Id
let collectionId: Int32
let remainingCount: Int32
let profileGifts: ProfileGiftsContext
init(
context: AccountContext,
peerId: EnginePeer.Id,
collectionId: Int32,
remainingCount: Int32,
profileGifts: ProfileGiftsContext
) {
self.context = context
self.peerId = peerId
self.collectionId = collectionId
self.remainingCount = remainingCount
self.profileGifts = profileGifts
}
@ -111,6 +114,7 @@ final class AddGiftsScreenComponent: Component {
var contentSize = CGSize(width: self.scrollView.bounds.width, height: contentHeight)
contentSize.height += environment.safeInsets.bottom
contentSize.height = max(contentSize.height, self.scrollView.bounds.size.height)
contentSize.height += 50.0 + 24.0
transition.setFrame(view: giftsListView, frame: CGRect(origin: CGPoint(), size: contentSize))
if self.scrollView.contentSize != contentSize {
@ -128,7 +132,13 @@ final class AddGiftsScreenComponent: Component {
if let current = self.giftsListView {
giftsListView = current
} else {
giftsListView = GiftsListView(context: component.context, peerId: component.peerId, profileGifts: component.profileGifts, giftsCollections: nil, canSelect: true, ignoreCollection: component.collectionId)
giftsListView = GiftsListView(context: component.context, peerId: component.peerId, profileGifts: component.profileGifts, giftsCollections: nil, canSelect: true, ignoreCollection: component.collectionId, remainingSelectionCount: component.remainingCount)
giftsListView.onContentUpdated = { [weak self] in
guard let self else {
return
}
self.state?.updated(transition: .immediate)
}
giftsListView.selectionUpdated = { [weak self] in
guard let self else {
return
@ -248,6 +258,7 @@ public final class AddGiftsScreen: ViewControllerComponentContainer {
context: AccountContext,
peerId: EnginePeer.Id,
collectionId: Int32,
remainingCount: Int32,
completion: @escaping ([ProfileGiftsContext.State.StarGift]) -> Void
) {
self.context = context
@ -264,10 +275,10 @@ public final class AddGiftsScreen: ViewControllerComponentContainer {
context: context,
peerId: peerId,
collectionId: collectionId,
remainingCount: remainingCount,
profileGifts: self.profileGifts
), navigationBarAppearance: .default, theme: .default, updatedPresentationData: nil)
self.title = presentationData.strings.AddGifts_Title
self.navigationPresentation = .modal

View file

@ -36,6 +36,7 @@ final class GiftsListView: UIView {
private let canSelect: Bool
private let ignoreCollection: Int32?
private let remainingSelectionCount: Int32
private var dataDisposable: Disposable?
@ -124,13 +125,14 @@ final class GiftsListView: UIView {
var contextAction: ((ProfileGiftsContext.State.StarGift, UIView, ContextGesture) -> Void)?
var addToCollection: (() -> Void)?
init(context: AccountContext, peerId: PeerId, profileGifts: ProfileGiftsContext, giftsCollections: ProfileGiftsCollectionsContext?, canSelect: Bool, ignoreCollection: Int32? = nil) {
init(context: AccountContext, peerId: PeerId, profileGifts: ProfileGiftsContext, giftsCollections: ProfileGiftsCollectionsContext?, canSelect: Bool, ignoreCollection: Int32? = nil, remainingSelectionCount: Int32 = 0) {
self.context = context
self.peerId = peerId
self.profileGifts = profileGifts
self.giftsCollections = giftsCollections
self.canSelect = canSelect
self.ignoreCollection = ignoreCollection
self.remainingSelectionCount = remainingSelectionCount
if let value = context.currentAppConfiguration.with({ $0 }).data?["stargifts_pinned_to_top_limit"] as? Double {
self.maxPinnedCount = Int(value)
@ -406,6 +408,13 @@ final class GiftsListView: UIView {
}
return self.updateScrolling(interactive: interactive, topInset: topInset, visibleBounds: visibleBounds, transition: transition)
}
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
if let topInset = self.topInset, point.y < topInset {
return false
}
return super.point(inside: point, with: event)
}
func updateScrolling(interactive: Bool = false, topInset: CGFloat, visibleBounds: CGRect, transition: ComponentTransition) -> CGFloat {
self.topInset = topInset
@ -548,8 +557,10 @@ final class GiftsListView: UIView {
if self.selectedItemIds.contains(itemReferenceId) {
self.selectedItemIds.remove(itemReferenceId)
} else {
self.selectedItemIds.insert(itemReferenceId)
self.selectedItemsMap[itemReferenceId] = product
if self.selectedItemIds.count < self.remainingSelectionCount {
self.selectedItemIds.insert(itemReferenceId)
self.selectedItemsMap[itemReferenceId] = product
}
}
self.selectionUpdated()
self.updateScrolling(transition: .easeInOut(duration: 0.25))

View file

@ -151,6 +151,15 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
super.init()
self.giftsListView.onContentUpdated = { [weak self] in
guard let self else {
return
}
if let params = self.currentParams {
self.update(size: params.size, topInset: params.topInset, sideInset: params.sideInset, bottomInset: params.bottomInset, deviceMetrics: params.deviceMetrics, visibleHeight: params.visibleHeight, isScrollingLockedAtTop: params.isScrollingLockedAtTop, expandProgress: params.expandProgress, navigationHeight: params.navigationHeight, presentationData: params.presentationData, synchronous: true, transition: .immediate)
}
}
self.addSubnode(self.backgroundNode)
self.addSubnode(self.scrollNode)
@ -184,7 +193,11 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
self.scrollNode.view.contentInsetAdjustmentBehavior = .never
self.scrollNode.view.delegate = self
self.scrollNode.view.insertSubview(self.giftsListView, at: 0)
if let tabSelectorView = self.tabSelector.view {
self.scrollNode.view.insertSubview(self.giftsListView, aboveSubview: tabSelectorView)
} else {
self.scrollNode.view.insertSubview(self.giftsListView, at: 0)
}
}
private func item(at point: CGPoint) -> (AnyHashable, ComponentView<Empty>)? {
@ -201,7 +214,7 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
return
}
let promptController = promptController(sharedContext: self.context.sharedContext, updatedPresentationData: nil, text: params.presentationData.strings.PeerInfo_Gifts_CreateCollection_Title, titleFont: .bold, subtitle: params.presentationData.strings.PeerInfo_Gifts_CreateCollection_Text, value: "", placeholder: params.presentationData.strings.PeerInfo_Gifts_CreateCollection_Placeholder, characterLimit: 20, displayCharacterLimit: true, apply: { [weak self] value in
let promptController = promptController(sharedContext: self.context.sharedContext, updatedPresentationData: nil, text: params.presentationData.strings.PeerInfo_Gifts_CreateCollection_Title, titleFont: .bold, subtitle: params.presentationData.strings.PeerInfo_Gifts_CreateCollection_Text, value: "", placeholder: params.presentationData.strings.PeerInfo_Gifts_CreateCollection_Placeholder, characterLimit: 12, displayCharacterLimit: true, apply: { [weak self] value in
guard let self, let value else {
return
}
@ -211,6 +224,10 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
}
if let collection {
self.setCurrentCollection(collection: .collection(collection.id))
if let tabSelectorView = self.tabSelector.view as? TabSelectorComponent.View {
tabSelectorView.scrollToEnd()
}
}
})
})
@ -230,6 +247,10 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
self?.setCurrentCollection(collection: .all)
let _ = self?.profileGiftsCollections.deleteCollection(id: id).start()
if let tabSelectorView = self?.tabSelector.view as? TabSelectorComponent.View {
tabSelectorView.scrollToStart()
}
})
]),
ActionSheetItemGroup(items: [
@ -242,7 +263,15 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
}
public func addGiftsToCollection(id: Int32) {
let screen = AddGiftsScreen(context: self.context, peerId: self.peerId, collectionId: id, completion: { [weak self] gifts in
var collectionGiftsMaxCount: Int32 = 1000
if let value = self.context.currentAppConfiguration.with({ $0 }).data?["stargifts_collection_gifts_limit"] as? Double {
collectionGiftsMaxCount = Int32(value)
}
var remainingCount = collectionGiftsMaxCount
if let currentCount = self.giftsListView.profileGifts.currentState?.count {
remainingCount = max(0, collectionGiftsMaxCount - currentCount)
}
let screen = AddGiftsScreen(context: self.context, peerId: self.peerId, collectionId: id, remainingCount: remainingCount, completion: { [weak self] gifts in
guard let self else {
return
}
@ -256,7 +285,7 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
return
}
let promptController = promptController(sharedContext: self.context.sharedContext, updatedPresentationData: nil, text: params.presentationData.strings.PeerInfo_Gifts_RenameCollection_Title, titleFont: .bold, value: collection.title, placeholder: params.presentationData.strings.PeerInfo_Gifts_CreateCollection_Placeholder, characterLimit: 20, displayCharacterLimit: true, apply: { [weak self] value in
let promptController = promptController(sharedContext: self.context.sharedContext, updatedPresentationData: nil, text: params.presentationData.strings.PeerInfo_Gifts_RenameCollection_Title, titleFont: .bold, value: collection.title, placeholder: params.presentationData.strings.PeerInfo_Gifts_CreateCollection_Placeholder, characterLimit: 12, displayCharacterLimit: true, apply: { [weak self] value in
guard let self, let value else {
return
}
@ -460,20 +489,21 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
}
f(.default)
self.renameCollection(id: id)
Queue.mainQueue().after(0.15) {
self.renameCollection(id: id)
}
})))
items.append(.action(ContextMenuActionItem(text: params.presentationData.strings.PeerInfo_Gifts_ShareCollection, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Forward"), color: theme.actionSheet.primaryTextColor)
}, action: { [weak self] _, f in
guard let self else {
return
}
f(.default)
//TODO:release
let _ = self
})))
// items.append(.action(ContextMenuActionItem(text: params.presentationData.strings.PeerInfo_Gifts_ShareCollection, icon: { theme in
// return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Forward"), color: theme.actionSheet.primaryTextColor)
// }, action: { [weak self] _, f in
// guard let self else {
// return
// }
// f(.default)
//
// let _ = self
// })))
items.append(.action(ContextMenuActionItem(text: params.presentationData.strings.PeerInfo_Gifts_Reorder, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/ReorderItems"), color: theme.actionSheet.primaryTextColor)
@ -494,7 +524,9 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
}
f(.default)
self.deleteCollection(id: id)
Queue.mainQueue().after(0.15) {
self.deleteCollection(id: id)
}
})))
let contextController = ContextController(
@ -515,13 +547,19 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
var topInset: CGFloat = 60.0
if let collections = self.collections, !collections.isEmpty {
var canEditCollections = false
if self.peerId == self.context.account.peerId || self.canManage {
canEditCollections = true
}
let hasNonEmptyCollections = self.collections?.contains(where: { $0.count > 0 }) ?? false
if let collections = self.collections, !collections.isEmpty && (hasNonEmptyCollections || canEditCollections) {
var tabSelectorItems: [TabSelectorComponent.Item] = []
tabSelectorItems.append(TabSelectorComponent.Item(
id: AnyHashable(GiftCollection.all.rawValue),
title: "All Gifts"
title: params.presentationData.strings.PeerInfo_Gifts_Collections_All
))
var effectiveCollections: [StarGiftCollection] = collections
if let reorderedCollectionIds = self.reorderedCollectionIds {
var collectionMap: [Int32: StarGiftCollection] = [:]
@ -538,6 +576,9 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
}
for collection in effectiveCollections {
if !canEditCollections && collection.count == 0 {
continue
}
tabSelectorItems.append(TabSelectorComponent.Item(
id: AnyHashable(GiftCollection.collection(collection.id).rawValue),
content: .component(AnyComponent(
@ -557,19 +598,21 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
}
))
}
tabSelectorItems.append(TabSelectorComponent.Item(
id: AnyHashable(GiftCollection.create.rawValue),
content: .component(AnyComponent(
CollectionTabItemComponent(
context: self.context,
icon: .add,
title: "Add Collection",
theme: params.presentationData.theme
)
)),
isReorderable: false
))
if canEditCollections {
tabSelectorItems.append(TabSelectorComponent.Item(
id: AnyHashable(GiftCollection.create.rawValue),
content: .component(AnyComponent(
CollectionTabItemComponent(
context: self.context,
icon: .add,
title: params.presentationData.strings.PeerInfo_Gifts_Collections_Add,
theme: params.presentationData.theme
)
)),
isReorderable: false
))
}
let tabSelectorSize = self.tabSelector.update(
transition: transition,
@ -621,7 +664,7 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
if let tabSelectorView = self.tabSelector.view {
if tabSelectorView.superview == nil {
tabSelectorView.alpha = 1.0
self.scrollNode.view.addSubview(tabSelectorView)
self.scrollNode.view.insertSubview(tabSelectorView, at: 0)
if !transition.animation.isImmediate {
tabSelectorView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
@ -938,10 +981,52 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
}, iconPosition: collection.icon == nil ? .left : .right, action: { [weak self] _, f in
f(.default)
guard let self else {
return
}
if isAdded, let giftReference = gift.reference {
let _ = self?.profileGiftsCollections.removeGifts(id: collection.id, gifts: [giftReference]).start()
let _ = self.profileGiftsCollections.removeGifts(id: collection.id, gifts: [giftReference]).start()
} else {
let _ = self?.profileGiftsCollections.addGifts(id: collection.id, gifts: [gift]).start()
let _ = self.profileGiftsCollections.addGifts(id: collection.id, gifts: [gift]).start()
}
var giftFile: TelegramMediaFile?
var giftTitle: String?
switch gift.gift {
case let .generic(gift):
giftFile = gift.file
case let .unique(uniqueGift):
giftTitle = uniqueGift.title + " #\(presentationStringsFormattedNumber(uniqueGift.number, currentParams.presentationData.dateTimeFormat.groupingSeparator))"
for attribute in uniqueGift.attributes {
if case let .model(_, file, _) = attribute {
giftFile = file
}
}
}
if let giftFile {
let text: String
if let giftTitle {
if isAdded {
text = currentParams.presentationData.strings.PeerInfo_Gifts_RemovedFromCollectionUnique(giftTitle, collection.title).string
} else {
text = currentParams.presentationData.strings.PeerInfo_Gifts_AddedToCollectionUnique(giftTitle, collection.title).string
}
} else {
if isAdded {
text = currentParams.presentationData.strings.PeerInfo_Gifts_RemovedFromCollection(collection.title).string
} else {
text = currentParams.presentationData.strings.PeerInfo_Gifts_AddedToCollection(collection.title).string
}
}
let undoController = UndoOverlayController(
presentationData: currentParams.presentationData,
content: .sticker(context: self.context, file: giftFile, loop: false, title: nil, text: text, undoText: nil, customAction: nil),
action: { _ in return true }
)
self.parentController?.present(undoController, in: .current)
}
})))
}
@ -1351,14 +1436,12 @@ private final class CollectionTabItemComponent: Component {
let titleSize = self.title.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: component.title, font: Font.semibold(14.0), textColor: .white))
text: .plain(NSAttributedString(string: component.title, font: Font.semibold(14.0), textColor: component.theme.list.itemSecondaryTextColor))
)),
environment: {},
containerSize: CGSize(width: availableSize.width, height: 100.0)
)
let tintColor = component.theme.list.itemSecondaryTextColor
var iconOffset: CGFloat = 0.0
var iconSize = CGSize()
if let icon = component.icon {
@ -1392,7 +1475,7 @@ private final class CollectionTabItemComponent: Component {
transition: .immediate,
component: AnyComponent(BundleIconComponent(
name: "Chat/Input/Media/PanelBadgeAdd",
tintColor: tintColor
tintColor: component.theme.list.itemSecondaryTextColor
)),
environment: {},
containerSize: CGSize(width: 100.0, height: 100.0)
@ -1428,8 +1511,6 @@ private final class CollectionTabItemComponent: Component {
self.addSubview(titleView)
}
titleView.frame = titleFrame
transition.setTintColor(layer: titleView.layer, color: tintColor)
}
let size: CGSize

View file

@ -483,6 +483,14 @@ public final class TabSelectorComponent: Component {
}
}
public func scrollToStart() {
self.setContentOffset(.zero, animated: true)
}
public func scrollToEnd() {
self.setContentOffset(CGPoint(x: self.contentSize.width - self.bounds.width, y: 0.0), animated: true)
}
func update(component: TabSelectorComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
let selectionColorUpdated = component.colors.selection != self.component?.colors.selection

View file

@ -900,7 +900,21 @@ final class AuthorizedApplicationContext {
}
if openAppIfAny, case let .user(user) = peer, let botInfo = user.botInfo, botInfo.flags.contains(.hasWebApp), let parentController = self.rootController.viewControllers.last as? ViewController {
self.context.sharedContext.openWebApp(context: self.context, parentController: parentController, updatedPresentationData: nil, botPeer: peer, chatPeer: nil, threadId: nil, buttonText: "", url: "", simple: true, source: .generic, skipTermsOfService: true, payload: nil)
self.context.sharedContext.openWebApp(
context: self.context,
parentController: parentController,
updatedPresentationData: nil,
botPeer: peer,
chatPeer: nil,
threadId: nil,
buttonText: "",
url: "",
simple: true,
source: .generic,
skipTermsOfService: true,
payload: nil,
verifyAgeCompletion: nil
)
} else {
self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: self.rootController, context: self.context, chatLocation: chatLocation, subject: alwaysKeepMessageId || isOutgoingMessage ? messageId.flatMap { .message(id: .id($0), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil, setupReply: false) } : nil, activateInput: activateInput ? .text : nil))
}

View file

@ -128,8 +128,7 @@ extension ChatControllerImpl {
}
}
if "".isEmpty {
//TODO:release
if canReplyInChat(self.presentationInterfaceState, accountPeerId: self.context.account.peerId) {
items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.Chat_Todo_ReplyToItem, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Reply"), color: theme.actionSheet.primaryTextColor)
}, action: { [weak self] c, _ in

View file

@ -27,7 +27,8 @@ func openWebAppImpl(
simple: Bool,
source: ChatOpenWebViewSource,
skipTermsOfService: Bool,
payload: String?
payload: String?,
verifyAgeCompletion: ((Int) -> Void)?
) {
if context.isFrozen {
parentController.push(context.sharedContext.makeAccountFreezeInfoScreen(context: context))
@ -246,7 +247,7 @@ func openWebAppImpl(
navigationController = parentController.effectiveNavigationController
}
return navigationController ?? (context.sharedContext.mainWindow?.viewController as? NavigationController)
})
}, verifyAgeCompletion: verifyAgeCompletion)
controller.navigationPresentation = .flatModal
parentController.push(controller)
@ -351,7 +352,21 @@ public extension ChatControllerImpl {
}
self.chatDisplayNode.dismissInput()
self.context.sharedContext.openWebApp(context: self.context, parentController: self, updatedPresentationData: self.updatedPresentationData, botPeer: EnginePeer(peer), chatPeer: EnginePeer(peer), threadId: self.chatLocation.threadId, buttonText: buttonText, url: url, simple: simple, source: source, skipTermsOfService: false, payload: nil)
self.context.sharedContext.openWebApp(
context: self.context,
parentController: self,
updatedPresentationData: self.updatedPresentationData,
botPeer: EnginePeer(peer),
chatPeer: EnginePeer(peer),
threadId: self.chatLocation.threadId,
buttonText: buttonText,
url: url,
simple: simple,
source: source,
skipTermsOfService: false,
payload: nil,
verifyAgeCompletion: nil
)
}
fileprivate static func botRequestSwitchInline(context: AccountContext, controller: ChatControllerImpl?, peerId: EnginePeer.Id, botAddress: String, query: String, chatTypes: [ReplyMarkupButtonRequestPeerType]?, completion: @escaping () -> Void) -> Void {
@ -616,7 +631,21 @@ public extension ChatControllerImpl {
}
})
} else {
context.sharedContext.openWebApp(context: context, parentController: parentController, updatedPresentationData: updatedPresentationData, botPeer: botPeer, chatPeer: nil, threadId: nil, buttonText: "", url: "", simple: true, source: .generic, skipTermsOfService: false, payload: payload)
context.sharedContext.openWebApp(
context: context,
parentController: parentController,
updatedPresentationData: updatedPresentationData,
botPeer: botPeer,
chatPeer: nil,
threadId: nil,
buttonText: "",
url: "",
simple: true,
source: .generic,
skipTermsOfService: false,
payload: payload,
verifyAgeCompletion: nil
)
}
}
}

View file

@ -8907,7 +8907,21 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
commit()
})
} else {
self.context.sharedContext.openWebApp(context: self.context, parentController: self, updatedPresentationData: self.updatedPresentationData, botPeer: peer, chatPeer: nil, threadId: nil, buttonText: "", url: "", simple: true, source: .generic, skipTermsOfService: false, payload: botAppStart.payload)
self.context.sharedContext.openWebApp(
context: self.context,
parentController: self,
updatedPresentationData: self.updatedPresentationData,
botPeer: peer,
chatPeer: nil,
threadId: nil,
buttonText: "",
url: "",
simple: true,
source: .generic,
skipTermsOfService: false,
payload: botAppStart.payload,
verifyAgeCompletion: nil
)
commit()
}
})

View file

@ -3882,8 +3882,8 @@ public final class SharedAccountContextImpl: SharedAccountContext {
return incomingMessagePrivacyScreen(context: context, value: value, exceptions: exceptions, update: update)
}
public func openWebApp(context: AccountContext, parentController: ViewController, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?, botPeer: EnginePeer, chatPeer: EnginePeer?, threadId: Int64?, buttonText: String, url: String, simple: Bool, source: ChatOpenWebViewSource, skipTermsOfService: Bool, payload: String?) {
openWebAppImpl(context: context, parentController: parentController, updatedPresentationData: updatedPresentationData, botPeer: botPeer, chatPeer: chatPeer, threadId: threadId, buttonText: buttonText, url: url, simple: simple, source: source, skipTermsOfService: skipTermsOfService, payload: payload)
public func openWebApp(context: AccountContext, parentController: ViewController, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?, botPeer: EnginePeer, chatPeer: EnginePeer?, threadId: Int64?, buttonText: String, url: String, simple: Bool, source: ChatOpenWebViewSource, skipTermsOfService: Bool, payload: String?, verifyAgeCompletion: ((Int) -> Void)?) {
openWebAppImpl(context: context, parentController: parentController, updatedPresentationData: updatedPresentationData, botPeer: botPeer, chatPeer: chatPeer, threadId: threadId, buttonText: buttonText, url: url, simple: simple, source: source, skipTermsOfService: skipTermsOfService, payload: payload, verifyAgeCompletion: verifyAgeCompletion)
}
public func makeAffiliateProgramSetupScreenInitialData(context: AccountContext, peerId: EnginePeer.Id, mode: AffiliateProgramSetupScreenMode) -> Signal<AffiliateProgramSetupScreenInitialData, NoError> {

View file

@ -31,7 +31,6 @@ import PeerInfoScreen
import PeerInfoStoryGridScreen
import ShareWithPeersScreen
import ChatEmptyNode
//import FaceScanScreen
import UndoUI
private class DetailsChatPlaceholderNode: ASDisplayNode, NavigationDetailsPlaceholderNode {
@ -237,22 +236,6 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
self.accountSettingsController = accountSettingsController
self.rootTabController = tabBarController
self.pushViewController(tabBarController, animated: false)
// Queue.mainQueue().after(1.0) {
// let context = self.context
// let infoScreen = AgeVerificationScreen(context: context, completion: { [weak chatListController] proceed in
// if proceed {
// let scanScreen = FaceScanScreen(context: context, completion: { success in
// let controller = UndoOverlayController(presentationData: self.presentationData, content: .actionSucceeded(title: "Age check passed!", text: "You can now view this content.", cancel: nil, destructive: false), elevatedLayout: true, action: { _ in return true })
// Queue.mainQueue().after(0.1) {
// chatListController?.present(controller, in: .window(.root))
// }
// })
// chatListController?.push(scanScreen)
// }
// })
// chatListController.push(infoScreen)
// }
}
public func updateRootControllers(showCallsTab: Bool) {

View file

@ -653,10 +653,14 @@ public final class WebAppController: ViewController, AttachmentContainable {
self.animateTransitionIn()
})
}
@available(iOSApplicationExtension 15.0, iOS 15.0, *)
func webView(_ webView: WKWebView, requestMediaCapturePermissionFor origin: WKSecurityOrigin, initiatedByFrame frame: WKFrameInfo, type: WKMediaCaptureType, decisionHandler: @escaping (WKPermissionDecision) -> Void) {
decisionHandler(.prompt)
if self.controller?.isVerifyAgeBot == true && type == .camera {
decisionHandler(.grant)
} else {
decisionHandler(.prompt)
}
}
func webView(_ webView: WKWebView, runJavaScriptAlertPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping () -> Void) {
@ -1756,6 +1760,12 @@ public final class WebAppController: ViewController, AttachmentContainable {
}
case "web_app_hide_keyboard":
self.view.window?.endEditing(true)
case "web_app_verify_age":
if let json, self.controller?.isVerifyAgeBot == true {
if let ageValue = json["age"] as? Double {
self.controller?.verifyAgeCompletion?(Int(ageValue))
}
}
default:
break
}
@ -3281,6 +3291,8 @@ public final class WebAppController: ViewController, AttachmentContainable {
public var completion: () -> Void = {}
public var requestSwitchInline: (String, [ReplyMarkupButtonRequestPeerType]?, @escaping () -> Void) -> Void = { _, _, _ in }
public var verifyAgeCompletion: ((Int) -> Void)?
public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, params: WebAppParameters, replyToMessageId: MessageId?, threadId: Int64?) {
self.context = context
self.source = params.source
@ -3323,16 +3335,20 @@ public final class WebAppController: ViewController, AttachmentContainable {
self.navigationItem.leftBarButtonItem?.action = #selector(self.cancelPressed)
self.navigationItem.leftBarButtonItem?.target = self
self.navigationItem.rightBarButtonItem = UIBarButtonItem(customDisplayNode: self.moreButtonNode)
self.navigationItem.rightBarButtonItem?.action = #selector(self.moreButtonPressed)
self.navigationItem.rightBarButtonItem?.target = self
if !self.isVerifyAgeBot {
self.navigationItem.rightBarButtonItem = UIBarButtonItem(customDisplayNode: self.moreButtonNode)
self.navigationItem.rightBarButtonItem?.action = #selector(self.moreButtonPressed)
self.navigationItem.rightBarButtonItem?.target = self
}
self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Back, style: .plain, target: nil, action: nil)
let titleView = WebAppTitleView(context: self.context, theme: self.presentationData.theme)
titleView.title = WebAppTitle(title: params.botName, counter: self.presentationData.strings.WebApp_Miniapp, isVerified: params.botVerified)
self.navigationItem.titleView = titleView
self.titleView = titleView
if !self.isVerifyAgeBot {
let titleView = WebAppTitleView(context: self.context, theme: self.presentationData.theme)
titleView.title = WebAppTitle(title: params.botName, counter: self.presentationData.strings.WebApp_Miniapp, isVerified: params.botVerified)
self.navigationItem.titleView = titleView
self.titleView = titleView
}
self.moreButtonNode.action = { [weak self] _, gesture in
if let strongSelf = self {
@ -3387,6 +3403,13 @@ public final class WebAppController: ViewController, AttachmentContainable {
self.presentationDataDisposable?.dispose()
}
private var isVerifyAgeBot: Bool {
if let ageBotUsername = self.context.currentAppConfiguration.with({ $0 }).data?["verify_age_bot_username"] as? String {
return self.botAddress == ageBotUsername
}
return false
}
public func beforeMaximize(navigationController: NavigationController, completion: @escaping () -> Void) {
switch self.source {
case .generic, .settings:
@ -3857,7 +3880,8 @@ public func standaloneWebAppController(
willDismiss: @escaping () -> Void = {},
didDismiss: @escaping () -> Void = {},
getNavigationController: @escaping () -> NavigationController? = { return nil },
getSourceRect: (() -> CGRect?)? = nil
getSourceRect: (() -> CGRect?)? = nil,
verifyAgeCompletion: ((Int) -> Void)? = nil
) -> ViewController {
let controller = AttachmentController(context: context, updatedPresentationData: updatedPresentationData, chatLocation: .peer(id: params.peerId), buttons: [.standalone], initialButton: .standalone, fromMenu: params.source == .menu, hasTextInput: false, isFullSize: params.fullSize, makeEntityInputView: {
return nil
@ -3868,6 +3892,7 @@ public func standaloneWebAppController(
webAppController.completion = completion
webAppController.getNavigationController = getNavigationController
webAppController.requestSwitchInline = requestSwitchInline
webAppController.verifyAgeCompletion = verifyAgeCompletion
present(webAppController, webAppController.mediaPickerContext)
}
controller.willDismiss = willDismiss

View file

@ -1,5 +1,5 @@
{
"app": "11.14",
"app": "11.13.3",
"xcode": "16.2",
"bazel": "8.3.1:0cac3a67dc5429c68272dc6944104952e9e4cf84b29d126a5ff3fbaa59045217",
"macos": "15"