From 4c4080f5cbd8f6d033ad6582fd745a5a1dc974ae Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Tue, 5 Sep 2023 14:07:48 +0400 Subject: [PATCH] Various improvements --- .../Telegram-iOS/en.lproj/Localizable.strings | 10 + .../Sources/AccountContext.swift | 3 + .../Sources/AttachmentController.swift | 6 +- .../Sources/AttachmentPanel.swift | 6 +- .../Sources/CounterContollerTitleView.swift | 29 +- submodules/Display/Source/NavigationBar.swift | 4 +- .../DrawingUI/Sources/DrawingScreen.swift | 1 + .../Sources/StickerPickerScreen.swift | 79 +-- .../Sources/MoreButtonNode.swift | 26 +- .../Sources/PremiumIntroScreen.swift | 9 + .../Sources/PremiumLimitScreen.swift | 551 +++++++++++------- .../State/UserLimitsConfiguration.swift | 9 +- .../Data/ConfigurationData.swift | 8 +- .../Messages/AttachMenuBots.swift | 31 +- .../TelegramEngine/Messages/BotWebView.swift | 18 +- .../Messages/TelegramEngineMessages.swift | 4 +- .../Sources/MediaEditorScreen.swift | 84 ++- .../TelegramUI/Sources/AccountContext.swift | 17 +- .../TelegramUI/Sources/ChatController.swift | 8 +- .../TelegramUI/Sources/OpenResolvedUrl.swift | 7 +- .../Sources/PeerInfo/PeerInfoData.swift | 9 +- .../Sources/PeerInfo/PeerInfoScreen.swift | 75 ++- .../Sources/SharedAccountContext.swift | 2 + .../Sources/UndoOverlayControllerNode.swift | 2 +- .../WebUI/Sources/WebAppController.swift | 115 +++- .../Sources/WebAppTermsAlertController.swift | 302 ++++++++++ submodules/WebUI/Sources/WebAppWebView.swift | 3 + 27 files changed, 1097 insertions(+), 321 deletions(-) create mode 100644 submodules/WebUI/Sources/WebAppTermsAlertController.swift diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index b8b3f1f7f8..e2382215ed 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -9901,3 +9901,13 @@ Sorry for the inconvenience."; "WebApp.SharePhoneTitle" = "Share Phone Number?"; "WebApp.SharePhoneConfirmation" = "**%@** will know your phone number. This can be useful for integration with other services."; "WebApp.SharePhoneConfirmationUnblock" = "**%@** will know your phone number. This can be useful for integration with other services.\n\nThis will also unblock the bot."; + +"Story.Editor.TooltipPremiumReactionLimitValue_1" = "**%@** reaction tag"; +"Story.Editor.TooltipPremiumReactionLimitValue_any" = "**%@** reactions tags"; + +"Story.Editor.TooltipPremiumReactionLimitTitle" = "Increase Limit"; +"Story.Editor.TooltipPremiumReactionLimitText" = "Upgrade to [Telegram Premium]() to add up to %@ to a story."; + +"Story.Editor.TooltipReachedReactionLimitTitle" = "Limit Reached"; +"Story.Editor.TooltipReachedReactionLimitText" = "You can't add up more than %@ to a story."; + diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index f95577e481..32917f52d8 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -952,6 +952,7 @@ public enum PremiumIntroSource { case storiesPermanentViews case storiesFormatting case storiesExpirationDurations + case storiesSuggestedReactions } public enum PremiumDemoSubject { @@ -984,6 +985,7 @@ public enum PremiumLimitSubject { case expiringStories case storiesWeekly case storiesMonthly + case storiesChannelBoost(level: Int32, link: String?) } public protocol ComposeController: ViewController { @@ -1025,6 +1027,7 @@ public protocol AccountContext: AnyObject { var animatedEmojiStickers: [String: [StickerPackItem]] { get } + var isPremium: Bool { get } var userLimits: EngineConfiguration.UserLimits { get } var imageCache: AnyObject? { get } diff --git a/submodules/AttachmentUI/Sources/AttachmentController.swift b/submodules/AttachmentUI/Sources/AttachmentController.swift index d78ef8bdb4..4b35a35891 100644 --- a/submodules/AttachmentUI/Sources/AttachmentController.swift +++ b/submodules/AttachmentUI/Sources/AttachmentController.swift @@ -19,7 +19,7 @@ public enum AttachmentButtonType: Equatable { case location case contact case poll - case app(Peer, String, [AttachMenuBots.Bot.IconName: TelegramMediaFile]) + case app(EnginePeer, String, [AttachMenuBots.Bot.IconName: TelegramMediaFile]) case gift case standalone @@ -56,7 +56,7 @@ public enum AttachmentButtonType: Equatable { return false } case let .app(lhsPeer, lhsTitle, lhsIcons): - if case let .app(rhsPeer, rhsTitle, rhsIcons) = rhs, arePeersEqual(lhsPeer, rhsPeer), lhsTitle == rhsTitle, lhsIcons == rhsIcons { + if case let .app(rhsPeer, rhsTitle, rhsIcons) = rhs, lhsPeer == rhsPeer, lhsTitle == rhsTitle, lhsIcons == rhsIcons { return true } else { return false @@ -1040,7 +1040,7 @@ public class AttachmentController: ViewController { |> deliverOnMainQueue).start(next: { bots in for bot in bots { for (name, file) in bot.icons { - if [.iOSAnimated, .placeholder].contains(name), let peer = PeerReference(bot.peer) { + if [.iOSAnimated, .placeholder].contains(name), let peer = PeerReference(bot.peer._asPeer()) { if case .placeholder = name { let path = context.account.postbox.mediaBox.cachedRepresentationCompletePath(file.resource.id, representation: CachedPreparedSvgRepresentation()) if !FileManager.default.fileExists(atPath: path) { diff --git a/submodules/AttachmentUI/Sources/AttachmentPanel.swift b/submodules/AttachmentUI/Sources/AttachmentPanel.swift index e81fc2894a..505cd6b429 100644 --- a/submodules/AttachmentUI/Sources/AttachmentPanel.swift +++ b/submodules/AttachmentUI/Sources/AttachmentPanel.swift @@ -176,7 +176,7 @@ private final class AttachButtonComponent: CombinedComponent { let imageName: String var imageFile: TelegramMediaFile? var animationFile: TelegramMediaFile? - var botPeer: Peer? + var botPeer: EnginePeer? let component = context.component let strings = component.strings @@ -245,7 +245,7 @@ private final class AttachButtonComponent: CombinedComponent { ) } else { var fileReference: FileMediaReference? - if let peer = botPeer.flatMap({ PeerReference($0 )}), let imageFile = imageFile { + if let peer = botPeer.flatMap({ PeerReference($0._asPeer())}), let imageFile = imageFile { fileReference = .attachBot(peer: peer, media: imageFile) } @@ -1143,7 +1143,7 @@ final class AttachmentPanel: ASDisplayNode, UIScrollViewDelegate { if case let .app(peer, _, iconFiles) = type { for (name, file) in iconFiles { if [.default, .iOSAnimated, .placeholder].contains(name) { - if self.iconDisposables[file.fileId] == nil, let peer = PeerReference(peer) { + if self.iconDisposables[file.fileId] == nil, let peer = PeerReference(peer._asPeer()) { if case .placeholder = name { let account = self.context.account let path = account.postbox.mediaBox.cachedRepresentationCompletePath(file.resource.id, representation: CachedPreparedSvgRepresentation()) diff --git a/submodules/CounterContollerTitleView/Sources/CounterContollerTitleView.swift b/submodules/CounterContollerTitleView/Sources/CounterContollerTitleView.swift index 455947e152..17c9e99ac0 100644 --- a/submodules/CounterContollerTitleView/Sources/CounterContollerTitleView.swift +++ b/submodules/CounterContollerTitleView/Sources/CounterContollerTitleView.swift @@ -32,9 +32,34 @@ public final class CounterContollerTitleView: UIView { } } + private var primaryTextColor: UIColor? + private var secondaryTextColor: UIColor? + + public func updateTextColors(primary: UIColor?, secondary: UIColor?, transition: ContainedViewLayoutTransition) { + self.primaryTextColor = primary + self.secondaryTextColor = secondary + + if case let .animated(duration, curve) = transition { + if let snapshotView = self.snapshotContentTree() { + snapshotView.frame = self.bounds + self.addSubview(snapshotView) + + snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, timingFunction: curve.timingFunction, removeOnCompletion: false, completion: { _ in + snapshotView.removeFromSuperview() + }) + self.titleNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration, timingFunction: curve.timingFunction) + self.subtitleNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration, timingFunction: curve.timingFunction) + } + } + + self.update() + } + private func update() { - self.titleNode.attributedText = NSAttributedString(string: self.title.title, font: Font.semibold(17.0), textColor: self.theme.rootController.navigationBar.primaryTextColor) - self.subtitleNode.attributedText = NSAttributedString(string: self.title.counter, font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor) + let primaryTextColor = self.primaryTextColor ?? self.theme.rootController.navigationBar.primaryTextColor + let secondaryTextColor = self.secondaryTextColor ?? self.theme.rootController.navigationBar.secondaryTextColor + self.titleNode.attributedText = NSAttributedString(string: self.title.title, font: Font.semibold(17.0), textColor: primaryTextColor) + self.subtitleNode.attributedText = NSAttributedString(string: self.title.counter, font: Font.regular(13.0), textColor: secondaryTextColor) self.accessibilityLabel = self.title.title self.accessibilityValue = self.title.counter diff --git a/submodules/Display/Source/NavigationBar.swift b/submodules/Display/Source/NavigationBar.swift index 06f91b8997..5a4fe019be 100644 --- a/submodules/Display/Source/NavigationBar.swift +++ b/submodules/Display/Source/NavigationBar.swift @@ -1186,11 +1186,11 @@ open class NavigationBar: ASDisplayNode { transition.updateAlpha(node: self.stripeNode, alpha: alpha, delay: 0.15) } - public func updatePresentationData(_ presentationData: NavigationBarPresentationData) { + public func updatePresentationData(_ presentationData: NavigationBarPresentationData, transition: ContainedViewLayoutTransition = .immediate) { if presentationData.theme !== self.presentationData.theme || presentationData.strings !== self.presentationData.strings { self.presentationData = presentationData - self.backgroundNode.updateColor(color: self.presentationData.theme.backgroundColor, transition: .immediate) + self.backgroundNode.updateColor(color: self.presentationData.theme.backgroundColor, transition: transition) self.backButtonNode.color = self.presentationData.theme.buttonColor self.backButtonNode.disabledColor = self.presentationData.theme.disabledButtonColor diff --git a/submodules/DrawingUI/Sources/DrawingScreen.swift b/submodules/DrawingUI/Sources/DrawingScreen.swift index 9d85a1a64e..3d52e1f9d8 100644 --- a/submodules/DrawingUI/Sources/DrawingScreen.swift +++ b/submodules/DrawingUI/Sources/DrawingScreen.swift @@ -1022,6 +1022,7 @@ private final class DrawingScreenComponent: CombinedComponent { } else { self?.updateCurrentMode(.drawing) } + return true } self.present(controller) self.updated(transition: .easeInOut(duration: 0.2)) diff --git a/submodules/DrawingUI/Sources/StickerPickerScreen.swift b/submodules/DrawingUI/Sources/StickerPickerScreen.swift index 2301f1fdab..15ae4c16f7 100644 --- a/submodules/DrawingUI/Sources/StickerPickerScreen.swift +++ b/submodules/DrawingUI/Sources/StickerPickerScreen.swift @@ -147,7 +147,6 @@ private final class StickerSelectionComponent: Component { self.interaction = ChatEntityKeyboardInputNode.Interaction( sendSticker: { [weak self] file, silent, schedule, query, clearInput, sourceView, sourceRect, sourceLayer, _ in if let self, let controller = self.component?.getController() { - controller.completion(.file(file.media, .sticker)) controller.forEachController { c in if let c = c as? StickerPackScreenImpl { c.dismiss(animated: true) @@ -159,7 +158,9 @@ private final class StickerSelectionComponent: Component { c.dismiss(animated: true) } }) - controller.dismiss(animated: true) + if controller.completion(.file(file.media, .sticker)) { + controller.dismiss(animated: true) + } } return false }, @@ -167,8 +168,9 @@ private final class StickerSelectionComponent: Component { }, sendGif: { [weak self] file, _, _, _, _ in if let self, let controller = self.component?.getController() { - controller.completion(.video(file.media)) - controller.dismiss(animated: true) + if controller.completion(.video(file.media)) { + controller.dismiss(animated: true) + } } return false }, @@ -176,8 +178,9 @@ private final class StickerSelectionComponent: Component { if let self, let controller = self.component?.getController() { if case let .internalReference(reference) = result { if let file = reference.file { - controller.completion(.video(file)) - controller.dismiss(animated: true) + if controller.completion(.video(file)) { + controller.dismiss(animated: true) + } } } } @@ -570,11 +573,12 @@ public class StickerPickerScreen: ViewController { let gifInputInteraction = GifPagerContentComponent.InputInteraction( performItemAction: { [weak self] item, view, rect in - guard let self else { + guard let self, let controller = self.controller else { return } - self.controller?.completion(.video(item.file.media)) - self.controller?.dismiss(animated: true) + if controller.completion(.video(item.file.media)) { + controller.dismiss(animated: true) + } }, openGifContextMenu: { [weak self] item, sourceView, sourceRect, gesture, isSaved in guard let self else { @@ -729,15 +733,17 @@ public class StickerPickerScreen: ViewController { return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Add"), color: theme.actionSheet.primaryTextColor) }, action: { [weak self] _, f in f(.default) - if let self { + if let self, let controller = self.controller { if isSaved { - self.controller?.completion(.video(file.media)) - self.controller?.dismiss(animated: true) + if controller.completion(.video(file.media)) { + self.controller?.dismiss(animated: true) + } } else if let (_, result) = contextResult { if case let .internalReference(reference) = result { if let file = reference.file { - self.controller?.completion(.video(file)) - self.controller?.dismiss(animated: true) + if controller.completion(.video(file)) { + self.controller?.dismiss(animated: true) + } } } } @@ -867,8 +873,9 @@ public class StickerPickerScreen: ViewController { }) }) } else if let file = item.itemFile { - strongSelf.controller?.completion(.file(file, .sticker)) - strongSelf.controller?.dismiss(animated: true) + if controller.completion(.file(file, .sticker)) { + controller.dismiss(animated: true) + } } else if case let .staticEmoji(emoji) = item.content { if let image = generateImage(CGSize(width: 256.0, height: 256.0), scale: 1.0, rotatedContext: { size, context in context.clear(CGRect(origin: .zero, size: size)) @@ -889,9 +896,12 @@ public class StickerPickerScreen: ViewController { CTLineDraw(line, context) context.translateBy(x: -lineOrigin.x, y: -lineOrigin.y) }) { - strongSelf.controller?.completion(.image(image, .sticker)) + if controller.completion(.image(image, .sticker)) { + controller.dismiss(animated: true) + } + } else { + controller.dismiss(animated: true) } - strongSelf.controller?.dismiss(animated: true) } }, deleteBackwards: nil, @@ -1222,10 +1232,10 @@ public class StickerPickerScreen: ViewController { hideBackground: true, stateContext: nil, addImage: controller.hasGifs ? { [weak self] in - if let self { - self.controller?.completion(nil) - self.controller?.dismiss(animated: true) - self.controller?.presentGallery() + if let self, let controller = self.controller { + let _ = controller.completion(nil) + controller.dismiss(animated: true) + controller.presentGallery() } } : nil ) @@ -1263,11 +1273,12 @@ public class StickerPickerScreen: ViewController { highlightedPackId: featuredStickerPack.info.id, forceTheme: defaultDarkPresentationTheme, sendSticker: { [weak self] fileReference, _, _ in - guard let self else { + guard let self, let controller = self.controller else { return false } - self.controller?.completion(.file(fileReference.media, .sticker)) - self.controller?.dismiss(animated: true) + if controller.completion(.file(fileReference.media, .sticker)) { + controller.dismiss(animated: true) + } return true } )) @@ -1277,8 +1288,8 @@ public class StickerPickerScreen: ViewController { } }) } else { - self.controller?.completion(.file(file, .sticker)) - self.controller?.dismiss(animated: true) + let _ = controller.completion(.file(file, .sticker)) + controller.dismiss(animated: true) } }, deleteBackwards: nil, @@ -1490,10 +1501,10 @@ public class StickerPickerScreen: ViewController { hideBackground: true, stateContext: nil, addImage: controller.hasGifs ? { [weak self] in - if let self { - self.controller?.completion(nil) - self.controller?.dismiss(animated: true) - self.controller?.presentGallery() + if let self, let controller = self.controller { + let _ = controller.completion(nil) + controller.dismiss(animated: true) + controller.presentGallery() } } : nil ) @@ -1520,7 +1531,7 @@ public class StickerPickerScreen: ViewController { @objc func dimTapGesture(_ recognizer: UITapGestureRecognizer) { if case .ended = recognizer.state { - self.controller?.completion(nil) + let _ = self.controller?.completion(nil) self.controller?.dismiss(animated: true) } } @@ -1865,7 +1876,7 @@ public class StickerPickerScreen: ViewController { var dismissing = false if bounds.minY < -60 || (bounds.minY < 0.0 && velocity.y > 300.0) || (self.isExpanded && bounds.minY.isZero && velocity.y > 1800.0) { - self.controller?.completion(nil) + let _ = self.controller?.completion(nil) self.controller?.dismiss(animated: true, completion: nil) dismissing = true } else if self.isExpanded { @@ -1947,7 +1958,7 @@ public class StickerPickerScreen: ViewController { public var pushController: (ViewController) -> Void = { _ in } public var presentController: (ViewController) -> Void = { _ in } - public var completion: (DrawingStickerEntity.Content?) -> Void = { _ in } + public var completion: (DrawingStickerEntity.Content?) -> Bool = { _ in return true } public var presentGallery: () -> Void = { } public var presentLocationPicker: () -> Void = { } diff --git a/submodules/MoreButtonNode/Sources/MoreButtonNode.swift b/submodules/MoreButtonNode/Sources/MoreButtonNode.swift index 1c14ba94cc..ca41aeb431 100644 --- a/submodules/MoreButtonNode/Sources/MoreButtonNode.swift +++ b/submodules/MoreButtonNode/Sources/MoreButtonNode.swift @@ -74,12 +74,36 @@ public final class MoreButtonNode: ASDisplayNode { private let buttonNode: HighlightableButtonNode public let iconNode: MoreIconNode + private var color: UIColor? + public var theme: PresentationTheme { didSet { - self.iconNode.customColor = self.theme.rootController.navigationBar.buttonColor + self.update() } } + public func updateColor(_ color: UIColor?, transition: ContainedViewLayoutTransition) { + self.color = color + + if case let .animated(duration, curve) = transition { + if let snapshotView = self.iconNode.view.snapshotContentTree() { + snapshotView.frame = self.iconNode.frame + self.view.addSubview(snapshotView) + + snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, timingFunction: curve.timingFunction, removeOnCompletion: false, completion: { _ in + snapshotView.removeFromSuperview() + }) + self.iconNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration, timingFunction: curve.timingFunction) + } + } + self.update() + } + + private func update() { + let color = self.color ?? self.theme.rootController.navigationBar.buttonColor + self.iconNode.customColor = color + } + public init(theme: PresentationTheme) { self.theme = theme diff --git a/submodules/PremiumUI/Sources/PremiumIntroScreen.swift b/submodules/PremiumUI/Sources/PremiumIntroScreen.swift index 92a0a34dca..ab006575c8 100644 --- a/submodules/PremiumUI/Sources/PremiumIntroScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumIntroScreen.swift @@ -220,6 +220,12 @@ public enum PremiumSource: Equatable { } else { return false } + case .storiesSuggestedReactions: + if case .storiesSuggestedReactions = rhs { + return true + } else { + return false + } } } @@ -255,6 +261,7 @@ public enum PremiumSource: Equatable { case storiesPermanentViews case storiesFormatting case storiesExpirationDurations + case storiesSuggestedReactions var identifier: String? { switch self { @@ -324,6 +331,8 @@ public enum PremiumSource: Equatable { return "stories__links_and_formatting" case .storiesExpirationDurations: return "stories__expiration_durations" + case .storiesSuggestedReactions: + return "stories__suggested_reactions" } } } diff --git a/submodules/PremiumUI/Sources/PremiumLimitScreen.swift b/submodules/PremiumUI/Sources/PremiumLimitScreen.swift index 1c77a482c6..d9d72d00fa 100644 --- a/submodules/PremiumUI/Sources/PremiumLimitScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumLimitScreen.swift @@ -45,6 +45,7 @@ private class PremiumLimitAnimationComponent: Component { private let badgeText: String? private let badgePosition: CGFloat private let badgeGraphPosition: CGFloat + private let invertProgress: Bool private let isPremiumDisabled: Bool init( @@ -55,6 +56,7 @@ private class PremiumLimitAnimationComponent: Component { badgeText: String?, badgePosition: CGFloat, badgeGraphPosition: CGFloat, + invertProgress: Bool, isPremiumDisabled: Bool ) { self.iconName = iconName @@ -64,6 +66,7 @@ private class PremiumLimitAnimationComponent: Component { self.badgeText = badgeText self.badgePosition = badgePosition self.badgeGraphPosition = badgeGraphPosition + self.invertProgress = invertProgress self.isPremiumDisabled = isPremiumDisabled } @@ -89,6 +92,9 @@ private class PremiumLimitAnimationComponent: Component { if lhs.badgeGraphPosition != rhs.badgeGraphPosition { return false } + if lhs.invertProgress != rhs.invertProgress { + return false + } if lhs.isPremiumDisabled != rhs.isPremiumDisabled { return false } @@ -96,6 +102,8 @@ private class PremiumLimitAnimationComponent: Component { } final class View: UIView { + private var component: PremiumLimitAnimationComponent? + private let container: SimpleLayer private let inactiveBackground: SimpleLayer @@ -240,6 +248,7 @@ private class PremiumLimitAnimationComponent: Component { var previousAvailableSize: CGSize? func update(component: PremiumLimitAnimationComponent, availableSize: CGSize, transition: Transition) -> CGSize { + self.component = component self.inactiveBackground.backgroundColor = component.inactiveColor.cgColor self.activeBackground.backgroundColor = component.activeColors.last?.cgColor @@ -255,9 +264,13 @@ private class PremiumLimitAnimationComponent: Component { let activeWidth: CGFloat = containerFrame.width - activityPosition if !component.isPremiumDisabled { - self.inactiveBackground.frame = CGRect(origin: .zero, size: CGSize(width: activityPosition, height: lineHeight)) - self.activeContainer.frame = CGRect(origin: CGPoint(x: activityPosition, y: 0.0), size: CGSize(width: activeWidth, height: lineHeight)) - + if component.invertProgress { + self.inactiveBackground.frame = CGRect(origin: CGPoint(x: activityPosition, y: 0.0), size: CGSize(width: availableSize.width - activityPosition, height: lineHeight)) + self.activeContainer.frame = CGRect(origin: .zero, size: CGSize(width: activityPosition, height: lineHeight)) + } else { + self.inactiveBackground.frame = CGRect(origin: .zero, size: CGSize(width: activityPosition, height: lineHeight)) + self.activeContainer.frame = CGRect(origin: CGPoint(x: activityPosition, y: 0.0), size: CGSize(width: activeWidth, height: lineHeight)) + } self.activeBackground.frame = CGRect(origin: .zero, size: CGSize(width: activeWidth * (1.0 + 0.35), height: lineHeight)) if self.activeBackground.animation(forKey: "movement") == nil { self.activeBackground.position = CGPoint(x: -self.activeContainer.frame.width * 0.35, y: lineHeight / 2.0) @@ -373,6 +386,9 @@ private class PremiumLimitAnimationComponent: Component { } private func setupGradientAnimations() { + guard let _ = self.component else { + return + } if let _ = self.badgeForeground.animation(forKey: "movement") { } else { CATransaction.begin() @@ -440,6 +456,7 @@ public final class PremiumLimitDisplayComponent: CombinedComponent { let badgeText: String? let badgePosition: CGFloat let badgeGraphPosition: CGFloat + let invertProgress: Bool let isPremiumDisabled: Bool public init( @@ -455,6 +472,7 @@ public final class PremiumLimitDisplayComponent: CombinedComponent { badgeText: String?, badgePosition: CGFloat, badgeGraphPosition: CGFloat, + invertProgress: Bool = false, isPremiumDisabled: Bool ) { self.inactiveColor = inactiveColor @@ -469,6 +487,7 @@ public final class PremiumLimitDisplayComponent: CombinedComponent { self.badgeText = badgeText self.badgePosition = badgePosition self.badgeGraphPosition = badgeGraphPosition + self.invertProgress = invertProgress self.isPremiumDisabled = isPremiumDisabled } @@ -509,6 +528,9 @@ public final class PremiumLimitDisplayComponent: CombinedComponent { if lhs.badgeGraphPosition != rhs.badgeGraphPosition { return false } + if lhs.invertProgress != rhs.invertProgress { + return false + } if lhs.isPremiumDisabled != rhs.isPremiumDisabled { return false } @@ -528,6 +550,16 @@ public final class PremiumLimitDisplayComponent: CombinedComponent { let height: CGFloat = 120.0 let lineHeight: CGFloat = 30.0 + let leftTextColor: UIColor + let rightTextColor: UIColor + if component.invertProgress { + leftTextColor = component.activeTitleColor + rightTextColor = component.inactiveTitleColor + } else { + leftTextColor = component.inactiveTitleColor + rightTextColor = component.activeTitleColor + } + let animation = animation.update( component: PremiumLimitAnimationComponent( iconName: component.badgeIconName, @@ -537,6 +569,7 @@ public final class PremiumLimitDisplayComponent: CombinedComponent { badgeText: component.badgeText, badgePosition: component.badgePosition, badgeGraphPosition: component.badgeGraphPosition, + invertProgress: component.invertProgress, isPremiumDisabled: component.isPremiumDisabled ), availableSize: CGSize(width: context.availableSize.width, height: height), @@ -554,7 +587,7 @@ public final class PremiumLimitDisplayComponent: CombinedComponent { NSAttributedString( string: component.inactiveTitle, font: Font.semibold(15.0), - textColor: component.inactiveTitleColor + textColor: leftTextColor ) ) ), @@ -568,7 +601,7 @@ public final class PremiumLimitDisplayComponent: CombinedComponent { NSAttributedString( string: component.inactiveValue, font: Font.semibold(15.0), - textColor: component.inactiveTitleColor + textColor: leftTextColor ) ) ), @@ -582,7 +615,7 @@ public final class PremiumLimitDisplayComponent: CombinedComponent { NSAttributedString( string: component.activeTitle, font: Font.semibold(15.0), - textColor: component.activeTitleColor + textColor: rightTextColor ) ) ), @@ -596,7 +629,7 @@ public final class PremiumLimitDisplayComponent: CombinedComponent { NSAttributedString( string: component.activeValue, font: Font.semibold(15.0), - textColor: component.activeTitleColor + textColor: rightTextColor ) ) ), @@ -720,6 +753,7 @@ private final class LimitSheetContent: CombinedComponent { let title = Child(MultilineTextComponent.self) let text = Child(BalancedTextComponent.self) let limit = Child(PremiumLimitDisplayComponent.self) + let linkButton = Child(SolidRoundedButtonComponent.self) let button = Child(SolidRoundedButtonComponent.self) return { context in @@ -761,219 +795,262 @@ private final class LimitSheetContent: CombinedComponent { ) var titleText = strings.Premium_LimitReached + var actionButtonText: String? var buttonAnimationName: String? = "premium_x2" + var buttonIconName: String? let iconName: String var badgeText: String var string: String let defaultValue: String + var defaultTitle = strings.Premium_Free + var premiumTitle = strings.Premium_Premium let premiumValue: String let badgePosition: CGFloat let badgeGraphPosition: CGFloat + var invertProgress = false switch subject { - case .folders: - let limit = state.limits.maxFoldersCount - let premiumLimit = state.premiumLimits.maxFoldersCount - iconName = "Premium/Folder" - badgeText = "\(component.count)" - string = component.count >= premiumLimit ? strings.Premium_MaxFoldersCountFinalText("\(premiumLimit)").string : strings.Premium_MaxFoldersCountText("\(limit)", "\(premiumLimit)").string - defaultValue = component.count > limit ? "\(limit)" : "" - premiumValue = component.count >= premiumLimit ? "" : "\(premiumLimit)" - if component.count >= premiumLimit { - badgeGraphPosition = max(0.15, CGFloat(limit) / CGFloat(premiumLimit)) - } else { - badgeGraphPosition = max(0.15, CGFloat(component.count) / CGFloat(premiumLimit)) - } - badgePosition = max(0.15, CGFloat(component.count) / CGFloat(premiumLimit)) - - if !state.isPremium && badgePosition > 0.5 { - string = strings.Premium_MaxFoldersCountText("\(limit)", "\(premiumLimit)").string - } - - if isPremiumDisabled { - badgeText = "\(limit)" - string = strings.Premium_MaxFoldersCountNoPremiumText("\(limit)").string - } - case .chatsPerFolder: - let limit = state.limits.maxFolderChatsCount - let premiumLimit = state.premiumLimits.maxFolderChatsCount - iconName = "Premium/Chat" - badgeText = "\(component.count)" - string = component.count >= premiumLimit ? strings.Premium_MaxChatsInFolderFinalText("\(premiumLimit)").string : strings.Premium_MaxChatsInFolderText("\(limit)", "\(premiumLimit)").string - defaultValue = component.count > limit ? "\(limit)" : "" - premiumValue = component.count >= premiumLimit ? "" : "\(premiumLimit)" - badgePosition = CGFloat(component.count) / CGFloat(premiumLimit) - badgeGraphPosition = badgePosition - - if isPremiumDisabled { - badgeText = "\(limit)" - string = strings.Premium_MaxChatsInFolderNoPremiumText("\(limit)").string - } - case .channels: - let limit = state.limits.maxChannelsCount - let premiumLimit = state.premiumLimits.maxChannelsCount - iconName = "Premium/Chat" - badgeText = "\(component.count)" - string = component.count >= premiumLimit ? strings.Premium_MaxChannelsFinalText("\(premiumLimit)").string : strings.Premium_MaxChannelsText("\(limit)", "\(premiumLimit)").string - defaultValue = component.count > limit ? "\(limit)" : "" - premiumValue = component.count >= premiumLimit ? "" : "\(premiumLimit)" - if component.count >= premiumLimit { - badgeGraphPosition = max(0.15, CGFloat(limit) / CGFloat(premiumLimit)) - } else { - badgeGraphPosition = max(0.15, CGFloat(component.count) / CGFloat(premiumLimit)) - } - badgePosition = max(0.15, CGFloat(component.count) / CGFloat(premiumLimit)) - - if isPremiumDisabled { - badgeText = "\(limit)" - string = strings.Premium_MaxChannelsNoPremiumText("\(limit)").string - } - case .linksPerSharedFolder: - /*let count: Int32 = 5 + Int32("".count)// component.count - let limit: Int32 = 5 + Int32("".count)//state.limits.maxSharedFolderInviteLinks - let premiumLimit: Int32 = 100 + Int32("".count)//state.premiumLimits.maxSharedFolderInviteLinks*/ - - let count: Int32 = component.count - let limit: Int32 = state.limits.maxSharedFolderInviteLinks - let premiumLimit: Int32 = state.premiumLimits.maxSharedFolderInviteLinks - - iconName = "Premium/Link" - badgeText = "\(count)" - string = count >= premiumLimit ? strings.Premium_MaxSharedFolderLinksFinalText("\(premiumLimit)").string : strings.Premium_MaxSharedFolderLinksText("\(limit)", "\(premiumLimit)").string - defaultValue = count > limit ? "\(limit)" : "" - premiumValue = count >= premiumLimit ? "" : "\(premiumLimit)" - if count >= premiumLimit { - badgeGraphPosition = max(0.15, CGFloat(limit) / CGFloat(premiumLimit)) - } else { - badgeGraphPosition = max(0.15, CGFloat(count) / CGFloat(premiumLimit)) - } - badgePosition = max(0.15, CGFloat(count) / CGFloat(premiumLimit)) - - if isPremiumDisabled { - badgeText = "\(limit)" - string = strings.Premium_MaxSharedFolderLinksNoPremiumText("\(limit)").string - } - - buttonAnimationName = nil - case .membershipInSharedFolders: - let limit = state.limits.maxSharedFolderJoin - let premiumLimit = state.premiumLimits.maxSharedFolderJoin - iconName = "Premium/Folder" - badgeText = "\(component.count)" - string = component.count >= premiumLimit ? strings.Premium_MaxSharedFolderMembershipFinalText("\(premiumLimit)").string : strings.Premium_MaxSharedFolderMembershipText("\(limit)", "\(premiumLimit)").string - defaultValue = component.count > limit ? "\(limit)" : "" - premiumValue = component.count >= premiumLimit ? "" : "\(premiumLimit)" - if component.count >= premiumLimit { - badgeGraphPosition = max(0.15, CGFloat(limit) / CGFloat(premiumLimit)) - } else { - badgeGraphPosition = max(0.15, CGFloat(component.count) / CGFloat(premiumLimit)) - } - badgePosition = max(0.15, CGFloat(component.count) / CGFloat(premiumLimit)) - - if isPremiumDisabled { - badgeText = "\(limit)" - string = strings.Premium_MaxSharedFolderMembershipNoPremiumText("\(limit)").string - } - - buttonAnimationName = nil - case .pins: - let limit = state.limits.maxPinnedChatCount - let premiumLimit = state.premiumLimits.maxPinnedChatCount - iconName = "Premium/Pin" - badgeText = "\(component.count)" - string = component.count >= premiumLimit ? strings.Premium_MaxPinsFinalText("\(premiumLimit)").string : strings.Premium_MaxPinsText("\(limit)", "\(premiumLimit)").string - defaultValue = component.count > limit ? "\(limit)" : "" - premiumValue = component.count >= premiumLimit ? "" : "\(premiumLimit)" - badgePosition = CGFloat(component.count) / CGFloat(premiumLimit) - badgeGraphPosition = badgePosition - - if isPremiumDisabled { - badgeText = "\(limit)" - string = strings.Premium_MaxPinsNoPremiumText("\(limit)").string - } - case .files: - let limit = Int64(state.limits.maxUploadFileParts) * 512 * 1024 + 1024 * 1024 * 100 - let premiumLimit = Int64(state.premiumLimits.maxUploadFileParts) * 512 * 1024 + 1024 * 1024 * 100 - iconName = "Premium/File" - badgeText = dataSizeString(component.count == 4 ? premiumLimit : limit, formatting: DataSizeStringFormatting(strings: environment.strings, decimalSeparator: environment.dateTimeFormat.decimalSeparator)) - string = component.count == 4 ? strings.Premium_MaxFileSizeFinalText(dataSizeString(premiumLimit, formatting: DataSizeStringFormatting(strings: environment.strings, decimalSeparator: environment.dateTimeFormat.decimalSeparator))).string : strings.Premium_MaxFileSizeText(dataSizeString(premiumLimit, formatting: DataSizeStringFormatting(strings: environment.strings, decimalSeparator: environment.dateTimeFormat.decimalSeparator))).string - defaultValue = component.count == 4 ? dataSizeString(limit, formatting: DataSizeStringFormatting(strings: environment.strings, decimalSeparator: environment.dateTimeFormat.decimalSeparator)) : "" - premiumValue = component.count != 4 ? dataSizeString(premiumLimit, formatting: DataSizeStringFormatting(strings: environment.strings, decimalSeparator: environment.dateTimeFormat.decimalSeparator)) : "" - badgePosition = component.count == 4 ? 1.0 : 0.5 - badgeGraphPosition = badgePosition - titleText = strings.Premium_FileTooLarge - - if isPremiumDisabled { - badgeText = dataSizeString(limit, formatting: DataSizeStringFormatting(strings: environment.strings, decimalSeparator: environment.dateTimeFormat.decimalSeparator)) - string = strings.Premium_MaxFileSizeNoPremiumText(dataSizeString(premiumLimit, formatting: DataSizeStringFormatting(strings: environment.strings, decimalSeparator: environment.dateTimeFormat.decimalSeparator))).string - } - case .accounts: - let limit = 3 - let premiumLimit = limit + 1 - iconName = "Premium/Account" - badgeText = "\(component.count)" - string = component.count >= premiumLimit ? strings.Premium_MaxAccountsFinalText("\(premiumLimit)").string : strings.Premium_MaxAccountsText("\(limit)").string - defaultValue = component.count > limit ? "\(limit)" : "" - premiumValue = component.count >= premiumLimit ? "" : "\(premiumLimit)" - if component.count == limit { - badgePosition = 0.5 - } else { - badgePosition = min(1.0, CGFloat(component.count) / CGFloat(premiumLimit)) - } - badgeGraphPosition = badgePosition - buttonAnimationName = "premium_addone" - - if isPremiumDisabled { - badgeText = "\(limit)" - string = strings.Premium_MaxAccountsNoPremiumText("\(limit)").string - } - case .expiringStories: - let limit = state.limits.maxExpiringStoriesCount - let premiumLimit = state.premiumLimits.maxExpiringStoriesCount - iconName = "Premium/Stories" + case .folders: + let limit = state.limits.maxFoldersCount + let premiumLimit = state.premiumLimits.maxFoldersCount + iconName = "Premium/Folder" + badgeText = "\(component.count)" + string = component.count >= premiumLimit ? strings.Premium_MaxFoldersCountFinalText("\(premiumLimit)").string : strings.Premium_MaxFoldersCountText("\(limit)", "\(premiumLimit)").string + defaultValue = component.count > limit ? "\(limit)" : "" + premiumValue = component.count >= premiumLimit ? "" : "\(premiumLimit)" + if component.count >= premiumLimit { + badgeGraphPosition = max(0.15, CGFloat(limit) / CGFloat(premiumLimit)) + } else { + badgeGraphPosition = max(0.15, CGFloat(component.count) / CGFloat(premiumLimit)) + } + badgePosition = max(0.15, CGFloat(component.count) / CGFloat(premiumLimit)) + + if !state.isPremium && badgePosition > 0.5 { + string = strings.Premium_MaxFoldersCountText("\(limit)", "\(premiumLimit)").string + } + + if isPremiumDisabled { badgeText = "\(limit)" - string = component.count >= premiumLimit ? strings.Premium_MaxExpiringStoriesFinalText("\(premiumLimit)").string : strings.Premium_MaxExpiringStoriesText("\(limit)", "\(premiumLimit)").string - defaultValue = "" - premiumValue = component.count >= premiumLimit ? "" : "\(premiumLimit)" - badgePosition = max(0.32, CGFloat(component.count) / CGFloat(premiumLimit)) - badgeGraphPosition = badgePosition - - if isPremiumDisabled { - badgeText = "\(limit)" - string = strings.Premium_MaxExpiringStoriesNoPremiumText("\(limit)").string - } - buttonAnimationName = nil - case .storiesWeekly: - let limit = state.limits.maxStoriesWeeklyCount - let premiumLimit = state.premiumLimits.maxStoriesWeeklyCount - iconName = "Premium/Stories" + string = strings.Premium_MaxFoldersCountNoPremiumText("\(limit)").string + } + case .chatsPerFolder: + let limit = state.limits.maxFolderChatsCount + let premiumLimit = state.premiumLimits.maxFolderChatsCount + iconName = "Premium/Chat" + badgeText = "\(component.count)" + string = component.count >= premiumLimit ? strings.Premium_MaxChatsInFolderFinalText("\(premiumLimit)").string : strings.Premium_MaxChatsInFolderText("\(limit)", "\(premiumLimit)").string + defaultValue = component.count > limit ? "\(limit)" : "" + premiumValue = component.count >= premiumLimit ? "" : "\(premiumLimit)" + badgePosition = CGFloat(component.count) / CGFloat(premiumLimit) + badgeGraphPosition = badgePosition + + if isPremiumDisabled { badgeText = "\(limit)" - string = component.count >= premiumLimit ? strings.Premium_MaxStoriesWeeklyFinalText("\(premiumLimit)").string : strings.Premium_MaxStoriesWeeklyText("\(limit)", "\(premiumLimit)").string - defaultValue = "" - premiumValue = component.count >= premiumLimit ? "" : "\(premiumLimit)" - badgePosition = max(0.32, CGFloat(component.count) / CGFloat(premiumLimit)) - badgeGraphPosition = badgePosition - - if isPremiumDisabled { - badgeText = "\(limit)" - string = strings.Premium_MaxStoriesWeeklyNoPremiumText("\(limit)").string - } - buttonAnimationName = nil - case .storiesMonthly: - let limit = state.limits.maxStoriesMonthlyCount - let premiumLimit = state.premiumLimits.maxStoriesMonthlyCount - iconName = "Premium/Stories" + string = strings.Premium_MaxChatsInFolderNoPremiumText("\(limit)").string + } + case .channels: + let limit = state.limits.maxChannelsCount + let premiumLimit = state.premiumLimits.maxChannelsCount + iconName = "Premium/Chat" + badgeText = "\(component.count)" + string = component.count >= premiumLimit ? strings.Premium_MaxChannelsFinalText("\(premiumLimit)").string : strings.Premium_MaxChannelsText("\(limit)", "\(premiumLimit)").string + defaultValue = component.count > limit ? "\(limit)" : "" + premiumValue = component.count >= premiumLimit ? "" : "\(premiumLimit)" + if component.count >= premiumLimit { + badgeGraphPosition = max(0.15, CGFloat(limit) / CGFloat(premiumLimit)) + } else { + badgeGraphPosition = max(0.15, CGFloat(component.count) / CGFloat(premiumLimit)) + } + badgePosition = max(0.15, CGFloat(component.count) / CGFloat(premiumLimit)) + + if isPremiumDisabled { badgeText = "\(limit)" - string = component.count >= premiumLimit ? strings.Premium_MaxStoriesMonthlyFinalText("\(premiumLimit)").string : strings.Premium_MaxStoriesMonthlyText("\(limit)", "\(premiumLimit)").string - defaultValue = "" - premiumValue = component.count >= premiumLimit ? "" : "\(premiumLimit)" - badgePosition = max(0.32, CGFloat(component.count) / CGFloat(premiumLimit)) - badgeGraphPosition = badgePosition - - if isPremiumDisabled { - badgeText = "\(limit)" - string = strings.Premium_MaxStoriesMonthlyNoPremiumText("\(limit)").string + string = strings.Premium_MaxChannelsNoPremiumText("\(limit)").string + } + case .linksPerSharedFolder: + /*let count: Int32 = 5 + Int32("".count)// component.count + let limit: Int32 = 5 + Int32("".count)//state.limits.maxSharedFolderInviteLinks + let premiumLimit: Int32 = 100 + Int32("".count)//state.premiumLimits.maxSharedFolderInviteLinks*/ + + let count: Int32 = component.count + let limit: Int32 = state.limits.maxSharedFolderInviteLinks + let premiumLimit: Int32 = state.premiumLimits.maxSharedFolderInviteLinks + + iconName = "Premium/Link" + badgeText = "\(count)" + string = count >= premiumLimit ? strings.Premium_MaxSharedFolderLinksFinalText("\(premiumLimit)").string : strings.Premium_MaxSharedFolderLinksText("\(limit)", "\(premiumLimit)").string + defaultValue = count > limit ? "\(limit)" : "" + premiumValue = count >= premiumLimit ? "" : "\(premiumLimit)" + if count >= premiumLimit { + badgeGraphPosition = max(0.15, CGFloat(limit) / CGFloat(premiumLimit)) + } else { + badgeGraphPosition = max(0.15, CGFloat(count) / CGFloat(premiumLimit)) + } + badgePosition = max(0.15, CGFloat(count) / CGFloat(premiumLimit)) + + if isPremiumDisabled { + badgeText = "\(limit)" + string = strings.Premium_MaxSharedFolderLinksNoPremiumText("\(limit)").string + } + + buttonAnimationName = nil + case .membershipInSharedFolders: + let limit = state.limits.maxSharedFolderJoin + let premiumLimit = state.premiumLimits.maxSharedFolderJoin + iconName = "Premium/Folder" + badgeText = "\(component.count)" + string = component.count >= premiumLimit ? strings.Premium_MaxSharedFolderMembershipFinalText("\(premiumLimit)").string : strings.Premium_MaxSharedFolderMembershipText("\(limit)", "\(premiumLimit)").string + defaultValue = component.count > limit ? "\(limit)" : "" + premiumValue = component.count >= premiumLimit ? "" : "\(premiumLimit)" + if component.count >= premiumLimit { + badgeGraphPosition = max(0.15, CGFloat(limit) / CGFloat(premiumLimit)) + } else { + badgeGraphPosition = max(0.15, CGFloat(component.count) / CGFloat(premiumLimit)) + } + badgePosition = max(0.15, CGFloat(component.count) / CGFloat(premiumLimit)) + + if isPremiumDisabled { + badgeText = "\(limit)" + string = strings.Premium_MaxSharedFolderMembershipNoPremiumText("\(limit)").string + } + + buttonAnimationName = nil + case .pins: + let limit = state.limits.maxPinnedChatCount + let premiumLimit = state.premiumLimits.maxPinnedChatCount + iconName = "Premium/Pin" + badgeText = "\(component.count)" + string = component.count >= premiumLimit ? strings.Premium_MaxPinsFinalText("\(premiumLimit)").string : strings.Premium_MaxPinsText("\(limit)", "\(premiumLimit)").string + defaultValue = component.count > limit ? "\(limit)" : "" + premiumValue = component.count >= premiumLimit ? "" : "\(premiumLimit)" + badgePosition = CGFloat(component.count) / CGFloat(premiumLimit) + badgeGraphPosition = badgePosition + + if isPremiumDisabled { + badgeText = "\(limit)" + string = strings.Premium_MaxPinsNoPremiumText("\(limit)").string + } + case .files: + let limit = Int64(state.limits.maxUploadFileParts) * 512 * 1024 + 1024 * 1024 * 100 + let premiumLimit = Int64(state.premiumLimits.maxUploadFileParts) * 512 * 1024 + 1024 * 1024 * 100 + iconName = "Premium/File" + badgeText = dataSizeString(component.count == 4 ? premiumLimit : limit, formatting: DataSizeStringFormatting(strings: environment.strings, decimalSeparator: environment.dateTimeFormat.decimalSeparator)) + string = component.count == 4 ? strings.Premium_MaxFileSizeFinalText(dataSizeString(premiumLimit, formatting: DataSizeStringFormatting(strings: environment.strings, decimalSeparator: environment.dateTimeFormat.decimalSeparator))).string : strings.Premium_MaxFileSizeText(dataSizeString(premiumLimit, formatting: DataSizeStringFormatting(strings: environment.strings, decimalSeparator: environment.dateTimeFormat.decimalSeparator))).string + defaultValue = component.count == 4 ? dataSizeString(limit, formatting: DataSizeStringFormatting(strings: environment.strings, decimalSeparator: environment.dateTimeFormat.decimalSeparator)) : "" + premiumValue = component.count != 4 ? dataSizeString(premiumLimit, formatting: DataSizeStringFormatting(strings: environment.strings, decimalSeparator: environment.dateTimeFormat.decimalSeparator)) : "" + badgePosition = component.count == 4 ? 1.0 : 0.5 + badgeGraphPosition = badgePosition + titleText = strings.Premium_FileTooLarge + + if isPremiumDisabled { + badgeText = dataSizeString(limit, formatting: DataSizeStringFormatting(strings: environment.strings, decimalSeparator: environment.dateTimeFormat.decimalSeparator)) + string = strings.Premium_MaxFileSizeNoPremiumText(dataSizeString(premiumLimit, formatting: DataSizeStringFormatting(strings: environment.strings, decimalSeparator: environment.dateTimeFormat.decimalSeparator))).string + } + case .accounts: + let limit = 3 + let premiumLimit = limit + 1 + iconName = "Premium/Account" + badgeText = "\(component.count)" + string = component.count >= premiumLimit ? strings.Premium_MaxAccountsFinalText("\(premiumLimit)").string : strings.Premium_MaxAccountsText("\(limit)").string + defaultValue = component.count > limit ? "\(limit)" : "" + premiumValue = component.count >= premiumLimit ? "" : "\(premiumLimit)" + if component.count == limit { + badgePosition = 0.5 + } else { + badgePosition = min(1.0, CGFloat(component.count) / CGFloat(premiumLimit)) + } + badgeGraphPosition = badgePosition + buttonAnimationName = "premium_addone" + + if isPremiumDisabled { + badgeText = "\(limit)" + string = strings.Premium_MaxAccountsNoPremiumText("\(limit)").string + } + case .expiringStories: + let limit = state.limits.maxExpiringStoriesCount + let premiumLimit = state.premiumLimits.maxExpiringStoriesCount + iconName = "Premium/Stories" + badgeText = "\(limit)" + string = component.count >= premiumLimit ? strings.Premium_MaxExpiringStoriesFinalText("\(premiumLimit)").string : strings.Premium_MaxExpiringStoriesText("\(limit)", "\(premiumLimit)").string + defaultValue = "" + premiumValue = component.count >= premiumLimit ? "" : "\(premiumLimit)" + badgePosition = max(0.32, CGFloat(component.count) / CGFloat(premiumLimit)) + badgeGraphPosition = badgePosition + + if isPremiumDisabled { + badgeText = "\(limit)" + string = strings.Premium_MaxExpiringStoriesNoPremiumText("\(limit)").string + } + buttonAnimationName = nil + case .storiesWeekly: + let limit = state.limits.maxStoriesWeeklyCount + let premiumLimit = state.premiumLimits.maxStoriesWeeklyCount + iconName = "Premium/Stories" + badgeText = "\(limit)" + string = component.count >= premiumLimit ? strings.Premium_MaxStoriesWeeklyFinalText("\(premiumLimit)").string : strings.Premium_MaxStoriesWeeklyText("\(limit)", "\(premiumLimit)").string + defaultValue = "" + premiumValue = component.count >= premiumLimit ? "" : "\(premiumLimit)" + badgePosition = max(0.32, CGFloat(component.count) / CGFloat(premiumLimit)) + badgeGraphPosition = badgePosition + + if isPremiumDisabled { + badgeText = "\(limit)" + string = strings.Premium_MaxStoriesWeeklyNoPremiumText("\(limit)").string + } + buttonAnimationName = nil + case .storiesMonthly: + let limit = state.limits.maxStoriesMonthlyCount + let premiumLimit = state.premiumLimits.maxStoriesMonthlyCount + iconName = "Premium/Stories" + badgeText = "\(limit)" + string = component.count >= premiumLimit ? strings.Premium_MaxStoriesMonthlyFinalText("\(premiumLimit)").string : strings.Premium_MaxStoriesMonthlyText("\(limit)", "\(premiumLimit)").string + defaultValue = "" + premiumValue = component.count >= premiumLimit ? "" : "\(premiumLimit)" + badgePosition = max(0.32, CGFloat(component.count) / CGFloat(premiumLimit)) + badgeGraphPosition = badgePosition + + if isPremiumDisabled { + badgeText = "\(limit)" + string = strings.Premium_MaxStoriesMonthlyNoPremiumText("\(limit)").string + } + buttonAnimationName = nil + case let .storiesChannelBoost(level, link): + let limit = 2 +// let premiumLimit = 1 + iconName = "Premium/Boost" + badgeText = "\(limit)" + + if let _ = link { + if level == 0 { + titleText = "Enable Stories" + string = "Your channel needs **2** more boosts to enable posting stories.\n\nAsk your **Premium** subscribers to boost your channel with this link:" + } else { + titleText = "Increase Story Limit" + string = "Your channel needs **1** more boosts to post **2** stories per day.\n\nAsk your **Premium** subscribers to boost your channel with this link:" } - buttonAnimationName = nil + actionButtonText = "Copy Link" + buttonIconName = "Premium/CopyLink" + } else { + if level == 0 { + titleText = "Enable Stories for Channel" + string = "Channel needs **1** more boosts to enable posting stories. Help make it possible!" + } else { + titleText = "Help Upgrade Channel" + string = "**Channel** needs **2** more boosts to be able to post **2** stories per day." + } + actionButtonText = "Boost Channel" + buttonIconName = "Premium/BoostChannel" + } + buttonAnimationName = nil + defaultTitle = "Level \(level)" + defaultValue = "" + premiumValue = "Level \(level + 1)" + + premiumTitle = "" + + badgePosition = 0.32 + badgeGraphPosition = badgePosition + + invertProgress = true } var reachedMaximumLimit = badgePosition >= 1.0 if case .folders = subject, !state.isPremium { @@ -997,8 +1074,8 @@ private final class LimitSheetContent: CombinedComponent { transition: .immediate ) - let textFont = Font.regular(17.0) - let boldTextFont = Font.semibold(17.0) + let textFont = Font.regular(15.0) + let boldTextFont = Font.semibold(15.0) let textColor = theme.actionSheet.primaryTextColor let markdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor), link: MarkdownAttributeSet(font: textFont, textColor: textColor), linkAttribute: { _ in return nil @@ -1034,16 +1111,17 @@ private final class LimitSheetContent: CombinedComponent { component: PremiumLimitDisplayComponent( inactiveColor: theme.list.itemBlocksSeparatorColor.withAlphaComponent(0.5), activeColors: gradientColors, - inactiveTitle: strings.Premium_Free, + inactiveTitle: defaultTitle, inactiveValue: defaultValue, inactiveTitleColor: theme.list.itemPrimaryTextColor, - activeTitle: strings.Premium_Premium, + activeTitle: premiumTitle, activeValue: premiumValue, activeTitleColor: .white, badgeIconName: iconName, badgeText: badgeText, badgePosition: badgePosition, badgeGraphPosition: badgeGraphPosition, + invertProgress: invertProgress, isPremiumDisabled: isPremiumDisabled ), availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: context.availableSize.height), @@ -1056,8 +1134,7 @@ private final class LimitSheetContent: CombinedComponent { let isIncreaseButton = !reachedMaximumLimit && !isPremiumDisabled let button = button.update( component: SolidRoundedButtonComponent( - title: isIncreaseButton ? strings.Premium_IncreaseLimit : strings.Common_OK, - + title: actionButtonText ?? (isIncreaseButton ? strings.Premium_IncreaseLimit : strings.Common_OK), theme: SolidRoundedButtonComponent.Theme( backgroundColor: .black, backgroundColors: gradientColors, @@ -1068,8 +1145,9 @@ private final class LimitSheetContent: CombinedComponent { height: 50.0, cornerRadius: 10.0, gloss: isIncreaseButton, + iconName: buttonIconName, animationName: isIncreaseButton ? buttonAnimationName : nil, - iconPosition: .right, + iconPosition: buttonIconName != nil ? .left : .right, action: { component.dismiss() if isIncreaseButton { @@ -1081,7 +1159,37 @@ private final class LimitSheetContent: CombinedComponent { transition: context.transition ) + var buttonOffset: CGFloat = 0.0 var textOffset: CGFloat = 228.0 + if case let .storiesChannelBoost(level, link) = component.subject { + if let _ = link { + let linkButton = linkButton.update( + component: SolidRoundedButtonComponent( + title: "t.me/channel?boost", + theme: SolidRoundedButtonComponent.Theme( + backgroundColor: UIColor(rgb: 0x343436), + backgroundColors: [], + foregroundColor: .white + ), + font: .regular, + fontSize: 17.0, + height: 50.0, + cornerRadius: 10.0, + action: {} + ), + availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: 50.0), + transition: context.transition + ) + buttonOffset += 66.0 + + let linkFrame = CGRect(origin: CGPoint(x: sideInset, y: textOffset + ceil(text.size.height / 2.0) + 24.0), size: linkButton.size) + context.add(linkButton + .position(CGPoint(x: linkFrame.midX, y: linkFrame.midY)) + ) + } else if link == nil, level > 0 { + textOffset -= 26.0 + } + } if isPremiumDisabled { textOffset -= 68.0 } @@ -1093,7 +1201,7 @@ private final class LimitSheetContent: CombinedComponent { .position(CGPoint(x: context.availableSize.width / 2.0, y: textOffset)) ) - let buttonFrame = CGRect(origin: CGPoint(x: sideInset, y: textOffset + ceil(text.size.height / 2.0) + 38.0), size: button.size) + let buttonFrame = CGRect(origin: CGPoint(x: sideInset, y: textOffset + ceil(text.size.height / 2.0) + buttonOffset + 24.0), size: button.size) context.add(button .position(CGPoint(x: buttonFrame.midX, y: buttonFrame.midY)) ) @@ -1104,6 +1212,15 @@ private final class LimitSheetContent: CombinedComponent { if isPremiumDisabled { height -= 78.0 } + + if case let .storiesChannelBoost(_, link) = component.subject { + if link != nil { + height += 66.0 + } else { + height -= 53.0 + } + } + contentSize = CGSize(width: context.availableSize.width, height: height + environment.safeInsets.bottom) } @@ -1204,7 +1321,7 @@ private final class LimitSheetComponent: CombinedComponent { } public class PremiumLimitScreen: ViewControllerComponentContainer { - public enum Subject { + public enum Subject: Equatable { case folders case chatsPerFolder case pins @@ -1216,6 +1333,8 @@ public class PremiumLimitScreen: ViewControllerComponentContainer { case expiringStories case storiesWeekly case storiesMonthly + + case storiesChannelBoost(level: Int32, link: String?) } public init(context: AccountContext, subject: PremiumLimitScreen.Subject, count: Int32, forceDark: Bool = false, cancel: @escaping () -> Void = {}, action: @escaping () -> Void) { diff --git a/submodules/TelegramCore/Sources/State/UserLimitsConfiguration.swift b/submodules/TelegramCore/Sources/State/UserLimitsConfiguration.swift index db5138cd28..bf24347c6f 100644 --- a/submodules/TelegramCore/Sources/State/UserLimitsConfiguration.swift +++ b/submodules/TelegramCore/Sources/State/UserLimitsConfiguration.swift @@ -21,6 +21,7 @@ public struct UserLimitsConfiguration: Equatable { public let maxExpiringStoriesCount: Int32 public let maxStoriesWeeklyCount: Int32 public let maxStoriesMonthlyCount: Int32 + public let maxStoriesSuggestedReactions: Int32 public static var defaultValue: UserLimitsConfiguration { return UserLimitsConfiguration( @@ -42,7 +43,8 @@ public struct UserLimitsConfiguration: Equatable { maxStoryCaptionLength: 200, maxExpiringStoriesCount: 3, maxStoriesWeeklyCount: 7, - maxStoriesMonthlyCount: 30 + maxStoriesMonthlyCount: 30, + maxStoriesSuggestedReactions: 1 ) } @@ -65,7 +67,8 @@ public struct UserLimitsConfiguration: Equatable { maxStoryCaptionLength: Int32, maxExpiringStoriesCount: Int32, maxStoriesWeeklyCount: Int32, - maxStoriesMonthlyCount: Int32 + maxStoriesMonthlyCount: Int32, + maxStoriesSuggestedReactions: Int32 ) { self.maxPinnedChatCount = maxPinnedChatCount self.maxArchivedPinnedChatCount = maxArchivedPinnedChatCount @@ -86,6 +89,7 @@ public struct UserLimitsConfiguration: Equatable { self.maxExpiringStoriesCount = maxExpiringStoriesCount self.maxStoriesWeeklyCount = maxStoriesWeeklyCount self.maxStoriesMonthlyCount = maxStoriesMonthlyCount + self.maxStoriesSuggestedReactions = maxStoriesSuggestedReactions } } @@ -129,5 +133,6 @@ extension UserLimitsConfiguration { self.maxExpiringStoriesCount = getValue("story_expiring_limit", orElse: defaultValue.maxExpiringStoriesCount) self.maxStoriesWeeklyCount = getValue("stories_sent_weekly_limit", orElse: defaultValue.maxStoriesWeeklyCount) self.maxStoriesMonthlyCount = getValue("stories_sent_monthly_limit", orElse: defaultValue.maxStoriesMonthlyCount) + self.maxStoriesSuggestedReactions = getValue("stories_suggested_reactions_limit", orElse: defaultValue.maxStoriesMonthlyCount) } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Data/ConfigurationData.swift b/submodules/TelegramCore/Sources/TelegramEngine/Data/ConfigurationData.swift index 7859ddf79f..dad1f563c4 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Data/ConfigurationData.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Data/ConfigurationData.swift @@ -55,6 +55,7 @@ public enum EngineConfiguration { public let maxExpiringStoriesCount: Int32 public let maxStoriesWeeklyCount: Int32 public let maxStoriesMonthlyCount: Int32 + public let maxStoriesSuggestedReactions: Int32 public static var defaultValue: UserLimits { return UserLimits(UserLimitsConfiguration.defaultValue) @@ -79,7 +80,8 @@ public enum EngineConfiguration { maxStoryCaptionLength: Int32, maxExpiringStoriesCount: Int32, maxStoriesWeeklyCount: Int32, - maxStoriesMonthlyCount: Int32 + maxStoriesMonthlyCount: Int32, + maxStoriesSuggestedReactions: Int32 ) { self.maxPinnedChatCount = maxPinnedChatCount self.maxArchivedPinnedChatCount = maxArchivedPinnedChatCount @@ -100,6 +102,7 @@ public enum EngineConfiguration { self.maxExpiringStoriesCount = maxExpiringStoriesCount self.maxStoriesWeeklyCount = maxStoriesWeeklyCount self.maxStoriesMonthlyCount = maxStoriesMonthlyCount + self.maxStoriesSuggestedReactions = maxStoriesSuggestedReactions } } } @@ -155,7 +158,8 @@ public extension EngineConfiguration.UserLimits { maxStoryCaptionLength: userLimitsConfiguration.maxStoryCaptionLength, maxExpiringStoriesCount: userLimitsConfiguration.maxExpiringStoriesCount, maxStoriesWeeklyCount: userLimitsConfiguration.maxStoriesWeeklyCount, - maxStoriesMonthlyCount: userLimitsConfiguration.maxStoriesMonthlyCount + maxStoriesMonthlyCount: userLimitsConfiguration.maxStoriesMonthlyCount, + maxStoriesSuggestedReactions: userLimitsConfiguration.maxStoriesSuggestedReactions ) } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/AttachMenuBots.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/AttachMenuBots.swift index 98608c876d..d77f5b2ab1 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/AttachMenuBots.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/AttachMenuBots.swift @@ -52,6 +52,9 @@ public final class AttachMenuBots: Equatable, Codable { public static let hasSettings = Flags(rawValue: 1 << 0) public static let requiresWriteAccess = Flags(rawValue: 1 << 1) + public static let showInAttachMenu = Flags(rawValue: 1 << 2) + public static let showInSettings = Flags(rawValue: 1 << 3) + public static let showInSettingsDisclaimer = Flags(rawValue: 1 << 4) } public struct PeerFlags: OptionSet, Codable { @@ -323,6 +326,15 @@ func managedSynchronizeAttachMenuBots(accountPeerId: PeerId, postbox: Postbox, n if (apiFlags & (1 << 2)) != 0 { flags.insert(.requiresWriteAccess) } + if (apiFlags & (1 << 3)) != 0 { + flags.insert(.showInAttachMenu) + } + if (apiFlags & (1 << 4)) != 0 { + flags.insert(.showInSettings) + } + if (apiFlags & (1 << 5)) != 0 { + flags.insert(.showInSettingsDisclaimer) + } resultBots.append(AttachMenuBots.Bot(peerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(botId)), name: name, icons: icons, peerTypes: peerTypes, flags: flags)) } } @@ -427,13 +439,13 @@ func _internal_removeBotFromAttachMenu(accountPeerId: PeerId, postbox: Postbox, } public struct AttachMenuBot { - public let peer: Peer + public let peer: EnginePeer public let shortName: String public let icons: [AttachMenuBots.Bot.IconName: TelegramMediaFile] public let peerTypes: AttachMenuBots.Bot.PeerFlags public let flags: AttachMenuBots.Bot.Flags - init(peer: Peer, shortName: String, icons: [AttachMenuBots.Bot.IconName: TelegramMediaFile], peerTypes: AttachMenuBots.Bot.PeerFlags, flags: AttachMenuBots.Bot.Flags) { + init(peer: EnginePeer, shortName: String, icons: [AttachMenuBots.Bot.IconName: TelegramMediaFile], peerTypes: AttachMenuBots.Bot.PeerFlags, flags: AttachMenuBots.Bot.Flags) { self.peer = peer self.shortName = shortName self.icons = icons @@ -450,7 +462,7 @@ func _internal_attachMenuBots(postbox: Postbox) -> Signal<[AttachMenuBot], NoErr var resultBots: [AttachMenuBot] = [] for bot in cachedBots { if let peer = transaction.getPeer(bot.peerId) { - resultBots.append(AttachMenuBot(peer: peer, shortName: bot.name, icons: bot.icons, peerTypes: bot.peerTypes, flags: bot.flags)) + resultBots.append(AttachMenuBot(peer: EnginePeer(peer), shortName: bot.name, icons: bot.icons, peerTypes: bot.peerTypes, flags: bot.flags)) } } return resultBots @@ -465,7 +477,7 @@ func _internal_getAttachMenuBot(accountPeerId: PeerId, postbox: Postbox, network return postbox.transaction { transaction -> Signal in if cached, let cachedBots = cachedAttachMenuBots(transaction: transaction)?.bots { if let bot = cachedBots.first(where: { $0.peerId == botId }), let peer = transaction.getPeer(bot.peerId) { - return .single(AttachMenuBot(peer: peer, shortName: bot.name, icons: bot.icons, peerTypes: bot.peerTypes, flags: bot.flags)) + return .single(AttachMenuBot(peer: EnginePeer(peer), shortName: bot.name, icons: bot.icons, peerTypes: bot.peerTypes, flags: bot.flags)) } } @@ -526,7 +538,16 @@ func _internal_getAttachMenuBot(accountPeerId: PeerId, postbox: Postbox, network if (apiFlags & (1 << 2)) != 0 { flags.insert(.requiresWriteAccess) } - return .single(AttachMenuBot(peer: peer, shortName: name, icons: icons, peerTypes: peerTypes, flags: flags)) + if (apiFlags & (1 << 3)) != 0 { + flags.insert(.showInAttachMenu) + } + if (apiFlags & (1 << 4)) != 0 { + flags.insert(.showInSettings) + } + if (apiFlags & (1 << 5)) != 0 { + flags.insert(.showInSettingsDisclaimer) + } + return .single(AttachMenuBot(peer: EnginePeer(peer), shortName: name, icons: icons, peerTypes: peerTypes, flags: flags)) } } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/BotWebView.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/BotWebView.swift index 82b74778f9..9683f3d5a6 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/BotWebView.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/BotWebView.swift @@ -10,11 +10,17 @@ private let botWebViewPlatform = "macos" private let botWebViewPlatform = "ios" #endif +public enum RequestSimpleWebViewSource { + case generic + case inline + case settings +} + public enum RequestSimpleWebViewError { case generic } -func _internal_requestSimpleWebView(postbox: Postbox, network: Network, botId: PeerId, url: String, inline: Bool, themeParams: [String: Any]?) -> Signal { +func _internal_requestSimpleWebView(postbox: Postbox, network: Network, botId: PeerId, url: String?, source: RequestSimpleWebViewSource, themeParams: [String: Any]?) -> Signal { var serializedThemeParams: Api.DataJSON? if let themeParams = themeParams, let data = try? JSONSerialization.data(withJSONObject: themeParams, options: []), let dataString = String(data: data, encoding: .utf8) { serializedThemeParams = .dataJSON(data: dataString) @@ -28,8 +34,16 @@ func _internal_requestSimpleWebView(postbox: Postbox, network: Network, botId: P if let _ = serializedThemeParams { flags |= (1 << 0) } - if inline { + switch source { + case .inline: flags |= (1 << 1) + case .settings: + flags |= (1 << 2) + default: + break + } + if let _ = url { + flags |= (1 << 3) } return network.request(Api.functions.messages.requestSimpleWebView(flags: flags, bot: inputUser, url: url, startParam: nil, themeParams: serializedThemeParams, platform: botWebViewPlatform)) |> mapError { _ -> RequestSimpleWebViewError in diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift index 4700dda967..9a551a7a72 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift @@ -499,8 +499,8 @@ public extension TelegramEngine { return _internal_requestWebView(postbox: self.account.postbox, network: self.account.network, stateManager: self.account.stateManager, peerId: peerId, botId: botId, url: url, payload: payload, themeParams: themeParams, fromMenu: fromMenu, replyToMessageId: replyToMessageId, threadId: threadId) } - public func requestSimpleWebView(botId: PeerId, url: String, inline: Bool, themeParams: [String: Any]?) -> Signal { - return _internal_requestSimpleWebView(postbox: self.account.postbox, network: self.account.network, botId: botId, url: url, inline: inline, themeParams: themeParams) + public func requestSimpleWebView(botId: PeerId, url: String?, source: RequestSimpleWebViewSource, themeParams: [String: Any]?) -> Signal { + 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 { diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift index 5ae881fde0..3669acf6fb 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift @@ -1363,7 +1363,7 @@ final class MediaEditorScreenComponent: Component { } } bottomControlsTransition.setFrame(view: scrubberView, frame: scrubberFrame) - if !self.animatingButtons && !isAudioOnly { + if !self.animatingButtons && !(isAudioOnly && animateIn) { transition.setAlpha(view: scrubberView, alpha: component.isDisplayingTool || component.isDismissing || component.isInteractingWithEntities || isEditingCaption ? 0.0 : 1.0) } else if animateIn { scrubberView.layer.animatePosition(from: CGPoint(x: 0.0, y: 44.0), to: .zero, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, additive: true) @@ -3167,6 +3167,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate } else { mediaEditor.setAudioTrackTrimRange(0 ..< min(15, audioDuration), apply: true) } + mediaEditor.seek(mediaEditor.values.videoTrimRange?.lowerBound ?? 0.0, andPlay: true) self.requestUpdate(transition: .easeInOut(duration: 0.2)) if isScopedResource { @@ -3249,8 +3250,8 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate } } - private var drawingScreen: DrawingScreen? - private var stickerScreen: StickerPickerScreen? + fileprivate var drawingScreen: DrawingScreen? + fileprivate var stickerScreen: StickerPickerScreen? private var defaultToEmoji = false private var previousDrawingData: Data? @@ -3350,6 +3351,14 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate controller.completion = { [weak self] content in if let self { if let content { + if case let .file(file, _) = content { + if file.isCustomEmoji { + self.defaultToEmoji = true + } else { + self.defaultToEmoji = false + } + } + let stickerEntity = DrawingStickerEntity(content: content) let scale: CGFloat if case .image = content { @@ -3364,18 +3373,11 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate self.hasAnyChanges = true self.controller?.isSavingAvailable = true self.controller?.requestLayout(transition: .immediate) - - if case let .file(file, _) = content { - if file.isCustomEmoji { - self.defaultToEmoji = true - } else { - self.defaultToEmoji = false - } - } } self.stickerScreen = nil self.mediaEditor?.play() } + return true } controller.customModalStyleOverlayTransitionFactorUpdated = { [weak self, weak controller] transition in if let self, let controller { @@ -3405,6 +3407,18 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate } controller.addReaction = { [weak self, weak controller] in if let self { + let maxReactionCount = self.context.userLimits.maxStoriesSuggestedReactions + var currentReactionCount = 0 + self.entitiesView.eachView { entityView in + if let stickerEntity = entityView.entity as? DrawingStickerEntity, case let .file(_, type) = stickerEntity.content, case .reaction = type { + currentReactionCount += 1 + } + } + if currentReactionCount >= maxReactionCount { + self.controller?.presentReactionPremiumSuggestion() + return + } + self.stickerScreen = nil controller?.dismiss(animated: true) @@ -3421,6 +3435,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate self.controller?.present(controller, in: .window(.root)) return case .text: + self.mediaEditor?.stop() self.insertTextEntity() self.hasAnyChanges = true @@ -4094,6 +4109,53 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate }) self.present(controller, in: .current) } + + fileprivate func presentReactionPremiumSuggestion() { + self.dismissAllTooltips() + + let context = self.context + let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Configuration.UserLimits(isPremium: true)) + |> deliverOnMainQueue).start(next: { [weak self] premiumLimits in + guard let self else { + return + } + + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let limit = context.userLimits.maxStoriesSuggestedReactions + + let content: UndoOverlayContent + if context.isPremium { + let value = presentationData.strings.Story_Editor_TooltipPremiumReactionLimitValue(premiumLimits.maxStoriesSuggestedReactions) + content = .info( + title: presentationData.strings.Story_Editor_TooltipReachedReactionLimitTitle, + text: presentationData.strings.Story_Editor_TooltipReachedReactionLimitText(value).string, + timeout: nil + ) + } else { + let value = presentationData.strings.Story_Editor_TooltipPremiumReactionLimitValue(limit) + content = .premiumPaywall( + title: presentationData.strings.Story_Editor_TooltipPremiumReactionLimitTitle, + text: presentationData.strings.Story_Editor_TooltipPremiumReactionLimitText(value).string, + customUndoText: nil, + timeout: nil, + linkAction: nil + ) + } + + let controller = UndoOverlayController(presentationData: presentationData, content: content, elevatedLayout: true, position: .bottom, animateInAsReplacement: false, action: { [weak self] action in + if case .info = action, let self { + if let stickerScreen = self.node.stickerScreen { + self.node.stickerScreen = nil + stickerScreen.dismiss(animated: true) + } + let controller = context.sharedContext.makePremiumIntroController(context: context, source: .storiesSuggestedReactions, forceDark: true, dismissed: nil) + self.push(controller) + } + return true + }) + self.present(controller, in: .window(.root)) + }) + } fileprivate func presentCaptionLimitPremiumSuggestion(isPremium: Bool) { self.dismissAllTooltips() diff --git a/submodules/TelegramUI/Sources/AccountContext.swift b/submodules/TelegramUI/Sources/AccountContext.swift index e73db9d68b..6d66335297 100644 --- a/submodules/TelegramUI/Sources/AccountContext.swift +++ b/submodules/TelegramUI/Sources/AccountContext.swift @@ -244,6 +244,8 @@ public final class AccountContextImpl: AccountContext { private var userLimitsConfigurationDisposable: Disposable? public private(set) var userLimits: EngineConfiguration.UserLimits + public private(set) var isPremium: Bool + public let imageCache: AnyObject? public init(sharedContext: SharedAccountContextImpl, account: Account, limitsConfiguration: LimitsConfiguration, contentSettings: ContentSettings, appConfiguration: AppConfiguration, temp: Bool = false) @@ -255,6 +257,7 @@ public final class AccountContextImpl: AccountContext { self.imageCache = DirectMediaImageCache(account: account) self.userLimits = EngineConfiguration.UserLimits(UserLimitsConfiguration.defaultValue) + self.isPremium = false self.downloadedMediaStoreManager = DownloadedMediaStoreManagerImpl(postbox: account.postbox, accountManager: sharedContext.accountManager) @@ -385,14 +388,19 @@ public final class AccountContextImpl: AccountContext { }) self.userLimitsConfigurationDisposable = (self.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: account.peerId)) - |> mapToSignal { peer -> Signal in - return self.engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.UserLimits(isPremium: peer?.isPremium ?? false)) + |> mapToSignal { peer -> Signal<(Bool, EngineConfiguration.UserLimits), NoError> in + let isPremium = peer?.isPremium ?? false + return self.engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.UserLimits(isPremium: isPremium)) + |> map { userLimits in + return (isPremium, userLimits) + } } - |> deliverOnMainQueue).start(next: { [weak self] value in + |> deliverOnMainQueue).start(next: { [weak self] isPremium, userLimits in guard let strongSelf = self else { return } - strongSelf.userLimits = value + strongSelf.isPremium = isPremium + strongSelf.userLimits = userLimits }) } @@ -404,6 +412,7 @@ public final class AccountContextImpl: AccountContext { self.countriesConfigurationDisposable?.dispose() self.experimentalUISettingsDisposable?.dispose() self.animatedEmojiStickersDisposable?.dispose() + self.userLimitsConfigurationDisposable?.dispose() } public func storeSecureIdPassword(password: String) { diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 8c0ea7a206..941637ea07 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -4266,7 +4266,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G botAddress = bot.addressName ?? "" } - strongSelf.messageActionCallbackDisposable.set(((strongSelf.context.engine.messages.requestSimpleWebView(botId: botId, url: url, inline: isInline, themeParams: generateWebAppThemeParams(strongSelf.presentationData.theme)) + strongSelf.messageActionCallbackDisposable.set(((strongSelf.context.engine.messages.requestSimpleWebView(botId: botId, url: url, source: isInline ? .inline : .generic, themeParams: generateWebAppThemeParams(strongSelf.presentationData.theme)) |> afterDisposed { updateProgress() }) @@ -13233,7 +13233,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G case bot(id: PeerId, payload: String?, justInstalled: Bool) case gift } - //editMediaOptions: MessageMediaEditingOptions?, editMediaReference: AnyMediaReference?, botId: PeerId? = nil, botPayload: String? = nil, botJustInstalled: Bool = false + private func presentAttachmentMenu(subject: AttachMenuSubject) { guard let peer = self.presentationInterfaceState.renderedPeer?.peer else { return @@ -13401,9 +13401,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } else { let _ = (context.engine.messages.getAttachMenuBot(botId: botId) |> deliverOnMainQueue).start(next: { bot in - let peer = EnginePeer(bot.peer) - - let controller = addWebAppToAttachmentController(context: context, peerName: peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), icons: bot.icons, requestWriteAccess: bot.flags.contains(.requiresWriteAccess), completion: { allowWrite in + let controller = addWebAppToAttachmentController(context: context, peerName: bot.peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), icons: bot.icons, requestWriteAccess: bot.flags.contains(.requiresWriteAccess), completion: { allowWrite in let _ = (context.engine.messages.addBotToAttachMenu(botId: botId, allowWrite: allowWrite) |> deliverOnMainQueue).start(error: { _ in diff --git a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift index ac60a8d053..336b5cb04e 100644 --- a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift +++ b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift @@ -675,8 +675,7 @@ func openResolvedUrlImpl(_ resolvedUrl: ResolvedUrl, context: AccountContext, ur |> deliverOnMainQueue).start(next: { bot in let choose = filterChooseTypes(choose, peerTypes: bot.peerTypes) - let botPeer = EnginePeer(bot.peer) - let controller = addWebAppToAttachmentController(context: context, peerName: botPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), icons: bot.icons, requestWriteAccess: bot.flags.contains(.requiresWriteAccess), completion: { allowWrite in + let controller = addWebAppToAttachmentController(context: context, peerName: bot.peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), icons: bot.icons, requestWriteAccess: bot.flags.contains(.requiresWriteAccess), completion: { allowWrite in let _ = (context.engine.messages.addBotToAttachMenu(botId: peerId, allowWrite: allowWrite) |> deliverOnMainQueue).start(error: { _ in presentError(presentationData.strings.WebApp_AddToAttachmentUnavailableError) @@ -705,7 +704,7 @@ func openResolvedUrlImpl(_ resolvedUrl: ResolvedUrl, context: AccountContext, ur guard let navigationController else { return } - let _ = context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer), attachBotStart: ChatControllerInitialAttachBotStart(botId: botPeer.id, payload: payload, justInstalled: true), useExisting: true)) + let _ = context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer), attachBotStart: ChatControllerInitialAttachBotStart(botId: bot.peer.id, payload: payload, justInstalled: true), useExisting: true)) } navigationController.pushViewController(controller) } @@ -717,7 +716,7 @@ func openResolvedUrlImpl(_ resolvedUrl: ResolvedUrl, context: AccountContext, ur return } - let _ = context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(chatPeer), attachBotStart: ChatControllerInitialAttachBotStart(botId: botPeer.id, payload: payload, justInstalled: true), useExisting: true)) + let _ = context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(chatPeer), attachBotStart: ChatControllerInitialAttachBotStart(botId: bot.peer.id, payload: payload, justInstalled: true), useExisting: true)) }) } } diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoData.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoData.swift index 35975c2a2d..ea9bac48d9 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoData.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoData.swift @@ -132,6 +132,7 @@ final class TelegramGlobalSettings { let unreadTrendingStickerPacks: Int let archivedStickerPacks: [ArchivedStickerPackItem]? let userLimits: EngineConfiguration.UserLimits + let bots: [AttachMenuBot] let hasPassport: Bool let hasWatchApp: Bool let enableQRLogin: Bool @@ -153,6 +154,7 @@ final class TelegramGlobalSettings { unreadTrendingStickerPacks: Int, archivedStickerPacks: [ArchivedStickerPackItem]?, userLimits: EngineConfiguration.UserLimits, + bots: [AttachMenuBot], hasPassport: Bool, hasWatchApp: Bool, enableQRLogin: Bool @@ -173,6 +175,7 @@ final class TelegramGlobalSettings { self.unreadTrendingStickerPacks = unreadTrendingStickerPacks self.archivedStickerPacks = archivedStickerPacks self.userLimits = userLimits + self.bots = bots self.hasPassport = hasPassport self.hasWatchApp = hasWatchApp self.enableQRLogin = enableQRLogin @@ -508,9 +511,10 @@ func peerInfoScreenSettingsData(context: AccountContext, peerId: EnginePeer.Id, return automaticEnergyUsageShouldBeOn(settings: settings) } |> distinctUntilChanged, - hasStories + hasStories, + context.engine.messages.attachMenuBots() ) - |> map { peerView, accountsAndPeers, accountSessions, privacySettings, sharedPreferences, notifications, stickerPacks, hasPassport, hasWatchApp, accountPreferences, suggestions, limits, hasPassword, isPowerSavingEnabled, hasStories -> PeerInfoScreenData in + |> map { peerView, accountsAndPeers, accountSessions, privacySettings, sharedPreferences, notifications, stickerPacks, hasPassport, hasWatchApp, accountPreferences, suggestions, limits, hasPassword, isPowerSavingEnabled, hasStories, bots -> PeerInfoScreenData in let (notificationExceptions, notificationsAuthorizationStatus, notificationsWarningSuppressed) = notifications let (featuredStickerPacks, archivedStickerPacks) = stickerPacks @@ -550,6 +554,7 @@ func peerInfoScreenSettingsData(context: AccountContext, peerId: EnginePeer.Id, unreadTrendingStickerPacks: unreadTrendingStickerPacks, archivedStickerPacks: archivedStickerPacks, userLimits: peer?.isPremium == true ? limits.1 : limits.0, + bots: bots, hasPassport: hasPassport, hasWatchApp: hasWatchApp, enableQRLogin: enableQRLogin) diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift index 459a74eb6a..70ebfd524a 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift @@ -91,6 +91,7 @@ import PeerInfoStoryGridScreen import StoryContainerScreen import ChatAvatarNavigationNode import PeerReportScreen +import WebUI enum PeerInfoAvatarEditingMode { case generic @@ -549,6 +550,7 @@ private final class PeerInfoInteraction { let toggleForumTopics: (Bool) -> Void let displayTopicsLimited: (TopicsLimitedReason) -> Void let openPeerMention: (String, ChatControllerInteractionNavigateToPeer) -> Void + let openBotApp: (AttachMenuBot) -> Void init( openUsername: @escaping (String) -> Void, @@ -599,7 +601,8 @@ private final class PeerInfoInteraction { dismissInput: @escaping () -> Void, toggleForumTopics: @escaping (Bool) -> Void, displayTopicsLimited: @escaping (TopicsLimitedReason) -> Void, - openPeerMention: @escaping (String, ChatControllerInteractionNavigateToPeer) -> Void + openPeerMention: @escaping (String, ChatControllerInteractionNavigateToPeer) -> Void, + openBotApp: @escaping (AttachMenuBot) -> Void ) { self.openUsername = openUsername self.openPhone = openPhone @@ -650,6 +653,7 @@ private final class PeerInfoInteraction { self.toggleForumTopics = toggleForumTopics self.displayTopicsLimited = displayTopicsLimited self.openPeerMention = openPeerMention + self.openBotApp = openBotApp } } @@ -661,7 +665,7 @@ private enum SettingsSection: Int, CaseIterable { case phone case accounts case proxy - case stories + case apps case shortcuts case advanced case payment @@ -792,7 +796,19 @@ private func settingsItems(data: PeerInfoScreenData?, context: AccountContext, p } } - items[.stories]!.append(PeerInfoScreenDisclosureItem(id: 0, text: presentationData.strings.Settings_MyStories, icon: PresentationResourcesSettings.stories, action: { + var appIndex = 1000 + if let settings = data.globalSettings { + for bot in settings.bots { + if bot.flags.contains(.showInSettings) { + items[.apps]!.append(PeerInfoScreenDisclosureItem(id: appIndex, text: bot.peer.compactDisplayTitle, icon: PresentationResourcesSettings.passport, action: { + interaction.openBotApp(bot) + })) + appIndex += 1 + } + } + } + + items[.apps]!.append(PeerInfoScreenDisclosureItem(id: appIndex, text: presentationData.strings.Settings_MyStories, icon: PresentationResourcesSettings.stories, action: { interaction.openSettings(.stories) })) @@ -2340,6 +2356,9 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro }, openPeerMention: { [weak self] mention, navigation in self?.openPeerMention(mention, navigation: navigation) + }, + openBotApp: { [weak self] bot in + self?.openBotApp(bot) } ) @@ -4401,7 +4420,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro }, contentContext: nil) } - private func openUrl(url: String, concealed: Bool, external: Bool) { + private func openUrl(url: String, concealed: Bool, external: Bool, forceExternal: Bool = false, commit: @escaping () -> Void = {}) { openUserGeneratedUrl(context: self.context, peerId: self.peerId, url: url, concealed: concealed, present: { [weak self] c in self?.controller?.present(c, in: .window(.root)) }, openResolved: { [weak self] tempResolved in @@ -4411,8 +4430,9 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro let result: ResolvedUrl = external ? .externalUrl(url) : tempResolved - strongSelf.context.sharedContext.openResolvedUrl(result, context: strongSelf.context, urlContext: .generic, navigationController: strongSelf.controller?.navigationController as? NavigationController, forceExternal: false, openPeer: { peer, navigation in + strongSelf.context.sharedContext.openResolvedUrl(result, context: strongSelf.context, urlContext: .generic, navigationController: strongSelf.controller?.navigationController as? NavigationController, forceExternal: forceExternal, openPeer: { peer, navigation in self?.openPeer(peerId: peer.id, navigation: navigation) + commit() }, sendFile: nil, sendSticker: nil, requestMessageActionUrlAuth: nil, @@ -4582,6 +4602,51 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro })) } + private let openBotAppDisposable = MetaDisposable() + private func openBotApp(_ bot: AttachMenuBot) { + guard let controller = self.controller else { + return + } + + let proceed = { [weak self] in + guard let self else { + return + } + self.openBotAppDisposable.set(((self.context.engine.messages.requestSimpleWebView(botId: bot.peer.id, url: nil, source: .settings, themeParams: generateWebAppThemeParams(self.presentationData.theme)) + |> afterDisposed { +// updateProgress() + }) + |> deliverOnMainQueue).start(next: { [weak self] url in + guard let self else { + return + } + let params = WebAppParameters(peerId: self.context.account.peerId, botId: bot.peer.id, botName: bot.peer.compactDisplayTitle, url: url, queryId: nil, payload: nil, buttonText: nil, keepAliveSignal: nil, fromMenu: false, fromAttachMenu: false, isInline: false, isSimple: true) + let controller = standaloneWebAppController(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, params: params, threadId: nil, openUrl: { [weak self] url, concealed, commit in + self?.openUrl(url: url, concealed: concealed, external: false, forceExternal: true, commit: commit) + }, requestSwitchInline: { _, _, _ in + }, getNavigationController: { [weak self] in + return self?.controller?.navigationController as? NavigationController + }) + controller.navigationPresentation = .flatModal + self.controller?.push(controller) + }, error: { [weak self] error in + if let self { + self.controller?.present(textAlertController(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, title: nil, text: self.presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: { + })]), in: .window(.root)) + } + })) + } + + if bot.flags.contains(.showInSettingsDisclaimer) { + let alertController = webAppTermsAlertController(context: self.context, updatedPresentationData: controller.updatedPresentationData, peer: bot.peer, completion: { + proceed() + }) + controller.present(alertController, in: .window(.root)) + } else { + proceed() + } + } + private func performButtonAction(key: PeerInfoHeaderButtonKey, gesture: ContextGesture?) { guard let controller = self.controller else { return diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index afadfe225e..01646ad42d 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -1802,6 +1802,8 @@ public final class SharedAccountContextImpl: SharedAccountContext { mappedSource = .storiesFormatting case .storiesExpirationDurations: mappedSource = .storiesExpirationDurations + case .storiesSuggestedReactions: + mappedSource = .storiesSuggestedReactions } let controller = PremiumIntroScreen(context: context, source: mappedSource, forceDark: forceDark) controller.wasDismissed = dismissed diff --git a/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift b/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift index 656409d491..fa6d6e5083 100644 --- a/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift +++ b/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift @@ -962,7 +962,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { let body = MarkdownAttributeSet(font: Font.regular(14.0), textColor: .white) let bold = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: .white) - let link = MarkdownAttributeSet(font: Font.regular(14.0), textColor: undoTextColor) + let link = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: undoTextColor) let attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: body, bold: bold, link: link, linkAttribute: { contents in return ("URL", contents) }), textAlignment: .natural) diff --git a/submodules/WebUI/Sources/WebAppController.swift b/submodules/WebUI/Sources/WebAppController.swift index 78a0d927f6..494b520171 100644 --- a/submodules/WebUI/Sources/WebAppController.swift +++ b/submodules/WebUI/Sources/WebAppController.swift @@ -38,6 +38,8 @@ public class WebAppCancelButtonNode: ASDisplayNode { public var state: State = .cancel + private var color: UIColor? + private var _theme: PresentationTheme public var theme: PresentationTheme { get { @@ -50,6 +52,24 @@ public class WebAppCancelButtonNode: ASDisplayNode { } private let strings: PresentationStrings + public func updateColor(_ color: UIColor?, transition: ContainedViewLayoutTransition) { + self.color = color + + if case let .animated(duration, curve) = transition { + if let snapshotView = self.view.snapshotContentTree() { + snapshotView.frame = self.bounds + self.view.addSubview(snapshotView) + + snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, timingFunction: curve.timingFunction, removeOnCompletion: false, completion: { _ in + snapshotView.removeFromSuperview() + }) + self.arrowNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration, timingFunction: curve.timingFunction) + self.labelNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration, timingFunction: curve.timingFunction) + } + } + self.setState(self.state, animated: false, animateScale: false, force: true) + } + public init(theme: PresentationTheme, strings: PresentationStrings) { self._theme = theme self.strings = strings @@ -124,13 +144,15 @@ public class WebAppCancelButtonNode: ASDisplayNode { self.buttonNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration) } + let color = self.color ?? self.theme.rootController.navigationBar.accentTextColor + self.arrowNode.isHidden = state == .cancel - self.labelNode.attributedText = NSAttributedString(string: state == .cancel ? self.strings.Common_Cancel : self.strings.Common_Back, font: Font.regular(17.0), textColor: self.theme.rootController.navigationBar.accentTextColor) + self.labelNode.attributedText = NSAttributedString(string: state == .cancel ? self.strings.Common_Cancel : self.strings.Common_Back, font: Font.regular(17.0), textColor: color) let labelSize = self.labelNode.updateLayout(CGSize(width: 120.0, height: 56.0)) self.buttonNode.frame = CGRect(origin: .zero, size: CGSize(width: labelSize.width, height: self.buttonNode.frame.height)) - self.arrowNode.image = NavigationBarTheme.generateBackArrowImage(color: self.theme.rootController.navigationBar.accentTextColor) + self.arrowNode.image = NavigationBarTheme.generateBackArrowImage(color: color) if let image = self.arrowNode.image { self.arrowNode.frame = CGRect(origin: self.arrowNode.frame.origin, size: image.size) } @@ -305,7 +327,7 @@ public final class WebAppController: ViewController, AttachmentContainable { return .complete() } |> mapToSignal { bot -> Signal<(FileMediaReference, Bool)?, NoError> in - if let bot = bot, let peerReference = PeerReference(bot.peer) { + if let bot = bot, let peerReference = PeerReference(bot.peer._asPeer()) { var imageFile: TelegramMediaFile? var isPlaceholder = false if let file = bot.icons[.placeholder] { @@ -546,12 +568,16 @@ public final class WebAppController: ViewController, AttachmentContainable { } self.controller?.present(promptController, in: .window(.root)) } - + + private func updateNavigationBarAlpha(transition: ContainedViewLayoutTransition) { + let contentOffset = self.webView?.scrollView.contentOffset.y ?? 0.0 + let backgroundAlpha = min(30.0, contentOffset) / 30.0 + self.controller?.navigationBar?.updateBackgroundAlpha(backgroundAlpha, transition: transition) + } + private var targetContentOffset: CGPoint? func scrollViewDidScroll(_ scrollView: UIScrollView) { - let contentOffset = scrollView.contentOffset.y - self.controller?.navigationBar?.updateBackgroundAlpha(min(30.0, contentOffset) / 30.0, transition: .immediate) - + self.updateNavigationBarAlpha(transition: .immediate) if let targetContentOffset = self.targetContentOffset, scrollView.contentOffset != targetContentOffset { scrollView.contentOffset = targetContentOffset } @@ -821,8 +847,14 @@ public final class WebAppController: ViewController, AttachmentContainable { transition.updateBackgroundColor(node: self.backgroundNode, color: color) } case "web_app_set_header_color": - if let json = json, let colorKey = json["color_key"] as? String, ["bg_color", "secondary_bg_color"].contains(colorKey) { - self.headerColorKey = colorKey + if let json = json { + if let colorKey = json["color_key"] as? String, ["bg_color", "secondary_bg_color"].contains(colorKey) { + self.headerColor = nil + self.headerColorKey = colorKey + } else if let hexColor = json["color"] as? String, let color = UIColor(hexString: hexColor) { + self.headerColor = color + self.headerColorKey = nil + } self.updateHeaderBackgroundColor(transition: .animated(duration: 0.2, curve: .linear)) } case "web_app_open_popup": @@ -938,16 +970,37 @@ public final class WebAppController: ViewController, AttachmentContainable { fileprivate var needDismissConfirmation = false + fileprivate var headerColor: UIColor? + fileprivate var headerPrimaryTextColor: UIColor? private var headerColorKey: String? + private func updateHeaderBackgroundColor(transition: ContainedViewLayoutTransition) { + guard let controller = self.controller else { + return + } + let color: UIColor? + var primaryTextColor: UIColor? + var secondaryTextColor: UIColor? var backgroundColor = self.presentationData.theme.list.plainBackgroundColor var secondaryBackgroundColor = self.presentationData.theme.list.blocksBackgroundColor if self.presentationData.theme.list.blocksBackgroundColor.rgb == self.presentationData.theme.list.plainBackgroundColor.rgb { backgroundColor = self.presentationData.theme.list.modalPlainBackgroundColor secondaryBackgroundColor = self.presentationData.theme.list.plainBackgroundColor } - if let headerColorKey = self.headerColorKey { + if let headerColor = self.headerColor { + color = headerColor + let textColor = headerColor.lightness > 0.5 ? UIColor(rgb: 0x000000) : UIColor(rgb: 0xffffff) + func calculateSecondaryAlpha(luminance: CGFloat, targetContrast: CGFloat) -> CGFloat { + let targetLuminance = luminance > 0.5 ? 0.0 : 1.0 + let adaptiveAlpha = (luminance - targetLuminance + targetContrast) / targetContrast + return max(0.5, min(0.64, adaptiveAlpha)) + } + + primaryTextColor = textColor + self.headerPrimaryTextColor = textColor + secondaryTextColor = textColor.withAlphaComponent(calculateSecondaryAlpha(luminance: headerColor.lightness, targetContrast: 2.5)) + } else if let headerColorKey = self.headerColorKey { switch headerColorKey { case "bg_color": color = backgroundColor @@ -959,6 +1012,13 @@ public final class WebAppController: ViewController, AttachmentContainable { } else { color = nil } + + self.updateNavigationBarAlpha(transition: transition) + controller.updateNavigationBarTheme(transition: transition) + + controller.titleView?.updateTextColors(primary: primaryTextColor, secondary: secondaryTextColor, transition: transition) + controller.cancelButtonNode.updateColor(primaryTextColor, transition: transition) + controller.moreButtonNode.updateColor(primaryTextColor, transition: transition) transition.updateBackgroundColor(node: self.headerBackgroundNode, color: color ?? .clear) transition.updateBackgroundColor(node: self.topOverscrollNode, color: color ?? .clear) } @@ -1234,8 +1294,8 @@ public final class WebAppController: ViewController, AttachmentContainable { } private var titleView: CounterContollerTitleView? - private let cancelButtonNode: WebAppCancelButtonNode - private let moreButtonNode: MoreButtonNode + fileprivate let cancelButtonNode: WebAppCancelButtonNode + fileprivate let moreButtonNode: MoreButtonNode private let context: AccountContext private let peerId: PeerId @@ -1318,10 +1378,9 @@ public final class WebAppController: ViewController, AttachmentContainable { if let strongSelf = self { strongSelf.presentationData = presentationData - let navigationBarPresentationData = NavigationBarPresentationData(theme: NavigationBarTheme(rootControllerTheme: presentationData.theme), strings: NavigationBarStrings(back: "", close: "")) - strongSelf.navigationBar?.updatePresentationData(navigationBarPresentationData) + strongSelf.updateNavigationBarTheme(transition: .immediate) strongSelf.titleView?.theme = presentationData.theme - + strongSelf.cancelButtonNode.theme = presentationData.theme strongSelf.moreButtonNode.theme = presentationData.theme @@ -1341,6 +1400,32 @@ public final class WebAppController: ViewController, AttachmentContainable { self.presentationDataDisposable?.dispose() } + fileprivate func updateNavigationBarTheme(transition: ContainedViewLayoutTransition) { + let navigationBarPresentationData: NavigationBarPresentationData + if let backgroundColor = self.controllerNode.headerColor, let textColor = self.controllerNode.headerPrimaryTextColor { + navigationBarPresentationData = NavigationBarPresentationData( + theme: NavigationBarTheme( + buttonColor: textColor, + disabledButtonColor: textColor, + primaryTextColor: textColor, + backgroundColor: backgroundColor, + enableBackgroundBlur: true, + separatorColor: UIColor(rgb: 0x000000, alpha: 0.25), + badgeBackgroundColor: .clear, + badgeStrokeColor: .clear, + badgeTextColor: .clear + ), + strings: NavigationBarStrings(back: "", close: "") + ) + } else { + navigationBarPresentationData = NavigationBarPresentationData( + theme: NavigationBarTheme(rootControllerTheme: self.presentationData.theme), + strings: NavigationBarStrings(back: "", close: "") + ) + } + self.navigationBar?.updatePresentationData(navigationBarPresentationData) + } + @objc private func cancelPressed() { if case .back = self.cancelButtonNode.state { self.controllerNode.sendBackButtonEvent() diff --git a/submodules/WebUI/Sources/WebAppTermsAlertController.swift b/submodules/WebUI/Sources/WebAppTermsAlertController.swift new file mode 100644 index 0000000000..81a4e2b6df --- /dev/null +++ b/submodules/WebUI/Sources/WebAppTermsAlertController.swift @@ -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)?, 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 +} diff --git a/submodules/WebUI/Sources/WebAppWebView.swift b/submodules/WebUI/Sources/WebAppWebView.swift index b76554712e..26a8eca621 100644 --- a/submodules/WebUI/Sources/WebAppWebView.swift +++ b/submodules/WebUI/Sources/WebAppWebView.swift @@ -88,6 +88,9 @@ final class WebAppWebView: WKWebView { return point.x > 30.0 } self.allowsBackForwardNavigationGestures = false + if #available(iOS 16.4, *) { + self.isInspectable = true + } handleScriptMessageImpl = { [weak self] message in if let strongSelf = self {