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
@@ -493,7 +493,7 @@ private enum CreateGiveawayEntry: ItemListNodeEntry {
}
})
case let .starsMore(theme, title):
return ItemListPeerActionItem(presentationData: presentationData, icon: PresentationResourcesItemList.downArrowImage(theme), title: title, sectionId: self.section, editing: false, action: {
return ItemListPeerActionItem(presentationData: presentationData, systemStyle: .glass, icon: PresentationResourcesItemList.downArrowImage(theme), title: title, sectionId: self.section, editing: false, action: {
arguments.expandStars()
})
case let .starsInfo(_, text):
@@ -521,14 +521,14 @@ private enum CreateGiveawayEntry: ItemListNodeEntry {
if case let .channel(channel) = peer, case .group = channel.info {
isGroup = true
}
return ItemListPeerItem(presentationData: presentationData, dateTimeFormat: PresentationDateTimeFormat(), nameDisplayOrder: presentationData.nameDisplayOrder, context: arguments.context, peer: peer, presence: nil, text: boosts.flatMap { .text(isGroup ? presentationData.strings.BoostGift_GroupBoosts($0) : presentationData.strings.BoostGift_ChannelsBoosts($0), .secondary) } ?? .none, label: .none, editing: ItemListPeerItemEditing(editable: boosts == nil, editing: false, revealed: isRevealed), switchValue: nil, enabled: true, selectable: peer.id != arguments.context.account.peerId, sectionId: self.section, action: {
return ItemListPeerItem(presentationData: presentationData, systemStyle: .glass, dateTimeFormat: PresentationDateTimeFormat(), nameDisplayOrder: presentationData.nameDisplayOrder, context: arguments.context, peer: peer, presence: nil, text: boosts.flatMap { .text(isGroup ? presentationData.strings.BoostGift_GroupBoosts($0) : presentationData.strings.BoostGift_ChannelsBoosts($0), .secondary) } ?? .none, label: .none, editing: ItemListPeerItemEditing(editable: boosts == nil, editing: false, revealed: isRevealed), switchValue: nil, enabled: true, selectable: peer.id != arguments.context.account.peerId, sectionId: self.section, action: {
}, setPeerIdWithRevealedOptions: { lhs, rhs in
arguments.setItemIdWithRevealedOptions(lhs, rhs)
}, removePeer: { id in
arguments.removeChannel(id)
})
case let .channelAdd(theme, text):
return ItemListPeerActionItem(presentationData: presentationData, icon: PresentationResourcesItemList.roundPlusIconImage(theme), title: text, alwaysPlain: false, hasSeparator: true, sectionId: self.section, height: .compactPeerList, color: .accent, editing: false, action: {
return ItemListPeerActionItem(presentationData: presentationData, systemStyle: .glass, icon: PresentationResourcesItemList.roundPlusIconImage(theme), title: text, alwaysPlain: false, hasSeparator: true, sectionId: self.section, height: .compactPeerList, color: .accent, editing: false, action: {
arguments.openChannelsSelection()
})
case let .channelsInfo(_, text):
@@ -582,7 +582,7 @@ private enum CreateGiveawayEntry: ItemListNodeEntry {
arguments.openPremiumIntro()
})
case let .prizeDescription(_, text, value):
return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, sectionId: self.section, style: .blocks, updated: { value in
return ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: text, value: value, sectionId: self.section, style: .blocks, updated: { value in
arguments.updateState { state in
var updatedState = state
updatedState.showPrizeDescription = value
@@ -590,7 +590,7 @@ private enum CreateGiveawayEntry: ItemListNodeEntry {
}
})
case let .prizeDescriptionText(_, placeholder, value, count):
return ItemListSingleLineInputItem(presentationData: presentationData, title: NSAttributedString(string: "\(count)"), text: value, placeholder: placeholder, returnKeyType: .done, spacing: 24.0, maxLength: 128, tag: CreateGiveawayEntryTag.description, sectionId: self.section, textUpdated: { value in
return ItemListSingleLineInputItem(presentationData: presentationData, systemStyle: .glass, title: NSAttributedString(string: "\(count)"), text: value, placeholder: placeholder, returnKeyType: .done, spacing: 24.0, maxLength: 128, tag: CreateGiveawayEntryTag.description, sectionId: self.section, textUpdated: { value in
arguments.updateState { state in
var updatedState = state
updatedState.prizeDescription = value
@@ -616,7 +616,7 @@ private enum CreateGiveawayEntry: ItemListNodeEntry {
} else {
text = presentationData.strings.InviteLink_Create_TimeLimitExpiryDateNever
}
return ItemListDisclosureItem(presentationData: presentationData, title: presentationData.strings.BoostGift_DateEnds, label: text, labelStyle: active ? .coloredText(theme.list.itemAccentColor) : .text, sectionId: self.section, style: .blocks, disclosureStyle: .none, action: {
return ItemListDisclosureItem(presentationData: presentationData, systemStyle: .glass, title: presentationData.strings.BoostGift_DateEnds, label: text, labelStyle: active ? .coloredText(theme.list.itemAccentColor) : .text, sectionId: self.section, style: .blocks, disclosureStyle: .none, action: {
arguments.dismissInput()
var focus = false
arguments.updateState { state in
@@ -677,7 +677,7 @@ private enum CreateGiveawayEntry: ItemListNodeEntry {
case let .timeInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
case let .winners(_, text, value):
return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, sectionId: self.section, style: .blocks, updated: { value in
return ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: text, value: value, sectionId: self.section, style: .blocks, updated: { value in
arguments.updateState { state in
var updatedState = state
updatedState.showWinners = value
@@ -212,7 +212,7 @@ class GiftOptionItemNode: ItemListRevealOptionsItemNode {
self.activateArea = AccessibilityAreaNode()
super.init(layerBacked: false, dynamicBounce: false, rotated: false, seeThrough: false)
super.init(layerBacked: false, rotated: false, seeThrough: false)
self.addSubnode(self.containerNode)
@@ -7,8 +7,8 @@ import TelegramCore
import AccountContext
import TelegramStringFormatting
import TelegramPresentationData
import Markdown
import AlertUI
import ComponentFlow
import AlertComponent
public func presentGiveawayInfoController(
context: AccountContext,
@@ -23,6 +23,8 @@ public func presentGiveawayInfoController(
peerIds.append(adminId)
}
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let _ = (context.engine.data.get(
TelegramEngine.EngineData.Item.Messages.Message(id: messageId),
EngineDataMap(peerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init))
@@ -121,22 +123,16 @@ public func presentGiveawayInfoController(
channelsCount = 1 + giveawayResults.additionalChannelsCount
}
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let timeZone = TimeZone.current
let untilDate = stringForDate(timestamp: untilDateValue, timeZone: timeZone, strings: presentationData.strings)
let title: String
let text: String
var text: String
var warning: String?
var dismissImpl: (() -> Void)?
var actions: [TextAlertAction] = [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {
dismissImpl?()
})]
var actions: [AlertScreen.Action] = [
.init(title: presentationData.strings.Common_OK, type: .default)
]
var additionalPrizes = ""
if let prizeDescription, !prizeDescription.isEmpty {
@@ -322,278 +318,65 @@ public func presentGiveawayInfoController(
case .refunded:
result = ""
warning = presentationData.strings.Chat_Giveaway_Info_Refunded
actions = [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Close, action: {
dismissImpl?()
})]
case .notWon:
result = "**\(presentationData.strings.Chat_Giveaway_Info_DidntWin)**\n\n"
case let .wonPremium(slug):
result = "**\(presentationData.strings.Chat_Giveaway_Info_Won("").string)**\n\n"
actions = [TextAlertAction(type: .defaultAction, title: presentationData.strings.Chat_Giveaway_Info_ViewPrize, action: {
dismissImpl?()
openLink(slug)
}), TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {
dismissImpl?()
})]
actions = [
.init(title: presentationData.strings.Chat_Giveaway_Info_ViewPrize, type: .default, action: {
openLink(slug)
}),
.init(title: presentationData.strings.Common_Cancel)
]
case let .wonStars(stars):
let _ = stars
result = "**\(presentationData.strings.Chat_Giveaway_Info_Won("").string)**\n\n"
actions = [TextAlertAction(type: .defaultAction, title: presentationData.strings.Chat_Giveaway_Info_ViewPrize, action: {
dismissImpl?()
openLink("")
}), TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {
dismissImpl?()
})]
actions = [
.init(title: presentationData.strings.Chat_Giveaway_Info_ViewPrize, type: .default, action: {
openLink("")
}),
.init(title: presentationData.strings.Common_Cancel)
]
}
text = "\(result)\(intro)\(additionalPrizes)\n\n\(ending)"
}
let alertController = giveawayInfoAlertController(
context: context,
updatedPresentationData: updatedPresentationData,
title: title,
text: text,
warning: warning,
actions: actions
)
dismissImpl = { [weak alertController] in
alertController?.dismissAnimated()
var content: [AnyComponentWithIdentity<AlertComponentEnvironment>] = []
content.append(AnyComponentWithIdentity(
id: "title",
component: AnyComponent(
AlertTitleComponent(title: title)
)
))
content.append(AnyComponentWithIdentity(
id: "text",
component: AnyComponent(
AlertTextComponent(content: .plain(text))
)
))
if let warning {
content.append(AnyComponentWithIdentity(
id: "warning",
component: AnyComponent(
AlertTextComponent(content: .plain(warning), color: .destructive, style: .background(.bold))
)
))
}
var effectiveUpdatedPresentationData: (PresentationData, Signal<PresentationData, NoError>)
if let updatedPresentationData {
effectiveUpdatedPresentationData = updatedPresentationData
} else {
effectiveUpdatedPresentationData = (presentationData, context.sharedContext.presentationData)
}
let alertController = AlertScreen(
configuration: AlertScreen.Configuration(actionAlignment: .vertical),
content: content,
actions: actions,
updatedPresentationData: effectiveUpdatedPresentationData
)
present(alertController)
})
}
private final class GiveawayInfoAlertContentNode: AlertContentNode {
private let title: String
private let text: String
private let warning: String?
private let titleNode: ASTextNode
private let textNode: ASTextNode
fileprivate let warningBackgroundNode: ASImageNode
fileprivate let warningTextNode: ImmediateTextNode
private let actionNodesSeparator: ASDisplayNode
private let actionNodes: [TextAlertContentActionNode]
private let actionVerticalSeparators: [ASDisplayNode]
private var validLayout: CGSize?
public var theme: PresentationTheme
public override var dismissOnOutsideTap: Bool {
return self.isUserInteractionEnabled
}
public init(theme: AlertControllerTheme, ptheme: PresentationTheme, title: String, text: String, warning: String?, actions: [TextAlertAction]) {
self.theme = ptheme
self.title = title
self.text = text
self.warning = warning
self.titleNode = ASTextNode()
self.titleNode.maximumNumberOfLines = 0
self.textNode = ASTextNode()
self.textNode.maximumNumberOfLines = 0
self.warningBackgroundNode = ASImageNode()
self.warningBackgroundNode.displaysAsynchronously = false
self.warningTextNode = ImmediateTextNode()
self.warningTextNode.maximumNumberOfLines = 0
self.warningTextNode.lineSpacing = 0.1
self.actionNodesSeparator = ASDisplayNode()
self.actionNodesSeparator.isLayerBacked = true
self.actionNodes = actions.map { action -> TextAlertContentActionNode in
return TextAlertContentActionNode(theme: theme, action: action)
}
var actionVerticalSeparators: [ASDisplayNode] = []
if actions.count > 1 {
for _ in 0 ..< actions.count - 1 {
let separatorNode = ASDisplayNode()
separatorNode.isLayerBacked = true
actionVerticalSeparators.append(separatorNode)
}
}
self.actionVerticalSeparators = actionVerticalSeparators
super.init()
self.addSubnode(self.titleNode)
self.addSubnode(self.textNode)
self.addSubnode(self.warningBackgroundNode)
self.addSubnode(self.warningTextNode)
self.addSubnode(self.actionNodesSeparator)
for actionNode in self.actionNodes {
self.addSubnode(actionNode)
}
for separatorNode in self.actionVerticalSeparators {
self.addSubnode(separatorNode)
}
self.updateTheme(theme)
}
public override func updateTheme(_ theme: AlertControllerTheme) {
self.titleNode.attributedText = NSAttributedString(string: self.title, font: Font.bold(17.0), textColor: theme.primaryColor, paragraphAlignment: .center)
let body = MarkdownAttributeSet(font: Font.regular(13.0), textColor: theme.primaryColor)
let bold = MarkdownAttributeSet(font: Font.semibold(13.0), textColor: theme.primaryColor)
let attributedText = parseMarkdownIntoAttributedString(self.text, attributes: MarkdownAttributes(body: body, bold: bold, link: body, linkAttribute: { _ in return nil }), textAlignment: .center)
self.textNode.attributedText = attributedText
self.warningTextNode.attributedText = NSAttributedString(string: self.warning ?? "", font: Font.semibold(13.0), textColor: theme.destructiveColor, paragraphAlignment: .center)
self.warningBackgroundNode.image = generateStretchableFilledCircleImage(radius: 5.0, color: theme.destructiveColor.withAlphaComponent(0.1))
self.actionNodesSeparator.backgroundColor = theme.separatorColor
for actionNode in self.actionNodes {
actionNode.updateTheme(theme)
}
for separatorNode in self.actionVerticalSeparators {
separatorNode.backgroundColor = theme.separatorColor
}
if let size = self.validLayout {
_ = self.updateLayout(size: size, transition: .immediate)
}
}
public override func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize {
var size = size
size.width = min(size.width, 270.0)
let measureSize = CGSize(width: size.width - 16.0 * 2.0, height: CGFloat.greatestFiniteMagnitude)
self.validLayout = size
var origin: CGPoint = CGPoint(x: 0.0, y: 20.0)
let titleSize = self.titleNode.measure(measureSize)
transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - titleSize.width) / 2.0), y: origin.y), size: titleSize))
origin.y += titleSize.height + 4.0
let textSize = self.textNode.measure(measureSize)
transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - textSize.width) / 2.0), y: origin.y), size: textSize))
origin.y += textSize.height + 6.0
let actionButtonHeight: CGFloat = 44.0
var minActionsWidth: CGFloat = 0.0
let maxActionWidth: CGFloat = floor(size.width / CGFloat(self.actionNodes.count))
let actionTitleInsets: CGFloat = 8.0
var effectiveActionLayout = TextAlertContentActionLayout.horizontal
for actionNode in self.actionNodes {
let actionTitleSize = actionNode.titleNode.updateLayout(CGSize(width: maxActionWidth, height: actionButtonHeight))
if case .horizontal = effectiveActionLayout, actionTitleSize.height > actionButtonHeight * 0.6667 {
effectiveActionLayout = .vertical
}
switch effectiveActionLayout {
case .horizontal:
minActionsWidth += actionTitleSize.width + actionTitleInsets
case .vertical:
minActionsWidth = max(minActionsWidth, actionTitleSize.width + actionTitleInsets)
}
}
if "".isEmpty {
effectiveActionLayout = .vertical
}
let insets = UIEdgeInsets(top: 18.0, left: 18.0, bottom: 18.0, right: 18.0)
var contentWidth = max(titleSize.width, minActionsWidth)
contentWidth = max(contentWidth, 234.0)
var actionsHeight: CGFloat = 0.0
switch effectiveActionLayout {
case .horizontal:
actionsHeight = actionButtonHeight
case .vertical:
actionsHeight = actionButtonHeight * CGFloat(self.actionNodes.count)
}
let resultWidth = contentWidth + insets.left + insets.right
var warningHeight: CGFloat = 0.0
if let _ = self.warning {
let warningSize = self.warningTextNode.updateLayout(measureSize)
let warningFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - warningSize.width) / 2.0), y: origin.y + 20.0), size: warningSize)
transition.updateFrame(node: self.warningTextNode, frame: warningFrame)
transition.updateFrame(node: self.warningBackgroundNode, frame: warningFrame.insetBy(dx: -8.0, dy: -8.0))
warningHeight += warningSize.height + 26.0
}
let resultSize = CGSize(width: resultWidth, height: titleSize.height + textSize.height + 8.0 + actionsHeight + warningHeight + insets.top + insets.bottom)
transition.updateFrame(node: self.actionNodesSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel)))
var actionOffset: CGFloat = 0.0
let actionWidth: CGFloat = floor(resultSize.width / CGFloat(self.actionNodes.count))
var separatorIndex = -1
var nodeIndex = 0
for actionNode in self.actionNodes {
if separatorIndex >= 0 {
let separatorNode = self.actionVerticalSeparators[separatorIndex]
switch effectiveActionLayout {
case .horizontal:
transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: actionOffset - UIScreenPixel, y: resultSize.height - actionsHeight), size: CGSize(width: UIScreenPixel, height: actionsHeight - UIScreenPixel)))
case .vertical:
transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel)))
}
}
separatorIndex += 1
let currentActionWidth: CGFloat
switch effectiveActionLayout {
case .horizontal:
if nodeIndex == self.actionNodes.count - 1 {
currentActionWidth = resultSize.width - actionOffset
} else {
currentActionWidth = actionWidth
}
case .vertical:
currentActionWidth = resultSize.width
}
let actionNodeFrame: CGRect
switch effectiveActionLayout {
case .horizontal:
actionNodeFrame = CGRect(origin: CGPoint(x: actionOffset, y: resultSize.height - actionsHeight), size: CGSize(width: currentActionWidth, height: actionButtonHeight))
actionOffset += currentActionWidth
case .vertical:
actionNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset), size: CGSize(width: currentActionWidth, height: actionButtonHeight))
actionOffset += actionButtonHeight
}
transition.updateFrame(node: actionNode, frame: actionNodeFrame)
nodeIndex += 1
}
return resultSize
}
}
private func giveawayInfoAlertController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, title: String, text: String, warning: String?, actions: [TextAlertAction]) -> AlertController {
let presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 }
let contentNode = GiveawayInfoAlertContentNode(theme: AlertControllerTheme(presentationData: presentationData), ptheme: presentationData.theme, title: title, text: text, warning: warning, actions: actions)
let controller = AlertController(theme: AlertControllerTheme(presentationData: presentationData), contentNode: contentNode)
let presentationDataDisposable = (updatedPresentationData?.signal ?? context.sharedContext.presentationData).start(next: { [weak controller] presentationData in
controller?.theme = AlertControllerTheme(presentationData: presentationData)
})
controller.dismissed = { _ in
presentationDataDisposable.dispose()
}
return controller
}
@@ -99,7 +99,7 @@ class IncreaseLimitHeaderItemNode: ListViewItemNode {
self.textNode.contentMode = .left
self.textNode.contentsScale = UIScreen.main.scale
super.init(layerBacked: false, dynamicBounce: false)
super.init(layerBacked: false)
self.addSubnode(self.titleNode)
self.addSubnode(self.textNode)
@@ -1133,7 +1133,7 @@ private final class DemoSheetContent: CombinedComponent {
let closeButton = closeButton.update(
component: GlassBarButtonComponent(
size: CGSize(width: 40.0, height: 40.0),
size: CGSize(width: 44.0, height: 44.0),
backgroundColor: UIColor(rgb: 0x7f76f4),
isDark: false,
state: .tintedGlass,
@@ -1147,7 +1147,7 @@ private final class DemoSheetContent: CombinedComponent {
component.dismiss()
}
),
availableSize: CGSize(width: 40.0, height: 40.0),
availableSize: CGSize(width: 44.0, height: 44.0),
transition: .immediate
)
context.add(closeButton
@@ -23,6 +23,8 @@ import InvisibleInkDustNode
import PremiumStarComponent
import GlassBarButtonComponent
import ButtonComponent
import TableComponent
import PeerTableCellComponent
private final class PremiumGiftCodeSheetContent: CombinedComponent {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
@@ -163,7 +165,7 @@ private final class PremiumGiftCodeSheetContent: CombinedComponent {
component: AnyComponentWithIdentity(id: "close", component: AnyComponent(
BundleIconComponent(
name: "Navigation/Close",
tintColor: theme.rootController.navigationBar.glassBarButtonForegroundColor
tintColor: theme.chat.inputPanel.panelControlColor
)
)),
action: { _ in
@@ -326,7 +328,7 @@ private final class PremiumGiftCodeSheetContent: CombinedComponent {
title: strings.GiftLink_From,
component: AnyComponent(
Button(
content: AnyComponent(PeerCellComponent(context: context.component.context, textColor: tableLinkColor, peer: fromPeer)),
content: AnyComponent(PeerTableCellComponent(context: context.component.context, theme: theme, strings: strings, peer: fromPeer)),
action: {
if let peer = fromPeer, peer.id != accountContext.account.peerId {
component.openPeer(peer)
@@ -344,7 +346,7 @@ private final class PremiumGiftCodeSheetContent: CombinedComponent {
title: strings.GiftLink_To,
component: AnyComponent(
Button(
content: AnyComponent(PeerCellComponent(context: context.component.context, textColor: tableLinkColor, peer: toPeer)),
content: AnyComponent(PeerTableCellComponent(context: context.component.context, theme: theme, strings: strings, peer: toPeer)),
action: {
if toPeer.id != accountContext.account.peerId {
component.openPeer(toPeer)
@@ -857,306 +859,6 @@ final class GiftLinkButtonContentComponent: CombinedComponent {
}
}
private final class TableComponent: CombinedComponent {
class Item: Equatable {
public let id: AnyHashable
public let title: String
public let component: AnyComponent<Empty>
public init<IdType: Hashable>(id: IdType, title: String, component: AnyComponent<Empty>) {
self.id = AnyHashable(id)
self.title = title
self.component = component
}
public static func == (lhs: Item, rhs: Item) -> Bool {
if lhs.id != rhs.id {
return false
}
if lhs.title != rhs.title {
return false
}
if lhs.component != rhs.component {
return false
}
return true
}
}
private let theme: PresentationTheme
private let items: [Item]
public init(theme: PresentationTheme, items: [Item]) {
self.theme = theme
self.items = items
}
public static func ==(lhs: TableComponent, rhs: TableComponent) -> Bool {
if lhs.theme !== rhs.theme {
return false
}
if lhs.items != rhs.items {
return false
}
return true
}
final class State: ComponentState {
var cachedBorderImage: (UIImage, PresentationTheme)?
}
func makeState() -> State {
return State()
}
public static var body: Body {
let leftColumnBackground = Child(Rectangle.self)
let verticalBorder = Child(Rectangle.self)
let titleChildren = ChildMap(environment: Empty.self, keyedBy: AnyHashable.self)
let valueChildren = ChildMap(environment: Empty.self, keyedBy: AnyHashable.self)
let borderChildren = ChildMap(environment: Empty.self, keyedBy: AnyHashable.self)
let outerBorder = Child(Image.self)
return { context in
let verticalPadding: CGFloat = 11.0
let horizontalPadding: CGFloat = 12.0
let borderWidth: CGFloat = 1.0
let backgroundColor = context.component.theme.actionSheet.opaqueItemBackgroundColor
let borderColor = backgroundColor.mixedWith(context.component.theme.list.itemBlocksSeparatorColor, alpha: 0.6)
var leftColumnWidth: CGFloat = 0.0
var updatedTitleChildren: [_UpdatedChildComponent] = []
var updatedValueChildren: [_UpdatedChildComponent] = []
var updatedBorderChildren: [_UpdatedChildComponent] = []
for item in context.component.items {
let titleChild = titleChildren[item.id].update(
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: item.title, font: Font.regular(15.0), textColor: context.component.theme.list.itemPrimaryTextColor))
)),
availableSize: context.availableSize,
transition: context.transition
)
updatedTitleChildren.append(titleChild)
if titleChild.size.width > leftColumnWidth {
leftColumnWidth = titleChild.size.width
}
}
leftColumnWidth = max(100.0, leftColumnWidth + horizontalPadding * 2.0)
let rightColumnWidth = context.availableSize.width - leftColumnWidth
var i = 0
var rowHeights: [Int: CGFloat] = [:]
var totalHeight: CGFloat = 0.0
for item in context.component.items {
let titleChild = updatedTitleChildren[i]
let valueChild = valueChildren[item.id].update(
component: item.component,
availableSize: CGSize(width: rightColumnWidth - horizontalPadding * 2.0, height: context.availableSize.height),
transition: context.transition
)
updatedValueChildren.append(valueChild)
let rowHeight = max(40.0, max(titleChild.size.height, valueChild.size.height) + verticalPadding * 2.0)
rowHeights[i] = rowHeight
totalHeight += rowHeight
if i < context.component.items.count - 1 {
let borderChild = borderChildren[item.id].update(
component: AnyComponent(Rectangle(color: borderColor)),
availableSize: CGSize(width: context.availableSize.width, height: borderWidth),
transition: context.transition
)
updatedBorderChildren.append(borderChild)
}
i += 1
}
let leftColumnBackground = leftColumnBackground.update(
component: Rectangle(color: context.component.theme.list.itemInputField.backgroundColor),
availableSize: CGSize(width: leftColumnWidth, height: totalHeight),
transition: context.transition
)
context.add(
leftColumnBackground
.position(CGPoint(x: leftColumnWidth / 2.0, y: totalHeight / 2.0))
)
let borderImage: UIImage
if let (currentImage, theme) = context.state.cachedBorderImage, theme === context.component.theme {
borderImage = currentImage
} else {
let borderRadius: CGFloat = 14.0
borderImage = generateImage(CGSize(width: borderRadius * 2.0 + 6.0, height: borderRadius * 2.0 + 6.0), rotatedContext: { size, context in
let bounds = CGRect(origin: .zero, size: size)
context.setFillColor(backgroundColor.cgColor)
context.fill(bounds)
let path = CGPath(roundedRect: bounds.insetBy(dx: borderWidth / 2.0, dy: borderWidth / 2.0), cornerWidth: borderRadius, cornerHeight: borderRadius, transform: nil)
context.setBlendMode(.clear)
context.addPath(path)
context.fillPath()
context.setBlendMode(.normal)
context.setStrokeColor(borderColor.cgColor)
context.setLineWidth(borderWidth)
context.addPath(path)
context.strokePath()
})!.stretchableImage(withLeftCapWidth: Int(borderRadius), topCapHeight: Int(borderRadius))
context.state.cachedBorderImage = (borderImage, context.component.theme)
}
let outerBorder = outerBorder.update(
component: Image(image: borderImage),
availableSize: CGSize(width: context.availableSize.width, height: totalHeight),
transition: context.transition
)
context.add(outerBorder
.position(CGPoint(x: context.availableSize.width / 2.0, y: totalHeight / 2.0))
)
let verticalBorder = verticalBorder.update(
component: Rectangle(color: borderColor),
availableSize: CGSize(width: borderWidth, height: totalHeight),
transition: context.transition
)
context.add(
verticalBorder
.position(CGPoint(x: leftColumnWidth - borderWidth / 2.0, y: totalHeight / 2.0))
)
i = 0
var originY: CGFloat = 0.0
for (titleChild, valueChild) in zip(updatedTitleChildren, updatedValueChildren) {
let rowHeight = rowHeights[i] ?? 0.0
let titleFrame = CGRect(origin: CGPoint(x: horizontalPadding, y: originY + verticalPadding), size: titleChild.size)
let valueFrame = CGRect(origin: CGPoint(x: leftColumnWidth + horizontalPadding, y: originY + verticalPadding), size: valueChild.size)
context.add(titleChild
.position(titleFrame.center)
)
context.add(valueChild
.position(valueFrame.center)
)
if i < updatedBorderChildren.count {
let borderChild = updatedBorderChildren[i]
context.add(borderChild
.position(CGPoint(x: context.availableSize.width / 2.0, y: originY + rowHeight - borderWidth / 2.0))
)
}
originY += rowHeight
i += 1
}
return CGSize(width: context.availableSize.width, height: totalHeight)
}
}
}
private final class PeerCellComponent: Component {
let context: AccountContext
let textColor: UIColor
let peer: EnginePeer?
init(context: AccountContext, textColor: UIColor, peer: EnginePeer?) {
self.context = context
self.textColor = textColor
self.peer = peer
}
static func ==(lhs: PeerCellComponent, rhs: PeerCellComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.textColor !== rhs.textColor {
return false
}
if lhs.peer != rhs.peer {
return false
}
return true
}
final class View: UIView {
private let avatarNode: AvatarNode
private let text = ComponentView<Empty>()
private var component: PeerCellComponent?
private weak var state: EmptyComponentState?
override init(frame: CGRect) {
self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 13.0))
super.init(frame: frame)
self.addSubnode(self.avatarNode)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(component: PeerCellComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
self.component = component
self.state = state
self.avatarNode.setPeer(
context: component.context,
theme: component.context.sharedContext.currentPresentationData.with({ $0 }).theme,
peer: component.peer,
synchronousLoad: true
)
let avatarSize = CGSize(width: 22.0, height: 22.0)
let spacing: CGFloat = 6.0
let textSize = self.text.update(
transition: .immediate,
component: AnyComponent(
MultilineTextComponent(
text: .plain(NSAttributedString(string: component.peer?.compactDisplayTitle ?? "", font: Font.regular(15.0), textColor: component.textColor, paragraphAlignment: .left))
)
),
environment: {},
containerSize: CGSize(width: availableSize.width - avatarSize.width - spacing, height: availableSize.height)
)
let size = CGSize(width: avatarSize.width + textSize.width + spacing, height: textSize.height)
let avatarFrame = CGRect(origin: CGPoint(x: 0.0, y: floorToScreenPixels((size.height - avatarSize.height) / 2.0)), size: avatarSize)
self.avatarNode.frame = avatarFrame
if let view = self.text.view {
if view.superview == nil {
self.addSubview(view)
}
let textFrame = CGRect(origin: CGPoint(x: avatarSize.width + spacing, y: floorToScreenPixels((size.height - textSize.height) / 2.0)), size: textSize)
transition.setFrame(view: view, frame: textFrame)
}
return size
}
}
func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
private final class DustComponent: Component {
let color: UIColor
@@ -1679,34 +1679,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
}
}
})
self.newPerksDisposable = combineLatest(
queue: Queue.mainQueue(),
ApplicationSpecificNotice.dismissedBusinessBadge(accountManager: context.sharedContext.accountManager),
ApplicationSpecificNotice.dismissedBusinessLinksBadge(accountManager: context.sharedContext.accountManager),
ApplicationSpecificNotice.dismissedBusinessIntroBadge(accountManager: context.sharedContext.accountManager),
ApplicationSpecificNotice.dismissedBusinessChatbotsBadge(accountManager: context.sharedContext.accountManager)
).startStrict(next: { [weak self] dismissedBusinessBadge, dismissedBusinessLinksBadge, dismissedBusinessIntroBadge, dismissedBusinessChatbotsBadge in
guard let self else {
return
}
var newPerks: [String] = []
if !dismissedBusinessBadge {
newPerks.append(PremiumPerk.business.identifier)
}
if !dismissedBusinessLinksBadge {
newPerks.append(PremiumPerk.businessLinks.identifier)
}
if !dismissedBusinessIntroBadge {
newPerks.append(PremiumPerk.businessIntro.identifier)
}
if !dismissedBusinessChatbotsBadge {
newPerks.append(PremiumPerk.businessChatBots.identifier)
}
self.newPerks = newPerks
self.updated()
})
self.adsEnabledDisposable = (context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.AdsEnabled(id: context.account.peerId))
|> deliverOnMainQueue).start(next: { [weak self] adsEnabled in
guard let self else {
@@ -2219,7 +2192,6 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
demoSubject = .todo
case .business:
demoSubject = .business
let _ = ApplicationSpecificNotice.setDismissedBusinessBadge(accountManager: accountContext.sharedContext.accountManager).startStandalone()
default:
demoSubject = .doubleLimits
}
@@ -2418,7 +2390,6 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
}
push(accountContext.sharedContext.makeChatbotSetupScreen(context: accountContext, initialData: initialData))
})
let _ = ApplicationSpecificNotice.setDismissedBusinessChatbotsBadge(accountManager: accountContext.sharedContext.accountManager).startStandalone()
case .businessIntro:
let _ = (accountContext.sharedContext.makeBusinessIntroSetupScreenInitialData(context: accountContext)
|> take(1)
@@ -2428,7 +2399,6 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
}
push(accountContext.sharedContext.makeBusinessIntroSetupScreen(context: accountContext, initialData: initialData))
})
let _ = ApplicationSpecificNotice.setDismissedBusinessIntroBadge(accountManager: accountContext.sharedContext.accountManager).startStandalone()
case .businessLinks:
let _ = (accountContext.sharedContext.makeBusinessLinksSetupScreenInitialData(context: accountContext)
|> take(1)
@@ -2438,7 +2408,6 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
}
push(accountContext.sharedContext.makeBusinessLinksSetupScreen(context: accountContext, initialData: initialData))
})
let _ = ApplicationSpecificNotice.setDismissedBusinessLinksBadge(accountManager: accountContext.sharedContext.accountManager).startStandalone()
default:
fatalError()
}
@@ -2457,13 +2426,10 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
demoSubject = .businessAwayMessage
case .businessChatBots:
demoSubject = .businessChatBots
let _ = ApplicationSpecificNotice.setDismissedBusinessChatbotsBadge(accountManager: accountContext.sharedContext.accountManager).startStandalone()
case .businessIntro:
demoSubject = .businessIntro
let _ = ApplicationSpecificNotice.setDismissedBusinessIntroBadge(accountManager: accountContext.sharedContext.accountManager).startStandalone()
case .businessLinks:
demoSubject = .businessLinks
let _ = ApplicationSpecificNotice.setDismissedBusinessLinksBadge(accountManager: accountContext.sharedContext.accountManager).startStandalone()
default:
fatalError()
}
@@ -2960,6 +2926,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
private final class PremiumIntroScreenComponent: CombinedComponent {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let overNavigationContainer: UIView
let screenContext: PremiumIntroScreen.ScreenContext
let mode: PremiumIntroScreen.Mode
let source: PremiumSource
@@ -2972,7 +2939,8 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
let copyLink: (String) -> Void
let shareLink: (String) -> Void
init(screenContext: PremiumIntroScreen.ScreenContext, mode: PremiumIntroScreen.Mode, source: PremiumSource, forceDark: Bool, forceHasPremium: Bool, updateInProgress: @escaping (Bool) -> Void, present: @escaping (ViewController) -> Void, push: @escaping (ViewController) -> Void, completion: @escaping () -> Void, copyLink: @escaping (String) -> Void, shareLink: @escaping (String) -> Void) {
init(overNavigationContainer: UIView, screenContext: PremiumIntroScreen.ScreenContext, mode: PremiumIntroScreen.Mode, source: PremiumSource, forceDark: Bool, forceHasPremium: Bool, updateInProgress: @escaping (Bool) -> Void, present: @escaping (ViewController) -> Void, push: @escaping (ViewController) -> Void, completion: @escaping () -> Void, copyLink: @escaping (String) -> Void, shareLink: @escaping (String) -> Void) {
self.overNavigationContainer = overNavigationContainer
self.screenContext = screenContext
self.mode = mode
self.source = source
@@ -3415,8 +3383,6 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
let star = Child(PremiumStarComponent.self)
let emoji = Child(EmojiHeaderComponent.self)
let coin = Child(PremiumCoinComponent.self)
let topPanel = Child(BlurredBackgroundComponent.self)
let topSeparator = Child(Rectangle.self)
let title = Child(MultilineTextComponent.self)
let secondaryTitle = Child(MultilineTextWithEntitiesComponent.self)
let bottomEdgeEffect = Child(EdgeEffectComponent.self)
@@ -3504,22 +3470,6 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
)
}
let topPanel = topPanel.update(
component: BlurredBackgroundComponent(
color: environment.theme.rootController.navigationBar.blurredBackgroundColor
),
availableSize: CGSize(width: context.availableSize.width, height: environment.navigationHeight),
transition: context.transition
)
let topSeparator = topSeparator.update(
component: Rectangle(
color: environment.theme.rootController.navigationBar.separatorColor
),
availableSize: CGSize(width: context.availableSize.width, height: UIScreenPixel),
transition: context.transition
)
let titleString: String
if case .premiumGift = context.component.source {
titleString = environment.strings.Premium_PremiumGift_Title
@@ -3743,14 +3693,12 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
.position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0))
)
let topPanelAlpha: CGFloat
let titleOffset: CGFloat
let titleScale: CGFloat
let titleOffsetDelta = (topInset + 160.0) - (environment.statusBarHeight + (environment.navigationHeight - environment.statusBarHeight) / 2.0)
let titleAlpha: CGFloat
if let topContentOffset = state.topContentOffset {
topPanelAlpha = min(20.0, max(0.0, topContentOffset - 95.0)) / 20.0
let topContentOffset = topContentOffset + max(0.0, min(1.0, topContentOffset / titleOffsetDelta)) * 10.0
titleOffset = topContentOffset
let fraction = max(0.0, min(1.0, titleOffset / titleOffsetDelta))
@@ -3762,36 +3710,29 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
titleAlpha = 1.0
}
} else {
topPanelAlpha = 0.0
titleScale = 1.0
titleOffset = 0.0
titleAlpha = state.otherPeerName != nil ? 0.0 : 1.0
}
context.add(header
context.addWithExternalContainer(header
.position(CGPoint(x: context.availableSize.width / 2.0, y: topInset + header.size.height / 2.0 - 30.0 - titleOffset * titleScale))
.scale(titleScale)
.scale(titleScale),
container: context.component.overNavigationContainer
)
context.add(topPanel
.position(CGPoint(x: context.availableSize.width / 2.0, y: topPanel.size.height / 2.0))
.opacity(topPanelAlpha)
)
context.add(topSeparator
.position(CGPoint(x: context.availableSize.width / 2.0, y: topPanel.size.height))
.opacity(topPanelAlpha)
)
context.add(title
context.addWithExternalContainer(title
.position(CGPoint(x: context.availableSize.width / 2.0, y: max(topInset + 160.0 - titleOffset, environment.statusBarHeight + (environment.navigationHeight - environment.statusBarHeight) / 2.0)))
.scale(titleScale)
.opacity(titleAlpha)
.opacity(titleAlpha),
container: context.component.overNavigationContainer
)
context.add(secondaryTitle
context.addWithExternalContainer(secondaryTitle
.position(CGPoint(x: context.availableSize.width / 2.0, y: max(topInset + 160.0 - titleOffset, environment.statusBarHeight + (environment.navigationHeight - environment.statusBarHeight) / 2.0)))
.scale(titleScale)
.opacity(max(0.0, 1.0 - titleAlpha * 1.8))
.opacity(max(0.0, 1.0 - titleAlpha * 1.8)),
container: context.component.overNavigationContainer
)
var isUnusedGift = false
@@ -3989,6 +3930,8 @@ public final class PremiumIntroScreen: ViewControllerComponentContainer {
public weak var containerView: UIView?
public var animationColor: UIColor?
private let overNavigationContainer: UIView
public convenience init(context: AccountContext, mode: Mode = .premium, source: PremiumSource, modal: Bool = true, forceDark: Bool = false, forceHasPremium: Bool = false) {
self.init(screenContext: .accountContext(context), mode: mode, source: source, modal: modal, forceDark: forceDark, forceHasPremium: forceHasPremium)
}
@@ -4005,7 +3948,11 @@ public final class PremiumIntroScreen: ViewControllerComponentContainer {
var completionImpl: (() -> Void)?
var copyLinkImpl: ((String) -> Void)?
var shareLinkImpl: ((String) -> Void)?
self.overNavigationContainer = UIView()
super.init(component: PremiumIntroScreenComponent(
overNavigationContainer: self.overNavigationContainer,
screenContext: screenContext,
mode: mode,
source: source,
@@ -4029,11 +3976,11 @@ public final class PremiumIntroScreen: ViewControllerComponentContainer {
shareLink: { link in
shareLinkImpl?(link)
}
), navigationBarAppearance: .transparent, presentationMode: modal ? .modal : .default, theme: forceDark ? .dark : .default, updatedPresentationData: screenContext.updatedPresentationData)
), navigationBarAppearance: .default, presentationMode: modal ? .modal : .default, theme: forceDark ? .dark : .default, updatedPresentationData: screenContext.updatedPresentationData)
self._hasGlassStyle = true
if modal {
let cancelItem = UIBarButtonItem(title: presentationData.strings.Common_Close, style: .plain, target: self, action: #selector(self.cancelPressed))
self.navigationItem.setLeftBarButton(cancelItem, animated: false)
self.navigationPresentation = .modal
} else {
self.navigationPresentation = .modalInLargeLayout
@@ -4107,12 +4054,20 @@ public final class PremiumIntroScreen: ViewControllerComponentContainer {
context.account.viewTracker.keepQuickRepliesApproximatelyUpdated()
context.account.viewTracker.keepBusinessLinksApproximatelyUpdated()
}
if let navigationBar = self.navigationBar {
navigationBar.view.insertSubview(self.overNavigationContainer, aboveSubview: navigationBar.backgroundView)
}
}
required public init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override public func viewDidLoad() {
super.viewDidLoad()
}
public override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
self.dismissAllTooltips()
@@ -4145,10 +4100,10 @@ public final class PremiumIntroScreen: ViewControllerComponentContainer {
super.containerLayoutUpdated(layout, transition: transition)
if !self.didSetReady {
if let view = self.node.hostView.findTaggedView(tag: PremiumCoinComponent.View.Tag()) as? PremiumCoinComponent.View {
if let view = findTaggedComponentViewImpl(view: self.view, tag: PremiumCoinComponent.View.Tag()) as? PremiumCoinComponent.View {
self.didSetReady = true
self._ready.set(view.ready)
} else if let view = self.node.hostView.findTaggedView(tag: PremiumStarComponent.View.Tag()) as? PremiumStarComponent.View {
} else if let view = findTaggedComponentViewImpl(view: self.view, tag: PremiumStarComponent.View.Tag()) as? PremiumStarComponent.View {
self.didSetReady = true
self._ready.set(view.ready)
@@ -4161,7 +4116,7 @@ public final class PremiumIntroScreen: ViewControllerComponentContainer {
self.containerView = nil
self.animationColor = nil
}
} else if let view = self.node.hostView.findTaggedView(tag: EmojiHeaderComponent.View.Tag()) as? EmojiHeaderComponent.View {
} else if let view = findTaggedComponentViewImpl(view: self.view, tag: EmojiHeaderComponent.View.Tag()) as? EmojiHeaderComponent.View {
self.didSetReady = true
self._ready.set(view.ready)
@@ -864,7 +864,7 @@ private final class LimitSheetContent: CombinedComponent {
component: AnyComponentWithIdentity(id: "close", component: AnyComponent(
BundleIconComponent(
name: "Navigation/Close",
tintColor: theme.rootController.navigationBar.glassBarButtonForegroundColor
tintColor: theme.chat.inputPanel.panelControlColor
)
)),
action: { _ in
@@ -170,7 +170,7 @@ private final class SheetContent: CombinedComponent {
component: AnyComponentWithIdentity(id: "close", component: AnyComponent(
BundleIconComponent(
name: "Navigation/Close",
tintColor: theme.rootController.navigationBar.glassBarButtonForegroundColor
tintColor: theme.chat.inputPanel.panelControlColor
)
)),
action: { _ in
@@ -91,7 +91,7 @@ private final class SubscriptionsCountItemNode: ListViewItemNode {
return textNode
}
super.init(layerBacked: false, dynamicBounce: false)
super.init(layerBacked: false)
self.textNodes.forEach(self.addSubnode)
}