diff --git a/submodules/ComponentFlow/Source/Components/HStack.swift b/submodules/ComponentFlow/Source/Components/HStack.swift new file mode 100644 index 0000000000..f86e2c9148 --- /dev/null +++ b/submodules/ComponentFlow/Source/Components/HStack.swift @@ -0,0 +1,57 @@ +import Foundation +import UIKit + +public final class HStack: CombinedComponent { + public typealias EnvironmentType = ChildEnvironment + + private let items: [AnyComponentWithIdentity] + private let spacing: CGFloat + + public init(_ items: [AnyComponentWithIdentity], spacing: CGFloat) { + self.items = items + self.spacing = spacing + } + + public static func ==(lhs: HStack, rhs: HStack) -> Bool { + if lhs.items != rhs.items { + return false + } + if lhs.spacing != rhs.spacing { + return false + } + return true + } + + public static var body: Body { + let children = ChildMap(environment: ChildEnvironment.self, keyedBy: AnyHashable.self) + + return { context in + let updatedChildren = context.component.items.map { item in + return children[item.id].update( + component: item.component, environment: { + context.environment[ChildEnvironment.self] + }, + availableSize: context.availableSize, + transition: context.transition + ) + } + + var size = CGSize(width: 0.0, height: 0.0) + for child in updatedChildren { + size.width += child.size.width + size.height = max(size.height, child.size.height) + } + + var nextX = 0.0 + for child in updatedChildren { + context.add(child + .position(child.size.centered(in: CGRect(origin: CGPoint(x: nextX, y: floor((size.height - child.size.height) * 0.5)), size: child.size)).center) + ) + nextX += child.size.width + nextX += context.component.spacing + } + + return size + } + } +} diff --git a/submodules/SettingsUI/Sources/Privacy and Security/PrivacyAndSecurityController.swift b/submodules/SettingsUI/Sources/Privacy and Security/PrivacyAndSecurityController.swift index 099a9f2824..188374b830 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/PrivacyAndSecurityController.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/PrivacyAndSecurityController.swift @@ -578,7 +578,7 @@ private func privacyAndSecurityControllerEntries( entries.append(.voiceMessagePrivacy(presentationData.theme, presentationData.strings.Privacy_VoiceMessages, stringForSelectiveSettings(strings: presentationData.strings, settings: privacySettings.voiceMessages), !isPremium)) } - entries.append(.selectivePrivacyInfo(presentationData.theme, presentationData.strings.PrivacyLastSeenSettings_GroupsAndChannelsHelp)) + //entries.append(.selectivePrivacyInfo(presentationData.theme, presentationData.strings.PrivacyLastSeenSettings_GroupsAndChannelsHelp)) } else { entries.append(.phoneNumberPrivacy(presentationData.theme, presentationData.strings.PrivacySettings_PhoneNumber, presentationData.strings.Channel_NotificationLoading)) entries.append(.lastSeenPrivacy(presentationData.theme, presentationData.strings.PrivacySettings_LastSeen, presentationData.strings.Channel_NotificationLoading)) @@ -591,7 +591,7 @@ private func privacyAndSecurityControllerEntries( entries.append(.voiceMessagePrivacy(presentationData.theme, presentationData.strings.Privacy_VoiceMessages, presentationData.strings.Channel_NotificationLoading, !isPremium)) } - entries.append(.selectivePrivacyInfo(presentationData.theme, presentationData.strings.PrivacyLastSeenSettings_GroupsAndChannelsHelp)) + //entries.append(.selectivePrivacyInfo(presentationData.theme, presentationData.strings.PrivacyLastSeenSettings_GroupsAndChannelsHelp)) } if canAutoarchive { diff --git a/submodules/TelegramUI/Components/LottieComponent/Sources/LottieComponent.swift b/submodules/TelegramUI/Components/LottieComponent/Sources/LottieComponent.swift index 64c1ed9d5b..9877c192a0 100644 --- a/submodules/TelegramUI/Components/LottieComponent/Sources/LottieComponent.swift +++ b/submodules/TelegramUI/Components/LottieComponent/Sources/LottieComponent.swift @@ -82,17 +82,20 @@ public final class LottieComponent: Component { public let color: UIColor? public let startingPosition: StartingPosition public let size: CGSize? + public let loop: Bool public init( content: Content, color: UIColor? = nil, startingPosition: StartingPosition = .end, - size: CGSize? = nil + size: CGSize? = nil, + loop: Bool = false ) { self.content = content self.color = color self.startingPosition = startingPosition self.size = size + self.loop = loop } public static func ==(lhs: LottieComponent, rhs: LottieComponent) -> Bool { @@ -108,6 +111,9 @@ public final class LottieComponent: Component { if lhs.size != rhs.size { return false } + if lhs.loop != rhs.loop { + return false + } return true } @@ -116,6 +122,8 @@ public final class LottieComponent: Component { private var component: LottieComponent? private var scheduledPlayOnce: Bool = false + private var isPlaying: Bool = false + private var playOnceCompletion: (() -> Void)? private var animationInstance: LottieInstance? private var animationFrameRange: Range? @@ -196,6 +204,7 @@ public final class LottieComponent: Component { } self.scheduledPlayOnce = false + self.isPlaying = true if self.currentFrame != animationFrameRange.lowerBound { self.currentFrame = animationFrameRange.lowerBound @@ -284,11 +293,19 @@ public final class LottieComponent: Component { advanceFrameCount = 4 } self.currentFrame += advanceFrameCount + + if self.currentFrame >= animationFrameRange.upperBound - 1 { + if let component = self.component, component.loop { + self.currentFrame = animationFrameRange.lowerBound + } + } + if self.currentFrame >= animationFrameRange.upperBound - 1 { self.currentFrame = animationFrameRange.upperBound - 1 self.updateImage() self.displayLink?.invalidate() self.displayLink = nil + self.isPlaying = false if let playOnceCompletion = self.playOnceCompletion { self.playOnceCompletion = nil @@ -362,6 +379,10 @@ public final class LottieComponent: Component { transition.setTintColor(view: self, color: color) } + if component.loop && !self.isPlaying { + self.playOnce() + } + return size } } diff --git a/submodules/TelegramUI/Components/NavigationSearchComponent/Sources/NavigationSearchComponent.swift b/submodules/TelegramUI/Components/NavigationSearchComponent/Sources/NavigationSearchComponent.swift index 5a03791fb5..db8614b47e 100644 --- a/submodules/TelegramUI/Components/NavigationSearchComponent/Sources/NavigationSearchComponent.swift +++ b/submodules/TelegramUI/Components/NavigationSearchComponent/Sources/NavigationSearchComponent.swift @@ -75,6 +75,9 @@ public final class NavigationSearchComponent: Component { private let searchIconView: UIImageView private let placeholderText = ComponentView() + private let clearButton: HighlightableButton + private let clearIconView: UIImageView + private var button: ComponentView? private var textField: UITextField? @@ -85,12 +88,21 @@ public final class NavigationSearchComponent: Component { self.searchIconView = UIImageView(image: UIImage(bundleImageName: "Components/Search Bar/Loupe")?.withRenderingMode(.alwaysTemplate)) + self.clearButton = HighlightableButton() + self.clearIconView = UIImageView(image: UIImage(bundleImageName: "Components/Search Bar/Clear")?.withRenderingMode(.alwaysTemplate)) + super.init(frame: frame) self.addSubview(self.backgroundView) self.addSubview(self.searchIconView) + self.addSubview(self.clearButton) + self.clearButton.addSubview(self.clearIconView) + self.clearButton.isHidden = true + self.backgroundView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.backgroundTapGesture(_:)))) + + self.clearButton.addTarget(self, action: #selector(self.clearPressed), for: .touchUpInside) } required init?(coder: NSCoder) { @@ -109,6 +121,7 @@ public final class NavigationSearchComponent: Component { textField.addTarget(self, action: #selector(self.textChanged), for: .editingChanged) self.addSubview(textField) textField.keyboardAppearance = .dark + textField.returnKeyType = .done } self.textField?.becomeFirstResponder() @@ -124,14 +137,26 @@ public final class NavigationSearchComponent: Component { } } + public func textFieldShouldReturn(_ textField: UITextField) -> Bool { + textField.resignFirstResponder() + return false + } + @objc private func textChanged() { self.updateText(updateComponent: true) } + @objc private func clearPressed() { + self.textField?.text = "" + self.updateText(updateComponent: true) + } + @objc private func updateText(updateComponent: Bool) { let isEmpty = self.textField?.text?.isEmpty ?? true self.placeholderText.view?.isHidden = !isEmpty + self.clearButton.isHidden = isEmpty + if updateComponent, let component = self.component { component.updateQuery(self.textField?.text ?? "") } @@ -212,6 +237,7 @@ public final class NavigationSearchComponent: Component { } if previousComponent?.colors.inactiveForeground != component.colors.inactiveForeground { self.searchIconView.tintColor = component.colors.inactiveForeground + self.clearIconView.tintColor = component.colors.inactiveForeground } let placeholderSize = self.placeholderText.update( @@ -241,6 +267,15 @@ public final class NavigationSearchComponent: Component { let searchIconFrame = CGRect(origin: CGPoint(x: placeholderTextFrame.minX - searchIconSpacing - searchIconSize.width, y: backgroundFrame.minY + floor((fieldHeight - searchIconSize.height) * 0.5)), size: searchIconSize) transition.setFrame(view: self.searchIconView, frame: searchIconFrame) + if let image = self.clearIconView.image { + let clearSize = image.size + let clearFrame = CGRect(origin: CGPoint(x: backgroundFrame.maxX - 6.0 - clearSize.width, y: backgroundFrame.minY + floor((backgroundFrame.height - clearSize.height) / 2.0)), size: clearSize) + + let clearButtonFrame = CGRect(origin: CGPoint(x: clearFrame.minX - 4.0, y: backgroundFrame.minY), size: CGSize(width: clearFrame.width + 8.0, height: backgroundFrame.height)) + transition.setFrame(view: self.clearButton, frame: clearButtonFrame) + transition.setFrame(view: self.clearIconView, frame: clearFrame.offsetBy(dx: -clearButtonFrame.minX, dy: -clearButtonFrame.minY)) + } + if let textField = self.textField { var textFieldTransition = transition var animateIn = false @@ -255,7 +290,7 @@ public final class NavigationSearchComponent: Component { } let textLeftInset: CGFloat = fieldSideInset + searchIconSize.width + searchIconSpacing - let textRightInset: CGFloat = 8.0 + let textRightInset: CGFloat = 8.0 + 30.0 textFieldTransition.setFrame(view: textField, frame: CGRect(origin: CGPoint(x: placeholderTextFrame.minX, y: backgroundFrame.minY - 1.0), size: CGSize(width: backgroundFrame.width - textLeftInset - textRightInset, height: backgroundFrame.height))) if animateIn { diff --git a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsageScreen.swift b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsageScreen.swift index 796b5da509..b697b39f71 100644 --- a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsageScreen.swift +++ b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsageScreen.swift @@ -287,7 +287,7 @@ final class StorageUsageScreenComponent: Component { case .misc: return UIColor(rgb: 0xFF9500) case .stories: - return UIColor(rgb: 0x3478F6) + return UIColor(rgb: 0xFF2D55) } } diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift index 3a763b06f1..0f7af5404a 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift @@ -888,7 +888,10 @@ private final class StoryContainerScreenComponent: Component { } controller.forEachController { controller in if let controller = controller as? UndoOverlayController { - controller.dismissWithCommitAction() + if let tag = controller.tag as? String, tag == "no_auto_dismiss" { + } else { + controller.dismissWithCommitAction() + } } else if let controller = controller as? TooltipScreen { controller.dismiss() } diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift index 24a2ba52b3..f69f84adaf 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift @@ -348,6 +348,7 @@ public final class StoryItemSetContainerComponent: Component { weak var scrollView: UIScrollView? var startContentOffsetY: CGFloat = 0.0 var accumulatedOffset: CGFloat = 0.0 + var dismissedTooltips: Bool = false init(fraction: CGFloat, scrollView: UIScrollView?) { self.fraction = fraction @@ -998,6 +999,11 @@ public final class StoryItemSetContainerComponent: Component { } if let verticalPanState = self.verticalPanState { + if abs(verticalPanState.fraction) >= 0.1 && !verticalPanState.dismissedTooltips { + verticalPanState.dismissedTooltips = true + self.dismissAllTooltips() + } + if let scrollView = verticalPanState.scrollView { let relativeTranslationY = recognizer.translation(in: self).y - verticalPanState.startContentOffsetY let overflowY = scrollView.contentOffset.y - relativeTranslationY @@ -1061,6 +1067,7 @@ public final class StoryItemSetContainerComponent: Component { } else if verticalPanState.fraction >= 0.05 && velocity.y >= -80.0 { self.viewListDisplayState = .half } + self.dismissAllTooltips() } self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring))) @@ -1073,6 +1080,7 @@ public final class StoryItemSetContainerComponent: Component { if component.slice.peer.id == component.context.account.peerId { self.viewListDisplayState = self.targetViewListDisplayStateIsFull ? .full : .half self.state?.updated(transition: Transition(animation: .curve(duration: 0.3, curve: .spring))) + self.dismissAllTooltips() } else { self.state?.updated(transition: Transition(animation: .curve(duration: 0.3, curve: .spring))) @@ -1511,6 +1519,8 @@ public final class StoryItemSetContainerComponent: Component { self.preparingToDisplayViewList = false self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring))) + + self.dismissAllTooltips() } }, deleteAction: { [weak self] in @@ -1627,6 +1637,9 @@ public final class StoryItemSetContainerComponent: Component { if component.slice.peer.id == component.context.account.peerId { self.viewListDisplayState = .half self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring))) + + self.dismissAllTooltips() + return true } else { var canReply = true @@ -2127,9 +2140,6 @@ public final class StoryItemSetContainerComponent: Component { } func maybeDisplayReactionTooltip() { - if "".isEmpty { - return - } guard let component = self.component else { return } @@ -2207,7 +2217,10 @@ public final class StoryItemSetContainerComponent: Component { } if let tooltipScreen = self.sendMessageContext.tooltipScreen { - tooltipScreen.dismiss() + if let tooltipScreen = tooltipScreen as? UndoOverlayController, let tag = tooltipScreen.tag as? String, tag == "no_auto_dismiss" { + } else { + tooltipScreen.dismiss() + } } } var itemsTransition = transition @@ -2736,6 +2749,11 @@ public final class StoryItemSetContainerComponent: Component { var safeInsets = component.safeInsets safeInsets.bottom = max(safeInsets.bottom, component.inputHeight) + var hasPremium = false + if case let .user(user) = component.slice.peer { + hasPremium = user.isPremium + } + viewList.view.parentState = state let viewListSize = viewList.view.update( transition: viewListTransition.withUserData(PeerListItemComponent.TransitionHint( @@ -2751,6 +2769,7 @@ public final class StoryItemSetContainerComponent: Component { peerId: component.slice.peer.id, safeInsets: safeInsets, storyItem: item.storyItem, + hasPremium: hasPremium, effectiveHeight: viewListHeight, minHeight: midViewListHeight, availableReactions: component.availableReactions, @@ -3007,7 +3026,12 @@ public final class StoryItemSetContainerComponent: Component { } self.openPeerStories(peer: peer, avatarNode: avatarNode) }, - openPremiumIntro: {}, + openPremiumIntro: { [weak self] in + guard let self else { + return + } + self.presentStoriesUpgradeScreen() + }, setIsSearchActive: { [weak self] value in guard let self else { return @@ -4136,7 +4160,7 @@ public final class StoryItemSetContainerComponent: Component { component.externalState.derivedMediaSize = contentFrame.size if component.slice.peer.id == component.context.account.peerId { - component.externalState.derivedBottomInset = availableSize.height - contentFrame.maxY + component.externalState.derivedBottomInset = availableSize.height - itemsContainerFrame.maxY } else { component.externalState.derivedBottomInset = availableSize.height - min(inputPanelFrame.minY, contentFrame.maxY) } @@ -4674,6 +4698,15 @@ public final class StoryItemSetContainerComponent: Component { ), nil) } + private func presentStealthModeUpgradeScreen() { + self.sendMessageContext.presentStealthModeUpgrade(view: self, action: { [weak self] in + guard let self else { + return + } + self.presentStoriesUpgradeScreen() + }) + } + private func presentStoriesUpgradeScreen() { guard let component = self.component else { return @@ -5095,7 +5128,7 @@ public final class StoryItemSetContainerComponent: Component { if accountUser.isPremium { self.sendMessageContext.requestStealthMode(view: self) } else { - self.presentStoriesUpgradeScreen() + self.presentStealthModeUpgradeScreen() } }))) } @@ -5327,7 +5360,7 @@ public final class StoryItemSetContainerComponent: Component { if accountUser.isPremium { self.sendMessageContext.requestStealthMode(view: self) } else { - self.presentStoriesUpgradeScreen() + self.presentStealthModeUpgradeScreen() } }))) diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift index dd72cef086..212468c59e 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift @@ -3004,6 +3004,7 @@ final class StoryItemSetContainerSendMessage { return false } ) + tooltipScreen.tag = "no_auto_dismiss" weak var tooltipScreenValue: UndoOverlayController? = tooltipScreen self.currentTooltipUpdateTimer?.invalidate() self.currentTooltipUpdateTimer = Foundation.Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true, block: { [weak self, weak view] _ in @@ -3046,7 +3047,7 @@ final class StoryItemSetContainerSendMessage { let sheet = StoryStealthModeSheetScreen( context: component.context, - cooldownUntilTimestamp: config.stealthModeState.actualizedNow().cooldownUntilTimestamp, + mode: .control(cooldownUntilTimestamp: config.stealthModeState.actualizedNow().cooldownUntilTimestamp), backwardDuration: pastPeriod, forwardDuration: futurePeriod, buttonAction: { [weak self, weak view] in @@ -3095,6 +3096,52 @@ final class StoryItemSetContainerSendMessage { }) } + func presentStealthModeUpgrade(view: StoryItemSetContainerComponent.View, action: @escaping () -> Void) { + guard let component = view.component else { + return + } + + let _ = (component.context.engine.data.get( + TelegramEngine.EngineData.Item.Configuration.StoryConfigurationState(), + TelegramEngine.EngineData.Item.Configuration.App() + ) + |> deliverOnMainQueue).start(next: { [weak self, weak view] config, appConfig in + guard let self, let view, let component = view.component, let controller = component.controller() else { + return + } + + let pastPeriod: Int32 + let futurePeriod: Int32 + if let data = appConfig.data, let futurePeriodF = data["stories_stealth_future_period"] as? Double, let pastPeriodF = data["stories_stealth_past_period"] as? Double { + futurePeriod = Int32(futurePeriodF) + pastPeriod = Int32(pastPeriodF) + } else { + pastPeriod = 5 * 60 + futurePeriod = 25 * 60 + } + + let sheet = StoryStealthModeSheetScreen( + context: component.context, + mode: .upgrade, + backwardDuration: pastPeriod, + forwardDuration: futurePeriod, + buttonAction: { + action() + } + ) + sheet.wasDismissed = { [weak self, weak view] in + guard let self, let view else { + return + } + self.actionSheet = nil + view.updateIsProgressPaused() + } + self.actionSheet = sheet + view.updateIsProgressPaused() + controller.push(sheet) + }) + } + func activateMediaArea(view: StoryItemSetContainerComponent.View, mediaArea: MediaArea) { guard let component = view.component, let controller = component.controller() else { return diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetViewListComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetViewListComponent.swift index 8f20328d11..66da8e553f 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetViewListComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetViewListComponent.swift @@ -59,6 +59,7 @@ final class StoryItemSetViewListComponent: Component { let peerId: EnginePeer.Id let safeInsets: UIEdgeInsets let storyItem: EngineStoryItem + let hasPremium: Bool let effectiveHeight: CGFloat let minHeight: CGFloat let availableReactions: StoryAvailableReactions? @@ -82,6 +83,7 @@ final class StoryItemSetViewListComponent: Component { peerId: EnginePeer.Id, safeInsets: UIEdgeInsets, storyItem: EngineStoryItem, + hasPremium: Bool, effectiveHeight: CGFloat, minHeight: CGFloat, availableReactions: StoryAvailableReactions?, @@ -104,6 +106,7 @@ final class StoryItemSetViewListComponent: Component { self.peerId = peerId self.safeInsets = safeInsets self.storyItem = storyItem + self.hasPremium = hasPremium self.effectiveHeight = effectiveHeight self.minHeight = minHeight self.availableReactions = availableReactions @@ -136,6 +139,9 @@ final class StoryItemSetViewListComponent: Component { if lhs.storyItem != rhs.storyItem { return false } + if lhs.hasPremium != rhs.hasPremium { + return false + } if lhs.effectiveHeight != rhs.effectiveHeight { return false } @@ -248,6 +254,7 @@ final class StoryItemSetViewListComponent: Component { var emptyIcon: ComponentView? var emptyText: ComponentView? + var emptyButton: ComponentView? let scrollView: UIScrollView var itemLayout: ItemLayout? @@ -267,6 +274,8 @@ final class StoryItemSetViewListComponent: Component { var contentLoaded: Bool = false var contentLoadedUpdated: ((Bool) -> Void)? + var dismissInput: (() -> Void)? + init(configuration: ContentConfigurationKey) { self.configuration = configuration @@ -317,6 +326,8 @@ final class StoryItemSetViewListComponent: Component { func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { cancelContextGestures(view: scrollView) + + self.dismissInput?() } private func updateScrolling(transition: Transition) { @@ -759,6 +770,8 @@ final class StoryItemSetViewListComponent: Component { self.updateScrolling(transition: transition) if let viewListState = self.viewListState, viewListState.loadMoreToken == nil, viewListState.items.isEmpty, viewListState.totalCount == 0 { + self.scrollView.isUserInteractionEnabled = false + var emptyTransition = transition let emptyIcon: ComponentView @@ -778,6 +791,25 @@ final class StoryItemSetViewListComponent: Component { self.emptyText = emptyText } + var emptyButtonTransition = transition + let emptyButton: ComponentView? + if !component.hasPremium, let views = component.storyItem.views, views.seenCount != 0 { + if let current = self.emptyButton { + emptyButton = current + } else { + emptyButtonTransition = emptyButtonTransition.withAnimation(.none) + emptyButton = ComponentView() + self.emptyButton = emptyButton + } + } else { + if let emptyButton = self.emptyButton { + self.emptyButton = nil + emptyButton.view?.removeFromSuperview() + } + + emptyButton = nil + } + let emptyIconSize = emptyIcon.update( transition: emptyTransition, component: AnyComponent(LottieComponent( @@ -794,12 +826,17 @@ final class StoryItemSetViewListComponent: Component { let body = MarkdownAttributeSet(font: Font.regular(fontSize), textColor: component.theme.list.itemSecondaryTextColor) let bold = MarkdownAttributeSet(font: Font.semibold(fontSize), textColor: component.theme.list.itemSecondaryTextColor) let link = MarkdownAttributeSet(font: Font.semibold(fontSize), textColor: component.theme.list.itemAccentColor) - let attributes = MarkdownAttributes(body: body, bold: bold, link: link, linkAttribute: { _ in nil }) + let attributes = MarkdownAttributes(body: body, bold: bold, link: link, linkAttribute: { _ in return ("URL", "") }) let text: String if self.configuration.listMode == .everyone && (self.query == nil || self.query == "") { if component.storyItem.expirationTimestamp <= Int32(Date().timeIntervalSince1970) { - text = component.strings.Story_Views_ViewsExpired + if emptyButton == nil { + text = component.strings.Story_Views_ViewsExpired + } else { + //TODO:localize + text = "List of viewers isn't available after 24 hours of story expiration.\n\nTo unlock viewers' lists for expired and saved stories, subscribe to [Telegram Premium]()." + } } else { text = component.strings.Story_Views_NoViews } @@ -811,7 +848,12 @@ final class StoryItemSetViewListComponent: Component { text = "None of your contacts viewed this story." } else { if component.storyItem.expirationTimestamp <= Int32(Date().timeIntervalSince1970) { - text = component.strings.Story_Views_ViewsExpired + if emptyButton == nil { + text = component.strings.Story_Views_ViewsExpired + } else { + //TODO:localize + text = "List of viewers isn't available after 24 hours of story expiration.\n\nTo unlock viewers' lists for expired and saved stories, subscribe to [Telegram Premium]()." + } } else { text = component.strings.Story_Views_NoViews } @@ -822,15 +864,71 @@ final class StoryItemSetViewListComponent: Component { component: AnyComponent(BalancedTextComponent( text: .markdown(text: text, attributes: attributes), horizontalAlignment: .center, - maximumNumberOfLines: 0 + maximumNumberOfLines: 0, + highlightColor: component.theme.list.itemAccentColor.withMultipliedAlpha(0.5), + highlightAction: { attributes in + if let _ = attributes[NSAttributedString.Key(rawValue: "URL")] { + return NSAttributedString.Key(rawValue: "URL") + } else { + return nil + } + }, + tapAction: { [weak self] _, _ in + guard let self, let component = self.component else { + return + } + component.openPremiumIntro() + } )), environment: {}, - containerSize: CGSize(width: min(220.0, availableSize.width - 16.0 * 2.0), height: 1000.0) + containerSize: CGSize(width: min(320.0, availableSize.width - 16.0 * 2.0), height: 1000.0) ) let emptyContentSpacing: CGFloat = 20.0 - let emptyContentHeight = emptyIconSize.height + emptyContentSpacing + textSize.height - var emptyContentY = navigationMinY + floor((availableSize.height - navigationMinY - emptyContentHeight) * 0.5) + var emptyContentHeight = emptyIconSize.height + emptyContentSpacing + textSize.height + + var emptyButtonSize: CGSize? + if let emptyButton { + //TODO:localize + emptyButtonSize = emptyButton.update( + transition: emptyButtonTransition, + component: AnyComponent(ButtonComponent( + background: ButtonComponent.Background( + color: component.theme.list.itemCheckColors.fillColor, + foreground: component.theme.list.itemCheckColors.foregroundColor, + pressedColor: component.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9) + ), + content: AnyComponentWithIdentity( + id: AnyHashable(0), + component: AnyComponent(ButtonTextContentComponent( + text: "Learn More", + badge: 0, + textColor: component.theme.list.itemCheckColors.foregroundColor, + badgeBackground: component.theme.list.itemCheckColors.foregroundColor, + badgeForeground: component.theme.list.itemCheckColors.fillColor + )) + ), + isEnabled: true, + displaysProgress: false, + action: { [weak self] in + guard let self, let component = self.component else { + return + } + component.openPremiumIntro() + } + )), + environment: {}, + containerSize: CGSize(width: min(availableSize.width, 180.0), height: 50.0) + ) + } + + let emptyButtonSpacing: CGFloat = 32.0 + if let emptyButtonSize { + emptyContentHeight += emptyButtonSpacing + emptyContentHeight += emptyButtonSize.height + } + + var emptyContentY = navigationMinY + floor((availableSize.height - component.safeInsets.bottom - navigationMinY - emptyContentHeight) * 0.5) if let emptyIconView = emptyIcon.view as? LottieComponent.View { if emptyIconView.superview == nil { @@ -868,7 +966,17 @@ final class StoryItemSetViewListComponent: Component { emptyTransition.setFrame(view: emptyTextView, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - textSize.width) * 0.5), y: emptyContentY), size: textSize)) emptyContentY += textSize.height + emptyContentSpacing * 2.0 } + + if let emptyButtonSize, let emptyButton, let emptyButtonView = emptyButton.view { + if emptyButtonView.superview == nil { + self.insertSubview(emptyButtonView, belowSubview: self.scrollView) + } + emptyTransition.setFrame(view: emptyButtonView, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - emptyButtonSize.width) * 0.5), y: emptyContentY), size: emptyButtonSize)) + emptyContentY += emptyButtonSize.height + emptyButtonSpacing + } } else { + self.scrollView.isUserInteractionEnabled = true + if let emptyIcon = self.emptyIcon { self.emptyIcon = nil emptyIcon.view?.removeFromSuperview() @@ -877,6 +985,10 @@ final class StoryItemSetViewListComponent: Component { self.emptyText = nil emptyText.view?.removeFromSuperview() } + if let emptyButton = self.emptyButton { + self.emptyButton = nil + emptyButton.view?.removeFromSuperview() + } } } } @@ -1084,9 +1196,22 @@ final class StoryItemSetViewListComponent: Component { environment: {}, containerSize: CGSize(width: availableSize.width - 10.0 * 2.0, height: 50.0) ) + + //TODO:localize + let titleText: String + if let views = component.storyItem.views, views.seenCount != 0 { + if component.storyItem.expirationTimestamp <= Int32(Date().timeIntervalSince1970) { + titleText = component.strings.Story_Footer_Views(Int32(views.seenCount)) + } else { + titleText = "All Viewers" + } + } else { + titleText = "No Views" + } + let titleSize = self.title.update( transition: .immediate, - component: AnyComponent(Text(text: "All Viewers", font: Font.semibold(17.0), color: .white)), + component: AnyComponent(Text(text: titleText, font: Font.semibold(17.0), color: .white)), environment: {}, containerSize: CGSize(width: 260.0, height: 100.0) ) @@ -1265,6 +1390,9 @@ final class StoryItemSetViewListComponent: Component { navigationHeight: navigationHeight, transition: contentViewTransition ) + if currentContentView.contentLoaded { + currentContentView.isHidden = false + } } if !self.currentSearchQuery.isEmpty { @@ -1275,6 +1403,12 @@ final class StoryItemSetViewListComponent: Component { currentSearchContentView = ContentView(configuration: currentConfiguration) self.currentSearchContentView = currentSearchContentView currentSearchContentView.isHidden = true + currentSearchContentView.dismissInput = { [weak self] in + guard let self else { + return + } + self.navigationSearch.view?.endEditing(true) + } } var contentViewTransition = transition diff --git a/submodules/TelegramUI/Components/Stories/StoryFooterPanelComponent/Sources/StoryFooterPanelComponent.swift b/submodules/TelegramUI/Components/Stories/StoryFooterPanelComponent/Sources/StoryFooterPanelComponent.swift index 82b4b0663f..6241d22dc9 100644 --- a/submodules/TelegramUI/Components/Stories/StoryFooterPanelComponent/Sources/StoryFooterPanelComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryFooterPanelComponent/Sources/StoryFooterPanelComponent.swift @@ -284,8 +284,6 @@ public final class StoryFooterPanelComponent: Component { avatarsAlpha = pow(1.0 - component.expandFraction, 1.0) baseViewCountAlpha = 1.0 } - - //TODO:upload let _ = baseViewCountAlpha var peers: [EnginePeer] = [] @@ -306,7 +304,9 @@ public final class StoryFooterPanelComponent: Component { //TODO:localize var regularSegments: [AnimatedCountLabelView.Segment] = [] - regularSegments.append(.number(viewCount, NSAttributedString(string: "\(viewCount)", font: Font.regular(15.0), textColor: .white))) + if viewCount != 0 { + regularSegments.append(.number(viewCount, NSAttributedString(string: "\(viewCount)", font: Font.regular(15.0), textColor: .white))) + } let viewPart: String if viewCount == 0 { @@ -399,11 +399,13 @@ public final class StoryFooterPanelComponent: Component { contentWidth += (avatarsSize.width + avatarViewsSpacing) * (1.0 - component.expandFraction) if let image = self.viewsIconView.image { - contentWidth += (image.size.width + viewsIconSpacing) * component.expandFraction + if viewCount != 0 { + contentWidth += (image.size.width + viewsIconSpacing) * component.expandFraction + } } if viewCount == 0 { - contentWidth += viewStatsTextLayout.size.width * component.expandFraction + contentWidth += viewStatsTextLayout.size.width * (1.0 - component.expandFraction) } else { contentWidth += viewStatsTextLayout.size.width } @@ -430,7 +432,11 @@ public final class StoryFooterPanelComponent: Component { let viewsIconFrame = CGRect(origin: CGPoint(x: contentX, y: floor((size.height - image.size.height) * 0.5)), size: image.size) transition.setPosition(view: self.viewsIconView, position: viewsIconFrame.center) transition.setBounds(view: self.viewsIconView, bounds: CGRect(origin: CGPoint(), size: viewsIconFrame.size)) - transition.setAlpha(view: self.viewsIconView, alpha: component.expandFraction) + if viewCount == 0 { + transition.setAlpha(view: self.viewsIconView, alpha: 0.0) + } else { + transition.setAlpha(view: self.viewsIconView, alpha: component.expandFraction) + } transition.setScale(view: self.viewsIconView, scale: CGFloat(1.0).interpolate(to: CGFloat(0.1), amount: 1.0 - component.expandFraction)) } diff --git a/submodules/TelegramUI/Components/Stories/StoryStealthModeSheetScreen/Sources/StoryStealthModeInfoContentComponent.swift b/submodules/TelegramUI/Components/Stories/StoryStealthModeSheetScreen/Sources/StoryStealthModeInfoContentComponent.swift index 42c38914a4..d3fb424117 100644 --- a/submodules/TelegramUI/Components/Stories/StoryStealthModeSheetScreen/Sources/StoryStealthModeInfoContentComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryStealthModeSheetScreen/Sources/StoryStealthModeInfoContentComponent.swift @@ -15,17 +15,23 @@ public final class StoryStealthModeInfoContentComponent: Component { public let strings: PresentationStrings public let backwardDuration: Int32 public let forwardDuration: Int32 + public let mode: StoryStealthModeSheetScreen.Mode + public let dismiss: () -> Void public init( theme: PresentationTheme, strings: PresentationStrings, backwardDuration: Int32, - forwardDuration: Int32 + forwardDuration: Int32, + mode: StoryStealthModeSheetScreen.Mode, + dismiss: @escaping () -> Void ) { self.theme = theme self.strings = strings self.backwardDuration = backwardDuration self.forwardDuration = forwardDuration + self.mode = mode + self.dismiss = dismiss } public static func ==(lhs: StoryStealthModeInfoContentComponent, rhs: StoryStealthModeInfoContentComponent) -> Bool { @@ -41,6 +47,9 @@ public final class StoryStealthModeInfoContentComponent: Component { if lhs.forwardDuration != rhs.forwardDuration { return false } + if lhs.mode != rhs.mode { + return false + } return true } @@ -155,7 +164,13 @@ public final class StoryStealthModeInfoContentComponent: Component { contentHeight += 15.0 //TODO:localize - let text: String = "Turn Stealth Mode on to hide the fact that you viewed peoples' stories from them." + let text: String + switch component.mode { + case .control: + text = "Turn Stealth Mode on to hide the fact that you viewed peoples' stories from them." + case .upgrade: + text = "Subscribe to Telegram Premium to hide the fact that you viewed peoples' stories from them." + } let mainText = NSMutableAttributedString() mainText.append(parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes( body: MarkdownAttributeSet( diff --git a/submodules/TelegramUI/Components/Stories/StoryStealthModeSheetScreen/Sources/StoryStealthModeSheetScreen.swift b/submodules/TelegramUI/Components/Stories/StoryStealthModeSheetScreen/Sources/StoryStealthModeSheetScreen.swift index 149db5ebdd..24314533f3 100644 --- a/submodules/TelegramUI/Components/Stories/StoryStealthModeSheetScreen/Sources/StoryStealthModeSheetScreen.swift +++ b/submodules/TelegramUI/Components/Stories/StoryStealthModeSheetScreen/Sources/StoryStealthModeSheetScreen.swift @@ -15,25 +15,28 @@ import TelegramStringFormatting private final class StoryStealthModeSheetContentComponent: Component { typealias EnvironmentType = ViewControllerComponentContainer.Environment - let cooldownUntilTimestamp: Int32? + let mode: StoryStealthModeSheetScreen.Mode let backwardDuration: Int32 let forwardDuration: Int32 + let action: () -> Void let dismiss: () -> Void init( - cooldownUntilTimestamp: Int32?, + mode: StoryStealthModeSheetScreen.Mode, backwardDuration: Int32, forwardDuration: Int32, + action: @escaping () -> Void, dismiss: @escaping () -> Void ) { - self.cooldownUntilTimestamp = cooldownUntilTimestamp + self.mode = mode self.backwardDuration = backwardDuration self.forwardDuration = forwardDuration + self.action = action self.dismiss = dismiss } static func ==(lhs: StoryStealthModeSheetContentComponent, rhs: StoryStealthModeSheetContentComponent) -> Bool { - if lhs.cooldownUntilTimestamp != rhs.cooldownUntilTimestamp { + if lhs.mode != rhs.mode { return false } if lhs.backwardDuration != rhs.backwardDuration { @@ -50,6 +53,8 @@ private final class StoryStealthModeSheetContentComponent: Component { private let content = ComponentView() private let button = ComponentView() + private var cancelButton: ComponentView? + private var component: StoryStealthModeSheetContentComponent? private weak var state: EmptyComponentState? @@ -95,10 +100,13 @@ private final class StoryStealthModeSheetContentComponent: Component { self.state = state var remainingCooldownSeconds: Int32 = 0 - if let cooldownUntilTimestamp = component.cooldownUntilTimestamp { - remainingCooldownSeconds = cooldownUntilTimestamp - Int32(Date().timeIntervalSince1970) - remainingCooldownSeconds = max(0, remainingCooldownSeconds) + if case let .control(cooldownUntilTimestamp) = component.mode { + if let cooldownUntilTimestamp { + remainingCooldownSeconds = cooldownUntilTimestamp - Int32(Date().timeIntervalSince1970) + remainingCooldownSeconds = max(0, remainingCooldownSeconds) + } } + if remainingCooldownSeconds > 0 { if self.timer == nil { self.timer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true, block: { [weak self] _ in @@ -170,6 +178,39 @@ private final class StoryStealthModeSheetContentComponent: Component { } } + if case .upgrade = component.mode { + let cancelButton: ComponentView + if let current = self.cancelButton { + cancelButton = current + } else { + cancelButton = ComponentView() + self.cancelButton = cancelButton + } + let cancelButtonSize = cancelButton.update( + transition: transition, + component: AnyComponent(Button( + content: AnyComponent(Text(text: environment.strings.Common_Cancel, font: Font.regular(17.0), color: environment.theme.list.itemAccentColor)), + action: { [weak self] in + guard let self, let component = self.component else { + return + } + component.dismiss() + } + ).minSize(CGSize(width: 8.0, height: 44.0))), + environment: {}, + containerSize: CGSize(width: 200.0, height: 100.0) + ) + if let cancelButtonView = cancelButton.view { + if cancelButtonView.superview == nil { + self.addSubview(cancelButtonView) + } + transition.setFrame(view: cancelButtonView, frame: CGRect(origin: CGPoint(x: 16.0, y: 6.0), size: cancelButtonSize)) + } + } else if let cancelButton = self.cancelButton { + self.cancelButton = nil + cancelButton.view?.removeFromSuperview() + } + var contentHeight: CGFloat = 0.0 contentHeight += 32.0 @@ -179,7 +220,14 @@ private final class StoryStealthModeSheetContentComponent: Component { theme: environment.theme, strings: environment.strings, backwardDuration: component.backwardDuration, - forwardDuration: component.forwardDuration + forwardDuration: component.forwardDuration, + mode: component.mode, + dismiss: { [weak self] in + guard let self, let component = self.component else { + return + } + component.dismiss() + } )), environment: {}, containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: availableSize.height) @@ -195,10 +243,29 @@ private final class StoryStealthModeSheetContentComponent: Component { //TODO:localize let buttonText: String - if remainingCooldownSeconds <= 0 { - buttonText = "Enable Stealth Mode" - } else { - buttonText = "Available in \(stringForDuration(remainingCooldownSeconds))" + let content: AnyComponentWithIdentity + switch component.mode { + case .control: + if remainingCooldownSeconds <= 0 { + buttonText = "Enable Stealth Mode" + } else { + buttonText = "Available in \(stringForDuration(remainingCooldownSeconds))" + } + content = AnyComponentWithIdentity(id: AnyHashable(0 as Int), component: AnyComponent(Text(text: buttonText, font: Font.semibold(17.0), color: environment.theme.list.itemCheckColors.foregroundColor))) + case .upgrade: + buttonText = "Unlock Stealth Mode" + content = AnyComponentWithIdentity(id: AnyHashable(1 as Int), component: AnyComponent( + HStack([ + AnyComponentWithIdentity(id: AnyHashable(1 as Int), component: AnyComponent(Text(text: buttonText, font: Font.semibold(17.0), color: environment.theme.list.itemCheckColors.foregroundColor))), + AnyComponentWithIdentity(id: AnyHashable(0 as Int), component: AnyComponent(LottieComponent( + content: LottieComponent.AppBundleContent(name: "premium_unlock"), + color: environment.theme.list.itemCheckColors.foregroundColor, + startingPosition: .begin, + size: CGSize(width: 30.0, height: 30.0), + loop: true + ))) + ], spacing: 4.0) + )) } let buttonSize = self.button.update( transition: transition, @@ -208,9 +275,7 @@ private final class StoryStealthModeSheetContentComponent: Component { foreground: environment.theme.list.itemCheckColors.foregroundColor, pressedColor: environment.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.8) ), - content: AnyComponentWithIdentity(id: AnyHashable(0 as Int), component: AnyComponent( - Text(text: buttonText, font: Font.semibold(17.0), color: environment.theme.list.itemCheckColors.foregroundColor) - )), + content: content, isEnabled: remainingCooldownSeconds <= 0, allowActionWhenDisabled: true, displaysProgress: false, @@ -219,16 +284,21 @@ private final class StoryStealthModeSheetContentComponent: Component { return } - var remainingCooldownSeconds: Int32 = 0 - if let cooldownUntilTimestamp = component.cooldownUntilTimestamp { - remainingCooldownSeconds = cooldownUntilTimestamp - Int32(Date().timeIntervalSince1970) - remainingCooldownSeconds = max(0, remainingCooldownSeconds) - } - - if remainingCooldownSeconds > 0 { - self.displayCooldown() - } else { - component.dismiss() + switch component.mode { + case let .control(cooldownUntilTimestamp): + var remainingCooldownSeconds: Int32 = 0 + if let cooldownUntilTimestamp { + remainingCooldownSeconds = cooldownUntilTimestamp - Int32(Date().timeIntervalSince1970) + remainingCooldownSeconds = max(0, remainingCooldownSeconds) + } + + if remainingCooldownSeconds > 0 { + self.displayCooldown() + } else { + component.action() + } + case .upgrade: + component.action() } } )), @@ -267,20 +337,20 @@ private final class StoryStealthModeSheetScreenComponent: Component { typealias EnvironmentType = ViewControllerComponentContainer.Environment let context: AccountContext - let cooldownUntilTimestamp: Int32? + let mode: StoryStealthModeSheetScreen.Mode let backwardDuration: Int32 let forwardDuration: Int32 let buttonAction: (() -> Void)? init( context: AccountContext, - cooldownUntilTimestamp: Int32?, + mode: StoryStealthModeSheetScreen.Mode, backwardDuration: Int32, forwardDuration: Int32, buttonAction: (() -> Void)? ) { self.context = context - self.cooldownUntilTimestamp = cooldownUntilTimestamp + self.mode = mode self.backwardDuration = backwardDuration self.forwardDuration = forwardDuration self.buttonAction = buttonAction @@ -290,7 +360,7 @@ private final class StoryStealthModeSheetScreenComponent: Component { if lhs.context !== rhs.context { return false } - if lhs.cooldownUntilTimestamp != rhs.cooldownUntilTimestamp { + if lhs.mode != rhs.mode { return false } if lhs.backwardDuration != rhs.backwardDuration { @@ -343,10 +413,10 @@ private final class StoryStealthModeSheetScreenComponent: Component { transition: transition, component: AnyComponent(SheetComponent( content: AnyComponent(StoryStealthModeSheetContentComponent( - cooldownUntilTimestamp: component.cooldownUntilTimestamp, + mode: component.mode, backwardDuration: component.backwardDuration, forwardDuration: component.forwardDuration, - dismiss: { [weak self] in + action: { [weak self] in guard let self else { return } @@ -360,9 +430,16 @@ private final class StoryStealthModeSheetScreenComponent: Component { } self.component?.buttonAction?() }) + }, + dismiss: { + self.sheetAnimateOut.invoke(Action { _ in + if let controller = environment.controller() { + controller.dismiss(completion: nil) + } + }) } )), - backgroundColor: .color(environment.theme.list.plainBackgroundColor), + backgroundColor: .color(environment.theme.overallDarkAppearance ? environment.theme.list.itemBlocksBackgroundColor : environment.theme.list.blocksBackgroundColor), animateOut: self.sheetAnimateOut )), environment: { @@ -392,16 +469,21 @@ private final class StoryStealthModeSheetScreenComponent: Component { } public class StoryStealthModeSheetScreen: ViewControllerComponentContainer { + public enum Mode: Equatable { + case control(cooldownUntilTimestamp: Int32?) + case upgrade + } + public init( context: AccountContext, - cooldownUntilTimestamp: Int32?, + mode: Mode, backwardDuration: Int32, forwardDuration: Int32, buttonAction: (() -> Void)? = nil ) { super.init(context: context, component: StoryStealthModeSheetScreenComponent( context: context, - cooldownUntilTimestamp: cooldownUntilTimestamp, + mode: mode, backwardDuration: backwardDuration, forwardDuration: forwardDuration, buttonAction: buttonAction diff --git a/submodules/UndoUI/Sources/UndoOverlayController.swift b/submodules/UndoUI/Sources/UndoOverlayController.swift index 921ce3ad37..11f68ec1fb 100644 --- a/submodules/UndoUI/Sources/UndoOverlayController.swift +++ b/submodules/UndoUI/Sources/UndoOverlayController.swift @@ -75,6 +75,8 @@ public final class UndoOverlayController: ViewController { public var keepOnParentDismissal = false + public var tag: Any? + public init(presentationData: PresentationData, content: UndoOverlayContent, elevatedLayout: Bool, position: Position = .bottom, animateInAsReplacement: Bool = false, blurred: Bool = false, action: @escaping (UndoOverlayAction) -> Bool) { self.presentationData = presentationData self.content = content diff --git a/versions.json b/versions.json index f55f61b95c..1e00516333 100644 --- a/versions.json +++ b/versions.json @@ -1,5 +1,5 @@ { - "app": "9.6.7", + "app": "10.0.0", "bazel": "6.1.1", "xcode": "14.2" }