Various improvements

This commit is contained in:
Ilya Laktyushin 2023-09-05 14:07:48 +04:00
parent f8ebd4aa2f
commit 4c4080f5cb
27 changed files with 1097 additions and 321 deletions

View File

@ -9901,3 +9901,13 @@ Sorry for the inconvenience.";
"WebApp.SharePhoneTitle" = "Share Phone Number?";
"WebApp.SharePhoneConfirmation" = "**%@** will know your phone number. This can be useful for integration with other services.";
"WebApp.SharePhoneConfirmationUnblock" = "**%@** will know your phone number. This can be useful for integration with other services.\n\nThis will also unblock the bot.";
"Story.Editor.TooltipPremiumReactionLimitValue_1" = "**%@** reaction tag";
"Story.Editor.TooltipPremiumReactionLimitValue_any" = "**%@** reactions tags";
"Story.Editor.TooltipPremiumReactionLimitTitle" = "Increase Limit";
"Story.Editor.TooltipPremiumReactionLimitText" = "Upgrade to [Telegram Premium]() to add up to %@ to a story.";
"Story.Editor.TooltipReachedReactionLimitTitle" = "Limit Reached";
"Story.Editor.TooltipReachedReactionLimitText" = "You can't add up more than %@ to a story.";

View File

@ -952,6 +952,7 @@ public enum PremiumIntroSource {
case storiesPermanentViews
case storiesFormatting
case storiesExpirationDurations
case storiesSuggestedReactions
}
public enum PremiumDemoSubject {
@ -984,6 +985,7 @@ public enum PremiumLimitSubject {
case expiringStories
case storiesWeekly
case storiesMonthly
case storiesChannelBoost(level: Int32, link: String?)
}
public protocol ComposeController: ViewController {
@ -1025,6 +1027,7 @@ public protocol AccountContext: AnyObject {
var animatedEmojiStickers: [String: [StickerPackItem]] { get }
var isPremium: Bool { get }
var userLimits: EngineConfiguration.UserLimits { get }
var imageCache: AnyObject? { get }

View File

@ -19,7 +19,7 @@ public enum AttachmentButtonType: Equatable {
case location
case contact
case poll
case app(Peer, String, [AttachMenuBots.Bot.IconName: TelegramMediaFile])
case app(EnginePeer, String, [AttachMenuBots.Bot.IconName: TelegramMediaFile])
case gift
case standalone
@ -56,7 +56,7 @@ public enum AttachmentButtonType: Equatable {
return false
}
case let .app(lhsPeer, lhsTitle, lhsIcons):
if case let .app(rhsPeer, rhsTitle, rhsIcons) = rhs, arePeersEqual(lhsPeer, rhsPeer), lhsTitle == rhsTitle, lhsIcons == rhsIcons {
if case let .app(rhsPeer, rhsTitle, rhsIcons) = rhs, lhsPeer == rhsPeer, lhsTitle == rhsTitle, lhsIcons == rhsIcons {
return true
} else {
return false
@ -1040,7 +1040,7 @@ public class AttachmentController: ViewController {
|> deliverOnMainQueue).start(next: { bots in
for bot in bots {
for (name, file) in bot.icons {
if [.iOSAnimated, .placeholder].contains(name), let peer = PeerReference(bot.peer) {
if [.iOSAnimated, .placeholder].contains(name), let peer = PeerReference(bot.peer._asPeer()) {
if case .placeholder = name {
let path = context.account.postbox.mediaBox.cachedRepresentationCompletePath(file.resource.id, representation: CachedPreparedSvgRepresentation())
if !FileManager.default.fileExists(atPath: path) {

View File

@ -176,7 +176,7 @@ private final class AttachButtonComponent: CombinedComponent {
let imageName: String
var imageFile: TelegramMediaFile?
var animationFile: TelegramMediaFile?
var botPeer: Peer?
var botPeer: EnginePeer?
let component = context.component
let strings = component.strings
@ -245,7 +245,7 @@ private final class AttachButtonComponent: CombinedComponent {
)
} else {
var fileReference: FileMediaReference?
if let peer = botPeer.flatMap({ PeerReference($0 )}), let imageFile = imageFile {
if let peer = botPeer.flatMap({ PeerReference($0._asPeer())}), let imageFile = imageFile {
fileReference = .attachBot(peer: peer, media: imageFile)
}
@ -1143,7 +1143,7 @@ final class AttachmentPanel: ASDisplayNode, UIScrollViewDelegate {
if case let .app(peer, _, iconFiles) = type {
for (name, file) in iconFiles {
if [.default, .iOSAnimated, .placeholder].contains(name) {
if self.iconDisposables[file.fileId] == nil, let peer = PeerReference(peer) {
if self.iconDisposables[file.fileId] == nil, let peer = PeerReference(peer._asPeer()) {
if case .placeholder = name {
let account = self.context.account
let path = account.postbox.mediaBox.cachedRepresentationCompletePath(file.resource.id, representation: CachedPreparedSvgRepresentation())

View File

@ -32,9 +32,34 @@ public final class CounterContollerTitleView: UIView {
}
}
private var primaryTextColor: UIColor?
private var secondaryTextColor: UIColor?
public func updateTextColors(primary: UIColor?, secondary: UIColor?, transition: ContainedViewLayoutTransition) {
self.primaryTextColor = primary
self.secondaryTextColor = secondary
if case let .animated(duration, curve) = transition {
if let snapshotView = self.snapshotContentTree() {
snapshotView.frame = self.bounds
self.addSubview(snapshotView)
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, timingFunction: curve.timingFunction, removeOnCompletion: false, completion: { _ in
snapshotView.removeFromSuperview()
})
self.titleNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration, timingFunction: curve.timingFunction)
self.subtitleNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration, timingFunction: curve.timingFunction)
}
}
self.update()
}
private func update() {
self.titleNode.attributedText = NSAttributedString(string: self.title.title, font: Font.semibold(17.0), textColor: self.theme.rootController.navigationBar.primaryTextColor)
self.subtitleNode.attributedText = NSAttributedString(string: self.title.counter, font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor)
let primaryTextColor = self.primaryTextColor ?? self.theme.rootController.navigationBar.primaryTextColor
let secondaryTextColor = self.secondaryTextColor ?? self.theme.rootController.navigationBar.secondaryTextColor
self.titleNode.attributedText = NSAttributedString(string: self.title.title, font: Font.semibold(17.0), textColor: primaryTextColor)
self.subtitleNode.attributedText = NSAttributedString(string: self.title.counter, font: Font.regular(13.0), textColor: secondaryTextColor)
self.accessibilityLabel = self.title.title
self.accessibilityValue = self.title.counter

View File

@ -1186,11 +1186,11 @@ open class NavigationBar: ASDisplayNode {
transition.updateAlpha(node: self.stripeNode, alpha: alpha, delay: 0.15)
}
public func updatePresentationData(_ presentationData: NavigationBarPresentationData) {
public func updatePresentationData(_ presentationData: NavigationBarPresentationData, transition: ContainedViewLayoutTransition = .immediate) {
if presentationData.theme !== self.presentationData.theme || presentationData.strings !== self.presentationData.strings {
self.presentationData = presentationData
self.backgroundNode.updateColor(color: self.presentationData.theme.backgroundColor, transition: .immediate)
self.backgroundNode.updateColor(color: self.presentationData.theme.backgroundColor, transition: transition)
self.backButtonNode.color = self.presentationData.theme.buttonColor
self.backButtonNode.disabledColor = self.presentationData.theme.disabledButtonColor

View File

@ -1022,6 +1022,7 @@ private final class DrawingScreenComponent: CombinedComponent {
} else {
self?.updateCurrentMode(.drawing)
}
return true
}
self.present(controller)
self.updated(transition: .easeInOut(duration: 0.2))

View File

@ -147,7 +147,6 @@ private final class StickerSelectionComponent: Component {
self.interaction = ChatEntityKeyboardInputNode.Interaction(
sendSticker: { [weak self] file, silent, schedule, query, clearInput, sourceView, sourceRect, sourceLayer, _ in
if let self, let controller = self.component?.getController() {
controller.completion(.file(file.media, .sticker))
controller.forEachController { c in
if let c = c as? StickerPackScreenImpl {
c.dismiss(animated: true)
@ -159,7 +158,9 @@ private final class StickerSelectionComponent: Component {
c.dismiss(animated: true)
}
})
controller.dismiss(animated: true)
if controller.completion(.file(file.media, .sticker)) {
controller.dismiss(animated: true)
}
}
return false
},
@ -167,8 +168,9 @@ private final class StickerSelectionComponent: Component {
},
sendGif: { [weak self] file, _, _, _, _ in
if let self, let controller = self.component?.getController() {
controller.completion(.video(file.media))
controller.dismiss(animated: true)
if controller.completion(.video(file.media)) {
controller.dismiss(animated: true)
}
}
return false
},
@ -176,8 +178,9 @@ private final class StickerSelectionComponent: Component {
if let self, let controller = self.component?.getController() {
if case let .internalReference(reference) = result {
if let file = reference.file {
controller.completion(.video(file))
controller.dismiss(animated: true)
if controller.completion(.video(file)) {
controller.dismiss(animated: true)
}
}
}
}
@ -570,11 +573,12 @@ public class StickerPickerScreen: ViewController {
let gifInputInteraction = GifPagerContentComponent.InputInteraction(
performItemAction: { [weak self] item, view, rect in
guard let self else {
guard let self, let controller = self.controller else {
return
}
self.controller?.completion(.video(item.file.media))
self.controller?.dismiss(animated: true)
if controller.completion(.video(item.file.media)) {
controller.dismiss(animated: true)
}
},
openGifContextMenu: { [weak self] item, sourceView, sourceRect, gesture, isSaved in
guard let self else {
@ -729,15 +733,17 @@ public class StickerPickerScreen: ViewController {
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Add"), color: theme.actionSheet.primaryTextColor)
}, action: { [weak self] _, f in
f(.default)
if let self {
if let self, let controller = self.controller {
if isSaved {
self.controller?.completion(.video(file.media))
self.controller?.dismiss(animated: true)
if controller.completion(.video(file.media)) {
self.controller?.dismiss(animated: true)
}
} else if let (_, result) = contextResult {
if case let .internalReference(reference) = result {
if let file = reference.file {
self.controller?.completion(.video(file))
self.controller?.dismiss(animated: true)
if controller.completion(.video(file)) {
self.controller?.dismiss(animated: true)
}
}
}
}
@ -867,8 +873,9 @@ public class StickerPickerScreen: ViewController {
})
})
} else if let file = item.itemFile {
strongSelf.controller?.completion(.file(file, .sticker))
strongSelf.controller?.dismiss(animated: true)
if controller.completion(.file(file, .sticker)) {
controller.dismiss(animated: true)
}
} else if case let .staticEmoji(emoji) = item.content {
if let image = generateImage(CGSize(width: 256.0, height: 256.0), scale: 1.0, rotatedContext: { size, context in
context.clear(CGRect(origin: .zero, size: size))
@ -889,9 +896,12 @@ public class StickerPickerScreen: ViewController {
CTLineDraw(line, context)
context.translateBy(x: -lineOrigin.x, y: -lineOrigin.y)
}) {
strongSelf.controller?.completion(.image(image, .sticker))
if controller.completion(.image(image, .sticker)) {
controller.dismiss(animated: true)
}
} else {
controller.dismiss(animated: true)
}
strongSelf.controller?.dismiss(animated: true)
}
},
deleteBackwards: nil,
@ -1222,10 +1232,10 @@ public class StickerPickerScreen: ViewController {
hideBackground: true,
stateContext: nil,
addImage: controller.hasGifs ? { [weak self] in
if let self {
self.controller?.completion(nil)
self.controller?.dismiss(animated: true)
self.controller?.presentGallery()
if let self, let controller = self.controller {
let _ = controller.completion(nil)
controller.dismiss(animated: true)
controller.presentGallery()
}
} : nil
)
@ -1263,11 +1273,12 @@ public class StickerPickerScreen: ViewController {
highlightedPackId: featuredStickerPack.info.id,
forceTheme: defaultDarkPresentationTheme,
sendSticker: { [weak self] fileReference, _, _ in
guard let self else {
guard let self, let controller = self.controller else {
return false
}
self.controller?.completion(.file(fileReference.media, .sticker))
self.controller?.dismiss(animated: true)
if controller.completion(.file(fileReference.media, .sticker)) {
controller.dismiss(animated: true)
}
return true
}
))
@ -1277,8 +1288,8 @@ public class StickerPickerScreen: ViewController {
}
})
} else {
self.controller?.completion(.file(file, .sticker))
self.controller?.dismiss(animated: true)
let _ = controller.completion(.file(file, .sticker))
controller.dismiss(animated: true)
}
},
deleteBackwards: nil,
@ -1490,10 +1501,10 @@ public class StickerPickerScreen: ViewController {
hideBackground: true,
stateContext: nil,
addImage: controller.hasGifs ? { [weak self] in
if let self {
self.controller?.completion(nil)
self.controller?.dismiss(animated: true)
self.controller?.presentGallery()
if let self, let controller = self.controller {
let _ = controller.completion(nil)
controller.dismiss(animated: true)
controller.presentGallery()
}
} : nil
)
@ -1520,7 +1531,7 @@ public class StickerPickerScreen: ViewController {
@objc func dimTapGesture(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state {
self.controller?.completion(nil)
let _ = self.controller?.completion(nil)
self.controller?.dismiss(animated: true)
}
}
@ -1865,7 +1876,7 @@ public class StickerPickerScreen: ViewController {
var dismissing = false
if bounds.minY < -60 || (bounds.minY < 0.0 && velocity.y > 300.0) || (self.isExpanded && bounds.minY.isZero && velocity.y > 1800.0) {
self.controller?.completion(nil)
let _ = self.controller?.completion(nil)
self.controller?.dismiss(animated: true, completion: nil)
dismissing = true
} else if self.isExpanded {
@ -1947,7 +1958,7 @@ public class StickerPickerScreen: ViewController {
public var pushController: (ViewController) -> Void = { _ in }
public var presentController: (ViewController) -> Void = { _ in }
public var completion: (DrawingStickerEntity.Content?) -> Void = { _ in }
public var completion: (DrawingStickerEntity.Content?) -> Bool = { _ in return true }
public var presentGallery: () -> Void = { }
public var presentLocationPicker: () -> Void = { }

View File

@ -74,12 +74,36 @@ public final class MoreButtonNode: ASDisplayNode {
private let buttonNode: HighlightableButtonNode
public let iconNode: MoreIconNode
private var color: UIColor?
public var theme: PresentationTheme {
didSet {
self.iconNode.customColor = self.theme.rootController.navigationBar.buttonColor
self.update()
}
}
public func updateColor(_ color: UIColor?, transition: ContainedViewLayoutTransition) {
self.color = color
if case let .animated(duration, curve) = transition {
if let snapshotView = self.iconNode.view.snapshotContentTree() {
snapshotView.frame = self.iconNode.frame
self.view.addSubview(snapshotView)
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, timingFunction: curve.timingFunction, removeOnCompletion: false, completion: { _ in
snapshotView.removeFromSuperview()
})
self.iconNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration, timingFunction: curve.timingFunction)
}
}
self.update()
}
private func update() {
let color = self.color ?? self.theme.rootController.navigationBar.buttonColor
self.iconNode.customColor = color
}
public init(theme: PresentationTheme) {
self.theme = theme

View File

@ -220,6 +220,12 @@ public enum PremiumSource: Equatable {
} else {
return false
}
case .storiesSuggestedReactions:
if case .storiesSuggestedReactions = rhs {
return true
} else {
return false
}
}
}
@ -255,6 +261,7 @@ public enum PremiumSource: Equatable {
case storiesPermanentViews
case storiesFormatting
case storiesExpirationDurations
case storiesSuggestedReactions
var identifier: String? {
switch self {
@ -324,6 +331,8 @@ public enum PremiumSource: Equatable {
return "stories__links_and_formatting"
case .storiesExpirationDurations:
return "stories__expiration_durations"
case .storiesSuggestedReactions:
return "stories__suggested_reactions"
}
}
}

View File

@ -45,6 +45,7 @@ private class PremiumLimitAnimationComponent: Component {
private let badgeText: String?
private let badgePosition: CGFloat
private let badgeGraphPosition: CGFloat
private let invertProgress: Bool
private let isPremiumDisabled: Bool
init(
@ -55,6 +56,7 @@ private class PremiumLimitAnimationComponent: Component {
badgeText: String?,
badgePosition: CGFloat,
badgeGraphPosition: CGFloat,
invertProgress: Bool,
isPremiumDisabled: Bool
) {
self.iconName = iconName
@ -64,6 +66,7 @@ private class PremiumLimitAnimationComponent: Component {
self.badgeText = badgeText
self.badgePosition = badgePosition
self.badgeGraphPosition = badgeGraphPosition
self.invertProgress = invertProgress
self.isPremiumDisabled = isPremiumDisabled
}
@ -89,6 +92,9 @@ private class PremiumLimitAnimationComponent: Component {
if lhs.badgeGraphPosition != rhs.badgeGraphPosition {
return false
}
if lhs.invertProgress != rhs.invertProgress {
return false
}
if lhs.isPremiumDisabled != rhs.isPremiumDisabled {
return false
}
@ -96,6 +102,8 @@ private class PremiumLimitAnimationComponent: Component {
}
final class View: UIView {
private var component: PremiumLimitAnimationComponent?
private let container: SimpleLayer
private let inactiveBackground: SimpleLayer
@ -240,6 +248,7 @@ private class PremiumLimitAnimationComponent: Component {
var previousAvailableSize: CGSize?
func update(component: PremiumLimitAnimationComponent, availableSize: CGSize, transition: Transition) -> CGSize {
self.component = component
self.inactiveBackground.backgroundColor = component.inactiveColor.cgColor
self.activeBackground.backgroundColor = component.activeColors.last?.cgColor
@ -255,9 +264,13 @@ private class PremiumLimitAnimationComponent: Component {
let activeWidth: CGFloat = containerFrame.width - activityPosition
if !component.isPremiumDisabled {
self.inactiveBackground.frame = CGRect(origin: .zero, size: CGSize(width: activityPosition, height: lineHeight))
self.activeContainer.frame = CGRect(origin: CGPoint(x: activityPosition, y: 0.0), size: CGSize(width: activeWidth, height: lineHeight))
if component.invertProgress {
self.inactiveBackground.frame = CGRect(origin: CGPoint(x: activityPosition, y: 0.0), size: CGSize(width: availableSize.width - activityPosition, height: lineHeight))
self.activeContainer.frame = CGRect(origin: .zero, size: CGSize(width: activityPosition, height: lineHeight))
} else {
self.inactiveBackground.frame = CGRect(origin: .zero, size: CGSize(width: activityPosition, height: lineHeight))
self.activeContainer.frame = CGRect(origin: CGPoint(x: activityPosition, y: 0.0), size: CGSize(width: activeWidth, height: lineHeight))
}
self.activeBackground.frame = CGRect(origin: .zero, size: CGSize(width: activeWidth * (1.0 + 0.35), height: lineHeight))
if self.activeBackground.animation(forKey: "movement") == nil {
self.activeBackground.position = CGPoint(x: -self.activeContainer.frame.width * 0.35, y: lineHeight / 2.0)
@ -373,6 +386,9 @@ private class PremiumLimitAnimationComponent: Component {
}
private func setupGradientAnimations() {
guard let _ = self.component else {
return
}
if let _ = self.badgeForeground.animation(forKey: "movement") {
} else {
CATransaction.begin()
@ -440,6 +456,7 @@ public final class PremiumLimitDisplayComponent: CombinedComponent {
let badgeText: String?
let badgePosition: CGFloat
let badgeGraphPosition: CGFloat
let invertProgress: Bool
let isPremiumDisabled: Bool
public init(
@ -455,6 +472,7 @@ public final class PremiumLimitDisplayComponent: CombinedComponent {
badgeText: String?,
badgePosition: CGFloat,
badgeGraphPosition: CGFloat,
invertProgress: Bool = false,
isPremiumDisabled: Bool
) {
self.inactiveColor = inactiveColor
@ -469,6 +487,7 @@ public final class PremiumLimitDisplayComponent: CombinedComponent {
self.badgeText = badgeText
self.badgePosition = badgePosition
self.badgeGraphPosition = badgeGraphPosition
self.invertProgress = invertProgress
self.isPremiumDisabled = isPremiumDisabled
}
@ -509,6 +528,9 @@ public final class PremiumLimitDisplayComponent: CombinedComponent {
if lhs.badgeGraphPosition != rhs.badgeGraphPosition {
return false
}
if lhs.invertProgress != rhs.invertProgress {
return false
}
if lhs.isPremiumDisabled != rhs.isPremiumDisabled {
return false
}
@ -528,6 +550,16 @@ public final class PremiumLimitDisplayComponent: CombinedComponent {
let height: CGFloat = 120.0
let lineHeight: CGFloat = 30.0
let leftTextColor: UIColor
let rightTextColor: UIColor
if component.invertProgress {
leftTextColor = component.activeTitleColor
rightTextColor = component.inactiveTitleColor
} else {
leftTextColor = component.inactiveTitleColor
rightTextColor = component.activeTitleColor
}
let animation = animation.update(
component: PremiumLimitAnimationComponent(
iconName: component.badgeIconName,
@ -537,6 +569,7 @@ public final class PremiumLimitDisplayComponent: CombinedComponent {
badgeText: component.badgeText,
badgePosition: component.badgePosition,
badgeGraphPosition: component.badgeGraphPosition,
invertProgress: component.invertProgress,
isPremiumDisabled: component.isPremiumDisabled
),
availableSize: CGSize(width: context.availableSize.width, height: height),
@ -554,7 +587,7 @@ public final class PremiumLimitDisplayComponent: CombinedComponent {
NSAttributedString(
string: component.inactiveTitle,
font: Font.semibold(15.0),
textColor: component.inactiveTitleColor
textColor: leftTextColor
)
)
),
@ -568,7 +601,7 @@ public final class PremiumLimitDisplayComponent: CombinedComponent {
NSAttributedString(
string: component.inactiveValue,
font: Font.semibold(15.0),
textColor: component.inactiveTitleColor
textColor: leftTextColor
)
)
),
@ -582,7 +615,7 @@ public final class PremiumLimitDisplayComponent: CombinedComponent {
NSAttributedString(
string: component.activeTitle,
font: Font.semibold(15.0),
textColor: component.activeTitleColor
textColor: rightTextColor
)
)
),
@ -596,7 +629,7 @@ public final class PremiumLimitDisplayComponent: CombinedComponent {
NSAttributedString(
string: component.activeValue,
font: Font.semibold(15.0),
textColor: component.activeTitleColor
textColor: rightTextColor
)
)
),
@ -720,6 +753,7 @@ private final class LimitSheetContent: CombinedComponent {
let title = Child(MultilineTextComponent.self)
let text = Child(BalancedTextComponent.self)
let limit = Child(PremiumLimitDisplayComponent.self)
let linkButton = Child(SolidRoundedButtonComponent.self)
let button = Child(SolidRoundedButtonComponent.self)
return { context in
@ -761,219 +795,262 @@ private final class LimitSheetContent: CombinedComponent {
)
var titleText = strings.Premium_LimitReached
var actionButtonText: String?
var buttonAnimationName: String? = "premium_x2"
var buttonIconName: String?
let iconName: String
var badgeText: String
var string: String
let defaultValue: String
var defaultTitle = strings.Premium_Free
var premiumTitle = strings.Premium_Premium
let premiumValue: String
let badgePosition: CGFloat
let badgeGraphPosition: CGFloat
var invertProgress = false
switch subject {
case .folders:
let limit = state.limits.maxFoldersCount
let premiumLimit = state.premiumLimits.maxFoldersCount
iconName = "Premium/Folder"
badgeText = "\(component.count)"
string = component.count >= premiumLimit ? strings.Premium_MaxFoldersCountFinalText("\(premiumLimit)").string : strings.Premium_MaxFoldersCountText("\(limit)", "\(premiumLimit)").string
defaultValue = component.count > limit ? "\(limit)" : ""
premiumValue = component.count >= premiumLimit ? "" : "\(premiumLimit)"
if component.count >= premiumLimit {
badgeGraphPosition = max(0.15, CGFloat(limit) / CGFloat(premiumLimit))
} else {
badgeGraphPosition = max(0.15, CGFloat(component.count) / CGFloat(premiumLimit))
}
badgePosition = max(0.15, CGFloat(component.count) / CGFloat(premiumLimit))
if !state.isPremium && badgePosition > 0.5 {
string = strings.Premium_MaxFoldersCountText("\(limit)", "\(premiumLimit)").string
}
if isPremiumDisabled {
badgeText = "\(limit)"
string = strings.Premium_MaxFoldersCountNoPremiumText("\(limit)").string
}
case .chatsPerFolder:
let limit = state.limits.maxFolderChatsCount
let premiumLimit = state.premiumLimits.maxFolderChatsCount
iconName = "Premium/Chat"
badgeText = "\(component.count)"
string = component.count >= premiumLimit ? strings.Premium_MaxChatsInFolderFinalText("\(premiumLimit)").string : strings.Premium_MaxChatsInFolderText("\(limit)", "\(premiumLimit)").string
defaultValue = component.count > limit ? "\(limit)" : ""
premiumValue = component.count >= premiumLimit ? "" : "\(premiumLimit)"
badgePosition = CGFloat(component.count) / CGFloat(premiumLimit)
badgeGraphPosition = badgePosition
if isPremiumDisabled {
badgeText = "\(limit)"
string = strings.Premium_MaxChatsInFolderNoPremiumText("\(limit)").string
}
case .channels:
let limit = state.limits.maxChannelsCount
let premiumLimit = state.premiumLimits.maxChannelsCount
iconName = "Premium/Chat"
badgeText = "\(component.count)"
string = component.count >= premiumLimit ? strings.Premium_MaxChannelsFinalText("\(premiumLimit)").string : strings.Premium_MaxChannelsText("\(limit)", "\(premiumLimit)").string
defaultValue = component.count > limit ? "\(limit)" : ""
premiumValue = component.count >= premiumLimit ? "" : "\(premiumLimit)"
if component.count >= premiumLimit {
badgeGraphPosition = max(0.15, CGFloat(limit) / CGFloat(premiumLimit))
} else {
badgeGraphPosition = max(0.15, CGFloat(component.count) / CGFloat(premiumLimit))
}
badgePosition = max(0.15, CGFloat(component.count) / CGFloat(premiumLimit))
if isPremiumDisabled {
badgeText = "\(limit)"
string = strings.Premium_MaxChannelsNoPremiumText("\(limit)").string
}
case .linksPerSharedFolder:
/*let count: Int32 = 5 + Int32("".count)// component.count
let limit: Int32 = 5 + Int32("".count)//state.limits.maxSharedFolderInviteLinks
let premiumLimit: Int32 = 100 + Int32("".count)//state.premiumLimits.maxSharedFolderInviteLinks*/
let count: Int32 = component.count
let limit: Int32 = state.limits.maxSharedFolderInviteLinks
let premiumLimit: Int32 = state.premiumLimits.maxSharedFolderInviteLinks
iconName = "Premium/Link"
badgeText = "\(count)"
string = count >= premiumLimit ? strings.Premium_MaxSharedFolderLinksFinalText("\(premiumLimit)").string : strings.Premium_MaxSharedFolderLinksText("\(limit)", "\(premiumLimit)").string
defaultValue = count > limit ? "\(limit)" : ""
premiumValue = count >= premiumLimit ? "" : "\(premiumLimit)"
if count >= premiumLimit {
badgeGraphPosition = max(0.15, CGFloat(limit) / CGFloat(premiumLimit))
} else {
badgeGraphPosition = max(0.15, CGFloat(count) / CGFloat(premiumLimit))
}
badgePosition = max(0.15, CGFloat(count) / CGFloat(premiumLimit))
if isPremiumDisabled {
badgeText = "\(limit)"
string = strings.Premium_MaxSharedFolderLinksNoPremiumText("\(limit)").string
}
buttonAnimationName = nil
case .membershipInSharedFolders:
let limit = state.limits.maxSharedFolderJoin
let premiumLimit = state.premiumLimits.maxSharedFolderJoin
iconName = "Premium/Folder"
badgeText = "\(component.count)"
string = component.count >= premiumLimit ? strings.Premium_MaxSharedFolderMembershipFinalText("\(premiumLimit)").string : strings.Premium_MaxSharedFolderMembershipText("\(limit)", "\(premiumLimit)").string
defaultValue = component.count > limit ? "\(limit)" : ""
premiumValue = component.count >= premiumLimit ? "" : "\(premiumLimit)"
if component.count >= premiumLimit {
badgeGraphPosition = max(0.15, CGFloat(limit) / CGFloat(premiumLimit))
} else {
badgeGraphPosition = max(0.15, CGFloat(component.count) / CGFloat(premiumLimit))
}
badgePosition = max(0.15, CGFloat(component.count) / CGFloat(premiumLimit))
if isPremiumDisabled {
badgeText = "\(limit)"
string = strings.Premium_MaxSharedFolderMembershipNoPremiumText("\(limit)").string
}
buttonAnimationName = nil
case .pins:
let limit = state.limits.maxPinnedChatCount
let premiumLimit = state.premiumLimits.maxPinnedChatCount
iconName = "Premium/Pin"
badgeText = "\(component.count)"
string = component.count >= premiumLimit ? strings.Premium_MaxPinsFinalText("\(premiumLimit)").string : strings.Premium_MaxPinsText("\(limit)", "\(premiumLimit)").string
defaultValue = component.count > limit ? "\(limit)" : ""
premiumValue = component.count >= premiumLimit ? "" : "\(premiumLimit)"
badgePosition = CGFloat(component.count) / CGFloat(premiumLimit)
badgeGraphPosition = badgePosition
if isPremiumDisabled {
badgeText = "\(limit)"
string = strings.Premium_MaxPinsNoPremiumText("\(limit)").string
}
case .files:
let limit = Int64(state.limits.maxUploadFileParts) * 512 * 1024 + 1024 * 1024 * 100
let premiumLimit = Int64(state.premiumLimits.maxUploadFileParts) * 512 * 1024 + 1024 * 1024 * 100
iconName = "Premium/File"
badgeText = dataSizeString(component.count == 4 ? premiumLimit : limit, formatting: DataSizeStringFormatting(strings: environment.strings, decimalSeparator: environment.dateTimeFormat.decimalSeparator))
string = component.count == 4 ? strings.Premium_MaxFileSizeFinalText(dataSizeString(premiumLimit, formatting: DataSizeStringFormatting(strings: environment.strings, decimalSeparator: environment.dateTimeFormat.decimalSeparator))).string : strings.Premium_MaxFileSizeText(dataSizeString(premiumLimit, formatting: DataSizeStringFormatting(strings: environment.strings, decimalSeparator: environment.dateTimeFormat.decimalSeparator))).string
defaultValue = component.count == 4 ? dataSizeString(limit, formatting: DataSizeStringFormatting(strings: environment.strings, decimalSeparator: environment.dateTimeFormat.decimalSeparator)) : ""
premiumValue = component.count != 4 ? dataSizeString(premiumLimit, formatting: DataSizeStringFormatting(strings: environment.strings, decimalSeparator: environment.dateTimeFormat.decimalSeparator)) : ""
badgePosition = component.count == 4 ? 1.0 : 0.5
badgeGraphPosition = badgePosition
titleText = strings.Premium_FileTooLarge
if isPremiumDisabled {
badgeText = dataSizeString(limit, formatting: DataSizeStringFormatting(strings: environment.strings, decimalSeparator: environment.dateTimeFormat.decimalSeparator))
string = strings.Premium_MaxFileSizeNoPremiumText(dataSizeString(premiumLimit, formatting: DataSizeStringFormatting(strings: environment.strings, decimalSeparator: environment.dateTimeFormat.decimalSeparator))).string
}
case .accounts:
let limit = 3
let premiumLimit = limit + 1
iconName = "Premium/Account"
badgeText = "\(component.count)"
string = component.count >= premiumLimit ? strings.Premium_MaxAccountsFinalText("\(premiumLimit)").string : strings.Premium_MaxAccountsText("\(limit)").string
defaultValue = component.count > limit ? "\(limit)" : ""
premiumValue = component.count >= premiumLimit ? "" : "\(premiumLimit)"
if component.count == limit {
badgePosition = 0.5
} else {
badgePosition = min(1.0, CGFloat(component.count) / CGFloat(premiumLimit))
}
badgeGraphPosition = badgePosition
buttonAnimationName = "premium_addone"
if isPremiumDisabled {
badgeText = "\(limit)"
string = strings.Premium_MaxAccountsNoPremiumText("\(limit)").string
}
case .expiringStories:
let limit = state.limits.maxExpiringStoriesCount
let premiumLimit = state.premiumLimits.maxExpiringStoriesCount
iconName = "Premium/Stories"
case .folders:
let limit = state.limits.maxFoldersCount
let premiumLimit = state.premiumLimits.maxFoldersCount
iconName = "Premium/Folder"
badgeText = "\(component.count)"
string = component.count >= premiumLimit ? strings.Premium_MaxFoldersCountFinalText("\(premiumLimit)").string : strings.Premium_MaxFoldersCountText("\(limit)", "\(premiumLimit)").string
defaultValue = component.count > limit ? "\(limit)" : ""
premiumValue = component.count >= premiumLimit ? "" : "\(premiumLimit)"
if component.count >= premiumLimit {
badgeGraphPosition = max(0.15, CGFloat(limit) / CGFloat(premiumLimit))
} else {
badgeGraphPosition = max(0.15, CGFloat(component.count) / CGFloat(premiumLimit))
}
badgePosition = max(0.15, CGFloat(component.count) / CGFloat(premiumLimit))
if !state.isPremium && badgePosition > 0.5 {
string = strings.Premium_MaxFoldersCountText("\(limit)", "\(premiumLimit)").string
}
if isPremiumDisabled {
badgeText = "\(limit)"
string = component.count >= premiumLimit ? strings.Premium_MaxExpiringStoriesFinalText("\(premiumLimit)").string : strings.Premium_MaxExpiringStoriesText("\(limit)", "\(premiumLimit)").string
defaultValue = ""
premiumValue = component.count >= premiumLimit ? "" : "\(premiumLimit)"
badgePosition = max(0.32, CGFloat(component.count) / CGFloat(premiumLimit))
badgeGraphPosition = badgePosition
if isPremiumDisabled {
badgeText = "\(limit)"
string = strings.Premium_MaxExpiringStoriesNoPremiumText("\(limit)").string
}
buttonAnimationName = nil
case .storiesWeekly:
let limit = state.limits.maxStoriesWeeklyCount
let premiumLimit = state.premiumLimits.maxStoriesWeeklyCount
iconName = "Premium/Stories"
string = strings.Premium_MaxFoldersCountNoPremiumText("\(limit)").string
}
case .chatsPerFolder:
let limit = state.limits.maxFolderChatsCount
let premiumLimit = state.premiumLimits.maxFolderChatsCount
iconName = "Premium/Chat"
badgeText = "\(component.count)"
string = component.count >= premiumLimit ? strings.Premium_MaxChatsInFolderFinalText("\(premiumLimit)").string : strings.Premium_MaxChatsInFolderText("\(limit)", "\(premiumLimit)").string
defaultValue = component.count > limit ? "\(limit)" : ""
premiumValue = component.count >= premiumLimit ? "" : "\(premiumLimit)"
badgePosition = CGFloat(component.count) / CGFloat(premiumLimit)
badgeGraphPosition = badgePosition
if isPremiumDisabled {
badgeText = "\(limit)"
string = component.count >= premiumLimit ? strings.Premium_MaxStoriesWeeklyFinalText("\(premiumLimit)").string : strings.Premium_MaxStoriesWeeklyText("\(limit)", "\(premiumLimit)").string
defaultValue = ""
premiumValue = component.count >= premiumLimit ? "" : "\(premiumLimit)"
badgePosition = max(0.32, CGFloat(component.count) / CGFloat(premiumLimit))
badgeGraphPosition = badgePosition
if isPremiumDisabled {
badgeText = "\(limit)"
string = strings.Premium_MaxStoriesWeeklyNoPremiumText("\(limit)").string
}
buttonAnimationName = nil
case .storiesMonthly:
let limit = state.limits.maxStoriesMonthlyCount
let premiumLimit = state.premiumLimits.maxStoriesMonthlyCount
iconName = "Premium/Stories"
string = strings.Premium_MaxChatsInFolderNoPremiumText("\(limit)").string
}
case .channels:
let limit = state.limits.maxChannelsCount
let premiumLimit = state.premiumLimits.maxChannelsCount
iconName = "Premium/Chat"
badgeText = "\(component.count)"
string = component.count >= premiumLimit ? strings.Premium_MaxChannelsFinalText("\(premiumLimit)").string : strings.Premium_MaxChannelsText("\(limit)", "\(premiumLimit)").string
defaultValue = component.count > limit ? "\(limit)" : ""
premiumValue = component.count >= premiumLimit ? "" : "\(premiumLimit)"
if component.count >= premiumLimit {
badgeGraphPosition = max(0.15, CGFloat(limit) / CGFloat(premiumLimit))
} else {
badgeGraphPosition = max(0.15, CGFloat(component.count) / CGFloat(premiumLimit))
}
badgePosition = max(0.15, CGFloat(component.count) / CGFloat(premiumLimit))
if isPremiumDisabled {
badgeText = "\(limit)"
string = component.count >= premiumLimit ? strings.Premium_MaxStoriesMonthlyFinalText("\(premiumLimit)").string : strings.Premium_MaxStoriesMonthlyText("\(limit)", "\(premiumLimit)").string
defaultValue = ""
premiumValue = component.count >= premiumLimit ? "" : "\(premiumLimit)"
badgePosition = max(0.32, CGFloat(component.count) / CGFloat(premiumLimit))
badgeGraphPosition = badgePosition
if isPremiumDisabled {
badgeText = "\(limit)"
string = strings.Premium_MaxStoriesMonthlyNoPremiumText("\(limit)").string
string = strings.Premium_MaxChannelsNoPremiumText("\(limit)").string
}
case .linksPerSharedFolder:
/*let count: Int32 = 5 + Int32("".count)// component.count
let limit: Int32 = 5 + Int32("".count)//state.limits.maxSharedFolderInviteLinks
let premiumLimit: Int32 = 100 + Int32("".count)//state.premiumLimits.maxSharedFolderInviteLinks*/
let count: Int32 = component.count
let limit: Int32 = state.limits.maxSharedFolderInviteLinks
let premiumLimit: Int32 = state.premiumLimits.maxSharedFolderInviteLinks
iconName = "Premium/Link"
badgeText = "\(count)"
string = count >= premiumLimit ? strings.Premium_MaxSharedFolderLinksFinalText("\(premiumLimit)").string : strings.Premium_MaxSharedFolderLinksText("\(limit)", "\(premiumLimit)").string
defaultValue = count > limit ? "\(limit)" : ""
premiumValue = count >= premiumLimit ? "" : "\(premiumLimit)"
if count >= premiumLimit {
badgeGraphPosition = max(0.15, CGFloat(limit) / CGFloat(premiumLimit))
} else {
badgeGraphPosition = max(0.15, CGFloat(count) / CGFloat(premiumLimit))
}
badgePosition = max(0.15, CGFloat(count) / CGFloat(premiumLimit))
if isPremiumDisabled {
badgeText = "\(limit)"
string = strings.Premium_MaxSharedFolderLinksNoPremiumText("\(limit)").string
}
buttonAnimationName = nil
case .membershipInSharedFolders:
let limit = state.limits.maxSharedFolderJoin
let premiumLimit = state.premiumLimits.maxSharedFolderJoin
iconName = "Premium/Folder"
badgeText = "\(component.count)"
string = component.count >= premiumLimit ? strings.Premium_MaxSharedFolderMembershipFinalText("\(premiumLimit)").string : strings.Premium_MaxSharedFolderMembershipText("\(limit)", "\(premiumLimit)").string
defaultValue = component.count > limit ? "\(limit)" : ""
premiumValue = component.count >= premiumLimit ? "" : "\(premiumLimit)"
if component.count >= premiumLimit {
badgeGraphPosition = max(0.15, CGFloat(limit) / CGFloat(premiumLimit))
} else {
badgeGraphPosition = max(0.15, CGFloat(component.count) / CGFloat(premiumLimit))
}
badgePosition = max(0.15, CGFloat(component.count) / CGFloat(premiumLimit))
if isPremiumDisabled {
badgeText = "\(limit)"
string = strings.Premium_MaxSharedFolderMembershipNoPremiumText("\(limit)").string
}
buttonAnimationName = nil
case .pins:
let limit = state.limits.maxPinnedChatCount
let premiumLimit = state.premiumLimits.maxPinnedChatCount
iconName = "Premium/Pin"
badgeText = "\(component.count)"
string = component.count >= premiumLimit ? strings.Premium_MaxPinsFinalText("\(premiumLimit)").string : strings.Premium_MaxPinsText("\(limit)", "\(premiumLimit)").string
defaultValue = component.count > limit ? "\(limit)" : ""
premiumValue = component.count >= premiumLimit ? "" : "\(premiumLimit)"
badgePosition = CGFloat(component.count) / CGFloat(premiumLimit)
badgeGraphPosition = badgePosition
if isPremiumDisabled {
badgeText = "\(limit)"
string = strings.Premium_MaxPinsNoPremiumText("\(limit)").string
}
case .files:
let limit = Int64(state.limits.maxUploadFileParts) * 512 * 1024 + 1024 * 1024 * 100
let premiumLimit = Int64(state.premiumLimits.maxUploadFileParts) * 512 * 1024 + 1024 * 1024 * 100
iconName = "Premium/File"
badgeText = dataSizeString(component.count == 4 ? premiumLimit : limit, formatting: DataSizeStringFormatting(strings: environment.strings, decimalSeparator: environment.dateTimeFormat.decimalSeparator))
string = component.count == 4 ? strings.Premium_MaxFileSizeFinalText(dataSizeString(premiumLimit, formatting: DataSizeStringFormatting(strings: environment.strings, decimalSeparator: environment.dateTimeFormat.decimalSeparator))).string : strings.Premium_MaxFileSizeText(dataSizeString(premiumLimit, formatting: DataSizeStringFormatting(strings: environment.strings, decimalSeparator: environment.dateTimeFormat.decimalSeparator))).string
defaultValue = component.count == 4 ? dataSizeString(limit, formatting: DataSizeStringFormatting(strings: environment.strings, decimalSeparator: environment.dateTimeFormat.decimalSeparator)) : ""
premiumValue = component.count != 4 ? dataSizeString(premiumLimit, formatting: DataSizeStringFormatting(strings: environment.strings, decimalSeparator: environment.dateTimeFormat.decimalSeparator)) : ""
badgePosition = component.count == 4 ? 1.0 : 0.5
badgeGraphPosition = badgePosition
titleText = strings.Premium_FileTooLarge
if isPremiumDisabled {
badgeText = dataSizeString(limit, formatting: DataSizeStringFormatting(strings: environment.strings, decimalSeparator: environment.dateTimeFormat.decimalSeparator))
string = strings.Premium_MaxFileSizeNoPremiumText(dataSizeString(premiumLimit, formatting: DataSizeStringFormatting(strings: environment.strings, decimalSeparator: environment.dateTimeFormat.decimalSeparator))).string
}
case .accounts:
let limit = 3
let premiumLimit = limit + 1
iconName = "Premium/Account"
badgeText = "\(component.count)"
string = component.count >= premiumLimit ? strings.Premium_MaxAccountsFinalText("\(premiumLimit)").string : strings.Premium_MaxAccountsText("\(limit)").string
defaultValue = component.count > limit ? "\(limit)" : ""
premiumValue = component.count >= premiumLimit ? "" : "\(premiumLimit)"
if component.count == limit {
badgePosition = 0.5
} else {
badgePosition = min(1.0, CGFloat(component.count) / CGFloat(premiumLimit))
}
badgeGraphPosition = badgePosition
buttonAnimationName = "premium_addone"
if isPremiumDisabled {
badgeText = "\(limit)"
string = strings.Premium_MaxAccountsNoPremiumText("\(limit)").string
}
case .expiringStories:
let limit = state.limits.maxExpiringStoriesCount
let premiumLimit = state.premiumLimits.maxExpiringStoriesCount
iconName = "Premium/Stories"
badgeText = "\(limit)"
string = component.count >= premiumLimit ? strings.Premium_MaxExpiringStoriesFinalText("\(premiumLimit)").string : strings.Premium_MaxExpiringStoriesText("\(limit)", "\(premiumLimit)").string
defaultValue = ""
premiumValue = component.count >= premiumLimit ? "" : "\(premiumLimit)"
badgePosition = max(0.32, CGFloat(component.count) / CGFloat(premiumLimit))
badgeGraphPosition = badgePosition
if isPremiumDisabled {
badgeText = "\(limit)"
string = strings.Premium_MaxExpiringStoriesNoPremiumText("\(limit)").string
}
buttonAnimationName = nil
case .storiesWeekly:
let limit = state.limits.maxStoriesWeeklyCount
let premiumLimit = state.premiumLimits.maxStoriesWeeklyCount
iconName = "Premium/Stories"
badgeText = "\(limit)"
string = component.count >= premiumLimit ? strings.Premium_MaxStoriesWeeklyFinalText("\(premiumLimit)").string : strings.Premium_MaxStoriesWeeklyText("\(limit)", "\(premiumLimit)").string
defaultValue = ""
premiumValue = component.count >= premiumLimit ? "" : "\(premiumLimit)"
badgePosition = max(0.32, CGFloat(component.count) / CGFloat(premiumLimit))
badgeGraphPosition = badgePosition
if isPremiumDisabled {
badgeText = "\(limit)"
string = strings.Premium_MaxStoriesWeeklyNoPremiumText("\(limit)").string
}
buttonAnimationName = nil
case .storiesMonthly:
let limit = state.limits.maxStoriesMonthlyCount
let premiumLimit = state.premiumLimits.maxStoriesMonthlyCount
iconName = "Premium/Stories"
badgeText = "\(limit)"
string = component.count >= premiumLimit ? strings.Premium_MaxStoriesMonthlyFinalText("\(premiumLimit)").string : strings.Premium_MaxStoriesMonthlyText("\(limit)", "\(premiumLimit)").string
defaultValue = ""
premiumValue = component.count >= premiumLimit ? "" : "\(premiumLimit)"
badgePosition = max(0.32, CGFloat(component.count) / CGFloat(premiumLimit))
badgeGraphPosition = badgePosition
if isPremiumDisabled {
badgeText = "\(limit)"
string = strings.Premium_MaxStoriesMonthlyNoPremiumText("\(limit)").string
}
buttonAnimationName = nil
case let .storiesChannelBoost(level, link):
let limit = 2
// let premiumLimit = 1
iconName = "Premium/Boost"
badgeText = "\(limit)"
if let _ = link {
if level == 0 {
titleText = "Enable Stories"
string = "Your channel needs **2** more boosts to enable posting stories.\n\nAsk your **Premium** subscribers to boost your channel with this link:"
} else {
titleText = "Increase Story Limit"
string = "Your channel needs **1** more boosts to post **2** stories per day.\n\nAsk your **Premium** subscribers to boost your channel with this link:"
}
buttonAnimationName = nil
actionButtonText = "Copy Link"
buttonIconName = "Premium/CopyLink"
} else {
if level == 0 {
titleText = "Enable Stories for Channel"
string = "Channel needs **1** more boosts to enable posting stories. Help make it possible!"
} else {
titleText = "Help Upgrade Channel"
string = "**Channel** needs **2** more boosts to be able to post **2** stories per day."
}
actionButtonText = "Boost Channel"
buttonIconName = "Premium/BoostChannel"
}
buttonAnimationName = nil
defaultTitle = "Level \(level)"
defaultValue = ""
premiumValue = "Level \(level + 1)"
premiumTitle = ""
badgePosition = 0.32
badgeGraphPosition = badgePosition
invertProgress = true
}
var reachedMaximumLimit = badgePosition >= 1.0
if case .folders = subject, !state.isPremium {
@ -997,8 +1074,8 @@ private final class LimitSheetContent: CombinedComponent {
transition: .immediate
)
let textFont = Font.regular(17.0)
let boldTextFont = Font.semibold(17.0)
let textFont = Font.regular(15.0)
let boldTextFont = Font.semibold(15.0)
let textColor = theme.actionSheet.primaryTextColor
let markdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor), link: MarkdownAttributeSet(font: textFont, textColor: textColor), linkAttribute: { _ in
return nil
@ -1034,16 +1111,17 @@ private final class LimitSheetContent: CombinedComponent {
component: PremiumLimitDisplayComponent(
inactiveColor: theme.list.itemBlocksSeparatorColor.withAlphaComponent(0.5),
activeColors: gradientColors,
inactiveTitle: strings.Premium_Free,
inactiveTitle: defaultTitle,
inactiveValue: defaultValue,
inactiveTitleColor: theme.list.itemPrimaryTextColor,
activeTitle: strings.Premium_Premium,
activeTitle: premiumTitle,
activeValue: premiumValue,
activeTitleColor: .white,
badgeIconName: iconName,
badgeText: badgeText,
badgePosition: badgePosition,
badgeGraphPosition: badgeGraphPosition,
invertProgress: invertProgress,
isPremiumDisabled: isPremiumDisabled
),
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: context.availableSize.height),
@ -1056,8 +1134,7 @@ private final class LimitSheetContent: CombinedComponent {
let isIncreaseButton = !reachedMaximumLimit && !isPremiumDisabled
let button = button.update(
component: SolidRoundedButtonComponent(
title: isIncreaseButton ? strings.Premium_IncreaseLimit : strings.Common_OK,
title: actionButtonText ?? (isIncreaseButton ? strings.Premium_IncreaseLimit : strings.Common_OK),
theme: SolidRoundedButtonComponent.Theme(
backgroundColor: .black,
backgroundColors: gradientColors,
@ -1068,8 +1145,9 @@ private final class LimitSheetContent: CombinedComponent {
height: 50.0,
cornerRadius: 10.0,
gloss: isIncreaseButton,
iconName: buttonIconName,
animationName: isIncreaseButton ? buttonAnimationName : nil,
iconPosition: .right,
iconPosition: buttonIconName != nil ? .left : .right,
action: {
component.dismiss()
if isIncreaseButton {
@ -1081,7 +1159,37 @@ private final class LimitSheetContent: CombinedComponent {
transition: context.transition
)
var buttonOffset: CGFloat = 0.0
var textOffset: CGFloat = 228.0
if case let .storiesChannelBoost(level, link) = component.subject {
if let _ = link {
let linkButton = linkButton.update(
component: SolidRoundedButtonComponent(
title: "t.me/channel?boost",
theme: SolidRoundedButtonComponent.Theme(
backgroundColor: UIColor(rgb: 0x343436),
backgroundColors: [],
foregroundColor: .white
),
font: .regular,
fontSize: 17.0,
height: 50.0,
cornerRadius: 10.0,
action: {}
),
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: 50.0),
transition: context.transition
)
buttonOffset += 66.0
let linkFrame = CGRect(origin: CGPoint(x: sideInset, y: textOffset + ceil(text.size.height / 2.0) + 24.0), size: linkButton.size)
context.add(linkButton
.position(CGPoint(x: linkFrame.midX, y: linkFrame.midY))
)
} else if link == nil, level > 0 {
textOffset -= 26.0
}
}
if isPremiumDisabled {
textOffset -= 68.0
}
@ -1093,7 +1201,7 @@ private final class LimitSheetContent: CombinedComponent {
.position(CGPoint(x: context.availableSize.width / 2.0, y: textOffset))
)
let buttonFrame = CGRect(origin: CGPoint(x: sideInset, y: textOffset + ceil(text.size.height / 2.0) + 38.0), size: button.size)
let buttonFrame = CGRect(origin: CGPoint(x: sideInset, y: textOffset + ceil(text.size.height / 2.0) + buttonOffset + 24.0), size: button.size)
context.add(button
.position(CGPoint(x: buttonFrame.midX, y: buttonFrame.midY))
)
@ -1104,6 +1212,15 @@ private final class LimitSheetContent: CombinedComponent {
if isPremiumDisabled {
height -= 78.0
}
if case let .storiesChannelBoost(_, link) = component.subject {
if link != nil {
height += 66.0
} else {
height -= 53.0
}
}
contentSize = CGSize(width: context.availableSize.width, height: height + environment.safeInsets.bottom)
}
@ -1204,7 +1321,7 @@ private final class LimitSheetComponent: CombinedComponent {
}
public class PremiumLimitScreen: ViewControllerComponentContainer {
public enum Subject {
public enum Subject: Equatable {
case folders
case chatsPerFolder
case pins
@ -1216,6 +1333,8 @@ public class PremiumLimitScreen: ViewControllerComponentContainer {
case expiringStories
case storiesWeekly
case storiesMonthly
case storiesChannelBoost(level: Int32, link: String?)
}
public init(context: AccountContext, subject: PremiumLimitScreen.Subject, count: Int32, forceDark: Bool = false, cancel: @escaping () -> Void = {}, action: @escaping () -> Void) {

View File

@ -21,6 +21,7 @@ public struct UserLimitsConfiguration: Equatable {
public let maxExpiringStoriesCount: Int32
public let maxStoriesWeeklyCount: Int32
public let maxStoriesMonthlyCount: Int32
public let maxStoriesSuggestedReactions: Int32
public static var defaultValue: UserLimitsConfiguration {
return UserLimitsConfiguration(
@ -42,7 +43,8 @@ public struct UserLimitsConfiguration: Equatable {
maxStoryCaptionLength: 200,
maxExpiringStoriesCount: 3,
maxStoriesWeeklyCount: 7,
maxStoriesMonthlyCount: 30
maxStoriesMonthlyCount: 30,
maxStoriesSuggestedReactions: 1
)
}
@ -65,7 +67,8 @@ public struct UserLimitsConfiguration: Equatable {
maxStoryCaptionLength: Int32,
maxExpiringStoriesCount: Int32,
maxStoriesWeeklyCount: Int32,
maxStoriesMonthlyCount: Int32
maxStoriesMonthlyCount: Int32,
maxStoriesSuggestedReactions: Int32
) {
self.maxPinnedChatCount = maxPinnedChatCount
self.maxArchivedPinnedChatCount = maxArchivedPinnedChatCount
@ -86,6 +89,7 @@ public struct UserLimitsConfiguration: Equatable {
self.maxExpiringStoriesCount = maxExpiringStoriesCount
self.maxStoriesWeeklyCount = maxStoriesWeeklyCount
self.maxStoriesMonthlyCount = maxStoriesMonthlyCount
self.maxStoriesSuggestedReactions = maxStoriesSuggestedReactions
}
}
@ -129,5 +133,6 @@ extension UserLimitsConfiguration {
self.maxExpiringStoriesCount = getValue("story_expiring_limit", orElse: defaultValue.maxExpiringStoriesCount)
self.maxStoriesWeeklyCount = getValue("stories_sent_weekly_limit", orElse: defaultValue.maxStoriesWeeklyCount)
self.maxStoriesMonthlyCount = getValue("stories_sent_monthly_limit", orElse: defaultValue.maxStoriesMonthlyCount)
self.maxStoriesSuggestedReactions = getValue("stories_suggested_reactions_limit", orElse: defaultValue.maxStoriesMonthlyCount)
}
}

View File

@ -55,6 +55,7 @@ public enum EngineConfiguration {
public let maxExpiringStoriesCount: Int32
public let maxStoriesWeeklyCount: Int32
public let maxStoriesMonthlyCount: Int32
public let maxStoriesSuggestedReactions: Int32
public static var defaultValue: UserLimits {
return UserLimits(UserLimitsConfiguration.defaultValue)
@ -79,7 +80,8 @@ public enum EngineConfiguration {
maxStoryCaptionLength: Int32,
maxExpiringStoriesCount: Int32,
maxStoriesWeeklyCount: Int32,
maxStoriesMonthlyCount: Int32
maxStoriesMonthlyCount: Int32,
maxStoriesSuggestedReactions: Int32
) {
self.maxPinnedChatCount = maxPinnedChatCount
self.maxArchivedPinnedChatCount = maxArchivedPinnedChatCount
@ -100,6 +102,7 @@ public enum EngineConfiguration {
self.maxExpiringStoriesCount = maxExpiringStoriesCount
self.maxStoriesWeeklyCount = maxStoriesWeeklyCount
self.maxStoriesMonthlyCount = maxStoriesMonthlyCount
self.maxStoriesSuggestedReactions = maxStoriesSuggestedReactions
}
}
}
@ -155,7 +158,8 @@ public extension EngineConfiguration.UserLimits {
maxStoryCaptionLength: userLimitsConfiguration.maxStoryCaptionLength,
maxExpiringStoriesCount: userLimitsConfiguration.maxExpiringStoriesCount,
maxStoriesWeeklyCount: userLimitsConfiguration.maxStoriesWeeklyCount,
maxStoriesMonthlyCount: userLimitsConfiguration.maxStoriesMonthlyCount
maxStoriesMonthlyCount: userLimitsConfiguration.maxStoriesMonthlyCount,
maxStoriesSuggestedReactions: userLimitsConfiguration.maxStoriesSuggestedReactions
)
}
}

View File

@ -52,6 +52,9 @@ public final class AttachMenuBots: Equatable, Codable {
public static let hasSettings = Flags(rawValue: 1 << 0)
public static let requiresWriteAccess = Flags(rawValue: 1 << 1)
public static let showInAttachMenu = Flags(rawValue: 1 << 2)
public static let showInSettings = Flags(rawValue: 1 << 3)
public static let showInSettingsDisclaimer = Flags(rawValue: 1 << 4)
}
public struct PeerFlags: OptionSet, Codable {
@ -323,6 +326,15 @@ func managedSynchronizeAttachMenuBots(accountPeerId: PeerId, postbox: Postbox, n
if (apiFlags & (1 << 2)) != 0 {
flags.insert(.requiresWriteAccess)
}
if (apiFlags & (1 << 3)) != 0 {
flags.insert(.showInAttachMenu)
}
if (apiFlags & (1 << 4)) != 0 {
flags.insert(.showInSettings)
}
if (apiFlags & (1 << 5)) != 0 {
flags.insert(.showInSettingsDisclaimer)
}
resultBots.append(AttachMenuBots.Bot(peerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(botId)), name: name, icons: icons, peerTypes: peerTypes, flags: flags))
}
}
@ -427,13 +439,13 @@ func _internal_removeBotFromAttachMenu(accountPeerId: PeerId, postbox: Postbox,
}
public struct AttachMenuBot {
public let peer: Peer
public let peer: EnginePeer
public let shortName: String
public let icons: [AttachMenuBots.Bot.IconName: TelegramMediaFile]
public let peerTypes: AttachMenuBots.Bot.PeerFlags
public let flags: AttachMenuBots.Bot.Flags
init(peer: Peer, shortName: String, icons: [AttachMenuBots.Bot.IconName: TelegramMediaFile], peerTypes: AttachMenuBots.Bot.PeerFlags, flags: AttachMenuBots.Bot.Flags) {
init(peer: EnginePeer, shortName: String, icons: [AttachMenuBots.Bot.IconName: TelegramMediaFile], peerTypes: AttachMenuBots.Bot.PeerFlags, flags: AttachMenuBots.Bot.Flags) {
self.peer = peer
self.shortName = shortName
self.icons = icons
@ -450,7 +462,7 @@ func _internal_attachMenuBots(postbox: Postbox) -> Signal<[AttachMenuBot], NoErr
var resultBots: [AttachMenuBot] = []
for bot in cachedBots {
if let peer = transaction.getPeer(bot.peerId) {
resultBots.append(AttachMenuBot(peer: peer, shortName: bot.name, icons: bot.icons, peerTypes: bot.peerTypes, flags: bot.flags))
resultBots.append(AttachMenuBot(peer: EnginePeer(peer), shortName: bot.name, icons: bot.icons, peerTypes: bot.peerTypes, flags: bot.flags))
}
}
return resultBots
@ -465,7 +477,7 @@ func _internal_getAttachMenuBot(accountPeerId: PeerId, postbox: Postbox, network
return postbox.transaction { transaction -> Signal<AttachMenuBot, GetAttachMenuBotError> in
if cached, let cachedBots = cachedAttachMenuBots(transaction: transaction)?.bots {
if let bot = cachedBots.first(where: { $0.peerId == botId }), let peer = transaction.getPeer(bot.peerId) {
return .single(AttachMenuBot(peer: peer, shortName: bot.name, icons: bot.icons, peerTypes: bot.peerTypes, flags: bot.flags))
return .single(AttachMenuBot(peer: EnginePeer(peer), shortName: bot.name, icons: bot.icons, peerTypes: bot.peerTypes, flags: bot.flags))
}
}
@ -526,7 +538,16 @@ func _internal_getAttachMenuBot(accountPeerId: PeerId, postbox: Postbox, network
if (apiFlags & (1 << 2)) != 0 {
flags.insert(.requiresWriteAccess)
}
return .single(AttachMenuBot(peer: peer, shortName: name, icons: icons, peerTypes: peerTypes, flags: flags))
if (apiFlags & (1 << 3)) != 0 {
flags.insert(.showInAttachMenu)
}
if (apiFlags & (1 << 4)) != 0 {
flags.insert(.showInSettings)
}
if (apiFlags & (1 << 5)) != 0 {
flags.insert(.showInSettingsDisclaimer)
}
return .single(AttachMenuBot(peer: EnginePeer(peer), shortName: name, icons: icons, peerTypes: peerTypes, flags: flags))
}
}
}

View File

@ -10,11 +10,17 @@ private let botWebViewPlatform = "macos"
private let botWebViewPlatform = "ios"
#endif
public enum RequestSimpleWebViewSource {
case generic
case inline
case settings
}
public enum RequestSimpleWebViewError {
case generic
}
func _internal_requestSimpleWebView(postbox: Postbox, network: Network, botId: PeerId, url: String, inline: Bool, themeParams: [String: Any]?) -> Signal<String, RequestSimpleWebViewError> {
func _internal_requestSimpleWebView(postbox: Postbox, network: Network, botId: PeerId, url: String?, source: RequestSimpleWebViewSource, themeParams: [String: Any]?) -> Signal<String, RequestSimpleWebViewError> {
var serializedThemeParams: Api.DataJSON?
if let themeParams = themeParams, let data = try? JSONSerialization.data(withJSONObject: themeParams, options: []), let dataString = String(data: data, encoding: .utf8) {
serializedThemeParams = .dataJSON(data: dataString)
@ -28,8 +34,16 @@ func _internal_requestSimpleWebView(postbox: Postbox, network: Network, botId: P
if let _ = serializedThemeParams {
flags |= (1 << 0)
}
if inline {
switch source {
case .inline:
flags |= (1 << 1)
case .settings:
flags |= (1 << 2)
default:
break
}
if let _ = url {
flags |= (1 << 3)
}
return network.request(Api.functions.messages.requestSimpleWebView(flags: flags, bot: inputUser, url: url, startParam: nil, themeParams: serializedThemeParams, platform: botWebViewPlatform))
|> mapError { _ -> RequestSimpleWebViewError in

View File

@ -499,8 +499,8 @@ public extension TelegramEngine {
return _internal_requestWebView(postbox: self.account.postbox, network: self.account.network, stateManager: self.account.stateManager, peerId: peerId, botId: botId, url: url, payload: payload, themeParams: themeParams, fromMenu: fromMenu, replyToMessageId: replyToMessageId, threadId: threadId)
}
public func requestSimpleWebView(botId: PeerId, url: String, inline: Bool, themeParams: [String: Any]?) -> Signal<String, RequestSimpleWebViewError> {
return _internal_requestSimpleWebView(postbox: self.account.postbox, network: self.account.network, botId: botId, url: url, inline: inline, themeParams: themeParams)
public func requestSimpleWebView(botId: PeerId, url: String?, source: RequestSimpleWebViewSource, themeParams: [String: Any]?) -> Signal<String, RequestSimpleWebViewError> {
return _internal_requestSimpleWebView(postbox: self.account.postbox, network: self.account.network, botId: botId, url: url, source: source, themeParams: themeParams)
}
public func requestAppWebView(peerId: PeerId, appReference: BotAppReference, payload: String?, themeParams: [String: Any]?, allowWrite: Bool) -> Signal<String, RequestAppWebViewError> {

View File

@ -1363,7 +1363,7 @@ final class MediaEditorScreenComponent: Component {
}
}
bottomControlsTransition.setFrame(view: scrubberView, frame: scrubberFrame)
if !self.animatingButtons && !isAudioOnly {
if !self.animatingButtons && !(isAudioOnly && animateIn) {
transition.setAlpha(view: scrubberView, alpha: component.isDisplayingTool || component.isDismissing || component.isInteractingWithEntities || isEditingCaption ? 0.0 : 1.0)
} else if animateIn {
scrubberView.layer.animatePosition(from: CGPoint(x: 0.0, y: 44.0), to: .zero, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
@ -3167,6 +3167,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
} else {
mediaEditor.setAudioTrackTrimRange(0 ..< min(15, audioDuration), apply: true)
}
mediaEditor.seek(mediaEditor.values.videoTrimRange?.lowerBound ?? 0.0, andPlay: true)
self.requestUpdate(transition: .easeInOut(duration: 0.2))
if isScopedResource {
@ -3249,8 +3250,8 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
}
}
private var drawingScreen: DrawingScreen?
private var stickerScreen: StickerPickerScreen?
fileprivate var drawingScreen: DrawingScreen?
fileprivate var stickerScreen: StickerPickerScreen?
private var defaultToEmoji = false
private var previousDrawingData: Data?
@ -3350,6 +3351,14 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
controller.completion = { [weak self] content in
if let self {
if let content {
if case let .file(file, _) = content {
if file.isCustomEmoji {
self.defaultToEmoji = true
} else {
self.defaultToEmoji = false
}
}
let stickerEntity = DrawingStickerEntity(content: content)
let scale: CGFloat
if case .image = content {
@ -3364,18 +3373,11 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
self.hasAnyChanges = true
self.controller?.isSavingAvailable = true
self.controller?.requestLayout(transition: .immediate)
if case let .file(file, _) = content {
if file.isCustomEmoji {
self.defaultToEmoji = true
} else {
self.defaultToEmoji = false
}
}
}
self.stickerScreen = nil
self.mediaEditor?.play()
}
return true
}
controller.customModalStyleOverlayTransitionFactorUpdated = { [weak self, weak controller] transition in
if let self, let controller {
@ -3405,6 +3407,18 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
}
controller.addReaction = { [weak self, weak controller] in
if let self {
let maxReactionCount = self.context.userLimits.maxStoriesSuggestedReactions
var currentReactionCount = 0
self.entitiesView.eachView { entityView in
if let stickerEntity = entityView.entity as? DrawingStickerEntity, case let .file(_, type) = stickerEntity.content, case .reaction = type {
currentReactionCount += 1
}
}
if currentReactionCount >= maxReactionCount {
self.controller?.presentReactionPremiumSuggestion()
return
}
self.stickerScreen = nil
controller?.dismiss(animated: true)
@ -3421,6 +3435,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
self.controller?.present(controller, in: .window(.root))
return
case .text:
self.mediaEditor?.stop()
self.insertTextEntity()
self.hasAnyChanges = true
@ -4094,6 +4109,53 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
})
self.present(controller, in: .current)
}
fileprivate func presentReactionPremiumSuggestion() {
self.dismissAllTooltips()
let context = self.context
let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Configuration.UserLimits(isPremium: true))
|> deliverOnMainQueue).start(next: { [weak self] premiumLimits in
guard let self else {
return
}
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let limit = context.userLimits.maxStoriesSuggestedReactions
let content: UndoOverlayContent
if context.isPremium {
let value = presentationData.strings.Story_Editor_TooltipPremiumReactionLimitValue(premiumLimits.maxStoriesSuggestedReactions)
content = .info(
title: presentationData.strings.Story_Editor_TooltipReachedReactionLimitTitle,
text: presentationData.strings.Story_Editor_TooltipReachedReactionLimitText(value).string,
timeout: nil
)
} else {
let value = presentationData.strings.Story_Editor_TooltipPremiumReactionLimitValue(limit)
content = .premiumPaywall(
title: presentationData.strings.Story_Editor_TooltipPremiumReactionLimitTitle,
text: presentationData.strings.Story_Editor_TooltipPremiumReactionLimitText(value).string,
customUndoText: nil,
timeout: nil,
linkAction: nil
)
}
let controller = UndoOverlayController(presentationData: presentationData, content: content, elevatedLayout: true, position: .bottom, animateInAsReplacement: false, action: { [weak self] action in
if case .info = action, let self {
if let stickerScreen = self.node.stickerScreen {
self.node.stickerScreen = nil
stickerScreen.dismiss(animated: true)
}
let controller = context.sharedContext.makePremiumIntroController(context: context, source: .storiesSuggestedReactions, forceDark: true, dismissed: nil)
self.push(controller)
}
return true
})
self.present(controller, in: .window(.root))
})
}
fileprivate func presentCaptionLimitPremiumSuggestion(isPremium: Bool) {
self.dismissAllTooltips()

View File

@ -244,6 +244,8 @@ public final class AccountContextImpl: AccountContext {
private var userLimitsConfigurationDisposable: Disposable?
public private(set) var userLimits: EngineConfiguration.UserLimits
public private(set) var isPremium: Bool
public let imageCache: AnyObject?
public init(sharedContext: SharedAccountContextImpl, account: Account, limitsConfiguration: LimitsConfiguration, contentSettings: ContentSettings, appConfiguration: AppConfiguration, temp: Bool = false)
@ -255,6 +257,7 @@ public final class AccountContextImpl: AccountContext {
self.imageCache = DirectMediaImageCache(account: account)
self.userLimits = EngineConfiguration.UserLimits(UserLimitsConfiguration.defaultValue)
self.isPremium = false
self.downloadedMediaStoreManager = DownloadedMediaStoreManagerImpl(postbox: account.postbox, accountManager: sharedContext.accountManager)
@ -385,14 +388,19 @@ public final class AccountContextImpl: AccountContext {
})
self.userLimitsConfigurationDisposable = (self.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: account.peerId))
|> mapToSignal { peer -> Signal<EngineConfiguration.UserLimits, NoError> in
return self.engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.UserLimits(isPremium: peer?.isPremium ?? false))
|> mapToSignal { peer -> Signal<(Bool, EngineConfiguration.UserLimits), NoError> in
let isPremium = peer?.isPremium ?? false
return self.engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.UserLimits(isPremium: isPremium))
|> map { userLimits in
return (isPremium, userLimits)
}
}
|> deliverOnMainQueue).start(next: { [weak self] value in
|> deliverOnMainQueue).start(next: { [weak self] isPremium, userLimits in
guard let strongSelf = self else {
return
}
strongSelf.userLimits = value
strongSelf.isPremium = isPremium
strongSelf.userLimits = userLimits
})
}
@ -404,6 +412,7 @@ public final class AccountContextImpl: AccountContext {
self.countriesConfigurationDisposable?.dispose()
self.experimentalUISettingsDisposable?.dispose()
self.animatedEmojiStickersDisposable?.dispose()
self.userLimitsConfigurationDisposable?.dispose()
}
public func storeSecureIdPassword(password: String) {

View File

@ -4266,7 +4266,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
botAddress = bot.addressName ?? ""
}
strongSelf.messageActionCallbackDisposable.set(((strongSelf.context.engine.messages.requestSimpleWebView(botId: botId, url: url, inline: isInline, themeParams: generateWebAppThemeParams(strongSelf.presentationData.theme))
strongSelf.messageActionCallbackDisposable.set(((strongSelf.context.engine.messages.requestSimpleWebView(botId: botId, url: url, source: isInline ? .inline : .generic, themeParams: generateWebAppThemeParams(strongSelf.presentationData.theme))
|> afterDisposed {
updateProgress()
})
@ -13233,7 +13233,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
case bot(id: PeerId, payload: String?, justInstalled: Bool)
case gift
}
//editMediaOptions: MessageMediaEditingOptions?, editMediaReference: AnyMediaReference?, botId: PeerId? = nil, botPayload: String? = nil, botJustInstalled: Bool = false
private func presentAttachmentMenu(subject: AttachMenuSubject) {
guard let peer = self.presentationInterfaceState.renderedPeer?.peer else {
return
@ -13401,9 +13401,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
} else {
let _ = (context.engine.messages.getAttachMenuBot(botId: botId)
|> deliverOnMainQueue).start(next: { bot in
let peer = EnginePeer(bot.peer)
let controller = addWebAppToAttachmentController(context: context, peerName: peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), icons: bot.icons, requestWriteAccess: bot.flags.contains(.requiresWriteAccess), completion: { allowWrite in
let controller = addWebAppToAttachmentController(context: context, peerName: bot.peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), icons: bot.icons, requestWriteAccess: bot.flags.contains(.requiresWriteAccess), completion: { allowWrite in
let _ = (context.engine.messages.addBotToAttachMenu(botId: botId, allowWrite: allowWrite)
|> deliverOnMainQueue).start(error: { _ in

View File

@ -675,8 +675,7 @@ func openResolvedUrlImpl(_ resolvedUrl: ResolvedUrl, context: AccountContext, ur
|> deliverOnMainQueue).start(next: { bot in
let choose = filterChooseTypes(choose, peerTypes: bot.peerTypes)
let botPeer = EnginePeer(bot.peer)
let controller = addWebAppToAttachmentController(context: context, peerName: botPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), icons: bot.icons, requestWriteAccess: bot.flags.contains(.requiresWriteAccess), completion: { allowWrite in
let controller = addWebAppToAttachmentController(context: context, peerName: bot.peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), icons: bot.icons, requestWriteAccess: bot.flags.contains(.requiresWriteAccess), completion: { allowWrite in
let _ = (context.engine.messages.addBotToAttachMenu(botId: peerId, allowWrite: allowWrite)
|> deliverOnMainQueue).start(error: { _ in
presentError(presentationData.strings.WebApp_AddToAttachmentUnavailableError)
@ -705,7 +704,7 @@ func openResolvedUrlImpl(_ resolvedUrl: ResolvedUrl, context: AccountContext, ur
guard let navigationController else {
return
}
let _ = context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer), attachBotStart: ChatControllerInitialAttachBotStart(botId: botPeer.id, payload: payload, justInstalled: true), useExisting: true))
let _ = context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer), attachBotStart: ChatControllerInitialAttachBotStart(botId: bot.peer.id, payload: payload, justInstalled: true), useExisting: true))
}
navigationController.pushViewController(controller)
}
@ -717,7 +716,7 @@ func openResolvedUrlImpl(_ resolvedUrl: ResolvedUrl, context: AccountContext, ur
return
}
let _ = context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(chatPeer), attachBotStart: ChatControllerInitialAttachBotStart(botId: botPeer.id, payload: payload, justInstalled: true), useExisting: true))
let _ = context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(chatPeer), attachBotStart: ChatControllerInitialAttachBotStart(botId: bot.peer.id, payload: payload, justInstalled: true), useExisting: true))
})
}
}

View File

@ -132,6 +132,7 @@ final class TelegramGlobalSettings {
let unreadTrendingStickerPacks: Int
let archivedStickerPacks: [ArchivedStickerPackItem]?
let userLimits: EngineConfiguration.UserLimits
let bots: [AttachMenuBot]
let hasPassport: Bool
let hasWatchApp: Bool
let enableQRLogin: Bool
@ -153,6 +154,7 @@ final class TelegramGlobalSettings {
unreadTrendingStickerPacks: Int,
archivedStickerPacks: [ArchivedStickerPackItem]?,
userLimits: EngineConfiguration.UserLimits,
bots: [AttachMenuBot],
hasPassport: Bool,
hasWatchApp: Bool,
enableQRLogin: Bool
@ -173,6 +175,7 @@ final class TelegramGlobalSettings {
self.unreadTrendingStickerPacks = unreadTrendingStickerPacks
self.archivedStickerPacks = archivedStickerPacks
self.userLimits = userLimits
self.bots = bots
self.hasPassport = hasPassport
self.hasWatchApp = hasWatchApp
self.enableQRLogin = enableQRLogin
@ -508,9 +511,10 @@ func peerInfoScreenSettingsData(context: AccountContext, peerId: EnginePeer.Id,
return automaticEnergyUsageShouldBeOn(settings: settings)
}
|> distinctUntilChanged,
hasStories
hasStories,
context.engine.messages.attachMenuBots()
)
|> map { peerView, accountsAndPeers, accountSessions, privacySettings, sharedPreferences, notifications, stickerPacks, hasPassport, hasWatchApp, accountPreferences, suggestions, limits, hasPassword, isPowerSavingEnabled, hasStories -> PeerInfoScreenData in
|> map { peerView, accountsAndPeers, accountSessions, privacySettings, sharedPreferences, notifications, stickerPacks, hasPassport, hasWatchApp, accountPreferences, suggestions, limits, hasPassword, isPowerSavingEnabled, hasStories, bots -> PeerInfoScreenData in
let (notificationExceptions, notificationsAuthorizationStatus, notificationsWarningSuppressed) = notifications
let (featuredStickerPacks, archivedStickerPacks) = stickerPacks
@ -550,6 +554,7 @@ func peerInfoScreenSettingsData(context: AccountContext, peerId: EnginePeer.Id,
unreadTrendingStickerPacks: unreadTrendingStickerPacks,
archivedStickerPacks: archivedStickerPacks,
userLimits: peer?.isPremium == true ? limits.1 : limits.0,
bots: bots,
hasPassport: hasPassport,
hasWatchApp: hasWatchApp,
enableQRLogin: enableQRLogin)

View File

@ -91,6 +91,7 @@ import PeerInfoStoryGridScreen
import StoryContainerScreen
import ChatAvatarNavigationNode
import PeerReportScreen
import WebUI
enum PeerInfoAvatarEditingMode {
case generic
@ -549,6 +550,7 @@ private final class PeerInfoInteraction {
let toggleForumTopics: (Bool) -> Void
let displayTopicsLimited: (TopicsLimitedReason) -> Void
let openPeerMention: (String, ChatControllerInteractionNavigateToPeer) -> Void
let openBotApp: (AttachMenuBot) -> Void
init(
openUsername: @escaping (String) -> Void,
@ -599,7 +601,8 @@ private final class PeerInfoInteraction {
dismissInput: @escaping () -> Void,
toggleForumTopics: @escaping (Bool) -> Void,
displayTopicsLimited: @escaping (TopicsLimitedReason) -> Void,
openPeerMention: @escaping (String, ChatControllerInteractionNavigateToPeer) -> Void
openPeerMention: @escaping (String, ChatControllerInteractionNavigateToPeer) -> Void,
openBotApp: @escaping (AttachMenuBot) -> Void
) {
self.openUsername = openUsername
self.openPhone = openPhone
@ -650,6 +653,7 @@ private final class PeerInfoInteraction {
self.toggleForumTopics = toggleForumTopics
self.displayTopicsLimited = displayTopicsLimited
self.openPeerMention = openPeerMention
self.openBotApp = openBotApp
}
}
@ -661,7 +665,7 @@ private enum SettingsSection: Int, CaseIterable {
case phone
case accounts
case proxy
case stories
case apps
case shortcuts
case advanced
case payment
@ -792,7 +796,19 @@ private func settingsItems(data: PeerInfoScreenData?, context: AccountContext, p
}
}
items[.stories]!.append(PeerInfoScreenDisclosureItem(id: 0, text: presentationData.strings.Settings_MyStories, icon: PresentationResourcesSettings.stories, action: {
var appIndex = 1000
if let settings = data.globalSettings {
for bot in settings.bots {
if bot.flags.contains(.showInSettings) {
items[.apps]!.append(PeerInfoScreenDisclosureItem(id: appIndex, text: bot.peer.compactDisplayTitle, icon: PresentationResourcesSettings.passport, action: {
interaction.openBotApp(bot)
}))
appIndex += 1
}
}
}
items[.apps]!.append(PeerInfoScreenDisclosureItem(id: appIndex, text: presentationData.strings.Settings_MyStories, icon: PresentationResourcesSettings.stories, action: {
interaction.openSettings(.stories)
}))
@ -2340,6 +2356,9 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
},
openPeerMention: { [weak self] mention, navigation in
self?.openPeerMention(mention, navigation: navigation)
},
openBotApp: { [weak self] bot in
self?.openBotApp(bot)
}
)
@ -4401,7 +4420,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
}, contentContext: nil)
}
private func openUrl(url: String, concealed: Bool, external: Bool) {
private func openUrl(url: String, concealed: Bool, external: Bool, forceExternal: Bool = false, commit: @escaping () -> Void = {}) {
openUserGeneratedUrl(context: self.context, peerId: self.peerId, url: url, concealed: concealed, present: { [weak self] c in
self?.controller?.present(c, in: .window(.root))
}, openResolved: { [weak self] tempResolved in
@ -4411,8 +4430,9 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
let result: ResolvedUrl = external ? .externalUrl(url) : tempResolved
strongSelf.context.sharedContext.openResolvedUrl(result, context: strongSelf.context, urlContext: .generic, navigationController: strongSelf.controller?.navigationController as? NavigationController, forceExternal: false, openPeer: { peer, navigation in
strongSelf.context.sharedContext.openResolvedUrl(result, context: strongSelf.context, urlContext: .generic, navigationController: strongSelf.controller?.navigationController as? NavigationController, forceExternal: forceExternal, openPeer: { peer, navigation in
self?.openPeer(peerId: peer.id, navigation: navigation)
commit()
}, sendFile: nil,
sendSticker: nil,
requestMessageActionUrlAuth: nil,
@ -4582,6 +4602,51 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
}))
}
private let openBotAppDisposable = MetaDisposable()
private func openBotApp(_ bot: AttachMenuBot) {
guard let controller = self.controller else {
return
}
let proceed = { [weak self] in
guard let self else {
return
}
self.openBotAppDisposable.set(((self.context.engine.messages.requestSimpleWebView(botId: bot.peer.id, url: nil, source: .settings, themeParams: generateWebAppThemeParams(self.presentationData.theme))
|> afterDisposed {
// updateProgress()
})
|> deliverOnMainQueue).start(next: { [weak self] url in
guard let self else {
return
}
let params = WebAppParameters(peerId: self.context.account.peerId, botId: bot.peer.id, botName: bot.peer.compactDisplayTitle, url: url, queryId: nil, payload: nil, buttonText: nil, keepAliveSignal: nil, fromMenu: false, fromAttachMenu: false, isInline: false, isSimple: true)
let controller = standaloneWebAppController(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, params: params, threadId: nil, openUrl: { [weak self] url, concealed, commit in
self?.openUrl(url: url, concealed: concealed, external: false, forceExternal: true, commit: commit)
}, requestSwitchInline: { _, _, _ in
}, getNavigationController: { [weak self] in
return self?.controller?.navigationController as? NavigationController
})
controller.navigationPresentation = .flatModal
self.controller?.push(controller)
}, error: { [weak self] error in
if let self {
self.controller?.present(textAlertController(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, title: nil, text: self.presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: {
})]), in: .window(.root))
}
}))
}
if bot.flags.contains(.showInSettingsDisclaimer) {
let alertController = webAppTermsAlertController(context: self.context, updatedPresentationData: controller.updatedPresentationData, peer: bot.peer, completion: {
proceed()
})
controller.present(alertController, in: .window(.root))
} else {
proceed()
}
}
private func performButtonAction(key: PeerInfoHeaderButtonKey, gesture: ContextGesture?) {
guard let controller = self.controller else {
return

View File

@ -1802,6 +1802,8 @@ public final class SharedAccountContextImpl: SharedAccountContext {
mappedSource = .storiesFormatting
case .storiesExpirationDurations:
mappedSource = .storiesExpirationDurations
case .storiesSuggestedReactions:
mappedSource = .storiesSuggestedReactions
}
let controller = PremiumIntroScreen(context: context, source: mappedSource, forceDark: forceDark)
controller.wasDismissed = dismissed

View File

@ -962,7 +962,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
let body = MarkdownAttributeSet(font: Font.regular(14.0), textColor: .white)
let bold = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: .white)
let link = MarkdownAttributeSet(font: Font.regular(14.0), textColor: undoTextColor)
let link = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: undoTextColor)
let attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: body, bold: bold, link: link, linkAttribute: { contents in
return ("URL", contents)
}), textAlignment: .natural)

View File

@ -38,6 +38,8 @@ public class WebAppCancelButtonNode: ASDisplayNode {
public var state: State = .cancel
private var color: UIColor?
private var _theme: PresentationTheme
public var theme: PresentationTheme {
get {
@ -50,6 +52,24 @@ public class WebAppCancelButtonNode: ASDisplayNode {
}
private let strings: PresentationStrings
public func updateColor(_ color: UIColor?, transition: ContainedViewLayoutTransition) {
self.color = color
if case let .animated(duration, curve) = transition {
if let snapshotView = self.view.snapshotContentTree() {
snapshotView.frame = self.bounds
self.view.addSubview(snapshotView)
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, timingFunction: curve.timingFunction, removeOnCompletion: false, completion: { _ in
snapshotView.removeFromSuperview()
})
self.arrowNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration, timingFunction: curve.timingFunction)
self.labelNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration, timingFunction: curve.timingFunction)
}
}
self.setState(self.state, animated: false, animateScale: false, force: true)
}
public init(theme: PresentationTheme, strings: PresentationStrings) {
self._theme = theme
self.strings = strings
@ -124,13 +144,15 @@ public class WebAppCancelButtonNode: ASDisplayNode {
self.buttonNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration)
}
let color = self.color ?? self.theme.rootController.navigationBar.accentTextColor
self.arrowNode.isHidden = state == .cancel
self.labelNode.attributedText = NSAttributedString(string: state == .cancel ? self.strings.Common_Cancel : self.strings.Common_Back, font: Font.regular(17.0), textColor: self.theme.rootController.navigationBar.accentTextColor)
self.labelNode.attributedText = NSAttributedString(string: state == .cancel ? self.strings.Common_Cancel : self.strings.Common_Back, font: Font.regular(17.0), textColor: color)
let labelSize = self.labelNode.updateLayout(CGSize(width: 120.0, height: 56.0))
self.buttonNode.frame = CGRect(origin: .zero, size: CGSize(width: labelSize.width, height: self.buttonNode.frame.height))
self.arrowNode.image = NavigationBarTheme.generateBackArrowImage(color: self.theme.rootController.navigationBar.accentTextColor)
self.arrowNode.image = NavigationBarTheme.generateBackArrowImage(color: color)
if let image = self.arrowNode.image {
self.arrowNode.frame = CGRect(origin: self.arrowNode.frame.origin, size: image.size)
}
@ -305,7 +327,7 @@ public final class WebAppController: ViewController, AttachmentContainable {
return .complete()
}
|> mapToSignal { bot -> Signal<(FileMediaReference, Bool)?, NoError> in
if let bot = bot, let peerReference = PeerReference(bot.peer) {
if let bot = bot, let peerReference = PeerReference(bot.peer._asPeer()) {
var imageFile: TelegramMediaFile?
var isPlaceholder = false
if let file = bot.icons[.placeholder] {
@ -546,12 +568,16 @@ public final class WebAppController: ViewController, AttachmentContainable {
}
self.controller?.present(promptController, in: .window(.root))
}
private func updateNavigationBarAlpha(transition: ContainedViewLayoutTransition) {
let contentOffset = self.webView?.scrollView.contentOffset.y ?? 0.0
let backgroundAlpha = min(30.0, contentOffset) / 30.0
self.controller?.navigationBar?.updateBackgroundAlpha(backgroundAlpha, transition: transition)
}
private var targetContentOffset: CGPoint?
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let contentOffset = scrollView.contentOffset.y
self.controller?.navigationBar?.updateBackgroundAlpha(min(30.0, contentOffset) / 30.0, transition: .immediate)
self.updateNavigationBarAlpha(transition: .immediate)
if let targetContentOffset = self.targetContentOffset, scrollView.contentOffset != targetContentOffset {
scrollView.contentOffset = targetContentOffset
}
@ -821,8 +847,14 @@ public final class WebAppController: ViewController, AttachmentContainable {
transition.updateBackgroundColor(node: self.backgroundNode, color: color)
}
case "web_app_set_header_color":
if let json = json, let colorKey = json["color_key"] as? String, ["bg_color", "secondary_bg_color"].contains(colorKey) {
self.headerColorKey = colorKey
if let json = json {
if let colorKey = json["color_key"] as? String, ["bg_color", "secondary_bg_color"].contains(colorKey) {
self.headerColor = nil
self.headerColorKey = colorKey
} else if let hexColor = json["color"] as? String, let color = UIColor(hexString: hexColor) {
self.headerColor = color
self.headerColorKey = nil
}
self.updateHeaderBackgroundColor(transition: .animated(duration: 0.2, curve: .linear))
}
case "web_app_open_popup":
@ -938,16 +970,37 @@ public final class WebAppController: ViewController, AttachmentContainable {
fileprivate var needDismissConfirmation = false
fileprivate var headerColor: UIColor?
fileprivate var headerPrimaryTextColor: UIColor?
private var headerColorKey: String?
private func updateHeaderBackgroundColor(transition: ContainedViewLayoutTransition) {
guard let controller = self.controller else {
return
}
let color: UIColor?
var primaryTextColor: UIColor?
var secondaryTextColor: UIColor?
var backgroundColor = self.presentationData.theme.list.plainBackgroundColor
var secondaryBackgroundColor = self.presentationData.theme.list.blocksBackgroundColor
if self.presentationData.theme.list.blocksBackgroundColor.rgb == self.presentationData.theme.list.plainBackgroundColor.rgb {
backgroundColor = self.presentationData.theme.list.modalPlainBackgroundColor
secondaryBackgroundColor = self.presentationData.theme.list.plainBackgroundColor
}
if let headerColorKey = self.headerColorKey {
if let headerColor = self.headerColor {
color = headerColor
let textColor = headerColor.lightness > 0.5 ? UIColor(rgb: 0x000000) : UIColor(rgb: 0xffffff)
func calculateSecondaryAlpha(luminance: CGFloat, targetContrast: CGFloat) -> CGFloat {
let targetLuminance = luminance > 0.5 ? 0.0 : 1.0
let adaptiveAlpha = (luminance - targetLuminance + targetContrast) / targetContrast
return max(0.5, min(0.64, adaptiveAlpha))
}
primaryTextColor = textColor
self.headerPrimaryTextColor = textColor
secondaryTextColor = textColor.withAlphaComponent(calculateSecondaryAlpha(luminance: headerColor.lightness, targetContrast: 2.5))
} else if let headerColorKey = self.headerColorKey {
switch headerColorKey {
case "bg_color":
color = backgroundColor
@ -959,6 +1012,13 @@ public final class WebAppController: ViewController, AttachmentContainable {
} else {
color = nil
}
self.updateNavigationBarAlpha(transition: transition)
controller.updateNavigationBarTheme(transition: transition)
controller.titleView?.updateTextColors(primary: primaryTextColor, secondary: secondaryTextColor, transition: transition)
controller.cancelButtonNode.updateColor(primaryTextColor, transition: transition)
controller.moreButtonNode.updateColor(primaryTextColor, transition: transition)
transition.updateBackgroundColor(node: self.headerBackgroundNode, color: color ?? .clear)
transition.updateBackgroundColor(node: self.topOverscrollNode, color: color ?? .clear)
}
@ -1234,8 +1294,8 @@ public final class WebAppController: ViewController, AttachmentContainable {
}
private var titleView: CounterContollerTitleView?
private let cancelButtonNode: WebAppCancelButtonNode
private let moreButtonNode: MoreButtonNode
fileprivate let cancelButtonNode: WebAppCancelButtonNode
fileprivate let moreButtonNode: MoreButtonNode
private let context: AccountContext
private let peerId: PeerId
@ -1318,10 +1378,9 @@ public final class WebAppController: ViewController, AttachmentContainable {
if let strongSelf = self {
strongSelf.presentationData = presentationData
let navigationBarPresentationData = NavigationBarPresentationData(theme: NavigationBarTheme(rootControllerTheme: presentationData.theme), strings: NavigationBarStrings(back: "", close: ""))
strongSelf.navigationBar?.updatePresentationData(navigationBarPresentationData)
strongSelf.updateNavigationBarTheme(transition: .immediate)
strongSelf.titleView?.theme = presentationData.theme
strongSelf.cancelButtonNode.theme = presentationData.theme
strongSelf.moreButtonNode.theme = presentationData.theme
@ -1341,6 +1400,32 @@ public final class WebAppController: ViewController, AttachmentContainable {
self.presentationDataDisposable?.dispose()
}
fileprivate func updateNavigationBarTheme(transition: ContainedViewLayoutTransition) {
let navigationBarPresentationData: NavigationBarPresentationData
if let backgroundColor = self.controllerNode.headerColor, let textColor = self.controllerNode.headerPrimaryTextColor {
navigationBarPresentationData = NavigationBarPresentationData(
theme: NavigationBarTheme(
buttonColor: textColor,
disabledButtonColor: textColor,
primaryTextColor: textColor,
backgroundColor: backgroundColor,
enableBackgroundBlur: true,
separatorColor: UIColor(rgb: 0x000000, alpha: 0.25),
badgeBackgroundColor: .clear,
badgeStrokeColor: .clear,
badgeTextColor: .clear
),
strings: NavigationBarStrings(back: "", close: "")
)
} else {
navigationBarPresentationData = NavigationBarPresentationData(
theme: NavigationBarTheme(rootControllerTheme: self.presentationData.theme),
strings: NavigationBarStrings(back: "", close: "")
)
}
self.navigationBar?.updatePresentationData(navigationBarPresentationData)
}
@objc private func cancelPressed() {
if case .back = self.cancelButtonNode.state {
self.controllerNode.sendBackButtonEvent()

View File

@ -0,0 +1,302 @@
import Foundation
import UIKit
import SwiftSignalKit
import AsyncDisplayKit
import Display
import Postbox
import TelegramCore
import TelegramPresentationData
import TelegramUIPreferences
import AccountContext
import AppBundle
import AvatarNode
import CheckNode
import Markdown
private let textFont = Font.regular(13.0)
private let boldTextFont = Font.semibold(13.0)
private func formattedText(_ text: String, color: UIColor, linkColor: UIColor, textAlignment: NSTextAlignment = .natural) -> NSAttributedString {
return parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: color), bold: MarkdownAttributeSet(font: boldTextFont, textColor: color), link: MarkdownAttributeSet(font: textFont, textColor: linkColor), linkAttribute: { _ in return nil}), textAlignment: textAlignment)
}
private final class WebAppTermsAlertContentNode: AlertContentNode {
private let strings: PresentationStrings
private let title: String
private let text: String
private let titleNode: ImmediateTextNode
private let textNode: ASTextNode
private let acceptTermsCheckNode: InteractiveCheckNode
private let acceptTermsLabelNode: ASTextNode
private let actionNodesSeparator: ASDisplayNode
private let actionNodes: [TextAlertContentActionNode]
private let actionVerticalSeparators: [ASDisplayNode]
private var validLayout: CGSize?
override var dismissOnOutsideTap: Bool {
return self.isUserInteractionEnabled
}
var acceptedTerms: Bool = false {
didSet {
self.acceptTermsCheckNode.setSelected(self.acceptedTerms, animated: true)
if let firstAction = self.actionNodes.first {
firstAction.actionEnabled = self.acceptedTerms
}
}
}
init(context: AccountContext, theme: AlertControllerTheme, ptheme: PresentationTheme, strings: PresentationStrings, title: String, text: String, actions: [TextAlertAction]) {
self.strings = strings
self.title = title
self.text = text
self.titleNode = ImmediateTextNode()
self.titleNode.displaysAsynchronously = false
self.titleNode.maximumNumberOfLines = 1
self.titleNode.textAlignment = .center
self.textNode = ASTextNode()
self.textNode.displaysAsynchronously = false
self.textNode.maximumNumberOfLines = 0
self.acceptTermsCheckNode = InteractiveCheckNode(theme: CheckNodeTheme(backgroundColor: theme.accentColor, strokeColor: theme.contrastColor, borderColor: theme.controlBorderColor, overlayBorder: false, hasInset: false, hasShadow: false))
self.acceptTermsLabelNode = ASTextNode()
self.acceptTermsLabelNode.maximumNumberOfLines = 4
self.acceptTermsLabelNode.isUserInteractionEnabled = true
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.acceptTermsCheckNode)
self.addSubnode(self.acceptTermsLabelNode)
self.addSubnode(self.actionNodesSeparator)
for actionNode in self.actionNodes {
self.addSubnode(actionNode)
}
for separatorNode in self.actionVerticalSeparators {
self.addSubnode(separatorNode)
}
if let firstAction = self.actionNodes.first {
firstAction.actionEnabled = false
}
self.acceptTermsCheckNode.valueChanged = { [weak self] value in
if let strongSelf = self {
strongSelf.acceptedTerms = !strongSelf.acceptedTerms
}
}
self.updateTheme(theme)
}
override func didLoad() {
super.didLoad()
self.acceptTermsLabelNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.acceptTap(_:))))
}
@objc private func acceptTap(_ gestureRecognizer: UITapGestureRecognizer) {
if self.acceptTermsCheckNode.isUserInteractionEnabled {
self.acceptedTerms = !self.acceptedTerms
}
}
override func updateTheme(_ theme: AlertControllerTheme) {
self.titleNode.attributedText = NSAttributedString(string: self.title, font: Font.semibold(17.0), textColor: theme.primaryColor, paragraphAlignment: .center)
self.textNode.attributedText = NSAttributedString(string: self.text, font: Font.regular(13.0), textColor: theme.primaryColor, paragraphAlignment: .center)
let text = "I agree to the [Terms of Use]()"
self.acceptTermsLabelNode.attributedText = formattedText(text, color: theme.primaryColor, linkColor: theme.accentColor)
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)
}
}
override func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize {
var size = size
size.width = min(size.width, 270.0)
self.validLayout = size
var origin: CGPoint = CGPoint(x: 0.0, y: 17.0)
let titleSize = self.titleNode.updateLayout(CGSize(width: size.width - 32.0, height: size.height))
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
var entriesHeight: CGFloat = 0.0
let textSize = self.textNode.measure(CGSize(width: size.width - 48.0, height: size.height))
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
if self.acceptTermsLabelNode.supernode != nil {
origin.y += 21.0
entriesHeight += 21.0
let checkSize = CGSize(width: 22.0, height: 22.0)
let condensedSize = CGSize(width: size.width - 76.0, height: size.height)
let spacing: CGFloat = 12.0
let acceptTermsSize = self.acceptTermsLabelNode.measure(condensedSize)
let acceptTermsTotalWidth = checkSize.width + spacing + acceptTermsSize.width
let acceptTermsOriginX = floorToScreenPixels((size.width - acceptTermsTotalWidth) / 2.0)
transition.updateFrame(node: self.acceptTermsCheckNode, frame: CGRect(origin: CGPoint(x: acceptTermsOriginX, y: origin.y - 3.0), size: checkSize))
transition.updateFrame(node: self.acceptTermsLabelNode, frame: CGRect(origin: CGPoint(x: acceptTermsOriginX + checkSize.width + spacing, y: origin.y), size: acceptTermsSize))
origin.y += acceptTermsSize.height
entriesHeight += acceptTermsSize.height
}
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.vertical
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)
}
}
let insets = UIEdgeInsets(top: 18.0, left: 18.0, bottom: 18.0, right: 18.0)
let contentWidth = max(size.width, minActionsWidth)
var actionsHeight: CGFloat = 0.0
switch effectiveActionLayout {
case .horizontal:
actionsHeight = actionButtonHeight
case .vertical:
actionsHeight = actionButtonHeight * CGFloat(self.actionNodes.count)
}
let resultSize = CGSize(width: contentWidth, height: titleSize.height + textSize.height + entriesHeight + actionsHeight + 3.0 + 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
}
}
public func webAppTermsAlertController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?, peer: EnginePeer, completion: @escaping () -> Void) -> AlertController {
let theme = defaultDarkColorPresentationTheme
let presentationData: PresentationData
if let updatedPresentationData {
presentationData = updatedPresentationData.initial
} else {
presentationData = context.sharedContext.currentPresentationData.with { $0 }
}
let strings = presentationData.strings
var dismissImpl: ((Bool) -> Void)?
let actions: [TextAlertAction] = [TextAlertAction(type: .defaultAction, title: "Continue", action: {
completion()
dismissImpl?(true)
}), TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {
dismissImpl?(true)
})]
let title = "Warning"
let text = "You are about to use a mini app operated by an independent party not affiliated with Telegram. You must agree to the Terms of Use of mini apps to continue."
let contentNode = WebAppTermsAlertContentNode(context: context, theme: AlertControllerTheme(presentationData: presentationData), ptheme: theme, strings: strings, title: title, text: text, actions: actions)
let controller = AlertController(theme: AlertControllerTheme(presentationData: presentationData), contentNode: contentNode)
dismissImpl = { [weak controller] animated in
if animated {
controller?.dismissAnimated()
} else {
controller?.dismiss()
}
}
return controller
}

View File

@ -88,6 +88,9 @@ final class WebAppWebView: WKWebView {
return point.x > 30.0
}
self.allowsBackForwardNavigationGestures = false
if #available(iOS 16.4, *) {
self.isInspectable = true
}
handleScriptMessageImpl = { [weak self] message in
if let strongSelf = self {