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.SharePhoneTitle" = "Share Phone Number?";
"WebApp.SharePhoneConfirmation" = "**%@** will know your phone number. This can be useful for integration with other services."; "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."; "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 storiesPermanentViews
case storiesFormatting case storiesFormatting
case storiesExpirationDurations case storiesExpirationDurations
case storiesSuggestedReactions
} }
public enum PremiumDemoSubject { public enum PremiumDemoSubject {
@ -984,6 +985,7 @@ public enum PremiumLimitSubject {
case expiringStories case expiringStories
case storiesWeekly case storiesWeekly
case storiesMonthly case storiesMonthly
case storiesChannelBoost(level: Int32, link: String?)
} }
public protocol ComposeController: ViewController { public protocol ComposeController: ViewController {
@ -1025,6 +1027,7 @@ public protocol AccountContext: AnyObject {
var animatedEmojiStickers: [String: [StickerPackItem]] { get } var animatedEmojiStickers: [String: [StickerPackItem]] { get }
var isPremium: Bool { get }
var userLimits: EngineConfiguration.UserLimits { get } var userLimits: EngineConfiguration.UserLimits { get }
var imageCache: AnyObject? { get } var imageCache: AnyObject? { get }

View File

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

View File

@ -176,7 +176,7 @@ private final class AttachButtonComponent: CombinedComponent {
let imageName: String let imageName: String
var imageFile: TelegramMediaFile? var imageFile: TelegramMediaFile?
var animationFile: TelegramMediaFile? var animationFile: TelegramMediaFile?
var botPeer: Peer? var botPeer: EnginePeer?
let component = context.component let component = context.component
let strings = component.strings let strings = component.strings
@ -245,7 +245,7 @@ private final class AttachButtonComponent: CombinedComponent {
) )
} else { } else {
var fileReference: FileMediaReference? 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) fileReference = .attachBot(peer: peer, media: imageFile)
} }
@ -1143,7 +1143,7 @@ final class AttachmentPanel: ASDisplayNode, UIScrollViewDelegate {
if case let .app(peer, _, iconFiles) = type { if case let .app(peer, _, iconFiles) = type {
for (name, file) in iconFiles { for (name, file) in iconFiles {
if [.default, .iOSAnimated, .placeholder].contains(name) { 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 { if case .placeholder = name {
let account = self.context.account let account = self.context.account
let path = account.postbox.mediaBox.cachedRepresentationCompletePath(file.resource.id, representation: CachedPreparedSvgRepresentation()) 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() { private func update() {
self.titleNode.attributedText = NSAttributedString(string: self.title.title, font: Font.semibold(17.0), textColor: self.theme.rootController.navigationBar.primaryTextColor) let primaryTextColor = self.primaryTextColor ?? 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 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.accessibilityLabel = self.title.title
self.accessibilityValue = self.title.counter 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) 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 { if presentationData.theme !== self.presentationData.theme || presentationData.strings !== self.presentationData.strings {
self.presentationData = presentationData 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.color = self.presentationData.theme.buttonColor
self.backButtonNode.disabledColor = self.presentationData.theme.disabledButtonColor self.backButtonNode.disabledColor = self.presentationData.theme.disabledButtonColor

View File

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

View File

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

View File

@ -74,12 +74,36 @@ public final class MoreButtonNode: ASDisplayNode {
private let buttonNode: HighlightableButtonNode private let buttonNode: HighlightableButtonNode
public let iconNode: MoreIconNode public let iconNode: MoreIconNode
private var color: UIColor?
public var theme: PresentationTheme { public var theme: PresentationTheme {
didSet { 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) { public init(theme: PresentationTheme) {
self.theme = theme self.theme = theme

View File

@ -220,6 +220,12 @@ public enum PremiumSource: Equatable {
} else { } else {
return false return false
} }
case .storiesSuggestedReactions:
if case .storiesSuggestedReactions = rhs {
return true
} else {
return false
}
} }
} }
@ -255,6 +261,7 @@ public enum PremiumSource: Equatable {
case storiesPermanentViews case storiesPermanentViews
case storiesFormatting case storiesFormatting
case storiesExpirationDurations case storiesExpirationDurations
case storiesSuggestedReactions
var identifier: String? { var identifier: String? {
switch self { switch self {
@ -324,6 +331,8 @@ public enum PremiumSource: Equatable {
return "stories__links_and_formatting" return "stories__links_and_formatting"
case .storiesExpirationDurations: case .storiesExpirationDurations:
return "stories__expiration_durations" 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 badgeText: String?
private let badgePosition: CGFloat private let badgePosition: CGFloat
private let badgeGraphPosition: CGFloat private let badgeGraphPosition: CGFloat
private let invertProgress: Bool
private let isPremiumDisabled: Bool private let isPremiumDisabled: Bool
init( init(
@ -55,6 +56,7 @@ private class PremiumLimitAnimationComponent: Component {
badgeText: String?, badgeText: String?,
badgePosition: CGFloat, badgePosition: CGFloat,
badgeGraphPosition: CGFloat, badgeGraphPosition: CGFloat,
invertProgress: Bool,
isPremiumDisabled: Bool isPremiumDisabled: Bool
) { ) {
self.iconName = iconName self.iconName = iconName
@ -64,6 +66,7 @@ private class PremiumLimitAnimationComponent: Component {
self.badgeText = badgeText self.badgeText = badgeText
self.badgePosition = badgePosition self.badgePosition = badgePosition
self.badgeGraphPosition = badgeGraphPosition self.badgeGraphPosition = badgeGraphPosition
self.invertProgress = invertProgress
self.isPremiumDisabled = isPremiumDisabled self.isPremiumDisabled = isPremiumDisabled
} }
@ -89,6 +92,9 @@ private class PremiumLimitAnimationComponent: Component {
if lhs.badgeGraphPosition != rhs.badgeGraphPosition { if lhs.badgeGraphPosition != rhs.badgeGraphPosition {
return false return false
} }
if lhs.invertProgress != rhs.invertProgress {
return false
}
if lhs.isPremiumDisabled != rhs.isPremiumDisabled { if lhs.isPremiumDisabled != rhs.isPremiumDisabled {
return false return false
} }
@ -96,6 +102,8 @@ private class PremiumLimitAnimationComponent: Component {
} }
final class View: UIView { final class View: UIView {
private var component: PremiumLimitAnimationComponent?
private let container: SimpleLayer private let container: SimpleLayer
private let inactiveBackground: SimpleLayer private let inactiveBackground: SimpleLayer
@ -240,6 +248,7 @@ private class PremiumLimitAnimationComponent: Component {
var previousAvailableSize: CGSize? var previousAvailableSize: CGSize?
func update(component: PremiumLimitAnimationComponent, availableSize: CGSize, transition: Transition) -> CGSize { func update(component: PremiumLimitAnimationComponent, availableSize: CGSize, transition: Transition) -> CGSize {
self.component = component
self.inactiveBackground.backgroundColor = component.inactiveColor.cgColor self.inactiveBackground.backgroundColor = component.inactiveColor.cgColor
self.activeBackground.backgroundColor = component.activeColors.last?.cgColor self.activeBackground.backgroundColor = component.activeColors.last?.cgColor
@ -255,9 +264,13 @@ private class PremiumLimitAnimationComponent: Component {
let activeWidth: CGFloat = containerFrame.width - activityPosition let activeWidth: CGFloat = containerFrame.width - activityPosition
if !component.isPremiumDisabled { if !component.isPremiumDisabled {
self.inactiveBackground.frame = CGRect(origin: .zero, size: CGSize(width: activityPosition, height: lineHeight)) if component.invertProgress {
self.activeContainer.frame = CGRect(origin: CGPoint(x: activityPosition, y: 0.0), size: CGSize(width: activeWidth, height: lineHeight)) 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)) self.activeBackground.frame = CGRect(origin: .zero, size: CGSize(width: activeWidth * (1.0 + 0.35), height: lineHeight))
if self.activeBackground.animation(forKey: "movement") == nil { if self.activeBackground.animation(forKey: "movement") == nil {
self.activeBackground.position = CGPoint(x: -self.activeContainer.frame.width * 0.35, y: lineHeight / 2.0) 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() { private func setupGradientAnimations() {
guard let _ = self.component else {
return
}
if let _ = self.badgeForeground.animation(forKey: "movement") { if let _ = self.badgeForeground.animation(forKey: "movement") {
} else { } else {
CATransaction.begin() CATransaction.begin()
@ -440,6 +456,7 @@ public final class PremiumLimitDisplayComponent: CombinedComponent {
let badgeText: String? let badgeText: String?
let badgePosition: CGFloat let badgePosition: CGFloat
let badgeGraphPosition: CGFloat let badgeGraphPosition: CGFloat
let invertProgress: Bool
let isPremiumDisabled: Bool let isPremiumDisabled: Bool
public init( public init(
@ -455,6 +472,7 @@ public final class PremiumLimitDisplayComponent: CombinedComponent {
badgeText: String?, badgeText: String?,
badgePosition: CGFloat, badgePosition: CGFloat,
badgeGraphPosition: CGFloat, badgeGraphPosition: CGFloat,
invertProgress: Bool = false,
isPremiumDisabled: Bool isPremiumDisabled: Bool
) { ) {
self.inactiveColor = inactiveColor self.inactiveColor = inactiveColor
@ -469,6 +487,7 @@ public final class PremiumLimitDisplayComponent: CombinedComponent {
self.badgeText = badgeText self.badgeText = badgeText
self.badgePosition = badgePosition self.badgePosition = badgePosition
self.badgeGraphPosition = badgeGraphPosition self.badgeGraphPosition = badgeGraphPosition
self.invertProgress = invertProgress
self.isPremiumDisabled = isPremiumDisabled self.isPremiumDisabled = isPremiumDisabled
} }
@ -509,6 +528,9 @@ public final class PremiumLimitDisplayComponent: CombinedComponent {
if lhs.badgeGraphPosition != rhs.badgeGraphPosition { if lhs.badgeGraphPosition != rhs.badgeGraphPosition {
return false return false
} }
if lhs.invertProgress != rhs.invertProgress {
return false
}
if lhs.isPremiumDisabled != rhs.isPremiumDisabled { if lhs.isPremiumDisabled != rhs.isPremiumDisabled {
return false return false
} }
@ -528,6 +550,16 @@ public final class PremiumLimitDisplayComponent: CombinedComponent {
let height: CGFloat = 120.0 let height: CGFloat = 120.0
let lineHeight: CGFloat = 30.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( let animation = animation.update(
component: PremiumLimitAnimationComponent( component: PremiumLimitAnimationComponent(
iconName: component.badgeIconName, iconName: component.badgeIconName,
@ -537,6 +569,7 @@ public final class PremiumLimitDisplayComponent: CombinedComponent {
badgeText: component.badgeText, badgeText: component.badgeText,
badgePosition: component.badgePosition, badgePosition: component.badgePosition,
badgeGraphPosition: component.badgeGraphPosition, badgeGraphPosition: component.badgeGraphPosition,
invertProgress: component.invertProgress,
isPremiumDisabled: component.isPremiumDisabled isPremiumDisabled: component.isPremiumDisabled
), ),
availableSize: CGSize(width: context.availableSize.width, height: height), availableSize: CGSize(width: context.availableSize.width, height: height),
@ -554,7 +587,7 @@ public final class PremiumLimitDisplayComponent: CombinedComponent {
NSAttributedString( NSAttributedString(
string: component.inactiveTitle, string: component.inactiveTitle,
font: Font.semibold(15.0), font: Font.semibold(15.0),
textColor: component.inactiveTitleColor textColor: leftTextColor
) )
) )
), ),
@ -568,7 +601,7 @@ public final class PremiumLimitDisplayComponent: CombinedComponent {
NSAttributedString( NSAttributedString(
string: component.inactiveValue, string: component.inactiveValue,
font: Font.semibold(15.0), font: Font.semibold(15.0),
textColor: component.inactiveTitleColor textColor: leftTextColor
) )
) )
), ),
@ -582,7 +615,7 @@ public final class PremiumLimitDisplayComponent: CombinedComponent {
NSAttributedString( NSAttributedString(
string: component.activeTitle, string: component.activeTitle,
font: Font.semibold(15.0), font: Font.semibold(15.0),
textColor: component.activeTitleColor textColor: rightTextColor
) )
) )
), ),
@ -596,7 +629,7 @@ public final class PremiumLimitDisplayComponent: CombinedComponent {
NSAttributedString( NSAttributedString(
string: component.activeValue, string: component.activeValue,
font: Font.semibold(15.0), 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 title = Child(MultilineTextComponent.self)
let text = Child(BalancedTextComponent.self) let text = Child(BalancedTextComponent.self)
let limit = Child(PremiumLimitDisplayComponent.self) let limit = Child(PremiumLimitDisplayComponent.self)
let linkButton = Child(SolidRoundedButtonComponent.self)
let button = Child(SolidRoundedButtonComponent.self) let button = Child(SolidRoundedButtonComponent.self)
return { context in return { context in
@ -761,219 +795,262 @@ private final class LimitSheetContent: CombinedComponent {
) )
var titleText = strings.Premium_LimitReached var titleText = strings.Premium_LimitReached
var actionButtonText: String?
var buttonAnimationName: String? = "premium_x2" var buttonAnimationName: String? = "premium_x2"
var buttonIconName: String?
let iconName: String let iconName: String
var badgeText: String var badgeText: String
var string: String var string: String
let defaultValue: String let defaultValue: String
var defaultTitle = strings.Premium_Free
var premiumTitle = strings.Premium_Premium
let premiumValue: String let premiumValue: String
let badgePosition: CGFloat let badgePosition: CGFloat
let badgeGraphPosition: CGFloat let badgeGraphPosition: CGFloat
var invertProgress = false
switch subject { switch subject {
case .folders: case .folders:
let limit = state.limits.maxFoldersCount let limit = state.limits.maxFoldersCount
let premiumLimit = state.premiumLimits.maxFoldersCount let premiumLimit = state.premiumLimits.maxFoldersCount
iconName = "Premium/Folder" iconName = "Premium/Folder"
badgeText = "\(component.count)" badgeText = "\(component.count)"
string = component.count >= premiumLimit ? strings.Premium_MaxFoldersCountFinalText("\(premiumLimit)").string : strings.Premium_MaxFoldersCountText("\(limit)", "\(premiumLimit)").string string = component.count >= premiumLimit ? strings.Premium_MaxFoldersCountFinalText("\(premiumLimit)").string : strings.Premium_MaxFoldersCountText("\(limit)", "\(premiumLimit)").string
defaultValue = component.count > limit ? "\(limit)" : "" defaultValue = component.count > limit ? "\(limit)" : ""
premiumValue = component.count >= premiumLimit ? "" : "\(premiumLimit)" premiumValue = component.count >= premiumLimit ? "" : "\(premiumLimit)"
if component.count >= premiumLimit { if component.count >= premiumLimit {
badgeGraphPosition = max(0.15, CGFloat(limit) / CGFloat(premiumLimit)) badgeGraphPosition = max(0.15, CGFloat(limit) / CGFloat(premiumLimit))
} else { } else {
badgeGraphPosition = max(0.15, CGFloat(component.count) / CGFloat(premiumLimit)) badgeGraphPosition = max(0.15, CGFloat(component.count) / CGFloat(premiumLimit))
} }
badgePosition = max(0.15, CGFloat(component.count) / CGFloat(premiumLimit)) badgePosition = max(0.15, CGFloat(component.count) / CGFloat(premiumLimit))
if !state.isPremium && badgePosition > 0.5 { if !state.isPremium && badgePosition > 0.5 {
string = strings.Premium_MaxFoldersCountText("\(limit)", "\(premiumLimit)").string string = strings.Premium_MaxFoldersCountText("\(limit)", "\(premiumLimit)").string
} }
if isPremiumDisabled { 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"
badgeText = "\(limit)" badgeText = "\(limit)"
string = component.count >= premiumLimit ? strings.Premium_MaxExpiringStoriesFinalText("\(premiumLimit)").string : strings.Premium_MaxExpiringStoriesText("\(limit)", "\(premiumLimit)").string string = strings.Premium_MaxFoldersCountNoPremiumText("\(limit)").string
defaultValue = "" }
premiumValue = component.count >= premiumLimit ? "" : "\(premiumLimit)" case .chatsPerFolder:
badgePosition = max(0.32, CGFloat(component.count) / CGFloat(premiumLimit)) let limit = state.limits.maxFolderChatsCount
badgeGraphPosition = badgePosition let premiumLimit = state.premiumLimits.maxFolderChatsCount
iconName = "Premium/Chat"
if isPremiumDisabled { badgeText = "\(component.count)"
badgeText = "\(limit)" string = component.count >= premiumLimit ? strings.Premium_MaxChatsInFolderFinalText("\(premiumLimit)").string : strings.Premium_MaxChatsInFolderText("\(limit)", "\(premiumLimit)").string
string = strings.Premium_MaxExpiringStoriesNoPremiumText("\(limit)").string defaultValue = component.count > limit ? "\(limit)" : ""
} premiumValue = component.count >= premiumLimit ? "" : "\(premiumLimit)"
buttonAnimationName = nil badgePosition = CGFloat(component.count) / CGFloat(premiumLimit)
case .storiesWeekly: badgeGraphPosition = badgePosition
let limit = state.limits.maxStoriesWeeklyCount
let premiumLimit = state.premiumLimits.maxStoriesWeeklyCount if isPremiumDisabled {
iconName = "Premium/Stories"
badgeText = "\(limit)" badgeText = "\(limit)"
string = component.count >= premiumLimit ? strings.Premium_MaxStoriesWeeklyFinalText("\(premiumLimit)").string : strings.Premium_MaxStoriesWeeklyText("\(limit)", "\(premiumLimit)").string string = strings.Premium_MaxChatsInFolderNoPremiumText("\(limit)").string
defaultValue = "" }
premiumValue = component.count >= premiumLimit ? "" : "\(premiumLimit)" case .channels:
badgePosition = max(0.32, CGFloat(component.count) / CGFloat(premiumLimit)) let limit = state.limits.maxChannelsCount
badgeGraphPosition = badgePosition let premiumLimit = state.premiumLimits.maxChannelsCount
iconName = "Premium/Chat"
if isPremiumDisabled { badgeText = "\(component.count)"
badgeText = "\(limit)" string = component.count >= premiumLimit ? strings.Premium_MaxChannelsFinalText("\(premiumLimit)").string : strings.Premium_MaxChannelsText("\(limit)", "\(premiumLimit)").string
string = strings.Premium_MaxStoriesWeeklyNoPremiumText("\(limit)").string defaultValue = component.count > limit ? "\(limit)" : ""
} premiumValue = component.count >= premiumLimit ? "" : "\(premiumLimit)"
buttonAnimationName = nil if component.count >= premiumLimit {
case .storiesMonthly: badgeGraphPosition = max(0.15, CGFloat(limit) / CGFloat(premiumLimit))
let limit = state.limits.maxStoriesMonthlyCount } else {
let premiumLimit = state.premiumLimits.maxStoriesMonthlyCount badgeGraphPosition = max(0.15, CGFloat(component.count) / CGFloat(premiumLimit))
iconName = "Premium/Stories" }
badgePosition = max(0.15, CGFloat(component.count) / CGFloat(premiumLimit))
if isPremiumDisabled {
badgeText = "\(limit)" badgeText = "\(limit)"
string = component.count >= premiumLimit ? strings.Premium_MaxStoriesMonthlyFinalText("\(premiumLimit)").string : strings.Premium_MaxStoriesMonthlyText("\(limit)", "\(premiumLimit)").string string = strings.Premium_MaxChannelsNoPremiumText("\(limit)").string
defaultValue = "" }
premiumValue = component.count >= premiumLimit ? "" : "\(premiumLimit)" case .linksPerSharedFolder:
badgePosition = max(0.32, CGFloat(component.count) / CGFloat(premiumLimit)) /*let count: Int32 = 5 + Int32("".count)// component.count
badgeGraphPosition = badgePosition let limit: Int32 = 5 + Int32("".count)//state.limits.maxSharedFolderInviteLinks
let premiumLimit: Int32 = 100 + Int32("".count)//state.premiumLimits.maxSharedFolderInviteLinks*/
if isPremiumDisabled {
badgeText = "\(limit)" let count: Int32 = component.count
string = strings.Premium_MaxStoriesMonthlyNoPremiumText("\(limit)").string 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 var reachedMaximumLimit = badgePosition >= 1.0
if case .folders = subject, !state.isPremium { if case .folders = subject, !state.isPremium {
@ -997,8 +1074,8 @@ private final class LimitSheetContent: CombinedComponent {
transition: .immediate transition: .immediate
) )
let textFont = Font.regular(17.0) let textFont = Font.regular(15.0)
let boldTextFont = Font.semibold(17.0) let boldTextFont = Font.semibold(15.0)
let textColor = theme.actionSheet.primaryTextColor 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 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 return nil
@ -1034,16 +1111,17 @@ private final class LimitSheetContent: CombinedComponent {
component: PremiumLimitDisplayComponent( component: PremiumLimitDisplayComponent(
inactiveColor: theme.list.itemBlocksSeparatorColor.withAlphaComponent(0.5), inactiveColor: theme.list.itemBlocksSeparatorColor.withAlphaComponent(0.5),
activeColors: gradientColors, activeColors: gradientColors,
inactiveTitle: strings.Premium_Free, inactiveTitle: defaultTitle,
inactiveValue: defaultValue, inactiveValue: defaultValue,
inactiveTitleColor: theme.list.itemPrimaryTextColor, inactiveTitleColor: theme.list.itemPrimaryTextColor,
activeTitle: strings.Premium_Premium, activeTitle: premiumTitle,
activeValue: premiumValue, activeValue: premiumValue,
activeTitleColor: .white, activeTitleColor: .white,
badgeIconName: iconName, badgeIconName: iconName,
badgeText: badgeText, badgeText: badgeText,
badgePosition: badgePosition, badgePosition: badgePosition,
badgeGraphPosition: badgeGraphPosition, badgeGraphPosition: badgeGraphPosition,
invertProgress: invertProgress,
isPremiumDisabled: isPremiumDisabled isPremiumDisabled: isPremiumDisabled
), ),
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: context.availableSize.height), 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 isIncreaseButton = !reachedMaximumLimit && !isPremiumDisabled
let button = button.update( let button = button.update(
component: SolidRoundedButtonComponent( component: SolidRoundedButtonComponent(
title: isIncreaseButton ? strings.Premium_IncreaseLimit : strings.Common_OK, title: actionButtonText ?? (isIncreaseButton ? strings.Premium_IncreaseLimit : strings.Common_OK),
theme: SolidRoundedButtonComponent.Theme( theme: SolidRoundedButtonComponent.Theme(
backgroundColor: .black, backgroundColor: .black,
backgroundColors: gradientColors, backgroundColors: gradientColors,
@ -1068,8 +1145,9 @@ private final class LimitSheetContent: CombinedComponent {
height: 50.0, height: 50.0,
cornerRadius: 10.0, cornerRadius: 10.0,
gloss: isIncreaseButton, gloss: isIncreaseButton,
iconName: buttonIconName,
animationName: isIncreaseButton ? buttonAnimationName : nil, animationName: isIncreaseButton ? buttonAnimationName : nil,
iconPosition: .right, iconPosition: buttonIconName != nil ? .left : .right,
action: { action: {
component.dismiss() component.dismiss()
if isIncreaseButton { if isIncreaseButton {
@ -1081,7 +1159,37 @@ private final class LimitSheetContent: CombinedComponent {
transition: context.transition transition: context.transition
) )
var buttonOffset: CGFloat = 0.0
var textOffset: CGFloat = 228.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 { if isPremiumDisabled {
textOffset -= 68.0 textOffset -= 68.0
} }
@ -1093,7 +1201,7 @@ private final class LimitSheetContent: CombinedComponent {
.position(CGPoint(x: context.availableSize.width / 2.0, y: textOffset)) .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 context.add(button
.position(CGPoint(x: buttonFrame.midX, y: buttonFrame.midY)) .position(CGPoint(x: buttonFrame.midX, y: buttonFrame.midY))
) )
@ -1104,6 +1212,15 @@ private final class LimitSheetContent: CombinedComponent {
if isPremiumDisabled { if isPremiumDisabled {
height -= 78.0 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) 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 class PremiumLimitScreen: ViewControllerComponentContainer {
public enum Subject { public enum Subject: Equatable {
case folders case folders
case chatsPerFolder case chatsPerFolder
case pins case pins
@ -1216,6 +1333,8 @@ public class PremiumLimitScreen: ViewControllerComponentContainer {
case expiringStories case expiringStories
case storiesWeekly case storiesWeekly
case storiesMonthly 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) { 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 maxExpiringStoriesCount: Int32
public let maxStoriesWeeklyCount: Int32 public let maxStoriesWeeklyCount: Int32
public let maxStoriesMonthlyCount: Int32 public let maxStoriesMonthlyCount: Int32
public let maxStoriesSuggestedReactions: Int32
public static var defaultValue: UserLimitsConfiguration { public static var defaultValue: UserLimitsConfiguration {
return UserLimitsConfiguration( return UserLimitsConfiguration(
@ -42,7 +43,8 @@ public struct UserLimitsConfiguration: Equatable {
maxStoryCaptionLength: 200, maxStoryCaptionLength: 200,
maxExpiringStoriesCount: 3, maxExpiringStoriesCount: 3,
maxStoriesWeeklyCount: 7, maxStoriesWeeklyCount: 7,
maxStoriesMonthlyCount: 30 maxStoriesMonthlyCount: 30,
maxStoriesSuggestedReactions: 1
) )
} }
@ -65,7 +67,8 @@ public struct UserLimitsConfiguration: Equatable {
maxStoryCaptionLength: Int32, maxStoryCaptionLength: Int32,
maxExpiringStoriesCount: Int32, maxExpiringStoriesCount: Int32,
maxStoriesWeeklyCount: Int32, maxStoriesWeeklyCount: Int32,
maxStoriesMonthlyCount: Int32 maxStoriesMonthlyCount: Int32,
maxStoriesSuggestedReactions: Int32
) { ) {
self.maxPinnedChatCount = maxPinnedChatCount self.maxPinnedChatCount = maxPinnedChatCount
self.maxArchivedPinnedChatCount = maxArchivedPinnedChatCount self.maxArchivedPinnedChatCount = maxArchivedPinnedChatCount
@ -86,6 +89,7 @@ public struct UserLimitsConfiguration: Equatable {
self.maxExpiringStoriesCount = maxExpiringStoriesCount self.maxExpiringStoriesCount = maxExpiringStoriesCount
self.maxStoriesWeeklyCount = maxStoriesWeeklyCount self.maxStoriesWeeklyCount = maxStoriesWeeklyCount
self.maxStoriesMonthlyCount = maxStoriesMonthlyCount self.maxStoriesMonthlyCount = maxStoriesMonthlyCount
self.maxStoriesSuggestedReactions = maxStoriesSuggestedReactions
} }
} }
@ -129,5 +133,6 @@ extension UserLimitsConfiguration {
self.maxExpiringStoriesCount = getValue("story_expiring_limit", orElse: defaultValue.maxExpiringStoriesCount) self.maxExpiringStoriesCount = getValue("story_expiring_limit", orElse: defaultValue.maxExpiringStoriesCount)
self.maxStoriesWeeklyCount = getValue("stories_sent_weekly_limit", orElse: defaultValue.maxStoriesWeeklyCount) self.maxStoriesWeeklyCount = getValue("stories_sent_weekly_limit", orElse: defaultValue.maxStoriesWeeklyCount)
self.maxStoriesMonthlyCount = getValue("stories_sent_monthly_limit", orElse: defaultValue.maxStoriesMonthlyCount) 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 maxExpiringStoriesCount: Int32
public let maxStoriesWeeklyCount: Int32 public let maxStoriesWeeklyCount: Int32
public let maxStoriesMonthlyCount: Int32 public let maxStoriesMonthlyCount: Int32
public let maxStoriesSuggestedReactions: Int32
public static var defaultValue: UserLimits { public static var defaultValue: UserLimits {
return UserLimits(UserLimitsConfiguration.defaultValue) return UserLimits(UserLimitsConfiguration.defaultValue)
@ -79,7 +80,8 @@ public enum EngineConfiguration {
maxStoryCaptionLength: Int32, maxStoryCaptionLength: Int32,
maxExpiringStoriesCount: Int32, maxExpiringStoriesCount: Int32,
maxStoriesWeeklyCount: Int32, maxStoriesWeeklyCount: Int32,
maxStoriesMonthlyCount: Int32 maxStoriesMonthlyCount: Int32,
maxStoriesSuggestedReactions: Int32
) { ) {
self.maxPinnedChatCount = maxPinnedChatCount self.maxPinnedChatCount = maxPinnedChatCount
self.maxArchivedPinnedChatCount = maxArchivedPinnedChatCount self.maxArchivedPinnedChatCount = maxArchivedPinnedChatCount
@ -100,6 +102,7 @@ public enum EngineConfiguration {
self.maxExpiringStoriesCount = maxExpiringStoriesCount self.maxExpiringStoriesCount = maxExpiringStoriesCount
self.maxStoriesWeeklyCount = maxStoriesWeeklyCount self.maxStoriesWeeklyCount = maxStoriesWeeklyCount
self.maxStoriesMonthlyCount = maxStoriesMonthlyCount self.maxStoriesMonthlyCount = maxStoriesMonthlyCount
self.maxStoriesSuggestedReactions = maxStoriesSuggestedReactions
} }
} }
} }
@ -155,7 +158,8 @@ public extension EngineConfiguration.UserLimits {
maxStoryCaptionLength: userLimitsConfiguration.maxStoryCaptionLength, maxStoryCaptionLength: userLimitsConfiguration.maxStoryCaptionLength,
maxExpiringStoriesCount: userLimitsConfiguration.maxExpiringStoriesCount, maxExpiringStoriesCount: userLimitsConfiguration.maxExpiringStoriesCount,
maxStoriesWeeklyCount: userLimitsConfiguration.maxStoriesWeeklyCount, 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 hasSettings = Flags(rawValue: 1 << 0)
public static let requiresWriteAccess = Flags(rawValue: 1 << 1) 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 { public struct PeerFlags: OptionSet, Codable {
@ -323,6 +326,15 @@ func managedSynchronizeAttachMenuBots(accountPeerId: PeerId, postbox: Postbox, n
if (apiFlags & (1 << 2)) != 0 { if (apiFlags & (1 << 2)) != 0 {
flags.insert(.requiresWriteAccess) 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)) 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 struct AttachMenuBot {
public let peer: Peer public let peer: EnginePeer
public let shortName: String public let shortName: String
public let icons: [AttachMenuBots.Bot.IconName: TelegramMediaFile] public let icons: [AttachMenuBots.Bot.IconName: TelegramMediaFile]
public let peerTypes: AttachMenuBots.Bot.PeerFlags public let peerTypes: AttachMenuBots.Bot.PeerFlags
public let flags: AttachMenuBots.Bot.Flags 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.peer = peer
self.shortName = shortName self.shortName = shortName
self.icons = icons self.icons = icons
@ -450,7 +462,7 @@ func _internal_attachMenuBots(postbox: Postbox) -> Signal<[AttachMenuBot], NoErr
var resultBots: [AttachMenuBot] = [] var resultBots: [AttachMenuBot] = []
for bot in cachedBots { for bot in cachedBots {
if let peer = transaction.getPeer(bot.peerId) { 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 return resultBots
@ -465,7 +477,7 @@ func _internal_getAttachMenuBot(accountPeerId: PeerId, postbox: Postbox, network
return postbox.transaction { transaction -> Signal<AttachMenuBot, GetAttachMenuBotError> in return postbox.transaction { transaction -> Signal<AttachMenuBot, GetAttachMenuBotError> in
if cached, let cachedBots = cachedAttachMenuBots(transaction: transaction)?.bots { if cached, let cachedBots = cachedAttachMenuBots(transaction: transaction)?.bots {
if let bot = cachedBots.first(where: { $0.peerId == botId }), let peer = transaction.getPeer(bot.peerId) { 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 { if (apiFlags & (1 << 2)) != 0 {
flags.insert(.requiresWriteAccess) 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" private let botWebViewPlatform = "ios"
#endif #endif
public enum RequestSimpleWebViewSource {
case generic
case inline
case settings
}
public enum RequestSimpleWebViewError { public enum RequestSimpleWebViewError {
case generic 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? var serializedThemeParams: Api.DataJSON?
if let themeParams = themeParams, let data = try? JSONSerialization.data(withJSONObject: themeParams, options: []), let dataString = String(data: data, encoding: .utf8) { if let themeParams = themeParams, let data = try? JSONSerialization.data(withJSONObject: themeParams, options: []), let dataString = String(data: data, encoding: .utf8) {
serializedThemeParams = .dataJSON(data: dataString) serializedThemeParams = .dataJSON(data: dataString)
@ -28,8 +34,16 @@ func _internal_requestSimpleWebView(postbox: Postbox, network: Network, botId: P
if let _ = serializedThemeParams { if let _ = serializedThemeParams {
flags |= (1 << 0) flags |= (1 << 0)
} }
if inline { switch source {
case .inline:
flags |= (1 << 1) 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)) return network.request(Api.functions.messages.requestSimpleWebView(flags: flags, bot: inputUser, url: url, startParam: nil, themeParams: serializedThemeParams, platform: botWebViewPlatform))
|> mapError { _ -> RequestSimpleWebViewError in |> 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) 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> { 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, inline: inline, themeParams: themeParams) 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> { 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) 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) transition.setAlpha(view: scrubberView, alpha: component.isDisplayingTool || component.isDismissing || component.isInteractingWithEntities || isEditingCaption ? 0.0 : 1.0)
} else if animateIn { } else if animateIn {
scrubberView.layer.animatePosition(from: CGPoint(x: 0.0, y: 44.0), to: .zero, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, additive: true) 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 { } else {
mediaEditor.setAudioTrackTrimRange(0 ..< min(15, audioDuration), apply: true) 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)) self.requestUpdate(transition: .easeInOut(duration: 0.2))
if isScopedResource { if isScopedResource {
@ -3249,8 +3250,8 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
} }
} }
private var drawingScreen: DrawingScreen? fileprivate var drawingScreen: DrawingScreen?
private var stickerScreen: StickerPickerScreen? fileprivate var stickerScreen: StickerPickerScreen?
private var defaultToEmoji = false private var defaultToEmoji = false
private var previousDrawingData: Data? private var previousDrawingData: Data?
@ -3350,6 +3351,14 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
controller.completion = { [weak self] content in controller.completion = { [weak self] content in
if let self { if let self {
if let content { 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 stickerEntity = DrawingStickerEntity(content: content)
let scale: CGFloat let scale: CGFloat
if case .image = content { if case .image = content {
@ -3364,18 +3373,11 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
self.hasAnyChanges = true self.hasAnyChanges = true
self.controller?.isSavingAvailable = true self.controller?.isSavingAvailable = true
self.controller?.requestLayout(transition: .immediate) 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.stickerScreen = nil
self.mediaEditor?.play() self.mediaEditor?.play()
} }
return true
} }
controller.customModalStyleOverlayTransitionFactorUpdated = { [weak self, weak controller] transition in controller.customModalStyleOverlayTransitionFactorUpdated = { [weak self, weak controller] transition in
if let self, let controller { if let self, let controller {
@ -3405,6 +3407,18 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
} }
controller.addReaction = { [weak self, weak controller] in controller.addReaction = { [weak self, weak controller] in
if let self { 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 self.stickerScreen = nil
controller?.dismiss(animated: true) controller?.dismiss(animated: true)
@ -3421,6 +3435,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
self.controller?.present(controller, in: .window(.root)) self.controller?.present(controller, in: .window(.root))
return return
case .text: case .text:
self.mediaEditor?.stop()
self.insertTextEntity() self.insertTextEntity()
self.hasAnyChanges = true self.hasAnyChanges = true
@ -4094,6 +4109,53 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
}) })
self.present(controller, in: .current) 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) { fileprivate func presentCaptionLimitPremiumSuggestion(isPremium: Bool) {
self.dismissAllTooltips() self.dismissAllTooltips()

View File

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

View File

@ -4266,7 +4266,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
botAddress = bot.addressName ?? "" 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 { |> afterDisposed {
updateProgress() updateProgress()
}) })
@ -13233,7 +13233,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
case bot(id: PeerId, payload: String?, justInstalled: Bool) case bot(id: PeerId, payload: String?, justInstalled: Bool)
case gift case gift
} }
//editMediaOptions: MessageMediaEditingOptions?, editMediaReference: AnyMediaReference?, botId: PeerId? = nil, botPayload: String? = nil, botJustInstalled: Bool = false
private func presentAttachmentMenu(subject: AttachMenuSubject) { private func presentAttachmentMenu(subject: AttachMenuSubject) {
guard let peer = self.presentationInterfaceState.renderedPeer?.peer else { guard let peer = self.presentationInterfaceState.renderedPeer?.peer else {
return return
@ -13401,9 +13401,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
} else { } else {
let _ = (context.engine.messages.getAttachMenuBot(botId: botId) let _ = (context.engine.messages.getAttachMenuBot(botId: botId)
|> deliverOnMainQueue).start(next: { bot in |> deliverOnMainQueue).start(next: { bot in
let peer = EnginePeer(bot.peer) 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 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 _ = (context.engine.messages.addBotToAttachMenu(botId: botId, allowWrite: allowWrite) let _ = (context.engine.messages.addBotToAttachMenu(botId: botId, allowWrite: allowWrite)
|> deliverOnMainQueue).start(error: { _ in |> deliverOnMainQueue).start(error: { _ in

View File

@ -675,8 +675,7 @@ func openResolvedUrlImpl(_ resolvedUrl: ResolvedUrl, context: AccountContext, ur
|> deliverOnMainQueue).start(next: { bot in |> deliverOnMainQueue).start(next: { bot in
let choose = filterChooseTypes(choose, peerTypes: bot.peerTypes) let choose = filterChooseTypes(choose, peerTypes: bot.peerTypes)
let botPeer = EnginePeer(bot.peer) 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 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 _ = (context.engine.messages.addBotToAttachMenu(botId: peerId, allowWrite: allowWrite) let _ = (context.engine.messages.addBotToAttachMenu(botId: peerId, allowWrite: allowWrite)
|> deliverOnMainQueue).start(error: { _ in |> deliverOnMainQueue).start(error: { _ in
presentError(presentationData.strings.WebApp_AddToAttachmentUnavailableError) presentError(presentationData.strings.WebApp_AddToAttachmentUnavailableError)
@ -705,7 +704,7 @@ func openResolvedUrlImpl(_ resolvedUrl: ResolvedUrl, context: AccountContext, ur
guard let navigationController else { guard let navigationController else {
return 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) navigationController.pushViewController(controller)
} }
@ -717,7 +716,7 @@ func openResolvedUrlImpl(_ resolvedUrl: ResolvedUrl, context: AccountContext, ur
return 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 unreadTrendingStickerPacks: Int
let archivedStickerPacks: [ArchivedStickerPackItem]? let archivedStickerPacks: [ArchivedStickerPackItem]?
let userLimits: EngineConfiguration.UserLimits let userLimits: EngineConfiguration.UserLimits
let bots: [AttachMenuBot]
let hasPassport: Bool let hasPassport: Bool
let hasWatchApp: Bool let hasWatchApp: Bool
let enableQRLogin: Bool let enableQRLogin: Bool
@ -153,6 +154,7 @@ final class TelegramGlobalSettings {
unreadTrendingStickerPacks: Int, unreadTrendingStickerPacks: Int,
archivedStickerPacks: [ArchivedStickerPackItem]?, archivedStickerPacks: [ArchivedStickerPackItem]?,
userLimits: EngineConfiguration.UserLimits, userLimits: EngineConfiguration.UserLimits,
bots: [AttachMenuBot],
hasPassport: Bool, hasPassport: Bool,
hasWatchApp: Bool, hasWatchApp: Bool,
enableQRLogin: Bool enableQRLogin: Bool
@ -173,6 +175,7 @@ final class TelegramGlobalSettings {
self.unreadTrendingStickerPacks = unreadTrendingStickerPacks self.unreadTrendingStickerPacks = unreadTrendingStickerPacks
self.archivedStickerPacks = archivedStickerPacks self.archivedStickerPacks = archivedStickerPacks
self.userLimits = userLimits self.userLimits = userLimits
self.bots = bots
self.hasPassport = hasPassport self.hasPassport = hasPassport
self.hasWatchApp = hasWatchApp self.hasWatchApp = hasWatchApp
self.enableQRLogin = enableQRLogin self.enableQRLogin = enableQRLogin
@ -508,9 +511,10 @@ func peerInfoScreenSettingsData(context: AccountContext, peerId: EnginePeer.Id,
return automaticEnergyUsageShouldBeOn(settings: settings) return automaticEnergyUsageShouldBeOn(settings: settings)
} }
|> distinctUntilChanged, |> 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 (notificationExceptions, notificationsAuthorizationStatus, notificationsWarningSuppressed) = notifications
let (featuredStickerPacks, archivedStickerPacks) = stickerPacks let (featuredStickerPacks, archivedStickerPacks) = stickerPacks
@ -550,6 +554,7 @@ func peerInfoScreenSettingsData(context: AccountContext, peerId: EnginePeer.Id,
unreadTrendingStickerPacks: unreadTrendingStickerPacks, unreadTrendingStickerPacks: unreadTrendingStickerPacks,
archivedStickerPacks: archivedStickerPacks, archivedStickerPacks: archivedStickerPacks,
userLimits: peer?.isPremium == true ? limits.1 : limits.0, userLimits: peer?.isPremium == true ? limits.1 : limits.0,
bots: bots,
hasPassport: hasPassport, hasPassport: hasPassport,
hasWatchApp: hasWatchApp, hasWatchApp: hasWatchApp,
enableQRLogin: enableQRLogin) enableQRLogin: enableQRLogin)

View File

@ -91,6 +91,7 @@ import PeerInfoStoryGridScreen
import StoryContainerScreen import StoryContainerScreen
import ChatAvatarNavigationNode import ChatAvatarNavigationNode
import PeerReportScreen import PeerReportScreen
import WebUI
enum PeerInfoAvatarEditingMode { enum PeerInfoAvatarEditingMode {
case generic case generic
@ -549,6 +550,7 @@ private final class PeerInfoInteraction {
let toggleForumTopics: (Bool) -> Void let toggleForumTopics: (Bool) -> Void
let displayTopicsLimited: (TopicsLimitedReason) -> Void let displayTopicsLimited: (TopicsLimitedReason) -> Void
let openPeerMention: (String, ChatControllerInteractionNavigateToPeer) -> Void let openPeerMention: (String, ChatControllerInteractionNavigateToPeer) -> Void
let openBotApp: (AttachMenuBot) -> Void
init( init(
openUsername: @escaping (String) -> Void, openUsername: @escaping (String) -> Void,
@ -599,7 +601,8 @@ private final class PeerInfoInteraction {
dismissInput: @escaping () -> Void, dismissInput: @escaping () -> Void,
toggleForumTopics: @escaping (Bool) -> Void, toggleForumTopics: @escaping (Bool) -> Void,
displayTopicsLimited: @escaping (TopicsLimitedReason) -> Void, displayTopicsLimited: @escaping (TopicsLimitedReason) -> Void,
openPeerMention: @escaping (String, ChatControllerInteractionNavigateToPeer) -> Void openPeerMention: @escaping (String, ChatControllerInteractionNavigateToPeer) -> Void,
openBotApp: @escaping (AttachMenuBot) -> Void
) { ) {
self.openUsername = openUsername self.openUsername = openUsername
self.openPhone = openPhone self.openPhone = openPhone
@ -650,6 +653,7 @@ private final class PeerInfoInteraction {
self.toggleForumTopics = toggleForumTopics self.toggleForumTopics = toggleForumTopics
self.displayTopicsLimited = displayTopicsLimited self.displayTopicsLimited = displayTopicsLimited
self.openPeerMention = openPeerMention self.openPeerMention = openPeerMention
self.openBotApp = openBotApp
} }
} }
@ -661,7 +665,7 @@ private enum SettingsSection: Int, CaseIterable {
case phone case phone
case accounts case accounts
case proxy case proxy
case stories case apps
case shortcuts case shortcuts
case advanced case advanced
case payment 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) interaction.openSettings(.stories)
})) }))
@ -2340,6 +2356,9 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
}, },
openPeerMention: { [weak self] mention, navigation in openPeerMention: { [weak self] mention, navigation in
self?.openPeerMention(mention, navigation: navigation) self?.openPeerMention(mention, navigation: navigation)
},
openBotApp: { [weak self] bot in
self?.openBotApp(bot)
} }
) )
@ -4401,7 +4420,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
}, contentContext: nil) }, 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 openUserGeneratedUrl(context: self.context, peerId: self.peerId, url: url, concealed: concealed, present: { [weak self] c in
self?.controller?.present(c, in: .window(.root)) self?.controller?.present(c, in: .window(.root))
}, openResolved: { [weak self] tempResolved in }, openResolved: { [weak self] tempResolved in
@ -4411,8 +4430,9 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
let result: ResolvedUrl = external ? .externalUrl(url) : tempResolved 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) self?.openPeer(peerId: peer.id, navigation: navigation)
commit()
}, sendFile: nil, }, sendFile: nil,
sendSticker: nil, sendSticker: nil,
requestMessageActionUrlAuth: 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?) { private func performButtonAction(key: PeerInfoHeaderButtonKey, gesture: ContextGesture?) {
guard let controller = self.controller else { guard let controller = self.controller else {
return return

View File

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

View File

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

View File

@ -38,6 +38,8 @@ public class WebAppCancelButtonNode: ASDisplayNode {
public var state: State = .cancel public var state: State = .cancel
private var color: UIColor?
private var _theme: PresentationTheme private var _theme: PresentationTheme
public var theme: PresentationTheme { public var theme: PresentationTheme {
get { get {
@ -50,6 +52,24 @@ public class WebAppCancelButtonNode: ASDisplayNode {
} }
private let strings: PresentationStrings 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) { public init(theme: PresentationTheme, strings: PresentationStrings) {
self._theme = theme self._theme = theme
self.strings = strings self.strings = strings
@ -124,13 +144,15 @@ public class WebAppCancelButtonNode: ASDisplayNode {
self.buttonNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration) 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.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)) 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.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 { if let image = self.arrowNode.image {
self.arrowNode.frame = CGRect(origin: self.arrowNode.frame.origin, size: image.size) self.arrowNode.frame = CGRect(origin: self.arrowNode.frame.origin, size: image.size)
} }
@ -305,7 +327,7 @@ public final class WebAppController: ViewController, AttachmentContainable {
return .complete() return .complete()
} }
|> mapToSignal { bot -> Signal<(FileMediaReference, Bool)?, NoError> in |> 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 imageFile: TelegramMediaFile?
var isPlaceholder = false var isPlaceholder = false
if let file = bot.icons[.placeholder] { if let file = bot.icons[.placeholder] {
@ -546,12 +568,16 @@ public final class WebAppController: ViewController, AttachmentContainable {
} }
self.controller?.present(promptController, in: .window(.root)) 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? private var targetContentOffset: CGPoint?
func scrollViewDidScroll(_ scrollView: UIScrollView) { func scrollViewDidScroll(_ scrollView: UIScrollView) {
let contentOffset = scrollView.contentOffset.y self.updateNavigationBarAlpha(transition: .immediate)
self.controller?.navigationBar?.updateBackgroundAlpha(min(30.0, contentOffset) / 30.0, transition: .immediate)
if let targetContentOffset = self.targetContentOffset, scrollView.contentOffset != targetContentOffset { if let targetContentOffset = self.targetContentOffset, scrollView.contentOffset != targetContentOffset {
scrollView.contentOffset = targetContentOffset scrollView.contentOffset = targetContentOffset
} }
@ -821,8 +847,14 @@ public final class WebAppController: ViewController, AttachmentContainable {
transition.updateBackgroundColor(node: self.backgroundNode, color: color) transition.updateBackgroundColor(node: self.backgroundNode, color: color)
} }
case "web_app_set_header_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) { if let json = json {
self.headerColorKey = colorKey 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)) self.updateHeaderBackgroundColor(transition: .animated(duration: 0.2, curve: .linear))
} }
case "web_app_open_popup": case "web_app_open_popup":
@ -938,16 +970,37 @@ public final class WebAppController: ViewController, AttachmentContainable {
fileprivate var needDismissConfirmation = false fileprivate var needDismissConfirmation = false
fileprivate var headerColor: UIColor?
fileprivate var headerPrimaryTextColor: UIColor?
private var headerColorKey: String? private var headerColorKey: String?
private func updateHeaderBackgroundColor(transition: ContainedViewLayoutTransition) { private func updateHeaderBackgroundColor(transition: ContainedViewLayoutTransition) {
guard let controller = self.controller else {
return
}
let color: UIColor? let color: UIColor?
var primaryTextColor: UIColor?
var secondaryTextColor: UIColor?
var backgroundColor = self.presentationData.theme.list.plainBackgroundColor var backgroundColor = self.presentationData.theme.list.plainBackgroundColor
var secondaryBackgroundColor = self.presentationData.theme.list.blocksBackgroundColor var secondaryBackgroundColor = self.presentationData.theme.list.blocksBackgroundColor
if self.presentationData.theme.list.blocksBackgroundColor.rgb == self.presentationData.theme.list.plainBackgroundColor.rgb { if self.presentationData.theme.list.blocksBackgroundColor.rgb == self.presentationData.theme.list.plainBackgroundColor.rgb {
backgroundColor = self.presentationData.theme.list.modalPlainBackgroundColor backgroundColor = self.presentationData.theme.list.modalPlainBackgroundColor
secondaryBackgroundColor = self.presentationData.theme.list.plainBackgroundColor 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 { switch headerColorKey {
case "bg_color": case "bg_color":
color = backgroundColor color = backgroundColor
@ -959,6 +1012,13 @@ public final class WebAppController: ViewController, AttachmentContainable {
} else { } else {
color = nil 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.headerBackgroundNode, color: color ?? .clear)
transition.updateBackgroundColor(node: self.topOverscrollNode, 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 var titleView: CounterContollerTitleView?
private let cancelButtonNode: WebAppCancelButtonNode fileprivate let cancelButtonNode: WebAppCancelButtonNode
private let moreButtonNode: MoreButtonNode fileprivate let moreButtonNode: MoreButtonNode
private let context: AccountContext private let context: AccountContext
private let peerId: PeerId private let peerId: PeerId
@ -1318,10 +1378,9 @@ public final class WebAppController: ViewController, AttachmentContainable {
if let strongSelf = self { if let strongSelf = self {
strongSelf.presentationData = presentationData strongSelf.presentationData = presentationData
let navigationBarPresentationData = NavigationBarPresentationData(theme: NavigationBarTheme(rootControllerTheme: presentationData.theme), strings: NavigationBarStrings(back: "", close: "")) strongSelf.updateNavigationBarTheme(transition: .immediate)
strongSelf.navigationBar?.updatePresentationData(navigationBarPresentationData)
strongSelf.titleView?.theme = presentationData.theme strongSelf.titleView?.theme = presentationData.theme
strongSelf.cancelButtonNode.theme = presentationData.theme strongSelf.cancelButtonNode.theme = presentationData.theme
strongSelf.moreButtonNode.theme = presentationData.theme strongSelf.moreButtonNode.theme = presentationData.theme
@ -1341,6 +1400,32 @@ public final class WebAppController: ViewController, AttachmentContainable {
self.presentationDataDisposable?.dispose() 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() { @objc private func cancelPressed() {
if case .back = self.cancelButtonNode.state { if case .back = self.cancelButtonNode.state {
self.controllerNode.sendBackButtonEvent() 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 return point.x > 30.0
} }
self.allowsBackForwardNavigationGestures = false self.allowsBackForwardNavigationGestures = false
if #available(iOS 16.4, *) {
self.isInspectable = true
}
handleScriptMessageImpl = { [weak self] message in handleScriptMessageImpl = { [weak self] message in
if let strongSelf = self { if let strongSelf = self {