mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-10-09 11:23:48 +00:00
Various improvements
This commit is contained in:
parent
ff7cc72a71
commit
5e65806ac3
@ -321,13 +321,18 @@ func chatContextMenuItems(context: AccountContext, peerId: PeerId, promoInfo: Ch
|
|||||||
case .limitExceeded:
|
case .limitExceeded:
|
||||||
f(.default)
|
f(.default)
|
||||||
|
|
||||||
let limitScreen = PremiumLimitScreen(context: context, subject: .pins, action: {
|
var dismissImpl: (() -> Void)?
|
||||||
|
let controller = PremiumLimitScreen(context: context, subject: .pins, action: {
|
||||||
|
dismissImpl?()
|
||||||
let premiumScreen = PremiumIntroScreen(context: context, action: {
|
let premiumScreen = PremiumIntroScreen(context: context, action: {
|
||||||
|
|
||||||
})
|
})
|
||||||
chatListController?.push(premiumScreen)
|
chatListController?.push(premiumScreen)
|
||||||
})
|
})
|
||||||
chatListController?.push(limitScreen)
|
chatListController?.push(controller)
|
||||||
|
dismissImpl = { [weak controller] in
|
||||||
|
controller?.dismiss()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})))
|
})))
|
||||||
|
@ -10,6 +10,7 @@ import ItemListUI
|
|||||||
import AccountContext
|
import AccountContext
|
||||||
import ItemListPeerActionItem
|
import ItemListPeerActionItem
|
||||||
import ChatListFilterSettingsHeaderItem
|
import ChatListFilterSettingsHeaderItem
|
||||||
|
import PremiumUI
|
||||||
|
|
||||||
private final class ChatListFilterPresetListControllerArguments {
|
private final class ChatListFilterPresetListControllerArguments {
|
||||||
let context: AccountContext
|
let context: AccountContext
|
||||||
@ -246,6 +247,17 @@ public func chatListFilterPresetListController(context: AccountContext, mode: Ch
|
|||||||
var pushControllerImpl: ((ViewController) -> Void)?
|
var pushControllerImpl: ((ViewController) -> Void)?
|
||||||
var presentControllerImpl: ((ViewController) -> Void)?
|
var presentControllerImpl: ((ViewController) -> Void)?
|
||||||
|
|
||||||
|
let filtersWithCountsSignal = context.engine.peers.updatedChatListFilters()
|
||||||
|
|> distinctUntilChanged
|
||||||
|
|> mapToSignal { filters -> Signal<[(ChatListFilter, Int)], NoError> in
|
||||||
|
return .single(filters.map { filter -> (ChatListFilter, Int) in
|
||||||
|
return (filter, 0)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
let filtersWithCounts = Promise<[(ChatListFilter, Int)]>()
|
||||||
|
filtersWithCounts.set(filtersWithCountsSignal)
|
||||||
|
|
||||||
let arguments = ChatListFilterPresetListControllerArguments(context: context,
|
let arguments = ChatListFilterPresetListControllerArguments(context: context,
|
||||||
addSuggestedPresed: { title, data in
|
addSuggestedPresed: { title, data in
|
||||||
let _ = (context.engine.peers.updateChatListFiltersInteractively { filters in
|
let _ = (context.engine.peers.updateChatListFiltersInteractively { filters in
|
||||||
@ -259,7 +271,42 @@ public func chatListFilterPresetListController(context: AccountContext, mode: Ch
|
|||||||
}, openPreset: { preset in
|
}, openPreset: { preset in
|
||||||
pushControllerImpl?(chatListFilterPresetController(context: context, currentPreset: preset, updated: { _ in }))
|
pushControllerImpl?(chatListFilterPresetController(context: context, currentPreset: preset, updated: { _ in }))
|
||||||
}, addNew: {
|
}, addNew: {
|
||||||
|
let _ = combineLatest(
|
||||||
|
queue: Queue.mainQueue(),
|
||||||
|
context.engine.data.get(
|
||||||
|
TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId),
|
||||||
|
TelegramEngine.EngineData.Item.Configuration.UserLimits(isPremium: false),
|
||||||
|
TelegramEngine.EngineData.Item.Configuration.UserLimits(isPremium: true)
|
||||||
|
),
|
||||||
|
filtersWithCounts.get() |> take(1)
|
||||||
|
).start(next: { result, filters in
|
||||||
|
let (accountPeer, limits, premiumLimits) = result
|
||||||
|
let limit = limits.maxFoldersCount
|
||||||
|
let premiumLimit = premiumLimits.maxFoldersCount
|
||||||
|
if let accountPeer = accountPeer, accountPeer.isPremium {
|
||||||
|
if filters.count >= premiumLimit {
|
||||||
|
//printPremiumError
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if filters.count >= limit {
|
||||||
|
var dismissImpl: (() -> Void)?
|
||||||
|
let controller = PremiumLimitScreen(context: context, subject: .folders, action: {
|
||||||
|
dismissImpl?()
|
||||||
|
let controller = PremiumIntroScreen(context: context, action: {
|
||||||
|
|
||||||
|
})
|
||||||
|
pushControllerImpl?(controller)
|
||||||
|
})
|
||||||
|
pushControllerImpl?(controller)
|
||||||
|
dismissImpl = { [weak controller] in
|
||||||
|
controller?.dismiss()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
pushControllerImpl?(chatListFilterPresetController(context: context, currentPreset: nil, updated: { _ in }))
|
pushControllerImpl?(chatListFilterPresetController(context: context, currentPreset: nil, updated: { _ in }))
|
||||||
|
})
|
||||||
}, setItemWithRevealedOptions: { preset, fromPreset in
|
}, setItemWithRevealedOptions: { preset, fromPreset in
|
||||||
updateState { state in
|
updateState { state in
|
||||||
var state = state
|
var state = state
|
||||||
@ -297,14 +344,6 @@ public func chatListFilterPresetListController(context: AccountContext, mode: Ch
|
|||||||
presentControllerImpl?(actionSheet)
|
presentControllerImpl?(actionSheet)
|
||||||
})
|
})
|
||||||
|
|
||||||
let filtersWithCountsSignal = context.engine.peers.updatedChatListFilters()
|
|
||||||
|> distinctUntilChanged
|
|
||||||
|> mapToSignal { filters -> Signal<[(ChatListFilter, Int)], NoError> in
|
|
||||||
return .single(filters.map { filter -> (ChatListFilter, Int) in
|
|
||||||
return (filter, 0)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
let featuredFilters = context.account.postbox.preferencesView(keys: [PreferencesKeys.chatListFiltersFeaturedState])
|
let featuredFilters = context.account.postbox.preferencesView(keys: [PreferencesKeys.chatListFiltersFeaturedState])
|
||||||
|> map { preferences -> [ChatListFeaturedFilter] in
|
|> map { preferences -> [ChatListFeaturedFilter] in
|
||||||
guard let state = preferences.values[PreferencesKeys.chatListFiltersFeaturedState]?.get(ChatListFiltersFeaturedState.self) else {
|
guard let state = preferences.values[PreferencesKeys.chatListFiltersFeaturedState]?.get(ChatListFiltersFeaturedState.self) else {
|
||||||
@ -314,9 +353,6 @@ public func chatListFilterPresetListController(context: AccountContext, mode: Ch
|
|||||||
}
|
}
|
||||||
|> distinctUntilChanged
|
|> distinctUntilChanged
|
||||||
|
|
||||||
let filtersWithCounts = Promise<[(ChatListFilter, Int)]>()
|
|
||||||
filtersWithCounts.set(filtersWithCountsSignal)
|
|
||||||
|
|
||||||
let updatedFilterOrder = Promise<[Int32]?>(nil)
|
let updatedFilterOrder = Promise<[Int32]?>(nil)
|
||||||
|
|
||||||
let preferences = context.account.postbox.preferencesView(keys: [ApplicationSpecificPreferencesKeys.chatListFilterSettings])
|
let preferences = context.account.postbox.preferencesView(keys: [ApplicationSpecificPreferencesKeys.chatListFilterSettings])
|
||||||
|
Binary file not shown.
@ -14,6 +14,14 @@ import SolidRoundedButtonComponent
|
|||||||
import Markdown
|
import Markdown
|
||||||
import SceneKit
|
import SceneKit
|
||||||
|
|
||||||
|
private func deg2rad(_ number: Float) -> Float {
|
||||||
|
return number * .pi / 180
|
||||||
|
}
|
||||||
|
|
||||||
|
private func rad2deg(_ number: Float) -> Float {
|
||||||
|
return number * 180.0 / .pi
|
||||||
|
}
|
||||||
|
|
||||||
private class StarComponent: Component {
|
private class StarComponent: Component {
|
||||||
static func ==(lhs: StarComponent, rhs: StarComponent) -> Bool {
|
static func ==(lhs: StarComponent, rhs: StarComponent) -> Bool {
|
||||||
return true
|
return true
|
||||||
@ -41,18 +49,97 @@ private class StarComponent: Component {
|
|||||||
self.sceneView = SCNView(frame: frame)
|
self.sceneView = SCNView(frame: frame)
|
||||||
self.sceneView.backgroundColor = .clear
|
self.sceneView.backgroundColor = .clear
|
||||||
self.sceneView.transform = CGAffineTransform(scaleX: 0.5, y: 0.5)
|
self.sceneView.transform = CGAffineTransform(scaleX: 0.5, y: 0.5)
|
||||||
|
self.sceneView.isUserInteractionEnabled = false
|
||||||
|
|
||||||
super.init(frame: frame)
|
super.init(frame: frame)
|
||||||
|
|
||||||
self.addSubview(self.sceneView)
|
self.addSubview(self.sceneView)
|
||||||
|
|
||||||
self.setup()
|
self.setup()
|
||||||
|
|
||||||
|
let panGestureRecoginzer = UIPanGestureRecognizer(target: self, action: #selector(self.handlePan(_:)))
|
||||||
|
self.addGestureRecognizer(panGestureRecoginzer)
|
||||||
|
|
||||||
|
let tapGestureRecoginzer = UITapGestureRecognizer(target: self, action: #selector(self.handleTap(_:)))
|
||||||
|
self.addGestureRecognizer(tapGestureRecoginzer)
|
||||||
|
|
||||||
|
self.disablesInteractiveModalDismiss = true
|
||||||
|
self.disablesInteractiveTransitionGestureRecognizer = true
|
||||||
}
|
}
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
required init?(coder: NSCoder) {
|
||||||
fatalError("init(coder:) has not been implemented")
|
fatalError("init(coder:) has not been implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@objc private func handleTap(_ gesture: UITapGestureRecognizer) {
|
||||||
|
guard let scene = self.sceneView.scene, let node = scene.rootNode.childNode(withName: "star", recursively: false) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var left = true
|
||||||
|
if let view = gesture.view {
|
||||||
|
let point = gesture.location(in: view)
|
||||||
|
if point.x > view.frame.width / 2.0 {
|
||||||
|
left = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let initial = node.rotation
|
||||||
|
let target = SCNVector4(x: 0.0, y: 1.0, z: 0.0, w: left ? -0.6 : 0.6)
|
||||||
|
|
||||||
|
let animation = CABasicAnimation(keyPath: "rotation")
|
||||||
|
animation.fromValue = NSValue(scnVector4: initial)
|
||||||
|
animation.toValue = NSValue(scnVector4: target)
|
||||||
|
animation.duration = 0.25
|
||||||
|
animation.timingFunction = CAMediaTimingFunction(name: .easeOut)
|
||||||
|
animation.fillMode = .forwards
|
||||||
|
node.addAnimation(animation, forKey: "rotate")
|
||||||
|
|
||||||
|
node.rotation = target
|
||||||
|
|
||||||
|
Queue.mainQueue().after(0.25) {
|
||||||
|
node.rotation = initial
|
||||||
|
let springAnimation = CASpringAnimation(keyPath: "rotation")
|
||||||
|
springAnimation.fromValue = NSValue(scnVector4: target)
|
||||||
|
springAnimation.toValue = NSValue(scnVector4: SCNVector4(x: 0.0, y: 1.0, z: 0.0, w: 0.0))
|
||||||
|
springAnimation.mass = 1.0
|
||||||
|
springAnimation.stiffness = 21.0
|
||||||
|
springAnimation.damping = 5.8
|
||||||
|
springAnimation.duration = springAnimation.settlingDuration * 0.8
|
||||||
|
node.addAnimation(springAnimation, forKey: "rotate")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var previousAngle: Float = 0.0
|
||||||
|
@objc private func handlePan(_ gesture: UIPanGestureRecognizer) {
|
||||||
|
guard let scene = self.sceneView.scene, let node = scene.rootNode.childNode(withName: "star", recursively: false) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch gesture.state {
|
||||||
|
case .began:
|
||||||
|
self.previousAngle = 0.0
|
||||||
|
case .changed:
|
||||||
|
let translation = gesture.translation(in: gesture.view)
|
||||||
|
let anglePan = deg2rad(Float(translation.x))
|
||||||
|
|
||||||
|
self.previousAngle = anglePan
|
||||||
|
node.rotation = SCNVector4(x: 0.0, y: 1.0, z: 0.0, w: self.previousAngle)
|
||||||
|
case .ended:
|
||||||
|
let velocity = gesture.velocity(in: gesture.view)
|
||||||
|
|
||||||
|
var small = false
|
||||||
|
if (self.previousAngle < .pi / 2 && self.previousAngle > -.pi / 2) && abs(velocity.x) < 200 {
|
||||||
|
small = true
|
||||||
|
}
|
||||||
|
|
||||||
|
self.playAppearanceAnimation(velocity: velocity.x, small: small)
|
||||||
|
node.rotation = SCNVector4(x: 0.0, y: 1.0, z: 0.0, w: 0.0)
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private func setup() {
|
private func setup() {
|
||||||
guard let scene = SCNScene(named: "star.scn") else {
|
guard let scene = SCNScene(named: "star.scn") else {
|
||||||
return
|
return
|
||||||
@ -77,11 +164,11 @@ private class StarComponent: Component {
|
|||||||
self.setupGradientAnimation()
|
self.setupGradientAnimation()
|
||||||
self.setupShineAnimation()
|
self.setupShineAnimation()
|
||||||
|
|
||||||
self.playAppearanceAnimation()
|
self.playAppearanceAnimation(boom: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func setupGradientAnimation() {
|
private func setupGradientAnimation() {
|
||||||
guard let scene = self.sceneView.scene, let node = scene.rootNode.childNode(withName: "star", recursively: true) else {
|
guard let scene = self.sceneView.scene, let node = scene.rootNode.childNode(withName: "star", recursively: false) else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
guard let initial = node.geometry?.materials.first?.diffuse.contentsTransform else {
|
guard let initial = node.geometry?.materials.first?.diffuse.contentsTransform else {
|
||||||
@ -124,21 +211,37 @@ private class StarComponent: Component {
|
|||||||
node.geometry?.materials.first?.emission.addAnimation(group, forKey: "shimmer")
|
node.geometry?.materials.first?.emission.addAnimation(group, forKey: "shimmer")
|
||||||
}
|
}
|
||||||
|
|
||||||
private func playAppearanceAnimation() {
|
private func playAppearanceAnimation(velocity: CGFloat? = nil, small: Bool = false, boom: Bool = false) {
|
||||||
guard let scene = self.sceneView.scene, let starNode = scene.rootNode.childNode(withName: "star", recursively: false) else {
|
guard let scene = self.sceneView.scene, let node = scene.rootNode.childNode(withName: "star", recursively: false) else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if boom, let node = scene.rootNode.childNode(withName: "swirl", recursively: false), let particles = scene.rootNode.childNode(withName: "particles", recursively: false) {
|
||||||
|
node.physicsField?.isActive = true
|
||||||
|
Queue.mainQueue().after(1.0) {
|
||||||
|
node.physicsField?.isActive = false
|
||||||
|
particles.particleSystems?.first?.birthRate = 0.8
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let from = node.rotation
|
||||||
|
var toValue: Float = small ? 0.0 : .pi * 2.0
|
||||||
|
if let velocity = velocity, !small && abs(velocity) > 200 && velocity < 0.0 {
|
||||||
|
toValue *= -1
|
||||||
|
}
|
||||||
|
let to = SCNVector4(x: 0.0, y: 1.0, z: 0.0, w: toValue)
|
||||||
|
let distance = rad2deg(to.w - from.w)
|
||||||
|
|
||||||
let springAnimation = CASpringAnimation(keyPath: "rotation")
|
let springAnimation = CASpringAnimation(keyPath: "rotation")
|
||||||
springAnimation.fromValue = NSValue(scnVector4: SCNVector4(x: 0.0, y: 1.0, z: 0.0, w: 0.0))
|
springAnimation.fromValue = NSValue(scnVector4: from)
|
||||||
springAnimation.toValue = NSValue(scnVector4: SCNVector4(x: 0.0, y: 1.0, z: 0.0, w: .pi * 2.0))
|
springAnimation.toValue = NSValue(scnVector4: to)
|
||||||
springAnimation.mass = 1.0
|
springAnimation.mass = 1.0
|
||||||
springAnimation.stiffness = 21.0
|
springAnimation.stiffness = 21.0
|
||||||
springAnimation.damping = 5.8
|
springAnimation.damping = 5.8
|
||||||
springAnimation.duration = 1.5
|
springAnimation.duration = springAnimation.settlingDuration * 0.75
|
||||||
springAnimation.initialVelocity = 1.7
|
springAnimation.initialVelocity = velocity.flatMap { abs($0 / CGFloat(distance)) } ?? 1.7
|
||||||
|
|
||||||
starNode.addAnimation(springAnimation, forKey: "rotate")
|
node.addAnimation(springAnimation, forKey: "rotate")
|
||||||
}
|
}
|
||||||
|
|
||||||
func update(component: StarComponent, availableSize: CGSize, transition: Transition) -> CGSize {
|
func update(component: StarComponent, availableSize: CGSize, transition: Transition) -> CGSize {
|
||||||
@ -631,7 +734,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
|
|||||||
.position(CGPoint(x: fade.size.width / 2.0, y: fade.size.height / 2.0))
|
.position(CGPoint(x: fade.size.width / 2.0, y: fade.size.height / 2.0))
|
||||||
)
|
)
|
||||||
|
|
||||||
size.height += 183.0
|
size.height += 183.0 + 10.0
|
||||||
|
|
||||||
let textColor = theme.list.itemPrimaryTextColor
|
let textColor = theme.list.itemPrimaryTextColor
|
||||||
let titleColor = theme.list.itemPrimaryTextColor
|
let titleColor = theme.list.itemPrimaryTextColor
|
||||||
@ -1053,7 +1156,7 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
|
|||||||
let topPanelAlpha: CGFloat
|
let topPanelAlpha: CGFloat
|
||||||
let titleOffset: CGFloat
|
let titleOffset: CGFloat
|
||||||
let titleScale: CGFloat
|
let titleScale: CGFloat
|
||||||
let titleOffsetDelta = 150.0 - environment.navigationHeight / 2.0
|
let titleOffsetDelta = 160.0 - environment.navigationHeight / 2.0
|
||||||
|
|
||||||
if let topContentOffset = state.topContentOffset {
|
if let topContentOffset = state.topContentOffset {
|
||||||
topPanelAlpha = min(30.0, max(0.0, topContentOffset - 64.0)) / 30.0
|
topPanelAlpha = min(30.0, max(0.0, topContentOffset - 64.0)) / 30.0
|
||||||
@ -1066,7 +1169,7 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
context.add(star
|
context.add(star
|
||||||
.position(CGPoint(x: context.availableSize.width / 2.0, y: star.size.height / 2.0 - 18.0 - titleOffset * titleScale))
|
.position(CGPoint(x: context.availableSize.width / 2.0, y: star.size.height / 2.0 - 10.0 - titleOffset * titleScale))
|
||||||
.scale(titleScale)
|
.scale(titleScale)
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -1080,7 +1183,7 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
|
|||||||
)
|
)
|
||||||
|
|
||||||
context.add(title
|
context.add(title
|
||||||
.position(CGPoint(x: context.availableSize.width / 2.0, y: max(150.0 - titleOffset, environment.navigationHeight / 2.0)))
|
.position(CGPoint(x: context.availableSize.width / 2.0, y: max(160.0 - titleOffset, environment.navigationHeight / 2.0)))
|
||||||
.scale(titleScale)
|
.scale(titleScale)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -16,6 +16,410 @@ import BundleIconComponent
|
|||||||
import SolidRoundedButtonComponent
|
import SolidRoundedButtonComponent
|
||||||
import Markdown
|
import Markdown
|
||||||
|
|
||||||
|
private class PremiumLimitAnimationComponent: Component {
|
||||||
|
private let iconName: String
|
||||||
|
private let inactiveColor: UIColor
|
||||||
|
private let activeColors: [UIColor]
|
||||||
|
private let textColor: UIColor
|
||||||
|
|
||||||
|
init(
|
||||||
|
iconName: String,
|
||||||
|
inactiveColor: UIColor,
|
||||||
|
activeColors: [UIColor],
|
||||||
|
textColor: UIColor
|
||||||
|
) {
|
||||||
|
self.iconName = iconName
|
||||||
|
self.inactiveColor = inactiveColor
|
||||||
|
self.activeColors = activeColors
|
||||||
|
self.textColor = textColor
|
||||||
|
}
|
||||||
|
|
||||||
|
static func ==(lhs: PremiumLimitAnimationComponent, rhs: PremiumLimitAnimationComponent) -> Bool {
|
||||||
|
if lhs.iconName != rhs.iconName {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.inactiveColor != rhs.inactiveColor {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.activeColors != rhs.activeColors {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.textColor != rhs.textColor {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
final class View: UIView {
|
||||||
|
private let container: SimpleLayer
|
||||||
|
private let inactiveBackground: SimpleLayer
|
||||||
|
|
||||||
|
private let activeContainer: SimpleLayer
|
||||||
|
private let activeBackground: SimpleLayer
|
||||||
|
|
||||||
|
private let badgeView: UIView
|
||||||
|
private let badgeMaskView: UIView
|
||||||
|
private let badgeMaskBackgroundView: UIView
|
||||||
|
private let badgeMaskArrowView: UIImageView
|
||||||
|
private let badgeForeground: SimpleLayer
|
||||||
|
private let badgeIcon: UIImageView
|
||||||
|
private let badgeCountLabel: RollingLabel
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
self.container = SimpleLayer()
|
||||||
|
self.container.masksToBounds = true
|
||||||
|
self.container.cornerRadius = 6.0
|
||||||
|
|
||||||
|
self.inactiveBackground = SimpleLayer()
|
||||||
|
|
||||||
|
self.activeContainer = SimpleLayer()
|
||||||
|
self.activeContainer.masksToBounds = true
|
||||||
|
|
||||||
|
self.activeBackground = SimpleLayer()
|
||||||
|
|
||||||
|
self.badgeView = UIView()
|
||||||
|
self.badgeView.layer.anchorPoint = CGPoint(x: 0.5, y: 1.0)
|
||||||
|
|
||||||
|
self.badgeMaskBackgroundView = UIView()
|
||||||
|
self.badgeMaskBackgroundView.backgroundColor = .white
|
||||||
|
self.badgeMaskBackgroundView.layer.cornerRadius = 24.0
|
||||||
|
|
||||||
|
self.badgeMaskArrowView = UIImageView()
|
||||||
|
self.badgeMaskArrowView.image = generateImage(CGSize(width: 44.0, height: 12.0), rotatedContext: { size, context in
|
||||||
|
context.clear(CGRect(origin: .zero, size: size))
|
||||||
|
context.setFillColor(UIColor.white.cgColor)
|
||||||
|
context.scaleBy(x: 3.76, y: 3.76)
|
||||||
|
context.translateBy(x: -9.3, y: -12.7)
|
||||||
|
try? drawSvgPath(context, path: "M6.4,0.0 C2.9,0.0 0.0,2.84 0.0,6.35 C0.0,9.86 2.9,12.7 6.4,12.7 H9.302 H11.3 C11.7,12.7 12.1,12.87 12.4,13.17 L14.4,15.13 C14.8,15.54 15.5,15.54 15.9,15.13 L17.8,13.17 C18.1,12.87 18.5,12.7 18.9,12.7 H20.9 H23.6 C27.1,12.7 29.9,9.86 29.9,6.35 C29.9,2.84 27.1,0.0 23.6,0.0 Z ")
|
||||||
|
})
|
||||||
|
|
||||||
|
self.badgeMaskView = UIView()
|
||||||
|
self.badgeMaskView.addSubview(self.badgeMaskBackgroundView)
|
||||||
|
self.badgeMaskView.addSubview(self.badgeMaskArrowView)
|
||||||
|
self.badgeView.mask = self.badgeMaskView
|
||||||
|
|
||||||
|
self.badgeForeground = SimpleLayer()
|
||||||
|
|
||||||
|
self.badgeIcon = UIImageView()
|
||||||
|
self.badgeIcon.contentMode = .center
|
||||||
|
|
||||||
|
self.badgeCountLabel = RollingLabel()
|
||||||
|
self.badgeCountLabel.font = Font.with(size: 24.0, design: .round, weight: .semibold, traits: [])
|
||||||
|
self.badgeCountLabel.textColor = .white
|
||||||
|
self.badgeCountLabel.text(num: 0)
|
||||||
|
|
||||||
|
super.init(frame: frame)
|
||||||
|
|
||||||
|
self.layer.addSublayer(self.container)
|
||||||
|
self.container.addSublayer(self.inactiveBackground)
|
||||||
|
self.container.addSublayer(self.activeContainer)
|
||||||
|
self.activeContainer.addSublayer(self.activeBackground)
|
||||||
|
|
||||||
|
self.addSubview(self.badgeView)
|
||||||
|
self.badgeView.layer.addSublayer(self.badgeForeground)
|
||||||
|
self.badgeView.addSubview(self.badgeIcon)
|
||||||
|
self.badgeView.addSubview(self.badgeCountLabel)
|
||||||
|
|
||||||
|
self.isUserInteractionEnabled = false
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
private var didPlayAppearanceAnimation = false
|
||||||
|
func playAppearanceAnimation(availableSize: CGSize) {
|
||||||
|
self.badgeView.layer.animateScale(from: 0.1, to: 1.0, duration: 0.4, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue)
|
||||||
|
|
||||||
|
let now = self.badgeView.layer.convertTime(CACurrentMediaTime(), from: nil)
|
||||||
|
|
||||||
|
let positionAnimation = CABasicAnimation(keyPath: "position.x")
|
||||||
|
positionAnimation.fromValue = NSValue(cgPoint: CGPoint(x: -availableSize.width / 2.0, y: 0.0))
|
||||||
|
positionAnimation.toValue = NSValue(cgPoint: CGPoint())
|
||||||
|
positionAnimation.isAdditive = true
|
||||||
|
positionAnimation.duration = 0.5
|
||||||
|
positionAnimation.fillMode = .forwards
|
||||||
|
positionAnimation.beginTime = now
|
||||||
|
|
||||||
|
let rotateAnimation = CABasicAnimation(keyPath: "transform.rotation.z")
|
||||||
|
rotateAnimation.fromValue = 0.0 as NSNumber
|
||||||
|
rotateAnimation.toValue = 0.2 as NSNumber
|
||||||
|
rotateAnimation.isAdditive = true
|
||||||
|
rotateAnimation.duration = 0.2
|
||||||
|
rotateAnimation.beginTime = now + 0.5
|
||||||
|
rotateAnimation.fillMode = .forwards
|
||||||
|
rotateAnimation.timingFunction = CAMediaTimingFunction(name: .easeOut)
|
||||||
|
|
||||||
|
let returnAnimation = CABasicAnimation(keyPath: "transform.rotation.z")
|
||||||
|
returnAnimation.fromValue = 0.2 as NSNumber
|
||||||
|
returnAnimation.toValue = 0.0 as NSNumber
|
||||||
|
returnAnimation.isAdditive = true
|
||||||
|
returnAnimation.duration = 0.18
|
||||||
|
returnAnimation.beginTime = now + 0.5 + 0.2
|
||||||
|
returnAnimation.fillMode = .forwards
|
||||||
|
returnAnimation.timingFunction = CAMediaTimingFunction(name: .easeIn)
|
||||||
|
|
||||||
|
self.badgeView.layer.add(positionAnimation, forKey: "appearance1")
|
||||||
|
self.badgeView.layer.add(rotateAnimation, forKey: "appearance2")
|
||||||
|
self.badgeView.layer.add(returnAnimation, forKey: "appearance3")
|
||||||
|
|
||||||
|
self.badgeCountLabel.text(num: 4)
|
||||||
|
}
|
||||||
|
|
||||||
|
var previousAvailableSize: CGSize?
|
||||||
|
func update(component: PremiumLimitAnimationComponent, availableSize: CGSize, transition: Transition) -> CGSize {
|
||||||
|
self.inactiveBackground.backgroundColor = component.inactiveColor.cgColor
|
||||||
|
self.activeBackground.backgroundColor = component.activeColors.last?.cgColor
|
||||||
|
|
||||||
|
self.badgeIcon.image = UIImage(bundleImageName: component.iconName)?.withRenderingMode(.alwaysTemplate)
|
||||||
|
self.badgeIcon.tintColor = component.textColor
|
||||||
|
|
||||||
|
let lineHeight: CGFloat = 30.0
|
||||||
|
let containerFrame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - lineHeight), size: CGSize(width: availableSize.width, height: lineHeight))
|
||||||
|
self.container.frame = containerFrame
|
||||||
|
|
||||||
|
self.inactiveBackground.frame = CGRect(origin: .zero, size: CGSize(width: containerFrame.width / 2.0 - 1.0, height: lineHeight))
|
||||||
|
self.activeContainer.frame = CGRect(origin: CGPoint(x: containerFrame.width / 2.0 + 1.0, y: 0.0), size: CGSize(width: containerFrame.width / 2.0 - 1.0, height: lineHeight))
|
||||||
|
|
||||||
|
self.activeBackground.bounds = CGRect(origin: .zero, size: CGSize(width: containerFrame.width * 3.0 / 2.0, height: lineHeight))
|
||||||
|
if self.activeBackground.animation(forKey: "movement") == nil {
|
||||||
|
self.activeBackground.position = CGPoint(x: containerFrame.width * 3.0 / 4.0, y: lineHeight / 2.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
let badgeSize = CGSize(width: 82.0, height: 48.0 + 12.0)
|
||||||
|
self.badgeMaskView.frame = CGRect(origin: .zero, size: badgeSize)
|
||||||
|
self.badgeMaskBackgroundView.frame = CGRect(origin: .zero, size: CGSize(width: badgeSize.width, height: 48.0))
|
||||||
|
self.badgeMaskArrowView.frame = CGRect(origin: CGPoint(x: (badgeSize.width - 44.0) / 2.0, y: badgeSize.height - 12.0), size: CGSize(width: 44.0, height: 12.0))
|
||||||
|
|
||||||
|
self.badgeView.bounds = CGRect(origin: .zero, size: badgeSize)
|
||||||
|
self.badgeView.center = CGPoint(x: availableSize.width / 2.0, y: 82.0)
|
||||||
|
if self.badgeForeground.animation(forKey: "movement") == nil {
|
||||||
|
self.badgeForeground.position = CGPoint(x: badgeSize.width * 3.0 / 2.0, y: badgeSize.height / 2.0)
|
||||||
|
}
|
||||||
|
self.badgeForeground.bounds = CGRect(origin: CGPoint(), size: CGSize(width: badgeSize.width * 3.0, height: badgeSize.height))
|
||||||
|
|
||||||
|
self.badgeIcon.frame = CGRect(x: 15.0, y: 9.0, width: 30.0, height: 30.0)
|
||||||
|
|
||||||
|
self.badgeCountLabel.frame = CGRect(x: badgeSize.width - 36.0, y: 10.0, width: 30.0, height: 48.0)
|
||||||
|
|
||||||
|
if !self.didPlayAppearanceAnimation {
|
||||||
|
self.didPlayAppearanceAnimation = true
|
||||||
|
self.playAppearanceAnimation(availableSize: availableSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.previousAvailableSize != availableSize {
|
||||||
|
self.previousAvailableSize = availableSize
|
||||||
|
|
||||||
|
var locations: [CGFloat] = []
|
||||||
|
let delta = 1.0 / CGFloat(component.activeColors.count - 1)
|
||||||
|
for i in 0 ..< component.activeColors.count {
|
||||||
|
locations.append(delta * CGFloat(i))
|
||||||
|
}
|
||||||
|
|
||||||
|
let gradient = generateGradientImage(size: CGSize(width: 200.0, height: 60.0), colors: component.activeColors, locations: locations, direction: .horizontal)
|
||||||
|
self.badgeForeground.contentsGravity = .resizeAspectFill
|
||||||
|
self.badgeForeground.contents = gradient?.cgImage
|
||||||
|
|
||||||
|
self.activeBackground.contentsGravity = .resizeAspectFill
|
||||||
|
self.activeBackground.contents = gradient?.cgImage
|
||||||
|
|
||||||
|
self.setupGradientAnimations()
|
||||||
|
}
|
||||||
|
|
||||||
|
return availableSize
|
||||||
|
}
|
||||||
|
|
||||||
|
private func setupGradientAnimations() {
|
||||||
|
if let _ = self.badgeForeground.animation(forKey: "movement") {
|
||||||
|
} else {
|
||||||
|
CATransaction.begin()
|
||||||
|
|
||||||
|
let badgeOffset = (self.badgeForeground.frame.width - self.badgeView.bounds.width) / 2.0
|
||||||
|
let badgePreviousValue = self.badgeForeground.position.x
|
||||||
|
var badgeNewValue: CGFloat = badgeOffset
|
||||||
|
if badgeOffset - badgePreviousValue < self.badgeForeground.frame.width * 0.25 {
|
||||||
|
badgeNewValue -= self.badgeForeground.frame.width * 0.35
|
||||||
|
}
|
||||||
|
self.badgeForeground.position = CGPoint(x: badgeNewValue, y: self.badgeForeground.bounds.size.height / 2.0)
|
||||||
|
|
||||||
|
let lineOffset = (self.activeBackground.frame.width - self.activeContainer.bounds.width) / 2.0
|
||||||
|
let linePreviousValue = self.activeBackground.position.x
|
||||||
|
var lineNewValue: CGFloat = lineOffset
|
||||||
|
if lineOffset - linePreviousValue < self.activeBackground.frame.width * 0.25 {
|
||||||
|
lineNewValue -= self.activeBackground.frame.width * 0.35
|
||||||
|
}
|
||||||
|
self.activeBackground.position = CGPoint(x: lineNewValue, y: self.activeBackground.bounds.size.height / 2.0)
|
||||||
|
|
||||||
|
let badgeAnimation = CABasicAnimation(keyPath: "position.x")
|
||||||
|
badgeAnimation.duration = 4.5
|
||||||
|
badgeAnimation.fromValue = badgePreviousValue
|
||||||
|
badgeAnimation.toValue = badgeNewValue
|
||||||
|
badgeAnimation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
|
||||||
|
|
||||||
|
CATransaction.setCompletionBlock { [weak self] in
|
||||||
|
self?.setupGradientAnimations()
|
||||||
|
}
|
||||||
|
self.badgeForeground.add(badgeAnimation, forKey: "movement")
|
||||||
|
|
||||||
|
let lineAnimation = CABasicAnimation(keyPath: "position.x")
|
||||||
|
lineAnimation.duration = 4.5
|
||||||
|
lineAnimation.fromValue = linePreviousValue
|
||||||
|
lineAnimation.toValue = lineNewValue
|
||||||
|
lineAnimation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
|
||||||
|
self.activeBackground.add(lineAnimation, forKey: "movement")
|
||||||
|
|
||||||
|
CATransaction.commit()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeView() -> View {
|
||||||
|
return View(frame: CGRect())
|
||||||
|
}
|
||||||
|
|
||||||
|
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||||
|
return view.update(component: self, availableSize: availableSize, transition: transition)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public final class PremiumLimitDisplayComponent: CombinedComponent {
|
||||||
|
public let inactiveColor: UIColor
|
||||||
|
public let activeColors: [UIColor]
|
||||||
|
public let inactiveTitle: String
|
||||||
|
public let inactiveTitleColor: UIColor
|
||||||
|
public let activeTitle: String
|
||||||
|
public let activeValue: String
|
||||||
|
public let activeTitleColor: UIColor
|
||||||
|
public let badgeIconName: String
|
||||||
|
public let badgeValue: String
|
||||||
|
|
||||||
|
public init(
|
||||||
|
inactiveColor: UIColor,
|
||||||
|
activeColors: [UIColor],
|
||||||
|
inactiveTitle: String,
|
||||||
|
inactiveTitleColor: UIColor,
|
||||||
|
activeTitle: String,
|
||||||
|
activeValue: String,
|
||||||
|
activeTitleColor: UIColor,
|
||||||
|
badgeIconName: String,
|
||||||
|
badgeValue: String
|
||||||
|
) {
|
||||||
|
self.inactiveColor = inactiveColor
|
||||||
|
self.activeColors = activeColors
|
||||||
|
self.inactiveTitle = inactiveTitle
|
||||||
|
self.inactiveTitleColor = inactiveTitleColor
|
||||||
|
self.activeTitle = activeTitle
|
||||||
|
self.activeValue = activeValue
|
||||||
|
self.activeTitleColor = activeTitleColor
|
||||||
|
self.badgeIconName = badgeIconName
|
||||||
|
self.badgeValue = badgeValue
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func ==(lhs: PremiumLimitDisplayComponent, rhs: PremiumLimitDisplayComponent) -> Bool {
|
||||||
|
if lhs.inactiveColor != rhs.inactiveColor {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.activeColors != rhs.activeColors {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.inactiveTitle != rhs.inactiveTitle {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.inactiveTitleColor != rhs.inactiveTitleColor {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.activeTitle != rhs.activeTitle {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.activeValue != rhs.activeValue {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.activeTitleColor != rhs.activeTitleColor {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.badgeIconName != rhs.badgeIconName {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.badgeValue != rhs.badgeValue {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
public static var body: Body {
|
||||||
|
let inactiveTitle = Child(Text.self)
|
||||||
|
let activeTitle = Child(Text.self)
|
||||||
|
let activeValue = Child(Text.self)
|
||||||
|
let animation = Child(PremiumLimitAnimationComponent.self)
|
||||||
|
|
||||||
|
return { context in
|
||||||
|
let component = context.component
|
||||||
|
|
||||||
|
let height: CGFloat = 120.0
|
||||||
|
let lineHeight: CGFloat = 30.0
|
||||||
|
|
||||||
|
let inactiveTitle = inactiveTitle.update(
|
||||||
|
component: Text(
|
||||||
|
text: component.inactiveTitle,
|
||||||
|
font: Font.semibold(15.0),
|
||||||
|
color: component.inactiveTitleColor
|
||||||
|
),
|
||||||
|
availableSize: context.availableSize,
|
||||||
|
transition: context.transition
|
||||||
|
)
|
||||||
|
|
||||||
|
let activeTitle = activeTitle.update(
|
||||||
|
component: Text(
|
||||||
|
text: component.activeTitle,
|
||||||
|
font: Font.semibold(15.0),
|
||||||
|
color: component.activeTitleColor
|
||||||
|
),
|
||||||
|
availableSize: context.availableSize,
|
||||||
|
transition: context.transition
|
||||||
|
)
|
||||||
|
|
||||||
|
let activeValue = activeValue.update(
|
||||||
|
component: Text(
|
||||||
|
text: component.activeValue,
|
||||||
|
font: Font.semibold(15.0),
|
||||||
|
color: component.activeTitleColor
|
||||||
|
),
|
||||||
|
availableSize: context.availableSize,
|
||||||
|
transition: context.transition
|
||||||
|
)
|
||||||
|
|
||||||
|
let animation = animation.update(
|
||||||
|
component: PremiumLimitAnimationComponent(
|
||||||
|
iconName: component.badgeIconName,
|
||||||
|
inactiveColor: component.inactiveColor,
|
||||||
|
activeColors: component.activeColors,
|
||||||
|
textColor: component.activeTitleColor
|
||||||
|
),
|
||||||
|
availableSize: CGSize(width: context.availableSize.width, height: height),
|
||||||
|
transition: context.transition
|
||||||
|
)
|
||||||
|
|
||||||
|
context.add(animation
|
||||||
|
.position(CGPoint(x: context.availableSize.width / 2.0, y: height / 2.0))
|
||||||
|
)
|
||||||
|
|
||||||
|
context.add(inactiveTitle
|
||||||
|
.position(CGPoint(x: inactiveTitle.size.width / 2.0 + 12.0, y: height - lineHeight / 2.0))
|
||||||
|
)
|
||||||
|
|
||||||
|
context.add(activeTitle
|
||||||
|
.position(CGPoint(x: context.availableSize.width / 2.0 + 1.0 + activeTitle.size.width / 2.0 + 12.0, y: height - lineHeight / 2.0))
|
||||||
|
)
|
||||||
|
|
||||||
|
context.add(activeValue
|
||||||
|
.position(CGPoint(x: context.availableSize.width - activeValue.size.width / 2.0 - 12.0, y: height - lineHeight / 2.0))
|
||||||
|
)
|
||||||
|
|
||||||
|
return CGSize(width: context.availableSize.width, height: height)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private final class LimitSheetContent: CombinedComponent {
|
private final class LimitSheetContent: CombinedComponent {
|
||||||
typealias EnvironmentType = ViewControllerComponentContainer.Environment
|
typealias EnvironmentType = ViewControllerComponentContainer.Environment
|
||||||
|
|
||||||
@ -78,13 +482,9 @@ private final class LimitSheetContent: CombinedComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static var body: Body {
|
static var body: Body {
|
||||||
let badgeBackground = Child(RoundedRectangle.self)
|
|
||||||
let badgeIcon = Child(BundleIconComponent.self)
|
|
||||||
let badgeText = Child(MultilineTextComponent.self)
|
|
||||||
|
|
||||||
let title = Child(MultilineTextComponent.self)
|
let title = Child(MultilineTextComponent.self)
|
||||||
let text = Child(MultilineTextComponent.self)
|
let text = Child(MultilineTextComponent.self)
|
||||||
|
let limit = Child(PremiumLimitDisplayComponent.self)
|
||||||
let button = Child(SolidRoundedButtonComponent.self)
|
let button = Child(SolidRoundedButtonComponent.self)
|
||||||
|
|
||||||
return { context in
|
return { context in
|
||||||
@ -100,68 +500,40 @@ private final class LimitSheetContent: CombinedComponent {
|
|||||||
let textSideInset: CGFloat = 24.0 + environment.safeInsets.left
|
let textSideInset: CGFloat = 24.0 + environment.safeInsets.left
|
||||||
|
|
||||||
let iconName: String
|
let iconName: String
|
||||||
let badgeString: String
|
let badgeValue: String
|
||||||
let string: String
|
let string: String
|
||||||
|
let premiumValue: String
|
||||||
switch subject {
|
switch subject {
|
||||||
case .folders:
|
case .folders:
|
||||||
let limit = state.limits.maxFoldersCount
|
let limit = state.limits.maxFoldersCount
|
||||||
let premiumLimit = state.premiumLimits.maxFoldersCount
|
let premiumLimit = state.premiumLimits.maxFoldersCount
|
||||||
iconName = "Premium/Folder"
|
iconName = "Premium/Folder"
|
||||||
badgeString = "\(limit)"
|
badgeValue = "\(limit)"
|
||||||
string = strings.Premium_MaxFoldersCountText("\(limit)", "\(premiumLimit)").string
|
string = strings.Premium_MaxFoldersCountText("\(limit)", "\(premiumLimit)").string
|
||||||
|
premiumValue = "\(premiumLimit)"
|
||||||
case .chatsInFolder:
|
case .chatsInFolder:
|
||||||
let limit = state.limits.maxFolderChatsCount
|
let limit = state.limits.maxFolderChatsCount
|
||||||
let premiumLimit = state.premiumLimits.maxFolderChatsCount
|
let premiumLimit = state.premiumLimits.maxFolderChatsCount
|
||||||
iconName = "Premium/Chat"
|
iconName = "Premium/Chat"
|
||||||
badgeString = "\(limit)"
|
badgeValue = "\(limit)"
|
||||||
string = strings.Premium_MaxChatsInFolderCountText("\(limit)", "\(premiumLimit)").string
|
string = strings.Premium_MaxChatsInFolderCountText("\(limit)", "\(premiumLimit)").string
|
||||||
|
premiumValue = "\(premiumLimit)"
|
||||||
case .pins:
|
case .pins:
|
||||||
let limit = state.limits.maxPinnedChatCount
|
let limit = 4//state.limits.maxPinnedChatCount
|
||||||
let premiumLimit = state.premiumLimits.maxPinnedChatCount
|
let premiumLimit = 6//state.premiumLimits.maxPinnedChatCount
|
||||||
iconName = "Premium/Pin"
|
iconName = "Premium/Pin"
|
||||||
badgeString = "\(limit)"
|
badgeValue = "\(limit)"
|
||||||
string = strings.Premium_MaxPinsText("\(limit)", "\(premiumLimit)").string
|
string = strings.Premium_MaxPinsText("\(limit)", "\(premiumLimit)").string
|
||||||
|
premiumValue = "\(premiumLimit)"
|
||||||
case .files:
|
case .files:
|
||||||
let limit = 2048 * 1024 * 1024 //state.limits.maxPinnedChatCount
|
let limit = 2048 * 1024 * 1024 //state.limits.maxPinnedChatCount
|
||||||
let premiumLimit = 4096 * 1024 * 1024 //state.premiumLimits.maxPinnedChatCount
|
let premiumLimit = 4096 * 1024 * 1024 //state.premiumLimits.maxPinnedChatCount
|
||||||
iconName = "Premium/File"
|
iconName = "Premium/File"
|
||||||
badgeString = dataSizeString(limit, formatting: DataSizeStringFormatting(strings: environment.strings, decimalSeparator: environment.dateTimeFormat.decimalSeparator))
|
badgeValue = dataSizeString(limit, 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
|
string = strings.Premium_MaxFileSizeText(dataSizeString(premiumLimit, formatting: DataSizeStringFormatting(strings: environment.strings, decimalSeparator: environment.dateTimeFormat.decimalSeparator))).string
|
||||||
|
premiumValue = dataSizeString(premiumLimit, formatting: DataSizeStringFormatting(strings: environment.strings, decimalSeparator: environment.dateTimeFormat.decimalSeparator))
|
||||||
}
|
}
|
||||||
|
|
||||||
let badgeIcon = badgeIcon.update(
|
|
||||||
component: BundleIconComponent(
|
|
||||||
name: iconName,
|
|
||||||
tintColor: .white
|
|
||||||
),
|
|
||||||
availableSize: context.availableSize,
|
|
||||||
transition: .immediate
|
|
||||||
)
|
|
||||||
|
|
||||||
let badgeText = badgeText.update(
|
|
||||||
component: MultilineTextComponent(
|
|
||||||
text: .plain(NSAttributedString(
|
|
||||||
string: badgeString,
|
|
||||||
font: Font.with(size: 24.0, design: .round, weight: .semibold, traits: []),
|
|
||||||
textColor: .white,
|
|
||||||
paragraphAlignment: .center
|
|
||||||
)),
|
|
||||||
horizontalAlignment: .center,
|
|
||||||
maximumNumberOfLines: 1
|
|
||||||
),
|
|
||||||
availableSize: context.availableSize,
|
|
||||||
transition: .immediate
|
|
||||||
)
|
|
||||||
|
|
||||||
let badgeBackground = badgeBackground.update(
|
|
||||||
component: RoundedRectangle(
|
|
||||||
colors: [UIColor(rgb: 0xa34fcf), UIColor(rgb: 0xc8498a), UIColor(rgb: 0xff7a23)],
|
|
||||||
cornerRadius: 23.5
|
|
||||||
),
|
|
||||||
availableSize: CGSize(width: badgeText.size.width + 67.0, height: 47.0),
|
|
||||||
transition: .immediate
|
|
||||||
)
|
|
||||||
|
|
||||||
let title = title.update(
|
let title = title.update(
|
||||||
component: MultilineTextComponent(
|
component: MultilineTextComponent(
|
||||||
text: .plain(NSAttributedString(
|
text: .plain(NSAttributedString(
|
||||||
@ -195,19 +567,45 @@ private final class LimitSheetContent: CombinedComponent {
|
|||||||
transition: .immediate
|
transition: .immediate
|
||||||
)
|
)
|
||||||
|
|
||||||
|
let limit = limit.update(
|
||||||
|
component: PremiumLimitDisplayComponent(
|
||||||
|
inactiveColor: UIColor(rgb: 0xE9E9EA),
|
||||||
|
activeColors: [
|
||||||
|
UIColor(rgb: 0x0077ff),
|
||||||
|
UIColor(rgb: 0x6b93ff),
|
||||||
|
UIColor(rgb: 0x8878ff),
|
||||||
|
UIColor(rgb: 0xe46ace)
|
||||||
|
],
|
||||||
|
inactiveTitle: "Free",
|
||||||
|
inactiveTitleColor: .black,
|
||||||
|
activeTitle: "Premium",
|
||||||
|
activeValue: premiumValue,
|
||||||
|
activeTitleColor: .white,
|
||||||
|
badgeIconName: iconName,
|
||||||
|
badgeValue: badgeValue
|
||||||
|
),
|
||||||
|
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: context.availableSize.height),
|
||||||
|
transition: .immediate
|
||||||
|
)
|
||||||
|
|
||||||
let button = button.update(
|
let button = button.update(
|
||||||
component: SolidRoundedButtonComponent(
|
component: SolidRoundedButtonComponent(
|
||||||
title: strings.Premium_IncreaseLimit,
|
title: strings.Premium_IncreaseLimit,
|
||||||
theme: SolidRoundedButtonComponent.Theme(
|
theme: SolidRoundedButtonComponent.Theme(
|
||||||
backgroundColor: .black,
|
backgroundColor: .black,
|
||||||
backgroundColors: [UIColor(rgb: 0x407af0), UIColor(rgb: 0x9551e8), UIColor(rgb: 0xbf499a), UIColor(rgb: 0xf17b30)],
|
backgroundColors: [
|
||||||
|
UIColor(rgb: 0x0077ff),
|
||||||
|
UIColor(rgb: 0x6b93ff),
|
||||||
|
UIColor(rgb: 0x8878ff),
|
||||||
|
UIColor(rgb: 0xe46ace)
|
||||||
|
],
|
||||||
foregroundColor: .white
|
foregroundColor: .white
|
||||||
),
|
),
|
||||||
font: .bold,
|
font: .bold,
|
||||||
fontSize: 17.0,
|
fontSize: 17.0,
|
||||||
height: 50.0,
|
height: 50.0,
|
||||||
cornerRadius: 10.0,
|
cornerRadius: 10.0,
|
||||||
gloss: false,
|
gloss: true,
|
||||||
iconName: "Premium/X2",
|
iconName: "Premium/X2",
|
||||||
iconPosition: .right,
|
iconPosition: .right,
|
||||||
action: { [weak component] in
|
action: { [weak component] in
|
||||||
@ -224,19 +622,8 @@ private final class LimitSheetContent: CombinedComponent {
|
|||||||
|
|
||||||
let width = context.availableSize.width
|
let width = context.availableSize.width
|
||||||
|
|
||||||
let badgeFrame = CGRect(origin: CGPoint(x: floor((context.availableSize.width - badgeBackground.size.width) / 2.0), y: 33.0), size: badgeBackground.size)
|
context.add(limit
|
||||||
context.add(badgeBackground
|
.position(CGPoint(x: width / 2.0, y: limit.size.height / 2.0 + 44.0))
|
||||||
.position(CGPoint(x: badgeFrame.midX, y: badgeFrame.midY))
|
|
||||||
)
|
|
||||||
|
|
||||||
let badgeIconFrame = CGRect(origin: CGPoint(x: badgeFrame.minX + 18.0, y: badgeFrame.minY + floor((badgeFrame.height - badgeIcon.size.height) / 2.0)), size: badgeIcon.size)
|
|
||||||
context.add(badgeIcon
|
|
||||||
.position(CGPoint(x: badgeIconFrame.midX, y: badgeIconFrame.midY))
|
|
||||||
)
|
|
||||||
|
|
||||||
let badgeTextFrame = CGRect(origin: CGPoint(x: badgeFrame.maxX - badgeText.size.width - 15.0, y: badgeFrame.minY + floor((badgeFrame.height - badgeText.size.height) / 2.0)), size: badgeText.size)
|
|
||||||
context.add(badgeText
|
|
||||||
.position(CGPoint(x: badgeTextFrame.midX, y: badgeTextFrame.midY))
|
|
||||||
)
|
)
|
||||||
|
|
||||||
context.add(title
|
context.add(title
|
||||||
|
197
submodules/PremiumUI/Sources/RollingCountLabel.swift
Normal file
197
submodules/PremiumUI/Sources/RollingCountLabel.swift
Normal file
@ -0,0 +1,197 @@
|
|||||||
|
import UIKit
|
||||||
|
|
||||||
|
private extension UILabel {
|
||||||
|
func textWidth() -> CGFloat {
|
||||||
|
return UILabel.textWidth(label: self)
|
||||||
|
}
|
||||||
|
|
||||||
|
class func textWidth(label: UILabel) -> CGFloat {
|
||||||
|
return textWidth(label: label, text: label.text!)
|
||||||
|
}
|
||||||
|
|
||||||
|
class func textWidth(label: UILabel, text: String) -> CGFloat {
|
||||||
|
return textWidth(font: label.font, text: text)
|
||||||
|
}
|
||||||
|
|
||||||
|
class func textWidth(font: UIFont, text: String) -> CGFloat {
|
||||||
|
let myText = text as NSString
|
||||||
|
|
||||||
|
let rect = CGSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude)
|
||||||
|
let labelSize = myText.boundingRect(with: rect, options: .usesLineFragmentOrigin, attributes: [NSAttributedString.Key.font: font], context: nil)
|
||||||
|
return ceil(labelSize.width)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
open class RollingLabel: UILabel {
|
||||||
|
private var fullText = ""
|
||||||
|
|
||||||
|
private var suffix: String = ""
|
||||||
|
open var showSymbol = false
|
||||||
|
private var scrollLayers: [CAScrollLayer] = []
|
||||||
|
private var scrollLabels: [UILabel] = []
|
||||||
|
private let duration = 1.12
|
||||||
|
private let durationOffset = 0.2
|
||||||
|
private let textsNotAnimated = [","]
|
||||||
|
|
||||||
|
public func text(num: Int) {
|
||||||
|
self.configure(with: num)
|
||||||
|
self.text = " "
|
||||||
|
self.animate()
|
||||||
|
}
|
||||||
|
|
||||||
|
public func setPrefix(prefix: String) {
|
||||||
|
self.suffix = prefix
|
||||||
|
}
|
||||||
|
|
||||||
|
private func configure(with number: Int) {
|
||||||
|
fullText = String(number)
|
||||||
|
|
||||||
|
clean()
|
||||||
|
setupSubviews()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func animate(ascending: Bool = true) {
|
||||||
|
createAnimations(ascending: ascending)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func clean() {
|
||||||
|
self.text = nil
|
||||||
|
self.subviews.forEach { $0.removeFromSuperview() }
|
||||||
|
self.layer.sublayers?.forEach { $0.removeFromSuperlayer() }
|
||||||
|
scrollLayers.removeAll()
|
||||||
|
scrollLabels.removeAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func setupSubviews() {
|
||||||
|
let stringArray = fullText.map { String($0) }
|
||||||
|
var x: CGFloat = 0
|
||||||
|
let y: CGFloat = 0
|
||||||
|
if self.textAlignment == .center {
|
||||||
|
if showSymbol {
|
||||||
|
self.text = "\(fullText) \(suffix)"
|
||||||
|
} else {
|
||||||
|
self.text = fullText
|
||||||
|
}
|
||||||
|
let w = UILabel.textWidth(font: self.font, text: self.text ?? "")
|
||||||
|
self.text = "" // 초기화
|
||||||
|
x = -(w / 2)
|
||||||
|
} else if self.textAlignment == .right {
|
||||||
|
if showSymbol {
|
||||||
|
self.text = "\(fullText) \(suffix) "
|
||||||
|
} else {
|
||||||
|
self.text = fullText
|
||||||
|
}
|
||||||
|
let w = UILabel.textWidth(font: self.font, text: self.text ?? "")
|
||||||
|
self.text = "" // 초기화
|
||||||
|
x = -w
|
||||||
|
}
|
||||||
|
|
||||||
|
if showSymbol {
|
||||||
|
let wLabel = UILabel()
|
||||||
|
wLabel.frame.origin = CGPoint(x: x, y: y)
|
||||||
|
wLabel.textColor = textColor
|
||||||
|
wLabel.font = font
|
||||||
|
wLabel.text = "\(suffix) "
|
||||||
|
wLabel.textAlignment = .center
|
||||||
|
wLabel.sizeToFit()
|
||||||
|
self.addSubview(wLabel)
|
||||||
|
x += wLabel.bounds.width
|
||||||
|
}
|
||||||
|
|
||||||
|
stringArray.enumerated().forEach { index, text in
|
||||||
|
if textsNotAnimated.contains(text) {
|
||||||
|
let label = UILabel()
|
||||||
|
label.frame.origin = CGPoint(x: x, y: y)
|
||||||
|
label.textColor = textColor
|
||||||
|
label.font = font
|
||||||
|
label.text = text
|
||||||
|
label.textAlignment = .center
|
||||||
|
label.sizeToFit()
|
||||||
|
self.addSubview(label)
|
||||||
|
|
||||||
|
x += label.bounds.width
|
||||||
|
} else {
|
||||||
|
let label = UILabel()
|
||||||
|
label.frame.origin = CGPoint(x: x, y: y)
|
||||||
|
label.textColor = textColor
|
||||||
|
label.font = font
|
||||||
|
label.text = "0"
|
||||||
|
label.textAlignment = .center
|
||||||
|
label.sizeToFit()
|
||||||
|
createScrollLayer(to: label, text: text)
|
||||||
|
|
||||||
|
x += label.bounds.width
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func createScrollLayer(to label: UILabel, text: String) {
|
||||||
|
let scrollLayer = CAScrollLayer()
|
||||||
|
scrollLayer.frame = label.frame
|
||||||
|
scrollLayers.append(scrollLayer)
|
||||||
|
self.layer.addSublayer(scrollLayer)
|
||||||
|
|
||||||
|
createContentForLayer(scrollLayer: scrollLayer, text: text)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func createContentForLayer(scrollLayer: CAScrollLayer, text: String) {
|
||||||
|
var textsForScroll: [String] = []
|
||||||
|
|
||||||
|
let max: Int
|
||||||
|
var found = false
|
||||||
|
if let val = Int(text) {
|
||||||
|
max = val
|
||||||
|
found = true
|
||||||
|
} else {
|
||||||
|
max = 9
|
||||||
|
}
|
||||||
|
|
||||||
|
for i in 0...max {
|
||||||
|
let str = String(i)
|
||||||
|
textsForScroll.append(str)
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
textsForScroll.append(text)
|
||||||
|
}
|
||||||
|
|
||||||
|
var height: CGFloat = 0
|
||||||
|
for text in textsForScroll {
|
||||||
|
let label = UILabel()
|
||||||
|
label.text = text
|
||||||
|
label.textColor = textColor
|
||||||
|
label.font = font
|
||||||
|
label.textAlignment = .center
|
||||||
|
label.frame = CGRect(x: 0, y: height, width: scrollLayer.frame.width, height: scrollLayer.frame.height)
|
||||||
|
scrollLayer.addSublayer(label.layer)
|
||||||
|
scrollLabels.append(label)
|
||||||
|
|
||||||
|
height = label.frame.maxY
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func createAnimations(ascending: Bool) {
|
||||||
|
var offset: CFTimeInterval = 0.0
|
||||||
|
|
||||||
|
for scrollLayer in scrollLayers {
|
||||||
|
let maxY = scrollLayer.sublayers?.last?.frame.origin.y ?? 0.0
|
||||||
|
|
||||||
|
let animation = CABasicAnimation(keyPath: "sublayerTransform.translation.y")
|
||||||
|
animation.duration = duration + offset
|
||||||
|
animation.timingFunction = CAMediaTimingFunction(name: .easeOut)
|
||||||
|
|
||||||
|
if ascending {
|
||||||
|
animation.fromValue = maxY
|
||||||
|
animation.toValue = 0
|
||||||
|
} else {
|
||||||
|
animation.fromValue = 0
|
||||||
|
animation.toValue = maxY
|
||||||
|
}
|
||||||
|
|
||||||
|
scrollLayer.scrollMode = .vertically
|
||||||
|
scrollLayer.add(animation, forKey: nil)
|
||||||
|
scrollLayer.scroll(to: CGPoint(x: 0, y: maxY))
|
||||||
|
|
||||||
|
offset += self.durationOffset
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -645,9 +645,7 @@ public final class SolidRoundedButtonView: UIView {
|
|||||||
let previousValue = buttonBackgroundAnimationView.center.x
|
let previousValue = buttonBackgroundAnimationView.center.x
|
||||||
var newValue: CGFloat = offset
|
var newValue: CGFloat = offset
|
||||||
if offset - previousValue < buttonBackgroundAnimationView.frame.width * 0.25 {
|
if offset - previousValue < buttonBackgroundAnimationView.frame.width * 0.25 {
|
||||||
newValue -= CGFloat.random(in: buttonBackgroundAnimationView.frame.width * 0.3 ..< buttonBackgroundAnimationView.frame.width * 0.4)
|
newValue -= buttonBackgroundAnimationView.frame.width * 0.35
|
||||||
} else {
|
|
||||||
// newValue -= CGFloat.random(in: 0.0 ..< buttonBackgroundAnimationView.frame.width * 0.1)
|
|
||||||
}
|
}
|
||||||
buttonBackgroundAnimationView.center = CGPoint(x: newValue, y: buttonBackgroundAnimationView.bounds.size.height / 2.0)
|
buttonBackgroundAnimationView.center = CGPoint(x: newValue, y: buttonBackgroundAnimationView.bounds.size.height / 2.0)
|
||||||
|
|
||||||
@ -794,7 +792,10 @@ public final class SolidRoundedButtonView: UIView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let buttonBackgroundAnimationView = self.buttonBackgroundAnimationView {
|
if let buttonBackgroundAnimationView = self.buttonBackgroundAnimationView {
|
||||||
transition.updateFrame(view: buttonBackgroundAnimationView, frame: CGRect(origin: CGPoint(), size: CGSize(width: buttonSize.width * 2.4, height: buttonSize.height)))
|
if buttonBackgroundAnimationView.layer.animation(forKey: "movement") == nil {
|
||||||
|
buttonBackgroundAnimationView.center = CGPoint(x: buttonSize.width * 2.4 / 2.0, y: buttonSize.height / 2.0)
|
||||||
|
}
|
||||||
|
buttonBackgroundAnimationView.bounds = CGRect(origin: CGPoint(), size: CGSize(width: buttonSize.width * 2.4, height: buttonSize.height))
|
||||||
self.setupGradientAnimations()
|
self.setupGradientAnimations()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -128,6 +128,37 @@ public extension TelegramEngine {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func subscribe<
|
||||||
|
T0: TelegramEngineDataItem,
|
||||||
|
T1: TelegramEngineDataItem,
|
||||||
|
T2: TelegramEngineDataItem
|
||||||
|
>(
|
||||||
|
_ t0: T0,
|
||||||
|
_ t1: T1,
|
||||||
|
_ t2: T2
|
||||||
|
) -> Signal<
|
||||||
|
(
|
||||||
|
T0.Result,
|
||||||
|
T1.Result,
|
||||||
|
T2.Result
|
||||||
|
),
|
||||||
|
NoError> {
|
||||||
|
return self._subscribe(items: [
|
||||||
|
t0 as! AnyPostboxViewDataItem,
|
||||||
|
t1 as! AnyPostboxViewDataItem,
|
||||||
|
t2 as! AnyPostboxViewDataItem
|
||||||
|
])
|
||||||
|
|> map { results -> (T0.Result, T1.Result, T2.Result) in
|
||||||
|
return (
|
||||||
|
results[0] as! T0.Result,
|
||||||
|
results[1] as! T1.Result,
|
||||||
|
results[2] as! T2.Result
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public func get<
|
public func get<
|
||||||
T0: TelegramEngineDataItem,
|
T0: TelegramEngineDataItem,
|
||||||
T1: TelegramEngineDataItem
|
T1: TelegramEngineDataItem
|
||||||
@ -142,5 +173,23 @@ public extension TelegramEngine {
|
|||||||
NoError> {
|
NoError> {
|
||||||
return self.subscribe(t0, t1) |> take(1)
|
return self.subscribe(t0, t1) |> take(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func get<
|
||||||
|
T0: TelegramEngineDataItem,
|
||||||
|
T1: TelegramEngineDataItem,
|
||||||
|
T2: TelegramEngineDataItem
|
||||||
|
>(
|
||||||
|
_ t0: T0,
|
||||||
|
_ t1: T1,
|
||||||
|
_ t2: T2
|
||||||
|
) -> Signal<
|
||||||
|
(
|
||||||
|
T0.Result,
|
||||||
|
T1.Result,
|
||||||
|
T2.Result
|
||||||
|
),
|
||||||
|
NoError> {
|
||||||
|
return self.subscribe(t0, t1, t2) |> take(1)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,21 +0,0 @@
|
|||||||
{
|
|
||||||
"images" : [
|
|
||||||
{
|
|
||||||
"idiom" : "universal",
|
|
||||||
"scale" : "1x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idiom" : "universal",
|
|
||||||
"scale" : "2x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"filename" : "Tmp.png",
|
|
||||||
"idiom" : "universal",
|
|
||||||
"scale" : "3x"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"info" : {
|
|
||||||
"author" : "xcode",
|
|
||||||
"version" : 1
|
|
||||||
}
|
|
||||||
}
|
|
Binary file not shown.
Before Width: | Height: | Size: 25 KiB |
@ -1,21 +0,0 @@
|
|||||||
{
|
|
||||||
"images" : [
|
|
||||||
{
|
|
||||||
"idiom" : "universal",
|
|
||||||
"scale" : "1x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idiom" : "universal",
|
|
||||||
"scale" : "2x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"filename" : "Tmp2.png",
|
|
||||||
"idiom" : "universal",
|
|
||||||
"scale" : "3x"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"info" : {
|
|
||||||
"author" : "xcode",
|
|
||||||
"version" : 1
|
|
||||||
}
|
|
||||||
}
|
|
Binary file not shown.
Before Width: | Height: | Size: 4.8 KiB |
@ -131,11 +131,11 @@ public final class TelegramRootController: NavigationController {
|
|||||||
self.rootTabController = tabBarController
|
self.rootTabController = tabBarController
|
||||||
self.pushViewController(tabBarController, animated: false)
|
self.pushViewController(tabBarController, animated: false)
|
||||||
|
|
||||||
Queue.mainQueue().after(1.0) {
|
// Queue.mainQueue().after(1.0) {
|
||||||
// let screen = PremiumLimitScreen(context: self.context, subject: .pins, action: {})
|
//// let screen = PremiumLimitScreen(context: self.context, subject: .pins, action: {})
|
||||||
let screen = PremiumIntroScreen(context: self.context, action: {})
|
// let screen = PremiumIntroScreen(context: self.context, action: {})
|
||||||
self.chatListController?.push(screen)
|
// self.chatListController?.push(screen)
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
public func updateRootControllers(showCallsTab: Bool) {
|
public func updateRootControllers(showCallsTab: Bool) {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user