mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Various improvements
This commit is contained in:
parent
f8ebd4aa2f
commit
4c4080f5cb
@ -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.";
|
||||
|
||||
|
@ -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 }
|
||||
|
@ -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) {
|
||||
|
@ -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())
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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))
|
||||
|
@ -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 = { }
|
||||
|
@ -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
|
||||
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -52,6 +52,9 @@ public final class AttachMenuBots: Equatable, Codable {
|
||||
|
||||
public static let hasSettings = Flags(rawValue: 1 << 0)
|
||||
public static let requiresWriteAccess = Flags(rawValue: 1 << 1)
|
||||
public static let showInAttachMenu = Flags(rawValue: 1 << 2)
|
||||
public static let showInSettings = Flags(rawValue: 1 << 3)
|
||||
public static let showInSettingsDisclaimer = Flags(rawValue: 1 << 4)
|
||||
}
|
||||
|
||||
public struct PeerFlags: OptionSet, Codable {
|
||||
@ -323,6 +326,15 @@ func managedSynchronizeAttachMenuBots(accountPeerId: PeerId, postbox: Postbox, n
|
||||
if (apiFlags & (1 << 2)) != 0 {
|
||||
flags.insert(.requiresWriteAccess)
|
||||
}
|
||||
if (apiFlags & (1 << 3)) != 0 {
|
||||
flags.insert(.showInAttachMenu)
|
||||
}
|
||||
if (apiFlags & (1 << 4)) != 0 {
|
||||
flags.insert(.showInSettings)
|
||||
}
|
||||
if (apiFlags & (1 << 5)) != 0 {
|
||||
flags.insert(.showInSettingsDisclaimer)
|
||||
}
|
||||
resultBots.append(AttachMenuBots.Bot(peerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(botId)), name: name, icons: icons, peerTypes: peerTypes, flags: flags))
|
||||
}
|
||||
}
|
||||
@ -427,13 +439,13 @@ func _internal_removeBotFromAttachMenu(accountPeerId: PeerId, postbox: Postbox,
|
||||
}
|
||||
|
||||
public struct AttachMenuBot {
|
||||
public let peer: Peer
|
||||
public let peer: EnginePeer
|
||||
public let shortName: String
|
||||
public let icons: [AttachMenuBots.Bot.IconName: TelegramMediaFile]
|
||||
public let peerTypes: AttachMenuBots.Bot.PeerFlags
|
||||
public let flags: AttachMenuBots.Bot.Flags
|
||||
|
||||
init(peer: Peer, shortName: String, icons: [AttachMenuBots.Bot.IconName: TelegramMediaFile], peerTypes: AttachMenuBots.Bot.PeerFlags, flags: AttachMenuBots.Bot.Flags) {
|
||||
init(peer: EnginePeer, shortName: String, icons: [AttachMenuBots.Bot.IconName: TelegramMediaFile], peerTypes: AttachMenuBots.Bot.PeerFlags, flags: AttachMenuBots.Bot.Flags) {
|
||||
self.peer = peer
|
||||
self.shortName = shortName
|
||||
self.icons = icons
|
||||
@ -450,7 +462,7 @@ func _internal_attachMenuBots(postbox: Postbox) -> Signal<[AttachMenuBot], NoErr
|
||||
var resultBots: [AttachMenuBot] = []
|
||||
for bot in cachedBots {
|
||||
if let peer = transaction.getPeer(bot.peerId) {
|
||||
resultBots.append(AttachMenuBot(peer: peer, shortName: bot.name, icons: bot.icons, peerTypes: bot.peerTypes, flags: bot.flags))
|
||||
resultBots.append(AttachMenuBot(peer: EnginePeer(peer), shortName: bot.name, icons: bot.icons, peerTypes: bot.peerTypes, flags: bot.flags))
|
||||
}
|
||||
}
|
||||
return resultBots
|
||||
@ -465,7 +477,7 @@ func _internal_getAttachMenuBot(accountPeerId: PeerId, postbox: Postbox, network
|
||||
return postbox.transaction { transaction -> Signal<AttachMenuBot, GetAttachMenuBotError> in
|
||||
if cached, let cachedBots = cachedAttachMenuBots(transaction: transaction)?.bots {
|
||||
if let bot = cachedBots.first(where: { $0.peerId == botId }), let peer = transaction.getPeer(bot.peerId) {
|
||||
return .single(AttachMenuBot(peer: peer, shortName: bot.name, icons: bot.icons, peerTypes: bot.peerTypes, flags: bot.flags))
|
||||
return .single(AttachMenuBot(peer: EnginePeer(peer), shortName: bot.name, icons: bot.icons, peerTypes: bot.peerTypes, flags: bot.flags))
|
||||
}
|
||||
}
|
||||
|
||||
@ -526,7 +538,16 @@ func _internal_getAttachMenuBot(accountPeerId: PeerId, postbox: Postbox, network
|
||||
if (apiFlags & (1 << 2)) != 0 {
|
||||
flags.insert(.requiresWriteAccess)
|
||||
}
|
||||
return .single(AttachMenuBot(peer: peer, shortName: name, icons: icons, peerTypes: peerTypes, flags: flags))
|
||||
if (apiFlags & (1 << 3)) != 0 {
|
||||
flags.insert(.showInAttachMenu)
|
||||
}
|
||||
if (apiFlags & (1 << 4)) != 0 {
|
||||
flags.insert(.showInSettings)
|
||||
}
|
||||
if (apiFlags & (1 << 5)) != 0 {
|
||||
flags.insert(.showInSettingsDisclaimer)
|
||||
}
|
||||
return .single(AttachMenuBot(peer: EnginePeer(peer), shortName: name, icons: icons, peerTypes: peerTypes, flags: flags))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -10,11 +10,17 @@ private let botWebViewPlatform = "macos"
|
||||
private let botWebViewPlatform = "ios"
|
||||
#endif
|
||||
|
||||
public enum RequestSimpleWebViewSource {
|
||||
case generic
|
||||
case inline
|
||||
case settings
|
||||
}
|
||||
|
||||
public enum RequestSimpleWebViewError {
|
||||
case generic
|
||||
}
|
||||
|
||||
func _internal_requestSimpleWebView(postbox: Postbox, network: Network, botId: PeerId, url: String, inline: Bool, themeParams: [String: Any]?) -> Signal<String, RequestSimpleWebViewError> {
|
||||
func _internal_requestSimpleWebView(postbox: Postbox, network: Network, botId: PeerId, url: String?, source: RequestSimpleWebViewSource, themeParams: [String: Any]?) -> Signal<String, RequestSimpleWebViewError> {
|
||||
var serializedThemeParams: Api.DataJSON?
|
||||
if let themeParams = themeParams, let data = try? JSONSerialization.data(withJSONObject: themeParams, options: []), let dataString = String(data: data, encoding: .utf8) {
|
||||
serializedThemeParams = .dataJSON(data: dataString)
|
||||
@ -28,8 +34,16 @@ func _internal_requestSimpleWebView(postbox: Postbox, network: Network, botId: P
|
||||
if let _ = serializedThemeParams {
|
||||
flags |= (1 << 0)
|
||||
}
|
||||
if inline {
|
||||
switch source {
|
||||
case .inline:
|
||||
flags |= (1 << 1)
|
||||
case .settings:
|
||||
flags |= (1 << 2)
|
||||
default:
|
||||
break
|
||||
}
|
||||
if let _ = url {
|
||||
flags |= (1 << 3)
|
||||
}
|
||||
return network.request(Api.functions.messages.requestSimpleWebView(flags: flags, bot: inputUser, url: url, startParam: nil, themeParams: serializedThemeParams, platform: botWebViewPlatform))
|
||||
|> mapError { _ -> RequestSimpleWebViewError in
|
||||
|
@ -499,8 +499,8 @@ public extension TelegramEngine {
|
||||
return _internal_requestWebView(postbox: self.account.postbox, network: self.account.network, stateManager: self.account.stateManager, peerId: peerId, botId: botId, url: url, payload: payload, themeParams: themeParams, fromMenu: fromMenu, replyToMessageId: replyToMessageId, threadId: threadId)
|
||||
}
|
||||
|
||||
public func requestSimpleWebView(botId: PeerId, url: String, inline: Bool, themeParams: [String: Any]?) -> Signal<String, RequestSimpleWebViewError> {
|
||||
return _internal_requestSimpleWebView(postbox: self.account.postbox, network: self.account.network, botId: botId, url: url, inline: inline, themeParams: themeParams)
|
||||
public func requestSimpleWebView(botId: PeerId, url: String?, source: RequestSimpleWebViewSource, themeParams: [String: Any]?) -> Signal<String, RequestSimpleWebViewError> {
|
||||
return _internal_requestSimpleWebView(postbox: self.account.postbox, network: self.account.network, botId: botId, url: url, source: source, themeParams: themeParams)
|
||||
}
|
||||
|
||||
public func requestAppWebView(peerId: PeerId, appReference: BotAppReference, payload: String?, themeParams: [String: Any]?, allowWrite: Bool) -> Signal<String, RequestAppWebViewError> {
|
||||
|
@ -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()
|
||||
|
@ -244,6 +244,8 @@ public final class AccountContextImpl: AccountContext {
|
||||
private var userLimitsConfigurationDisposable: Disposable?
|
||||
public private(set) var userLimits: EngineConfiguration.UserLimits
|
||||
|
||||
public private(set) var isPremium: Bool
|
||||
|
||||
public let imageCache: AnyObject?
|
||||
|
||||
public init(sharedContext: SharedAccountContextImpl, account: Account, limitsConfiguration: LimitsConfiguration, contentSettings: ContentSettings, appConfiguration: AppConfiguration, temp: Bool = false)
|
||||
@ -255,6 +257,7 @@ public final class AccountContextImpl: AccountContext {
|
||||
self.imageCache = DirectMediaImageCache(account: account)
|
||||
|
||||
self.userLimits = EngineConfiguration.UserLimits(UserLimitsConfiguration.defaultValue)
|
||||
self.isPremium = false
|
||||
|
||||
self.downloadedMediaStoreManager = DownloadedMediaStoreManagerImpl(postbox: account.postbox, accountManager: sharedContext.accountManager)
|
||||
|
||||
@ -385,14 +388,19 @@ public final class AccountContextImpl: AccountContext {
|
||||
})
|
||||
|
||||
self.userLimitsConfigurationDisposable = (self.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: account.peerId))
|
||||
|> mapToSignal { peer -> Signal<EngineConfiguration.UserLimits, NoError> in
|
||||
return self.engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.UserLimits(isPremium: peer?.isPremium ?? false))
|
||||
|> mapToSignal { peer -> Signal<(Bool, EngineConfiguration.UserLimits), NoError> in
|
||||
let isPremium = peer?.isPremium ?? false
|
||||
return self.engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.UserLimits(isPremium: isPremium))
|
||||
|> map { userLimits in
|
||||
return (isPremium, userLimits)
|
||||
}
|
||||
}
|
||||
|> deliverOnMainQueue).start(next: { [weak self] value in
|
||||
|> deliverOnMainQueue).start(next: { [weak self] isPremium, userLimits in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.userLimits = value
|
||||
strongSelf.isPremium = isPremium
|
||||
strongSelf.userLimits = userLimits
|
||||
})
|
||||
}
|
||||
|
||||
@ -404,6 +412,7 @@ public final class AccountContextImpl: AccountContext {
|
||||
self.countriesConfigurationDisposable?.dispose()
|
||||
self.experimentalUISettingsDisposable?.dispose()
|
||||
self.animatedEmojiStickersDisposable?.dispose()
|
||||
self.userLimitsConfigurationDisposable?.dispose()
|
||||
}
|
||||
|
||||
public func storeSecureIdPassword(password: String) {
|
||||
|
@ -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
|
||||
|
||||
|
@ -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))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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()
|
||||
|
302
submodules/WebUI/Sources/WebAppTermsAlertController.swift
Normal file
302
submodules/WebUI/Sources/WebAppTermsAlertController.swift
Normal file
@ -0,0 +1,302 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import SwiftSignalKit
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import TelegramPresentationData
|
||||
import TelegramUIPreferences
|
||||
import AccountContext
|
||||
import AppBundle
|
||||
import AvatarNode
|
||||
import CheckNode
|
||||
import Markdown
|
||||
|
||||
private let textFont = Font.regular(13.0)
|
||||
private let boldTextFont = Font.semibold(13.0)
|
||||
|
||||
private func formattedText(_ text: String, color: UIColor, linkColor: UIColor, textAlignment: NSTextAlignment = .natural) -> NSAttributedString {
|
||||
return parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: color), bold: MarkdownAttributeSet(font: boldTextFont, textColor: color), link: MarkdownAttributeSet(font: textFont, textColor: linkColor), linkAttribute: { _ in return nil}), textAlignment: textAlignment)
|
||||
}
|
||||
|
||||
private final class WebAppTermsAlertContentNode: AlertContentNode {
|
||||
private let strings: PresentationStrings
|
||||
private let title: String
|
||||
private let text: String
|
||||
|
||||
private let titleNode: ImmediateTextNode
|
||||
private let textNode: ASTextNode
|
||||
|
||||
private let acceptTermsCheckNode: InteractiveCheckNode
|
||||
private let acceptTermsLabelNode: ASTextNode
|
||||
|
||||
private let actionNodesSeparator: ASDisplayNode
|
||||
private let actionNodes: [TextAlertContentActionNode]
|
||||
private let actionVerticalSeparators: [ASDisplayNode]
|
||||
|
||||
private var validLayout: CGSize?
|
||||
|
||||
override var dismissOnOutsideTap: Bool {
|
||||
return self.isUserInteractionEnabled
|
||||
}
|
||||
|
||||
var acceptedTerms: Bool = false {
|
||||
didSet {
|
||||
self.acceptTermsCheckNode.setSelected(self.acceptedTerms, animated: true)
|
||||
if let firstAction = self.actionNodes.first {
|
||||
firstAction.actionEnabled = self.acceptedTerms
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init(context: AccountContext, theme: AlertControllerTheme, ptheme: PresentationTheme, strings: PresentationStrings, title: String, text: String, actions: [TextAlertAction]) {
|
||||
self.strings = strings
|
||||
self.title = title
|
||||
self.text = text
|
||||
|
||||
self.titleNode = ImmediateTextNode()
|
||||
self.titleNode.displaysAsynchronously = false
|
||||
self.titleNode.maximumNumberOfLines = 1
|
||||
self.titleNode.textAlignment = .center
|
||||
|
||||
self.textNode = ASTextNode()
|
||||
self.textNode.displaysAsynchronously = false
|
||||
self.textNode.maximumNumberOfLines = 0
|
||||
|
||||
self.acceptTermsCheckNode = InteractiveCheckNode(theme: CheckNodeTheme(backgroundColor: theme.accentColor, strokeColor: theme.contrastColor, borderColor: theme.controlBorderColor, overlayBorder: false, hasInset: false, hasShadow: false))
|
||||
self.acceptTermsLabelNode = ASTextNode()
|
||||
self.acceptTermsLabelNode.maximumNumberOfLines = 4
|
||||
self.acceptTermsLabelNode.isUserInteractionEnabled = true
|
||||
|
||||
self.actionNodesSeparator = ASDisplayNode()
|
||||
self.actionNodesSeparator.isLayerBacked = true
|
||||
|
||||
self.actionNodes = actions.map { action -> TextAlertContentActionNode in
|
||||
return TextAlertContentActionNode(theme: theme, action: action)
|
||||
}
|
||||
|
||||
var actionVerticalSeparators: [ASDisplayNode] = []
|
||||
if actions.count > 1 {
|
||||
for _ in 0 ..< actions.count - 1 {
|
||||
let separatorNode = ASDisplayNode()
|
||||
separatorNode.isLayerBacked = true
|
||||
actionVerticalSeparators.append(separatorNode)
|
||||
}
|
||||
}
|
||||
self.actionVerticalSeparators = actionVerticalSeparators
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.titleNode)
|
||||
self.addSubnode(self.textNode)
|
||||
|
||||
self.addSubnode(self.acceptTermsCheckNode)
|
||||
self.addSubnode(self.acceptTermsLabelNode)
|
||||
|
||||
self.addSubnode(self.actionNodesSeparator)
|
||||
|
||||
for actionNode in self.actionNodes {
|
||||
self.addSubnode(actionNode)
|
||||
}
|
||||
|
||||
for separatorNode in self.actionVerticalSeparators {
|
||||
self.addSubnode(separatorNode)
|
||||
}
|
||||
|
||||
if let firstAction = self.actionNodes.first {
|
||||
firstAction.actionEnabled = false
|
||||
}
|
||||
|
||||
self.acceptTermsCheckNode.valueChanged = { [weak self] value in
|
||||
if let strongSelf = self {
|
||||
strongSelf.acceptedTerms = !strongSelf.acceptedTerms
|
||||
}
|
||||
}
|
||||
|
||||
self.updateTheme(theme)
|
||||
}
|
||||
|
||||
override func didLoad() {
|
||||
super.didLoad()
|
||||
|
||||
self.acceptTermsLabelNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.acceptTap(_:))))
|
||||
}
|
||||
|
||||
@objc private func acceptTap(_ gestureRecognizer: UITapGestureRecognizer) {
|
||||
if self.acceptTermsCheckNode.isUserInteractionEnabled {
|
||||
self.acceptedTerms = !self.acceptedTerms
|
||||
}
|
||||
}
|
||||
|
||||
override func updateTheme(_ theme: AlertControllerTheme) {
|
||||
self.titleNode.attributedText = NSAttributedString(string: self.title, font: Font.semibold(17.0), textColor: theme.primaryColor, paragraphAlignment: .center)
|
||||
self.textNode.attributedText = NSAttributedString(string: self.text, font: Font.regular(13.0), textColor: theme.primaryColor, paragraphAlignment: .center)
|
||||
|
||||
let text = "I agree to the [Terms of Use]()"
|
||||
self.acceptTermsLabelNode.attributedText = formattedText(text, color: theme.primaryColor, linkColor: theme.accentColor)
|
||||
|
||||
self.actionNodesSeparator.backgroundColor = theme.separatorColor
|
||||
for actionNode in self.actionNodes {
|
||||
actionNode.updateTheme(theme)
|
||||
}
|
||||
for separatorNode in self.actionVerticalSeparators {
|
||||
separatorNode.backgroundColor = theme.separatorColor
|
||||
}
|
||||
|
||||
if let size = self.validLayout {
|
||||
_ = self.updateLayout(size: size, transition: .immediate)
|
||||
}
|
||||
}
|
||||
|
||||
override func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize {
|
||||
var size = size
|
||||
size.width = min(size.width, 270.0)
|
||||
|
||||
self.validLayout = size
|
||||
|
||||
var origin: CGPoint = CGPoint(x: 0.0, y: 17.0)
|
||||
|
||||
let titleSize = self.titleNode.updateLayout(CGSize(width: size.width - 32.0, height: size.height))
|
||||
transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - titleSize.width) / 2.0), y: origin.y), size: titleSize))
|
||||
origin.y += titleSize.height + 4.0
|
||||
|
||||
var entriesHeight: CGFloat = 0.0
|
||||
|
||||
let textSize = self.textNode.measure(CGSize(width: size.width - 48.0, height: size.height))
|
||||
transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - textSize.width) / 2.0), y: origin.y), size: textSize))
|
||||
origin.y += textSize.height
|
||||
|
||||
if self.acceptTermsLabelNode.supernode != nil {
|
||||
origin.y += 21.0
|
||||
entriesHeight += 21.0
|
||||
|
||||
let checkSize = CGSize(width: 22.0, height: 22.0)
|
||||
let condensedSize = CGSize(width: size.width - 76.0, height: size.height)
|
||||
|
||||
let spacing: CGFloat = 12.0
|
||||
let acceptTermsSize = self.acceptTermsLabelNode.measure(condensedSize)
|
||||
let acceptTermsTotalWidth = checkSize.width + spacing + acceptTermsSize.width
|
||||
let acceptTermsOriginX = floorToScreenPixels((size.width - acceptTermsTotalWidth) / 2.0)
|
||||
|
||||
transition.updateFrame(node: self.acceptTermsCheckNode, frame: CGRect(origin: CGPoint(x: acceptTermsOriginX, y: origin.y - 3.0), size: checkSize))
|
||||
transition.updateFrame(node: self.acceptTermsLabelNode, frame: CGRect(origin: CGPoint(x: acceptTermsOriginX + checkSize.width + spacing, y: origin.y), size: acceptTermsSize))
|
||||
origin.y += acceptTermsSize.height
|
||||
entriesHeight += acceptTermsSize.height
|
||||
}
|
||||
|
||||
let actionButtonHeight: CGFloat = 44.0
|
||||
var minActionsWidth: CGFloat = 0.0
|
||||
let maxActionWidth: CGFloat = floor(size.width / CGFloat(self.actionNodes.count))
|
||||
let actionTitleInsets: CGFloat = 8.0
|
||||
|
||||
var effectiveActionLayout = TextAlertContentActionLayout.vertical
|
||||
for actionNode in self.actionNodes {
|
||||
let actionTitleSize = actionNode.titleNode.updateLayout(CGSize(width: maxActionWidth, height: actionButtonHeight))
|
||||
if case .horizontal = effectiveActionLayout, actionTitleSize.height > actionButtonHeight * 0.6667 {
|
||||
effectiveActionLayout = .vertical
|
||||
}
|
||||
switch effectiveActionLayout {
|
||||
case .horizontal:
|
||||
minActionsWidth += actionTitleSize.width + actionTitleInsets
|
||||
case .vertical:
|
||||
minActionsWidth = max(minActionsWidth, actionTitleSize.width + actionTitleInsets)
|
||||
}
|
||||
}
|
||||
|
||||
let insets = UIEdgeInsets(top: 18.0, left: 18.0, bottom: 18.0, right: 18.0)
|
||||
|
||||
let contentWidth = max(size.width, minActionsWidth)
|
||||
|
||||
var actionsHeight: CGFloat = 0.0
|
||||
switch effectiveActionLayout {
|
||||
case .horizontal:
|
||||
actionsHeight = actionButtonHeight
|
||||
case .vertical:
|
||||
actionsHeight = actionButtonHeight * CGFloat(self.actionNodes.count)
|
||||
}
|
||||
|
||||
let resultSize = CGSize(width: contentWidth, height: titleSize.height + textSize.height + entriesHeight + actionsHeight + 3.0 + insets.top + insets.bottom)
|
||||
|
||||
transition.updateFrame(node: self.actionNodesSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel)))
|
||||
|
||||
var actionOffset: CGFloat = 0.0
|
||||
let actionWidth: CGFloat = floor(resultSize.width / CGFloat(self.actionNodes.count))
|
||||
var separatorIndex = -1
|
||||
var nodeIndex = 0
|
||||
for actionNode in self.actionNodes {
|
||||
if separatorIndex >= 0 {
|
||||
let separatorNode = self.actionVerticalSeparators[separatorIndex]
|
||||
switch effectiveActionLayout {
|
||||
case .horizontal:
|
||||
transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: actionOffset - UIScreenPixel, y: resultSize.height - actionsHeight), size: CGSize(width: UIScreenPixel, height: actionsHeight - UIScreenPixel)))
|
||||
case .vertical:
|
||||
transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel)))
|
||||
}
|
||||
}
|
||||
separatorIndex += 1
|
||||
|
||||
let currentActionWidth: CGFloat
|
||||
switch effectiveActionLayout {
|
||||
case .horizontal:
|
||||
if nodeIndex == self.actionNodes.count - 1 {
|
||||
currentActionWidth = resultSize.width - actionOffset
|
||||
} else {
|
||||
currentActionWidth = actionWidth
|
||||
}
|
||||
case .vertical:
|
||||
currentActionWidth = resultSize.width
|
||||
}
|
||||
|
||||
let actionNodeFrame: CGRect
|
||||
switch effectiveActionLayout {
|
||||
case .horizontal:
|
||||
actionNodeFrame = CGRect(origin: CGPoint(x: actionOffset, y: resultSize.height - actionsHeight), size: CGSize(width: currentActionWidth, height: actionButtonHeight))
|
||||
actionOffset += currentActionWidth
|
||||
case .vertical:
|
||||
actionNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset), size: CGSize(width: currentActionWidth, height: actionButtonHeight))
|
||||
actionOffset += actionButtonHeight
|
||||
}
|
||||
|
||||
transition.updateFrame(node: actionNode, frame: actionNodeFrame)
|
||||
|
||||
nodeIndex += 1
|
||||
}
|
||||
|
||||
return resultSize
|
||||
}
|
||||
}
|
||||
|
||||
public func webAppTermsAlertController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?, peer: EnginePeer, completion: @escaping () -> Void) -> AlertController {
|
||||
let theme = defaultDarkColorPresentationTheme
|
||||
let presentationData: PresentationData
|
||||
if let updatedPresentationData {
|
||||
presentationData = updatedPresentationData.initial
|
||||
} else {
|
||||
presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
}
|
||||
let strings = presentationData.strings
|
||||
|
||||
var dismissImpl: ((Bool) -> Void)?
|
||||
let actions: [TextAlertAction] = [TextAlertAction(type: .defaultAction, title: "Continue", action: {
|
||||
completion()
|
||||
dismissImpl?(true)
|
||||
}), TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {
|
||||
dismissImpl?(true)
|
||||
})]
|
||||
|
||||
let title = "Warning"
|
||||
let text = "You are about to use a mini app operated by an independent party not affiliated with Telegram. You must agree to the Terms of Use of mini apps to continue."
|
||||
|
||||
let contentNode = WebAppTermsAlertContentNode(context: context, theme: AlertControllerTheme(presentationData: presentationData), ptheme: theme, strings: strings, title: title, text: text, actions: actions)
|
||||
|
||||
let controller = AlertController(theme: AlertControllerTheme(presentationData: presentationData), contentNode: contentNode)
|
||||
dismissImpl = { [weak controller] animated in
|
||||
if animated {
|
||||
controller?.dismissAnimated()
|
||||
} else {
|
||||
controller?.dismiss()
|
||||
}
|
||||
}
|
||||
return controller
|
||||
}
|
@ -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 {
|
||||
|
Loading…
x
Reference in New Issue
Block a user