chore: migrate to new version + fixed several critical bugs

- Migrated project to latest Telegram iOS base (v12.3.2+)
- Fixed circular dependency between GhostModeManager and MiscSettingsManager
- Fixed multiple Bazel build configuration errors (select() default conditions)
- Fixed duplicate type definitions in PeerInfoScreen
- Fixed swiftmodule directory resolution in build scripts
- Added Ghostgram Settings tab in main Settings menu with all 5 features
- Cleared sensitive credentials from config.json (template-only now)
- Excluded bazel-cache from version control
This commit is contained in:
ichmagmaus 812
2026-02-23 23:04:32 +01:00
parent 703e291bcb
commit db53826061
1017 changed files with 62337 additions and 40559 deletions
@@ -22,6 +22,8 @@ swift_library(
"//submodules/UIKitRuntimeUtils",
"//submodules/TelegramUI/Components/LiquidLens",
"//submodules/AppBundle",
"//submodules/SearchBarNode",
"//submodules/TelegramUI/Components/TabSelectionRecognizer",
],
visibility = [
"//visibility:public",
@@ -12,92 +12,236 @@ import BundleIconComponent
import TextBadgeComponent
import LiquidLens
import AppBundle
import SearchBarNode
import TabSelectionRecognizer
private final class TabSelectionRecognizer: UIGestureRecognizer {
private var initialLocation: CGPoint?
private var currentLocation: CGPoint?
override init(target: Any?, action: Selector?) {
super.init(target: target, action: action)
self.delaysTouchesBegan = false
self.delaysTouchesEnded = false
}
override func reset() {
super.reset()
self.initialLocation = nil
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesBegan(touches, with: event)
if self.initialLocation == nil {
self.initialLocation = touches.first?.location(in: self.view)
}
self.currentLocation = self.initialLocation
self.state = .began
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesEnded(touches, with: event)
self.state = .ended
}
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesCancelled(touches, with: event)
self.state = .cancelled
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesMoved(touches, with: event)
self.currentLocation = touches.first?.location(in: self.view)
self.state = .changed
}
func translation(in: UIView?) -> CGPoint {
if let initialLocation = self.initialLocation, let currentLocation = self.currentLocation {
return CGPoint(x: currentLocation.x - initialLocation.x, y: currentLocation.y - initialLocation.y)
}
return CGPoint()
}
}
public final class NavigationSearchView: UIView {
private struct Params: Equatable {
let size: CGSize
let theme: PresentationTheme
let strings: PresentationStrings
let isActive: Bool
init(size: CGSize, theme: PresentationTheme, strings: PresentationStrings, isActive: Bool) {
self.size = size
self.theme = theme
self.strings = strings
self.isActive = isActive
}
static func ==(lhs: Params, rhs: Params) -> Bool {
if lhs.size != rhs.size {
return false
}
if lhs.theme !== rhs.theme {
return false
}
if lhs.strings !== rhs.strings {
return false
}
if lhs.isActive != rhs.isActive {
return false
}
return true
}
}
private let action: () -> Void
private let closeAction: () -> Void
public final class TabBarSearchView: UIView {
private let backgroundView: GlassBackgroundView
private let iconView: GlassBackgroundView.ContentImageView
private let iconView: UIImageView
private(set) var searchBarNode: SearchBarNode?
override public init(frame: CGRect) {
self.backgroundView = GlassBackgroundView()
self.iconView = GlassBackgroundView.ContentImageView()
private var close: (background: GlassBackgroundView, icon: UIImageView)?
super.init(frame: frame)
private var params: Params?
public init(action: @escaping () -> Void, closeAction: @escaping () -> Void) {
self.action = action
self.closeAction = closeAction
self.backgroundView = GlassBackgroundView()
self.backgroundView.contentView.clipsToBounds = true
self.iconView = UIImageView()
super.init(frame: CGRect())
self.addSubview(self.backgroundView)
self.backgroundView.contentView.addSubview(self.iconView)
self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.onTapGesture(_:))))
}
@objc private func onTapGesture(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state {
self.action()
}
}
@objc private func onCloseTapGesture(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state {
self.closeAction()
}
}
required public init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public func update(size: CGSize, isDark: Bool, tintColor: GlassBackgroundView.TintColor, iconColor: UIColor, transition: ComponentTransition) {
transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(), size: size))
self.backgroundView.update(size: size, cornerRadius: size.height * 0.5, isDark: isDark, tintColor: tintColor, transition: transition)
public func update(size: CGSize, theme: PresentationTheme, strings: PresentationStrings, isActive: Bool, transition: ComponentTransition) {
let params = Params(size: size, theme: theme, strings: strings, isActive: isActive)
if self.params == params {
return
}
self.params = params
self.update(params: params, transition: transition)
}
private func update(params: Params, transition: ComponentTransition) {
let backgroundSize: CGSize
if params.isActive {
backgroundSize = CGSize(width: params.size.width - 48.0 - 8.0, height: params.size.height)
} else {
backgroundSize = CGSize(width: params.size.width, height: params.size.height)
}
let previousBackgroundFrame = self.backgroundView.frame
transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(), size: backgroundSize))
let alphaTransition: ComponentTransition = transition.animation.isImmediate ? .immediate : .easeInOut(duration: 0.25)
self.backgroundView.update(size: backgroundSize, cornerRadius: backgroundSize.height * 0.5, isDark: params.theme.overallDarkAppearance, tintColor: .init(kind: .panel, color: UIColor(white: params.theme.overallDarkAppearance ? 0.0 : 1.0, alpha: 0.6)), isInteractive: true, transition: transition)
if self.iconView.image == nil {
self.iconView.image = UIImage(bundleImageName: "Navigation/Search")?.withRenderingMode(.alwaysTemplate)
}
self.iconView.tintColor = iconColor
transition.setTintColor(view: self.iconView, color: params.isActive ? params.theme.rootController.navigationSearchBar.inputIconColor : params.theme.chat.inputPanel.panelControlColor)
if let image = self.iconView.image {
transition.setFrame(view: self.iconView, frame: CGRect(origin: CGPoint(x: floor((size.width - image.size.width) * 0.5), y: floor((size.height - image.size.height) * 0.5)), size: image.size))
let imageSize: CGSize
let iconFrame: CGRect
if params.isActive {
let iconFraction: CGFloat = 0.8
imageSize = CGSize(width: image.size.width * iconFraction, height: image.size.height * iconFraction)
iconFrame = CGRect(origin: CGPoint(x: 12.0, y: floor((params.size.height - imageSize.height) * 0.5)), size: imageSize)
} else {
iconFrame = CGRect(origin: CGPoint(x: floor((backgroundSize.width - image.size.width) * 0.5), y: floor((params.size.height - image.size.height) * 0.5)), size: image.size)
}
transition.setPosition(view: self.iconView, position: iconFrame.center)
transition.setBounds(view: self.iconView, bounds: CGRect(origin: CGPoint(), size: iconFrame.size))
}
if params.isActive {
let searchBarNode: SearchBarNode
var searchBarNodeTransition = transition
if let current = self.searchBarNode {
searchBarNode = current
} else {
searchBarNodeTransition = searchBarNodeTransition.withAnimation(.none)
searchBarNode = SearchBarNode(
theme: SearchBarNodeTheme(
background: .clear,
separator: .clear,
inputFill: .clear,
primaryText: params.theme.chat.inputPanel.panelControlColor,
placeholder: params.theme.chat.inputPanel.inputPlaceholderColor,
inputIcon: params.theme.chat.inputPanel.inputControlColor,
inputClear: params.theme.chat.inputPanel.panelControlColor,
accent: params.theme.chat.inputPanel.panelControlAccentColor,
keyboard: params.theme.rootController.keyboardColor
),
presentationTheme: params.theme,
strings: params.strings,
fieldStyle: .inlineNavigation,
icon: .loupe,
forceSeparator: false,
displayBackground: false,
cancelText: nil
)
searchBarNode.placeholderString = NSAttributedString(string: params.strings.Common_Search, font: Font.regular(17.0), textColor: params.theme.chat.inputPanel.inputPlaceholderColor)
self.searchBarNode = searchBarNode
self.backgroundView.contentView.addSubview(searchBarNode.view)
searchBarNode.view.alpha = 0.0
}
let searchBarFrame = CGRect(origin: CGPoint(x: 36.0, y: 0.0), size: CGSize(width: backgroundSize.width - 36.0 - 4.0, height: params.size.height))
transition.setFrame(view: searchBarNode.view, frame: searchBarFrame)
searchBarNode.updateLayout(boundingSize: searchBarFrame.size, leftInset: 0.0, rightInset: 0.0, transition: transition.containedViewLayoutTransition)
alphaTransition.setAlpha(view: searchBarNode.view, alpha: 1.0)
} else {
if let searchBarNode = self.searchBarNode {
self.searchBarNode = nil
let searchBarNodeView = searchBarNode.view
alphaTransition.setAlpha(view: searchBarNode.view, alpha: 0.0, completion: { [weak searchBarNodeView] _ in
searchBarNodeView?.removeFromSuperview()
})
}
}
if params.isActive {
let closeFrame = CGRect(origin: CGPoint(x: params.size.width - 48.0, y: 0.0), size: CGSize(width: 48.0, height: 48.0))
let close: (background: GlassBackgroundView, icon: UIImageView)
var closeTransition = transition
if let current = self.close {
close = current
} else {
closeTransition = closeTransition.withAnimation(.none)
close = (GlassBackgroundView(), UIImageView())
self.close = close
close.icon.image = generateImage(CGSize(width: 40.0, height: 40.0), contextGenerator: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setLineWidth(2.0)
context.setLineCap(.round)
context.setStrokeColor(UIColor.white.cgColor)
context.beginPath()
context.move(to: CGPoint(x: 12.0, y: 12.0))
context.addLine(to: CGPoint(x: size.width - 12.0, y: size.height - 12.0))
context.move(to: CGPoint(x: size.width - 12.0, y: 12.0))
context.addLine(to: CGPoint(x: 12.0, y: size.height - 12.0))
context.strokePath()
})?.withRenderingMode(.alwaysTemplate)
close.background.contentView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.onCloseTapGesture(_:))))
close.background.contentView.addSubview(close.icon)
self.insertSubview(close.background, at: 0)
if let image = close.icon.image {
close.icon.frame = image.size.centered(in: CGRect(origin: CGPoint(), size: closeFrame.size))
}
close.background.frame = closeFrame.size.centered(in: previousBackgroundFrame)
close.background.update(size: close.background.bounds.size, cornerRadius: close.background.bounds.height * 0.5, isDark: params.theme.overallDarkAppearance, tintColor: .init(kind: .panel, color: UIColor(white: params.theme.overallDarkAppearance ? 0.0 : 1.0, alpha: 0.6)), isInteractive: true, transition: .immediate)
ComponentTransition.immediate.setScale(view: close.background, scale: 0.001)
}
close.icon.tintColor = params.theme.chat.inputPanel.panelControlColor
transition.setPosition(view: close.background, position: closeFrame.center)
transition.setBounds(view: close.background, bounds: CGRect(origin: CGPoint(), size: closeFrame.size))
transition.setScale(view: close.background, scale: 1.0)
if let image = close.icon.image {
transition.setFrame(view: close.icon, frame: image.size.centered(in: CGRect(origin: CGPoint(), size: closeFrame.size)))
}
close.background.update(size: closeFrame.size, cornerRadius: closeFrame.height * 0.5, isDark: params.theme.overallDarkAppearance, tintColor: .init(kind: .panel, color: UIColor(white: params.theme.overallDarkAppearance ? 0.0 : 1.0, alpha: 0.6)), isInteractive: true, transition: closeTransition)
} else {
if let close = self.close {
self.close = nil
let closeBackground = close.background
let closeFrame = CGSize(width: 48.0, height: 48.0).centered(in: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: params.size))
transition.setPosition(view: closeBackground, position: closeFrame.center)
transition.setBounds(view: closeBackground, bounds: CGRect(origin: CGPoint(), size: closeFrame.size))
closeBackground.update(size: closeFrame.size, cornerRadius: closeFrame.height * 0.5, isDark: params.theme.overallDarkAppearance, tintColor: .init(kind: .panel, color: UIColor(white: params.theme.overallDarkAppearance ? 0.0 : 1.0, alpha: 0.6)), isInteractive: true, transition: transition)
transition.setScale(view: closeBackground, scale: 0.001, completion: { [weak closeBackground] _ in
closeBackground?.removeFromSuperview()
})
}
}
}
}
@@ -131,46 +275,81 @@ public final class TabBarComponent: Component {
return true
}
}
public final class Search: Equatable {
public let isActive: Bool
public let activate: () -> Void
public let deactivate: () -> Void
public init(isActive: Bool, activate: @escaping () -> Void, deactivate: @escaping () -> Void) {
self.isActive = isActive
self.activate = activate
self.deactivate = deactivate
}
public static func ==(lhs: Search, rhs: Search) -> Bool {
if lhs.isActive != rhs.isActive {
return false
}
return true
}
}
public let theme: PresentationTheme
public let strings: PresentationStrings
public let items: [Item]
public let search: Search?
public let selectedId: AnyHashable?
public let isTablet: Bool
public let outerInsets: UIEdgeInsets
public init(
theme: PresentationTheme,
strings: PresentationStrings,
items: [Item],
search: Search?,
selectedId: AnyHashable?,
isTablet: Bool
outerInsets: UIEdgeInsets
) {
self.theme = theme
self.strings = strings
self.items = items
self.search = search
self.selectedId = selectedId
self.isTablet = isTablet
self.outerInsets = outerInsets
}
public static func ==(lhs: TabBarComponent, rhs: TabBarComponent) -> Bool {
if lhs.theme !== rhs.theme {
return false
}
if lhs.strings !== rhs.strings {
return false
}
if lhs.items != rhs.items {
return false
}
if lhs.search != rhs.search {
return false
}
if lhs.selectedId != rhs.selectedId {
return false
}
if lhs.isTablet != rhs.isTablet {
if lhs.outerInsets != rhs.outerInsets {
return false
}
return true
}
public final class View: UIView, UITabBarDelegate, UIGestureRecognizerDelegate {
private let backgroundContainer: GlassBackgroundContainerView
private let liquidLensView: LiquidLensView
private let contextGestureContainerView: ContextControllerSourceView
private var measureItemViews: [AnyHashable: ComponentView<Empty>] = [:]
private var itemViews: [AnyHashable: ComponentView<Empty>] = [:]
private var selectedItemViews: [AnyHashable: ComponentView<Empty>] = [:]
private var searchView: NavigationSearchView?
private var tabSelectionRecognizer: TabSelectionRecognizer?
private var itemWithActiveContextGesture: AnyHashable?
@@ -178,11 +357,16 @@ public final class TabBarComponent: Component {
private var component: TabBarComponent?
private weak var state: EmptyComponentState?
private var selectionGestureState: (startX: CGFloat, currentX: CGFloat)?
private var selectionGestureState: (startX: CGFloat, currentX: CGFloat, itemWidth: CGFloat, itemId: AnyHashable)?
private var overrideSelectedItemId: AnyHashable?
public var currentSearchNode: ASDisplayNode? {
return self.searchView?.searchBarNode
}
public override init(frame: CGRect) {
self.liquidLensView = LiquidLensView()
self.backgroundContainer = GlassBackgroundContainerView()
self.liquidLensView = LiquidLensView(kind: .externalContainer)
self.contextGestureContainerView = ContextControllerSourceView()
self.contextGestureContainerView.isGestureEnabled = true
@@ -194,12 +378,13 @@ public final class TabBarComponent: Component {
self.traitOverrides.horizontalSizeClass = .compact
}
self.addSubview(self.contextGestureContainerView)
self.addSubview(self.backgroundContainer)
self.backgroundContainer.contentView.addSubview(self.contextGestureContainerView)
self.contextGestureContainerView.addSubview(self.liquidLensView)
let tabSelectionRecognizer = TabSelectionRecognizer(target: self, action: #selector(self.onTabSelectionGesture(_:)))
self.tabSelectionRecognizer = tabSelectionRecognizer
self.addGestureRecognizer(tabSelectionRecognizer)
self.contextGestureContainerView.addGestureRecognizer(tabSelectionRecognizer)
self.contextGestureContainerView.shouldBegin = { [weak self] point in
guard let self, let component = self.component else {
@@ -281,29 +466,41 @@ public final class TabBarComponent: Component {
}
@objc private func onTabSelectionGesture(_ recognizer: TabSelectionRecognizer) {
guard let component = self.component else {
return
}
switch recognizer.state {
case .began:
if let itemId = self.item(at: recognizer.location(in: self)), let itemView = self.itemViews[itemId]?.view {
if let search = component.search, search.isActive {
} else if let itemId = self.item(at: recognizer.location(in: self)), let itemView = self.itemViews[itemId]?.view {
let startX = itemView.frame.minX - 4.0
self.selectionGestureState = (startX, startX)
self.selectionGestureState = (startX, startX, itemView.bounds.width, itemId)
self.state?.updated(transition: .spring(duration: 0.4), isLocal: true)
}
case .changed:
if var selectionGestureState = self.selectionGestureState {
if let search = component.search, search.isActive {
} else if var selectionGestureState = self.selectionGestureState {
selectionGestureState.currentX = selectionGestureState.startX + recognizer.translation(in: self).x
if let itemId = self.item(at: recognizer.location(in: self)) {
selectionGestureState.itemId = itemId
}
self.selectionGestureState = selectionGestureState
self.state?.updated(transition: .immediate, isLocal: true)
}
case .ended, .cancelled:
self.selectionGestureState = nil
if let component = self.component, let itemId = self.item(at: recognizer.location(in: self)) {
guard let item = component.items.first(where: { $0.id == itemId }) else {
return
if let search = component.search, search.isActive {
search.deactivate()
} else if let selectionGestureState = self.selectionGestureState {
self.selectionGestureState = nil
if case .ended = recognizer.state, let component = self.component {
guard let item = component.items.first(where: { $0.id == selectionGestureState.itemId }) else {
return
}
self.overrideSelectedItemId = selectionGestureState.itemId
item.action(false)
}
self.overrideSelectedItemId = itemId
item.action(false)
self.state?.updated(transition: .spring(duration: 0.4), isLocal: true)
}
self.state?.updated(transition: .spring(duration: 0.4), isLocal: true)
default:
break
}
@@ -355,29 +552,91 @@ public final class TabBarComponent: Component {
}
func update(component: TabBarComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
let alphaTransition: ComponentTransition = transition.animation.isImmediate ? .immediate : .easeInOut(duration: 0.25)
let _ = alphaTransition
let innerInset: CGFloat = 4.0
let availableSize = CGSize(width: min(500.0, availableSize.width), height: availableSize.height)
let previousComponent = self.component
self.component = component
self.state = state
let _ = innerInset
let _ = availableSize
let _ = previousComponent
self.overrideUserInterfaceStyle = component.theme.overallDarkAppearance ? .dark : .light
let itemSize = CGSize(width: floor((availableSize.width - innerInset * 2.0) / CGFloat(component.items.count)), height: 56.0)
let contentWidth: CGFloat = innerInset * 2.0 + CGFloat(component.items.count) * itemSize.width
let size = CGSize(width: min(availableSize.width, contentWidth), height: itemSize.height + innerInset * 2.0)
let barHeight: CGFloat = 56.0 + innerInset * 2.0
var availableItemsWidth: CGFloat = availableSize.width - innerInset * 2.0
if component.search != nil {
availableItemsWidth -= barHeight + 8.0
}
var unboundItemWidths: [CGFloat] = []
var validIds: [AnyHashable] = []
var selectionFrame: CGRect?
var unboundItemWidthSum: CGFloat = 0.0
for index in 0 ..< component.items.count {
let item = component.items[index]
validIds.append(item.id)
let measureItemView: ComponentView<Empty>
if let current = self.measureItemViews[item.id] {
measureItemView = current
} else {
measureItemView = ComponentView()
self.measureItemViews[item.id] = measureItemView
}
let itemSize = measureItemView.update(
transition: .immediate,
component: AnyComponent(ItemComponent(
item: item,
theme: component.theme,
isCompact: false,
isSelected: false,
isUnconstrained: true
)),
environment: {},
containerSize: CGSize(width: 200.0, height: 56.0)
)
unboundItemWidths.append(itemSize.width)
unboundItemWidthSum += itemSize.width
}
let itemWidths: [CGFloat]
let totalItemsWidth: CGFloat
let equalWidth = floorToScreenPixels(availableItemsWidth / CGFloat(component.items.count))
if unboundItemWidths.allSatisfy({ $0 <= equalWidth }) {
// All items fit in equal width use equal widths for optical alignment
itemWidths = Array(repeating: equalWidth, count: component.items.count)
totalItemsWidth = equalWidth * CGFloat(component.items.count)
} else {
// Some items need more space use weighted fit
let itemWeightNorm: CGFloat = availableItemsWidth / unboundItemWidthSum
var widths: [CGFloat] = []
var total: CGFloat = 0.0
for index in 0 ..< component.items.count {
let itemWidth = floorToScreenPixels(unboundItemWidths[index] * itemWeightNorm)
widths.append(itemWidth)
total += itemWidth
}
itemWidths = widths
totalItemsWidth = total
}
let itemHeight: CGFloat = 56.0
let contentWidth: CGFloat = innerInset * 2.0 + totalItemsWidth
let tabsSize = CGSize(width: min(availableSize.width, contentWidth), height: itemHeight + innerInset * 2.0)
var selectionFrame: CGRect?
var nextItemX: CGFloat = innerInset
for index in 0 ..< component.items.count {
let item = component.items[index]
let itemSize = CGSize(width: itemWidths[index], height: itemHeight)
let itemView: ComponentView<Empty>
var itemTransition = transition
@@ -409,7 +668,9 @@ public final class TabBarComponent: Component {
component: AnyComponent(ItemComponent(
item: item,
theme: component.theme,
isSelected: false
isCompact: component.search?.isActive == true,
isSelected: false,
isUnconstrained: false
)),
environment: {},
containerSize: itemSize
@@ -419,14 +680,23 @@ public final class TabBarComponent: Component {
component: AnyComponent(ItemComponent(
item: item,
theme: component.theme,
isSelected: true
isCompact: component.search?.isActive == true,
isSelected: true,
isUnconstrained: false
)),
environment: {},
containerSize: itemSize
)
let itemFrame = CGRect(origin: CGPoint(x: innerInset + CGFloat(index) * itemSize.width, y: floor((size.height - itemSize.height) * 0.5)), size: itemSize)
var itemFrame = CGRect(origin: CGPoint(x: nextItemX, y: floor((tabsSize.height - itemSize.height) * 0.5)), size: itemSize)
nextItemX += itemSize.width
if isItemSelected {
selectionFrame = itemFrame
}
if let itemComponentView = itemView.view as? ItemComponent.View, let selectedItemComponentView = selectedItemView.view as? ItemComponent.View {
let itemAlphaTransition: ComponentTransition = transition.animation.isImmediate ? .immediate : .easeInOut(duration: 0.25)
if itemComponentView.superview == nil {
itemComponentView.isUserInteractionEnabled = false
selectedItemComponentView.isUserInteractionEnabled = false
@@ -434,6 +704,27 @@ public final class TabBarComponent: Component {
self.liquidLensView.contentView.addSubview(itemComponentView)
self.liquidLensView.selectedContentView.addSubview(selectedItemComponentView)
}
if let search = component.search, search.isActive {
if isItemSelected {
itemFrame.origin.x = floor((48.0 - itemSize.width) * 0.5)
itemTransition.setAlpha(view: itemComponentView, alpha: 1.0)
itemAlphaTransition.setBlur(layer: itemComponentView.layer, radius: 0.0)
itemTransition.setAlpha(view: selectedItemComponentView, alpha: 1.0)
itemAlphaTransition.setBlur(layer: selectedItemComponentView.layer, radius: 0.0)
} else {
itemTransition.setAlpha(view: itemComponentView, alpha: 0.0)
itemAlphaTransition.setBlur(layer: itemComponentView.layer, radius: 10.0)
itemTransition.setAlpha(view: selectedItemComponentView, alpha: 0.0)
itemAlphaTransition.setBlur(layer: selectedItemComponentView.layer, radius: 10.0)
}
} else {
itemTransition.setAlpha(view: itemComponentView, alpha: 1.0)
itemAlphaTransition.setBlur(layer: itemComponentView.layer, radius: 0.0)
itemTransition.setAlpha(view: selectedItemComponentView, alpha: 1.0)
itemAlphaTransition.setBlur(layer: selectedItemComponentView.layer, radius: 0.0)
}
itemTransition.setFrame(view: itemComponentView, frame: itemFrame)
itemTransition.setPosition(view: selectedItemComponentView, position: itemFrame.center)
itemTransition.setBounds(view: selectedItemComponentView, bounds: CGRect(origin: CGPoint(), size: itemFrame.size))
@@ -444,9 +735,6 @@ public final class TabBarComponent: Component {
selectedItemComponentView.playSelectionAnimation()
}
}
if isItemSelected {
selectionFrame = itemFrame
}
}
var removeIds: [AnyHashable] = []
@@ -460,22 +748,95 @@ public final class TabBarComponent: Component {
for id in removeIds {
self.itemViews.removeValue(forKey: id)
self.selectedItemViews.removeValue(forKey: id)
self.measureItemViews.removeValue(forKey: id)
}
transition.setFrame(view: self.contextGestureContainerView, frame: CGRect(origin: CGPoint(), size: size))
transition.setFrame(view: self.liquidLensView, frame: CGRect(origin: CGPoint(), size: size))
let lensSelection: (x: CGFloat, width: CGFloat)
if let selectionGestureState = self.selectionGestureState {
lensSelection = (selectionGestureState.currentX, itemSize.width + innerInset * 2.0)
} else if let selectionFrame {
lensSelection = (selectionFrame.minX - innerInset, itemSize.width + innerInset * 2.0)
} else {
lensSelection = (0.0, itemSize.width)
var tabsFrame = CGRect(origin: CGPoint(), size: tabsSize)
if let search = component.search, search.isActive {
tabsFrame.size = CGSize(width: 48.0, height: 48.0)
tabsFrame.origin.y = tabsSize.height - 48.0
tabsFrame.origin.x = -component.outerInsets.left - tabsFrame.width
}
self.liquidLensView.update(size: size, selectionX: lensSelection.x, selectionWidth: lensSelection.width, isDark: component.theme.overallDarkAppearance, isLifted: self.selectionGestureState != nil, transition: transition)
transition.setFrame(view: self.contextGestureContainerView, frame: tabsFrame)
transition.setFrame(view: self.liquidLensView, frame: CGRect(origin: CGPoint(), size: tabsSize))
var lensSelection: (x: CGFloat, width: CGFloat)
if let selectionGestureState = self.selectionGestureState {
lensSelection = (selectionGestureState.currentX, selectionGestureState.itemWidth + innerInset * 2.0)
} else if let selectionFrame {
lensSelection = (selectionFrame.minX - innerInset, selectionFrame.width + innerInset * 2.0)
} else {
lensSelection = (0.0, 56.0)
}
var lensSize: CGSize = tabsSize
var isLensCollapsed = false
if let search = component.search, search.isActive {
isLensCollapsed = true
lensSize = CGSize(width: 48.0, height: 48.0)
lensSelection = (0.0, 48.0)
}
lensSelection.x = max(0.0, min(lensSelection.x, lensSize.width - lensSelection.width))
self.liquidLensView.update(size: lensSize, selectionOrigin: CGPoint(x: lensSelection.x, y: 0.0), selectionSize: CGSize(width: lensSelection.width, height: lensSize.height), inset: 4.0, isDark: component.theme.overallDarkAppearance, isLifted: self.selectionGestureState != nil, isCollapsed: isLensCollapsed, transition: transition)
var size = tabsSize
if let search = component.search {
let searchSize: CGSize
let searchFrame: CGRect
if search.isActive {
size.width = availableSize.width
searchSize = CGSize(width: availableSize.width, height: 48.0)
searchFrame = CGRect(origin: CGPoint(x: 0.0, y: size.height - searchSize.height), size: searchSize)
} else {
searchSize = CGSize(width: barHeight, height: barHeight)
size.width += barHeight + 8.0
searchFrame = CGRect(origin: CGPoint(x: availableSize.width - searchSize.width, y: 0.0), size: searchSize)
}
let searchView: NavigationSearchView
var searchViewTransition = transition
if let current = self.searchView {
searchView = current
} else {
searchViewTransition = searchViewTransition.withAnimation(.none)
searchView = NavigationSearchView(
action: { [weak self] in
guard let self, let component = self.component else {
return
}
component.search?.activate()
},
closeAction: { [weak self] in
guard let self, let component = self.component else {
return
}
component.search?.deactivate()
}
)
self.searchView = searchView
self.backgroundContainer.contentView.addSubview(searchView)
searchView.frame = CGRect(origin: CGPoint(x: availableSize.width + 50.0, y: 0.0), size: searchSize)
}
searchView.update(size: searchSize, theme: component.theme, strings: component.strings, isActive: search.isActive, transition: searchViewTransition)
transition.setFrame(view: searchView, frame: searchFrame)
} else {
if let searchView = self.searchView {
self.searchView = nil
transition.setFrame(view: searchView, frame: CGRect(origin: CGPoint(x: availableSize.width + 50.0, y: 0.0), size: searchView.bounds.size), completion: { [weak searchView] completed in
guard let searchView, completed else {
return
}
searchView.removeFromSuperview()
})
}
}
transition.setFrame(view: self.backgroundContainer, frame: CGRect(origin: CGPoint(), size: size))
self.backgroundContainer.update(size: size, isDark: component.theme.overallDarkAppearance, transition: transition)
return size
}
@@ -493,12 +854,16 @@ public final class TabBarComponent: Component {
private final class ItemComponent: Component {
let item: TabBarComponent.Item
let theme: PresentationTheme
let isCompact: Bool
let isSelected: Bool
let isUnconstrained: Bool
init(item: TabBarComponent.Item, theme: PresentationTheme, isSelected: Bool) {
init(item: TabBarComponent.Item, theme: PresentationTheme, isCompact: Bool, isSelected: Bool, isUnconstrained: Bool) {
self.item = item
self.theme = theme
self.isCompact = isCompact
self.isSelected = isSelected
self.isUnconstrained = isUnconstrained
}
static func ==(lhs: ItemComponent, rhs: ItemComponent) -> Bool {
@@ -508,9 +873,15 @@ private final class ItemComponent: Component {
if lhs.theme !== rhs.theme {
return false
}
if lhs.isCompact != rhs.isCompact {
return false
}
if lhs.isSelected != rhs.isSelected {
return false
}
if lhs.isUnconstrained != rhs.isUnconstrained {
return false
}
return true
}
@@ -562,6 +933,8 @@ private final class ItemComponent: Component {
}
func update(component: ItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
let alphaTransition: ComponentTransition = transition.animation.isImmediate ? .immediate : .easeInOut(duration: 0.25)
let previousComponent = self.component
if previousComponent?.item.item !== component.item.item {
@@ -619,7 +992,7 @@ private final class ItemComponent: Component {
content: LottieComponent.AppBundleContent(
name: animationName
),
color: component.isSelected ? component.theme.rootController.tabBar.selectedTextColor : component.theme.rootController.tabBar.textColor,
color: (component.isSelected && !component.isCompact) ? component.theme.rootController.tabBar.selectedTextColor : component.theme.rootController.tabBar.textColor,
placeholderColor: nil,
startingPosition: .end,
size: CGSize(width: 48.0, height: 48.0),
@@ -692,6 +1065,7 @@ private final class ItemComponent: Component {
self.contextContainerView.contentView.addSubview(titleView)
}
titleView.frame = titleFrame
alphaTransition.setAlpha(view: titleView, alpha: component.isCompact ? 0.0 : 1.0)
}
if let badgeText = component.item.item.badgeValue, !badgeText.isEmpty {
@@ -711,7 +1085,7 @@ private final class ItemComponent: Component {
font: Font.regular(13.0),
background: component.theme.rootController.tabBar.badgeBackgroundColor,
foreground: component.theme.rootController.tabBar.badgeTextColor,
insets: UIEdgeInsets(top: 0.0, left: 6.0, bottom: 1.0, right: 6.0)
insets: UIEdgeInsets(top: 0.0, left: 5.0, bottom: 1.0, right: 5.0)
)),
environment: {},
containerSize: CGSize(width: 100.0, height: 100.0)
@@ -723,6 +1097,7 @@ private final class ItemComponent: Component {
self.contextContainerView.contentView.addSubview(badgeView)
}
badgeTransition.setFrame(view: badgeView, frame: badgeFrame)
alphaTransition.setAlpha(view: badgeView, alpha: component.isCompact ? 0.0 : 1.0)
}
} else if let badge = self.badge {
self.badge = nil
@@ -733,7 +1108,11 @@ private final class ItemComponent: Component {
transition.setFrame(view: self.contextContainerView.contentView, frame: CGRect(origin: CGPoint(), size: availableSize))
self.contextContainerView.contentRect = CGRect(origin: CGPoint(), size: availableSize)
return availableSize
if component.isUnconstrained {
return CGSize(width: titleSize.width + 10.0 * 2.0, height: availableSize.height)
} else {
return availableSize
}
}
}