mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Effect improvements
This commit is contained in:
parent
6e28b2a4e2
commit
7c0216b907
@ -27,7 +27,7 @@ public final class AvatarVideoNode: ASDisplayNode {
|
||||
|
||||
private var fileDisposable = MetaDisposable()
|
||||
private var animationFile: TelegramMediaFile?
|
||||
private var itemLayer: EmojiPagerContentComponent.View.ItemLayer?
|
||||
private var itemLayer: EmojiKeyboardItemLayer?
|
||||
private var useAnimationNode = false
|
||||
private var animationNode: AnimatedStickerNode?
|
||||
private let stickerFetchedDisposable = MetaDisposable()
|
||||
@ -101,7 +101,7 @@ public final class AvatarVideoNode: ASDisplayNode {
|
||||
let itemNativeFitSize = self.internalSize.width > 100.0 ? CGSize(width: 192.0, height: 192.0) : CGSize(width: 64.0, height: 64.0)
|
||||
|
||||
let animationData = EntityKeyboardAnimationData(file: animationFile)
|
||||
let itemLayer = EmojiPagerContentComponent.View.ItemLayer(
|
||||
let itemLayer = EmojiKeyboardItemLayer(
|
||||
item: EmojiPagerContentComponent.Item(
|
||||
animationData: animationData,
|
||||
content: .animation(animationData),
|
||||
|
@ -179,6 +179,9 @@ final class ChatSendMessageContextScreenComponent: Component {
|
||||
private var animateOutToEmpty: Bool = false
|
||||
|
||||
private var initializationDisplayLink: SharedDisplayLinkDriver.Link?
|
||||
private var updateSourcePositionsDisplayLink: SharedDisplayLinkDriver.Link?
|
||||
|
||||
private var stableSourceSendButtonFrame: CGRect?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.backgroundView = BlurredBackgroundView(color: .clear, enableBlur: true)
|
||||
@ -273,6 +276,21 @@ final class ChatSendMessageContextScreenComponent: Component {
|
||||
|
||||
let environment = environment[EnvironmentType.self].value
|
||||
|
||||
if let previousEnvironment = self.environment, previousEnvironment.inputHeight != 0.0, environment.inputHeight == 0.0 {
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
}
|
||||
let stableSourceSendButtonFrame = convertFrame(component.sourceSendButton.bounds, from: component.sourceSendButton.view, to: self)
|
||||
if self.stableSourceSendButtonFrame != stableSourceSendButtonFrame {
|
||||
self.stableSourceSendButtonFrame = stableSourceSendButtonFrame
|
||||
if !self.isUpdating {
|
||||
self.state?.updated(transition: .spring(duration: 0.35))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var transition = transition
|
||||
|
||||
var transitionIsImmediate = transition.animation.isImmediate
|
||||
@ -365,7 +383,19 @@ final class ChatSendMessageContextScreenComponent: Component {
|
||||
self.addSubview(sendButton)
|
||||
}
|
||||
|
||||
let sourceSendButtonFrame = convertFrame(component.sourceSendButton.bounds, from: component.sourceSendButton.view, to: self)
|
||||
let sourceSendButtonFrame: CGRect
|
||||
switch self.presentationAnimationState {
|
||||
case .animatedOut:
|
||||
sourceSendButtonFrame = convertFrame(component.sourceSendButton.bounds, from: component.sourceSendButton.view, to: self)
|
||||
self.stableSourceSendButtonFrame = sourceSendButtonFrame
|
||||
default:
|
||||
if let stableSourceSendButtonFrame = self.stableSourceSendButtonFrame {
|
||||
sourceSendButtonFrame = stableSourceSendButtonFrame
|
||||
} else {
|
||||
sourceSendButtonFrame = convertFrame(component.sourceSendButton.bounds, from: component.sourceSendButton.view, to: self)
|
||||
self.stableSourceSendButtonFrame = sourceSendButtonFrame
|
||||
}
|
||||
}
|
||||
|
||||
let sendButtonScale: CGFloat
|
||||
switch self.presentationAnimationState {
|
||||
@ -843,6 +873,8 @@ final class ChatSendMessageContextScreenComponent: Component {
|
||||
if !self.isUpdating {
|
||||
self.state?.updated(transition: .easeInOut(duration: 0.2))
|
||||
}
|
||||
|
||||
self.endEditing(true)
|
||||
})
|
||||
}))
|
||||
}
|
||||
@ -1014,6 +1046,13 @@ final class ChatSendMessageContextScreenComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
if let standaloneReactionAnimation, let targetView = messageItemView.effectIconView {
|
||||
let effectSize = CGSize(width: 380.0, height: 380.0)
|
||||
var effectFrame = effectSize.centered(around: targetView.convert(targetView.bounds.center, to: self))
|
||||
effectFrame.origin.x -= effectFrame.width * 0.3
|
||||
transition.setFrame(view: standaloneReactionAnimation.view, frame: effectFrame)
|
||||
}
|
||||
|
||||
if let reactionContextNode = self.reactionContextNode {
|
||||
let reactionContextY = environment.statusBarHeight
|
||||
let size = availableSize
|
||||
|
@ -122,8 +122,8 @@ final class StickerPackEmojisItemNode: GridItemNode {
|
||||
|
||||
private var boundsChangeTrackerLayer = SimpleLayer()
|
||||
|
||||
private var visibleItemLayers: [EmojiPagerContentComponent.View.ItemLayer.Key: EmojiPagerContentComponent.View.ItemLayer] = [:]
|
||||
private var visibleItemPlaceholderViews: [EmojiPagerContentComponent.View.ItemLayer.Key: EmojiPagerContentComponent.View.ItemPlaceholderView] = [:]
|
||||
private var visibleItemLayers: [EmojiKeyboardItemLayer.Key: EmojiKeyboardItemLayer] = [:]
|
||||
private var visibleItemPlaceholderViews: [EmojiKeyboardItemLayer.Key: EmojiPagerContentComponent.View.ItemPlaceholderView] = [:]
|
||||
|
||||
private let containerNode: ASDisplayNode
|
||||
private let titleNode: ImmediateTextNode
|
||||
@ -195,7 +195,7 @@ final class StickerPackEmojisItemNode: GridItemNode {
|
||||
|
||||
func targetItem(at point: CGPoint) -> (TelegramMediaFile, CALayer)? {
|
||||
if let (item, _) = self.item(atPoint: point), let file = item.itemFile {
|
||||
let itemId = EmojiPagerContentComponent.View.ItemLayer.Key(
|
||||
let itemId = EmojiKeyboardItemLayer.Key(
|
||||
groupId: 0,
|
||||
itemId: .animation(.file(file.fileId))
|
||||
)
|
||||
@ -237,7 +237,7 @@ final class StickerPackEmojisItemNode: GridItemNode {
|
||||
private func item(atPoint point: CGPoint, extendedHitRange: Bool = false) -> (EmojiPagerContentComponent.Item, CGRect)? {
|
||||
let localPoint = point
|
||||
|
||||
var closestItem: (key: EmojiPagerContentComponent.View.ItemLayer.Key, distance: CGFloat)?
|
||||
var closestItem: (key: EmojiKeyboardItemLayer.Key, distance: CGFloat)?
|
||||
|
||||
for (key, itemLayer) in self.visibleItemLayers {
|
||||
if extendedHitRange {
|
||||
@ -308,7 +308,7 @@ final class StickerPackEmojisItemNode: GridItemNode {
|
||||
let animationRenderer = item.animationRenderer
|
||||
let theme = item.theme
|
||||
let items = item.items
|
||||
var validIds = Set<EmojiPagerContentComponent.View.ItemLayer.Key>()
|
||||
var validIds = Set<EmojiKeyboardItemLayer.Key>()
|
||||
|
||||
let itemLayout: ItemLayout
|
||||
if let current = self.itemLayout, current.width == self.size.width && current.itemsCount == items.count {
|
||||
@ -322,7 +322,7 @@ final class StickerPackEmojisItemNode: GridItemNode {
|
||||
|
||||
for index in 0 ..< items.count {
|
||||
let item = items[index]
|
||||
let itemId = EmojiPagerContentComponent.View.ItemLayer.Key(
|
||||
let itemId = EmojiKeyboardItemLayer.Key(
|
||||
groupId: 0,
|
||||
itemId: .animation(.file(item.file.fileId))
|
||||
)
|
||||
@ -334,7 +334,7 @@ final class StickerPackEmojisItemNode: GridItemNode {
|
||||
|
||||
var updateItemLayerPlaceholder = false
|
||||
var itemTransition = transition
|
||||
let itemLayer: EmojiPagerContentComponent.View.ItemLayer
|
||||
let itemLayer: EmojiKeyboardItemLayer
|
||||
if let current = self.visibleItemLayers[itemId] {
|
||||
itemLayer = current
|
||||
} else {
|
||||
@ -342,7 +342,7 @@ final class StickerPackEmojisItemNode: GridItemNode {
|
||||
itemTransition = .immediate
|
||||
|
||||
let animationData = EntityKeyboardAnimationData(file: item.file)
|
||||
itemLayer = EmojiPagerContentComponent.View.ItemLayer(
|
||||
itemLayer = EmojiKeyboardItemLayer(
|
||||
item: EmojiPagerContentComponent.Item(
|
||||
animationData: animationData,
|
||||
content: .animation(animationData),
|
||||
|
@ -638,6 +638,8 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
||||
private var replyRecognizer: ChatSwipeToReplyRecognizer?
|
||||
private var currentSwipeAction: ChatControllerInteractionSwipeAction?
|
||||
|
||||
private var fetchEffectDisposable: Disposable?
|
||||
|
||||
//private let debugNode: ASDisplayNode
|
||||
|
||||
override public var visibility: ListViewItemNodeVisibility {
|
||||
@ -839,6 +841,10 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.fetchEffectDisposable?.dispose()
|
||||
}
|
||||
|
||||
override public func cancelInsertionAnimations() {
|
||||
self.shadowNode.layer.removeAllAnimations()
|
||||
|
||||
@ -5877,6 +5883,9 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
||||
private var additionalAnimationNodes: [ChatMessageTransitionNode.DecorationItemNode] = []
|
||||
|
||||
private func playPremiumStickerAnimation(effect: AvailableMessageEffects.MessageEffect, force: Bool) {
|
||||
guard let item = self.item else {
|
||||
return
|
||||
}
|
||||
if self.playedPremiumStickerAnimation && !force {
|
||||
return
|
||||
}
|
||||
@ -5884,10 +5893,16 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
||||
|
||||
if let effectAnimation = effect.effectAnimation {
|
||||
self.playEffectAnimation(resource: effectAnimation.resource, isStickerEffect: true)
|
||||
if self.fetchEffectDisposable == nil {
|
||||
self.fetchEffectDisposable = freeMediaFileResourceInteractiveFetched(account: item.context.account, userLocation: .other, fileReference: .standalone(media: effectAnimation), resource: effectAnimation.resource).startStrict()
|
||||
}
|
||||
} else {
|
||||
let effectSticker = effect.effectSticker
|
||||
if let effectFile = effectSticker.videoThumbnails.first {
|
||||
self.playEffectAnimation(resource: effectFile.resource, isStickerEffect: true)
|
||||
if self.fetchEffectDisposable == nil {
|
||||
self.fetchEffectDisposable = freeMediaFileResourceInteractiveFetched(account: item.context.account, userLocation: .other, fileReference: .standalone(media: effectSticker), resource: effectFile.resource).startStrict()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,477 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import ComponentFlow
|
||||
import MultiAnimationRenderer
|
||||
import AnimationCache
|
||||
import SwiftSignalKit
|
||||
import TelegramCore
|
||||
import AccountContext
|
||||
import TelegramPresentationData
|
||||
import EmojiTextAttachmentView
|
||||
import EmojiStatusComponent
|
||||
|
||||
final class EmojiKeyboardCloneItemLayer: SimpleLayer {
|
||||
}
|
||||
|
||||
public final class EmojiKeyboardItemLayer: MultiAnimationRenderTarget {
|
||||
public struct Key: Hashable {
|
||||
var groupId: AnyHashable
|
||||
var itemId: EmojiPagerContentComponent.ItemContent.Id
|
||||
|
||||
public init(
|
||||
groupId: AnyHashable,
|
||||
itemId: EmojiPagerContentComponent.ItemContent.Id
|
||||
) {
|
||||
self.groupId = groupId
|
||||
self.itemId = itemId
|
||||
}
|
||||
}
|
||||
|
||||
enum Badge: Equatable {
|
||||
case premium
|
||||
case locked
|
||||
case featured
|
||||
case text(String)
|
||||
case customFile(TelegramMediaFile)
|
||||
}
|
||||
|
||||
public let item: EmojiPagerContentComponent.Item
|
||||
private let context: AccountContext
|
||||
|
||||
private var content: EmojiPagerContentComponent.ItemContent
|
||||
private var theme: PresentationTheme?
|
||||
|
||||
private let placeholderColor: UIColor
|
||||
let pixelSize: CGSize
|
||||
let pointSize: CGSize
|
||||
private let size: CGSize
|
||||
private var disposable: Disposable?
|
||||
private var fetchDisposable: Disposable?
|
||||
private var premiumBadgeView: PremiumBadgeView?
|
||||
|
||||
private var iconLayer: SimpleLayer?
|
||||
private var tintIconLayer: SimpleLayer?
|
||||
|
||||
private(set) var tintContentLayer: SimpleLayer?
|
||||
|
||||
private var badge: Badge?
|
||||
private var validSize: CGSize?
|
||||
|
||||
private var isInHierarchyValue: Bool = false
|
||||
public var isVisibleForAnimations: Bool = false {
|
||||
didSet {
|
||||
if self.isVisibleForAnimations != oldValue {
|
||||
self.updatePlayback()
|
||||
}
|
||||
}
|
||||
}
|
||||
public private(set) var displayPlaceholder: Bool = false
|
||||
public let onUpdateDisplayPlaceholder: (Bool, Double) -> Void
|
||||
|
||||
weak var cloneLayer: EmojiKeyboardCloneItemLayer? {
|
||||
didSet {
|
||||
if let cloneLayer = self.cloneLayer {
|
||||
cloneLayer.contents = self.contents
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override public var contents: Any? {
|
||||
didSet {
|
||||
self.onContentsUpdate()
|
||||
if let cloneLayer = self.cloneLayer {
|
||||
cloneLayer.contents = self.contents
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override public var position: CGPoint {
|
||||
get {
|
||||
return super.position
|
||||
} set(value) {
|
||||
if let mirrorLayer = self.tintContentLayer {
|
||||
mirrorLayer.position = value
|
||||
}
|
||||
super.position = value
|
||||
}
|
||||
}
|
||||
|
||||
override public var bounds: CGRect {
|
||||
get {
|
||||
return super.bounds
|
||||
} set(value) {
|
||||
if let mirrorLayer = self.tintContentLayer {
|
||||
mirrorLayer.bounds = value
|
||||
}
|
||||
super.bounds = value
|
||||
}
|
||||
}
|
||||
|
||||
override public func add(_ animation: CAAnimation, forKey key: String?) {
|
||||
if let mirrorLayer = self.tintContentLayer {
|
||||
mirrorLayer.add(animation, forKey: key)
|
||||
}
|
||||
|
||||
super.add(animation, forKey: key)
|
||||
}
|
||||
|
||||
override public func removeAllAnimations() {
|
||||
if let mirrorLayer = self.tintContentLayer {
|
||||
mirrorLayer.removeAllAnimations()
|
||||
}
|
||||
|
||||
super.removeAllAnimations()
|
||||
}
|
||||
|
||||
override public func removeAnimation(forKey: String) {
|
||||
if let mirrorLayer = self.tintContentLayer {
|
||||
mirrorLayer.removeAnimation(forKey: forKey)
|
||||
}
|
||||
|
||||
super.removeAnimation(forKey: forKey)
|
||||
}
|
||||
|
||||
public var onContentsUpdate: () -> Void = {}
|
||||
public var onLoop: () -> Void = {}
|
||||
|
||||
public init(
|
||||
item: EmojiPagerContentComponent.Item,
|
||||
context: AccountContext,
|
||||
attemptSynchronousLoad: Bool,
|
||||
content: EmojiPagerContentComponent.ItemContent,
|
||||
cache: AnimationCache,
|
||||
renderer: MultiAnimationRenderer,
|
||||
placeholderColor: UIColor,
|
||||
blurredBadgeColor: UIColor,
|
||||
accentIconColor: UIColor,
|
||||
pointSize: CGSize,
|
||||
onUpdateDisplayPlaceholder: @escaping (Bool, Double) -> Void
|
||||
) {
|
||||
self.item = item
|
||||
self.context = context
|
||||
self.content = content
|
||||
self.placeholderColor = placeholderColor
|
||||
self.onUpdateDisplayPlaceholder = onUpdateDisplayPlaceholder
|
||||
|
||||
let scale = min(2.0, UIScreenScale)
|
||||
let pixelSize = CGSize(width: pointSize.width * scale, height: pointSize.height * scale)
|
||||
self.pixelSize = pixelSize
|
||||
self.pointSize = pointSize
|
||||
self.size = CGSize(width: pixelSize.width / scale, height: pixelSize.height / scale)
|
||||
|
||||
super.init()
|
||||
|
||||
switch content {
|
||||
case let .animation(animationData):
|
||||
let loadAnimation: () -> Void = { [weak self] in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
|
||||
strongSelf.disposable = renderer.add(target: strongSelf, cache: cache, itemId: animationData.resource.resource.id.stringRepresentation, unique: false, size: pixelSize, fetch: animationCacheFetchFile(context: context, userLocation: .other, userContentType: .sticker, resource: animationData.resource, type: animationData.type.animationCacheAnimationType, keyframeOnly: pixelSize.width >= 120.0, customColor: animationData.isTemplate ? .white : nil))
|
||||
}
|
||||
|
||||
if attemptSynchronousLoad {
|
||||
if !renderer.loadFirstFrameSynchronously(target: self, cache: cache, itemId: animationData.resource.resource.id.stringRepresentation, size: pixelSize) {
|
||||
self.updateDisplayPlaceholder(displayPlaceholder: true)
|
||||
|
||||
self.fetchDisposable = renderer.loadFirstFrame(target: self, cache: cache, itemId: animationData.resource.resource.id.stringRepresentation, size: pixelSize, fetch: animationCacheFetchFile(context: context, userLocation: .other, userContentType: .sticker, resource: animationData.resource, type: animationData.type.animationCacheAnimationType, keyframeOnly: true, customColor: animationData.isTemplate ? .white : nil), completion: { [weak self] success, isFinal in
|
||||
if !isFinal {
|
||||
if !success {
|
||||
Queue.mainQueue().async {
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
|
||||
strongSelf.updateDisplayPlaceholder(displayPlaceholder: true)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
Queue.mainQueue().async {
|
||||
loadAnimation()
|
||||
|
||||
if !success {
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
|
||||
strongSelf.updateDisplayPlaceholder(displayPlaceholder: true)
|
||||
}
|
||||
}
|
||||
})
|
||||
} else {
|
||||
loadAnimation()
|
||||
}
|
||||
} else {
|
||||
self.fetchDisposable = renderer.loadFirstFrame(target: self, cache: cache, itemId: animationData.resource.resource.id.stringRepresentation, size: pixelSize, fetch: animationCacheFetchFile(context: context, userLocation: .other, userContentType: .sticker, resource: animationData.resource, type: animationData.type.animationCacheAnimationType, keyframeOnly: true, customColor: animationData.isTemplate ? .white : nil), completion: { [weak self] success, isFinal in
|
||||
if !isFinal {
|
||||
if !success {
|
||||
Queue.mainQueue().async {
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
|
||||
strongSelf.updateDisplayPlaceholder(displayPlaceholder: true)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
Queue.mainQueue().async {
|
||||
loadAnimation()
|
||||
|
||||
if !success {
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
|
||||
strongSelf.updateDisplayPlaceholder(displayPlaceholder: true)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
case let .staticEmoji(staticEmoji):
|
||||
let image = generateImage(pointSize, opaque: false, scale: min(UIScreenScale, 3.0), rotatedContext: { size, context in
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
|
||||
let preScaleFactor: CGFloat = 1.0
|
||||
let scaledSize = CGSize(width: floor(size.width * preScaleFactor), height: floor(size.height * preScaleFactor))
|
||||
let scaleFactor = scaledSize.width / size.width
|
||||
|
||||
context.scaleBy(x: 1.0 / scaleFactor, y: 1.0 / scaleFactor)
|
||||
|
||||
let string = NSAttributedString(string: staticEmoji, font: Font.regular(floor(32.0 * scaleFactor)), textColor: .black)
|
||||
let boundingRect = string.boundingRect(with: scaledSize, options: .usesLineFragmentOrigin, context: nil)
|
||||
UIGraphicsPushContext(context)
|
||||
string.draw(at: CGPoint(x: floorToScreenPixels((scaledSize.width - boundingRect.width) / 2.0 + boundingRect.minX), y: floorToScreenPixels((scaledSize.height - boundingRect.height) / 2.0 + boundingRect.minY)))
|
||||
UIGraphicsPopContext()
|
||||
})
|
||||
self.contents = image?.cgImage
|
||||
case let .icon(icon):
|
||||
let image = generateImage(pointSize, opaque: false, scale: min(UIScreenScale, 3.0), rotatedContext: { size, context in
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
|
||||
UIGraphicsPushContext(context)
|
||||
|
||||
switch icon {
|
||||
case .premiumStar:
|
||||
if let image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Media/EntityInputPremiumIcon"), color: accentIconColor) {
|
||||
let imageSize = image.size.aspectFitted(CGSize(width: size.width - 6.0, height: size.height - 6.0))
|
||||
image.draw(in: CGRect(origin: CGPoint(x: floor((size.width - imageSize.width) / 2.0), y: floor((size.height - imageSize.height) / 2.0)), size: imageSize))
|
||||
}
|
||||
case let .topic(title, color):
|
||||
let colors = topicIconColors(for: color)
|
||||
if let image = generateTopicIcon(backgroundColors: colors.0.map { UIColor(rgb: $0) }, strokeColors: colors.1.map { UIColor(rgb: $0) }, title: title) {
|
||||
let imageSize = image.size//.aspectFitted(CGSize(width: size.width - 6.0, height: size.height - 6.0))
|
||||
image.draw(in: CGRect(origin: CGPoint(x: floor((size.width - imageSize.width) / 2.0), y: floor((size.height - imageSize.height) / 2.0)), size: imageSize))
|
||||
}
|
||||
case .stop:
|
||||
if let image = generateTintedImage(image: UIImage(bundleImageName: "Premium/NoIcon"), color: .white) {
|
||||
let imageSize = image.size.aspectFitted(CGSize(width: size.width - 6.0, height: size.height - 6.0))
|
||||
image.draw(in: CGRect(origin: CGPoint(x: floor((size.width - imageSize.width) / 2.0), y: floor((size.height - imageSize.height) / 2.0)), size: imageSize))
|
||||
}
|
||||
case .add:
|
||||
break
|
||||
}
|
||||
|
||||
UIGraphicsPopContext()
|
||||
})?.withRenderingMode(icon == .stop ? .alwaysTemplate : .alwaysOriginal)
|
||||
self.contents = image?.cgImage
|
||||
}
|
||||
|
||||
if case .icon(.add) = content {
|
||||
let tintContentLayer = SimpleLayer()
|
||||
self.tintContentLayer = tintContentLayer
|
||||
|
||||
let iconLayer = SimpleLayer()
|
||||
self.iconLayer = iconLayer
|
||||
self.addSublayer(iconLayer)
|
||||
|
||||
let tintIconLayer = SimpleLayer()
|
||||
self.tintIconLayer = tintIconLayer
|
||||
tintContentLayer.addSublayer(tintIconLayer)
|
||||
}
|
||||
}
|
||||
|
||||
override public init(layer: Any) {
|
||||
guard let layer = layer as? EmojiKeyboardItemLayer else {
|
||||
preconditionFailure()
|
||||
}
|
||||
|
||||
self.context = layer.context
|
||||
self.item = layer.item
|
||||
|
||||
self.content = layer.content
|
||||
self.placeholderColor = layer.placeholderColor
|
||||
self.size = layer.size
|
||||
self.pixelSize = layer.pixelSize
|
||||
self.pointSize = layer.pointSize
|
||||
|
||||
self.onUpdateDisplayPlaceholder = { _, _ in }
|
||||
|
||||
super.init(layer: layer)
|
||||
}
|
||||
|
||||
required public init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.disposable?.dispose()
|
||||
self.fetchDisposable?.dispose()
|
||||
}
|
||||
|
||||
public override func action(forKey event: String) -> CAAction? {
|
||||
if event == kCAOnOrderIn {
|
||||
self.isInHierarchyValue = true
|
||||
} else if event == kCAOnOrderOut {
|
||||
self.isInHierarchyValue = false
|
||||
}
|
||||
self.updatePlayback()
|
||||
return nullAction
|
||||
}
|
||||
|
||||
func update(
|
||||
content: EmojiPagerContentComponent.ItemContent,
|
||||
theme: PresentationTheme
|
||||
) {
|
||||
var themeUpdated = false
|
||||
if self.theme !== theme {
|
||||
self.theme = theme
|
||||
themeUpdated = true
|
||||
}
|
||||
var contentUpdated = false
|
||||
if self.content != content {
|
||||
self.content = content
|
||||
contentUpdated = true
|
||||
}
|
||||
|
||||
if themeUpdated || contentUpdated {
|
||||
if case let .icon(icon) = content, case let .topic(title, color) = icon {
|
||||
let image = generateImage(self.size, opaque: false, scale: min(UIScreenScale, 3.0), rotatedContext: { size, context in
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
|
||||
UIGraphicsPushContext(context)
|
||||
|
||||
let colors = topicIconColors(for: color)
|
||||
if let image = generateTopicIcon(backgroundColors: colors.0.map { UIColor(rgb: $0) }, strokeColors: colors.1.map { UIColor(rgb: $0) }, title: title) {
|
||||
let imageSize = image.size
|
||||
image.draw(in: CGRect(origin: CGPoint(x: floor((size.width - imageSize.width) / 2.0), y: floor((size.height - imageSize.height) / 2.0)), size: imageSize))
|
||||
}
|
||||
|
||||
UIGraphicsPopContext()
|
||||
})
|
||||
self.contents = image?.cgImage
|
||||
} else if case .icon(.add) = content {
|
||||
guard let iconLayer = self.iconLayer, let tintIconLayer = self.tintIconLayer else {
|
||||
return
|
||||
}
|
||||
func generateIcon(color: UIColor) -> UIImage? {
|
||||
return generateImage(self.pointSize, opaque: false, scale: min(UIScreenScale, 3.0), rotatedContext: { size, context in
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
|
||||
UIGraphicsPushContext(context)
|
||||
|
||||
context.setFillColor(color.withMultipliedAlpha(0.2).cgColor)
|
||||
context.fillEllipse(in: CGRect(origin: .zero, size: size).insetBy(dx: 8.0, dy: 8.0))
|
||||
context.setFillColor(color.cgColor)
|
||||
|
||||
let plusSize = CGSize(width: 4.5, height: 31.5)
|
||||
context.addPath(UIBezierPath(roundedRect: CGRect(x: floorToScreenPixels((size.width - plusSize.width) / 2.0), y: floorToScreenPixels((size.height - plusSize.height) / 2.0), width: plusSize.width, height: plusSize.height), cornerRadius: plusSize.width / 2.0).cgPath)
|
||||
context.addPath(UIBezierPath(roundedRect: CGRect(x: floorToScreenPixels((size.width - plusSize.height) / 2.0), y: floorToScreenPixels((size.height - plusSize.width) / 2.0), width: plusSize.height, height: plusSize.width), cornerRadius: plusSize.width / 2.0).cgPath)
|
||||
context.fillPath()
|
||||
|
||||
UIGraphicsPopContext()
|
||||
})
|
||||
}
|
||||
|
||||
let needsVibrancy = !theme.overallDarkAppearance
|
||||
let color = theme.chat.inputMediaPanel.panelContentVibrantOverlayColor
|
||||
|
||||
iconLayer.contents = generateIcon(color: color)?.cgImage
|
||||
tintIconLayer.contents = generateIcon(color: .white)?.cgImage
|
||||
|
||||
tintIconLayer.isHidden = !needsVibrancy
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func update(
|
||||
transition: Transition,
|
||||
size: CGSize,
|
||||
badge: Badge?,
|
||||
blurredBadgeColor: UIColor,
|
||||
blurredBadgeBackgroundColor: UIColor
|
||||
) {
|
||||
if self.badge != badge || self.validSize != size {
|
||||
self.badge = badge
|
||||
self.validSize = size
|
||||
|
||||
if let iconLayer = self.iconLayer, let tintIconLayer = self.tintIconLayer {
|
||||
transition.setFrame(layer: iconLayer, frame: CGRect(origin: .zero, size: size))
|
||||
transition.setFrame(layer: tintIconLayer, frame: CGRect(origin: .zero, size: size))
|
||||
}
|
||||
|
||||
if let badge = badge {
|
||||
var badgeTransition = transition
|
||||
let premiumBadgeView: PremiumBadgeView
|
||||
if let current = self.premiumBadgeView {
|
||||
premiumBadgeView = current
|
||||
} else {
|
||||
badgeTransition = .immediate
|
||||
premiumBadgeView = PremiumBadgeView(context: self.context)
|
||||
self.premiumBadgeView = premiumBadgeView
|
||||
self.addSublayer(premiumBadgeView.layer)
|
||||
}
|
||||
|
||||
let badgeDiameter = min(16.0, floor(size.height * 0.5))
|
||||
let badgeSize = CGSize(width: badgeDiameter, height: badgeDiameter)
|
||||
badgeTransition.setFrame(view: premiumBadgeView, frame: CGRect(origin: CGPoint(x: size.width - badgeSize.width, y: size.height - badgeSize.height), size: badgeSize))
|
||||
premiumBadgeView.update(transition: badgeTransition, badge: badge, backgroundColor: blurredBadgeColor, size: badgeSize)
|
||||
|
||||
self.blurredRepresentationBackgroundColor = blurredBadgeBackgroundColor
|
||||
self.blurredRepresentationTarget = premiumBadgeView.contentLayer
|
||||
} else {
|
||||
if let premiumBadgeView = self.premiumBadgeView {
|
||||
self.premiumBadgeView = nil
|
||||
premiumBadgeView.removeFromSuperview()
|
||||
|
||||
self.blurredRepresentationBackgroundColor = nil
|
||||
self.blurredRepresentationTarget = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func updatePlayback() {
|
||||
let shouldBePlaying = self.isInHierarchyValue && self.isVisibleForAnimations
|
||||
|
||||
self.shouldBeAnimating = shouldBePlaying
|
||||
}
|
||||
|
||||
public override func updateDisplayPlaceholder(displayPlaceholder: Bool) {
|
||||
if self.displayPlaceholder == displayPlaceholder {
|
||||
return
|
||||
}
|
||||
|
||||
self.displayPlaceholder = displayPlaceholder
|
||||
self.onUpdateDisplayPlaceholder(displayPlaceholder, 0.0)
|
||||
}
|
||||
|
||||
public override func transitionToContents(_ contents: AnyObject, didLoop: Bool) {
|
||||
self.contents = contents
|
||||
|
||||
if self.displayPlaceholder {
|
||||
self.displayPlaceholder = false
|
||||
self.onUpdateDisplayPlaceholder(false, 0.2)
|
||||
self.animateAlpha(from: 0.0, to: 1.0, duration: 0.18)
|
||||
}
|
||||
|
||||
if didLoop {
|
||||
self.onLoop()
|
||||
}
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,607 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import ComponentFlow
|
||||
import TelegramCore
|
||||
import TelegramPresentationData
|
||||
import AccountContext
|
||||
import SwiftSignalKit
|
||||
|
||||
public final class EmojiSearchHeaderView: UIView, UITextFieldDelegate {
|
||||
private final class EmojiSearchTextField: UITextField {
|
||||
override func textRect(forBounds bounds: CGRect) -> CGRect {
|
||||
return bounds.integral
|
||||
}
|
||||
}
|
||||
|
||||
private struct Params: Equatable {
|
||||
var context: AccountContext
|
||||
var theme: PresentationTheme
|
||||
var forceNeedsVibrancy: Bool
|
||||
var strings: PresentationStrings
|
||||
var text: String
|
||||
var useOpaqueTheme: Bool
|
||||
var isActive: Bool
|
||||
var hasPresetSearch: Bool
|
||||
var textInputState: EmojiSearchSearchBarComponent.TextInputState
|
||||
var searchState: EmojiPagerContentComponent.SearchState
|
||||
var size: CGSize
|
||||
var canFocus: Bool
|
||||
var searchCategories: EmojiSearchCategories?
|
||||
|
||||
static func ==(lhs: Params, rhs: Params) -> Bool {
|
||||
if lhs.context !== rhs.context {
|
||||
return false
|
||||
}
|
||||
if lhs.theme !== rhs.theme {
|
||||
return false
|
||||
}
|
||||
if lhs.forceNeedsVibrancy != rhs.forceNeedsVibrancy {
|
||||
return false
|
||||
}
|
||||
if lhs.strings !== rhs.strings {
|
||||
return false
|
||||
}
|
||||
if lhs.text != rhs.text {
|
||||
return false
|
||||
}
|
||||
if lhs.useOpaqueTheme != rhs.useOpaqueTheme {
|
||||
return false
|
||||
}
|
||||
if lhs.isActive != rhs.isActive {
|
||||
return false
|
||||
}
|
||||
if lhs.hasPresetSearch != rhs.hasPresetSearch {
|
||||
return false
|
||||
}
|
||||
if lhs.textInputState != rhs.textInputState {
|
||||
return false
|
||||
}
|
||||
if lhs.searchState != rhs.searchState {
|
||||
return false
|
||||
}
|
||||
if lhs.size != rhs.size {
|
||||
return false
|
||||
}
|
||||
if lhs.canFocus != rhs.canFocus {
|
||||
return false
|
||||
}
|
||||
if lhs.searchCategories != rhs.searchCategories {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
override public static var layerClass: AnyClass {
|
||||
return PassthroughLayer.self
|
||||
}
|
||||
|
||||
private let activated: (Bool) -> Void
|
||||
private let deactivated: (Bool) -> Void
|
||||
private let updateQuery: (EmojiPagerContentComponent.SearchQuery?) -> Void
|
||||
|
||||
let tintContainerView: UIView
|
||||
|
||||
private let backgroundLayer: SimpleLayer
|
||||
private let tintBackgroundLayer: SimpleLayer
|
||||
|
||||
private let statusIcon = ComponentView<Empty>()
|
||||
|
||||
private let clearIconView: UIImageView
|
||||
private let clearIconTintView: UIImageView
|
||||
private let clearIconButton: HighlightTrackingButton
|
||||
|
||||
private let cancelButtonTintTitle: ComponentView<Empty>
|
||||
private let cancelButtonTitle: ComponentView<Empty>
|
||||
private let cancelButton: HighlightTrackingButton
|
||||
|
||||
private var placeholderContent = ComponentView<Empty>()
|
||||
|
||||
private var textFrame: CGRect?
|
||||
private var textField: EmojiSearchTextField?
|
||||
|
||||
private var tapRecognizer: UITapGestureRecognizer?
|
||||
private(set) var currentPresetSearchTerm: EmojiSearchCategories.Group?
|
||||
|
||||
private var params: Params?
|
||||
|
||||
public var wantsDisplayBelowKeyboard: Bool {
|
||||
return self.textField != nil
|
||||
}
|
||||
|
||||
init(activated: @escaping (Bool) -> Void, deactivated: @escaping (Bool) -> Void, updateQuery: @escaping (EmojiPagerContentComponent.SearchQuery?) -> Void) {
|
||||
self.activated = activated
|
||||
self.deactivated = deactivated
|
||||
self.updateQuery = updateQuery
|
||||
|
||||
self.tintContainerView = UIView()
|
||||
|
||||
self.backgroundLayer = SimpleLayer()
|
||||
self.tintBackgroundLayer = SimpleLayer()
|
||||
|
||||
self.clearIconView = UIImageView()
|
||||
self.clearIconTintView = UIImageView()
|
||||
self.clearIconButton = HighlightableButton()
|
||||
self.clearIconView.isHidden = true
|
||||
self.clearIconTintView.isHidden = true
|
||||
self.clearIconButton.isHidden = true
|
||||
|
||||
self.cancelButtonTintTitle = ComponentView()
|
||||
self.cancelButtonTitle = ComponentView()
|
||||
self.cancelButton = HighlightTrackingButton()
|
||||
|
||||
super.init(frame: CGRect())
|
||||
|
||||
self.layer.addSublayer(self.backgroundLayer)
|
||||
self.tintContainerView.layer.addSublayer(self.tintBackgroundLayer)
|
||||
|
||||
self.addSubview(self.clearIconView)
|
||||
self.tintContainerView.addSubview(self.clearIconTintView)
|
||||
self.addSubview(self.clearIconButton)
|
||||
|
||||
self.addSubview(self.cancelButton)
|
||||
self.clipsToBounds = true
|
||||
|
||||
(self.layer as? PassthroughLayer)?.mirrorLayer = self.tintContainerView.layer
|
||||
|
||||
let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))
|
||||
self.tapRecognizer = tapRecognizer
|
||||
self.addGestureRecognizer(tapRecognizer)
|
||||
|
||||
self.cancelButton.highligthedChanged = { [weak self] highlighted in
|
||||
if let strongSelf = self {
|
||||
if highlighted {
|
||||
if let cancelButtonTitleView = strongSelf.cancelButtonTitle.view {
|
||||
cancelButtonTitleView.layer.removeAnimation(forKey: "opacity")
|
||||
cancelButtonTitleView.alpha = 0.4
|
||||
}
|
||||
if let cancelButtonTintTitleView = strongSelf.cancelButtonTintTitle.view {
|
||||
cancelButtonTintTitleView.layer.removeAnimation(forKey: "opacity")
|
||||
cancelButtonTintTitleView.alpha = 0.4
|
||||
}
|
||||
} else {
|
||||
if let cancelButtonTitleView = strongSelf.cancelButtonTitle.view {
|
||||
cancelButtonTitleView.alpha = 1.0
|
||||
cancelButtonTitleView.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
|
||||
}
|
||||
if let cancelButtonTintTitleView = strongSelf.cancelButtonTintTitle.view {
|
||||
cancelButtonTintTitleView.alpha = 1.0
|
||||
cancelButtonTintTitleView.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
self.cancelButton.addTarget(self, action: #selector(self.cancelPressed), for: .touchUpInside)
|
||||
|
||||
self.clearIconButton.highligthedChanged = { [weak self] highlighted in
|
||||
if let strongSelf = self {
|
||||
if highlighted {
|
||||
strongSelf.clearIconView.layer.removeAnimation(forKey: "opacity")
|
||||
strongSelf.clearIconView.alpha = 0.4
|
||||
strongSelf.clearIconTintView.layer.removeAnimation(forKey: "opacity")
|
||||
strongSelf.clearIconTintView.alpha = 0.4
|
||||
} else {
|
||||
strongSelf.clearIconView.alpha = 1.0
|
||||
strongSelf.clearIconView.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
|
||||
strongSelf.clearIconTintView.alpha = 1.0
|
||||
strongSelf.clearIconTintView.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
|
||||
}
|
||||
}
|
||||
}
|
||||
self.clearIconButton.addTarget(self, action: #selector(self.clearPressed), for: .touchUpInside)
|
||||
}
|
||||
|
||||
required public init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
@objc private func tapGesture(_ recognizer: UITapGestureRecognizer) {
|
||||
if case .ended = recognizer.state {
|
||||
let location = recognizer.location(in: self)
|
||||
if let view = self.statusIcon.view, view.frame.contains(location), self.currentPresetSearchTerm != nil {
|
||||
self.clearCategorySearch()
|
||||
} else {
|
||||
self.activateTextInput()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func clearCategorySearch() {
|
||||
if let placeholderContentView = self.placeholderContent.view as? EmojiSearchSearchBarComponent.View {
|
||||
placeholderContentView.clearSelection(dispatchEvent : true)
|
||||
}
|
||||
}
|
||||
|
||||
private func activateTextInput() {
|
||||
guard let params = self.params else {
|
||||
return
|
||||
}
|
||||
if self.textField == nil, let textFrame = self.textFrame, params.canFocus == true {
|
||||
let backgroundFrame = self.backgroundLayer.frame
|
||||
let textFieldFrame = CGRect(origin: CGPoint(x: textFrame.minX, y: backgroundFrame.minY), size: CGSize(width: backgroundFrame.maxX - textFrame.minX, height: backgroundFrame.height))
|
||||
|
||||
let textField = EmojiSearchTextField(frame: textFieldFrame)
|
||||
textField.keyboardAppearance = params.theme.rootController.keyboardColor.keyboardAppearance
|
||||
textField.autocorrectionType = .no
|
||||
textField.returnKeyType = .search
|
||||
self.textField = textField
|
||||
self.insertSubview(textField, belowSubview: self.clearIconView)
|
||||
textField.delegate = self
|
||||
textField.addTarget(self, action: #selector(self.textFieldChanged(_:)), for: .editingChanged)
|
||||
}
|
||||
|
||||
if params.canFocus {
|
||||
self.currentPresetSearchTerm = nil
|
||||
if let placeholderContentView = self.placeholderContent.view as? EmojiSearchSearchBarComponent.View {
|
||||
placeholderContentView.clearSelection(dispatchEvent: false)
|
||||
}
|
||||
}
|
||||
|
||||
self.activated(true)
|
||||
|
||||
self.textField?.becomeFirstResponder()
|
||||
}
|
||||
|
||||
@objc private func cancelPressed() {
|
||||
self.currentPresetSearchTerm = nil
|
||||
self.updateQuery(nil)
|
||||
|
||||
self.clearIconView.isHidden = true
|
||||
self.clearIconTintView.isHidden = true
|
||||
self.clearIconButton.isHidden = true
|
||||
|
||||
let textField = self.textField
|
||||
self.textField = nil
|
||||
|
||||
self.deactivated(textField?.isFirstResponder ?? false)
|
||||
|
||||
if let textField {
|
||||
textField.resignFirstResponder()
|
||||
textField.removeFromSuperview()
|
||||
}
|
||||
|
||||
/*self.tintTextView.view?.isHidden = false
|
||||
self.textView.view?.isHidden = false*/
|
||||
}
|
||||
|
||||
@objc private func clearPressed() {
|
||||
self.currentPresetSearchTerm = nil
|
||||
self.updateQuery(nil)
|
||||
self.textField?.text = ""
|
||||
|
||||
self.clearIconView.isHidden = true
|
||||
self.clearIconTintView.isHidden = true
|
||||
self.clearIconButton.isHidden = true
|
||||
|
||||
/*self.tintTextView.view?.isHidden = false
|
||||
self.textView.view?.isHidden = false*/
|
||||
}
|
||||
|
||||
var isActive: Bool {
|
||||
return self.textField?.isFirstResponder ?? false
|
||||
}
|
||||
|
||||
func deactivate() {
|
||||
if let text = self.textField?.text, !text.isEmpty {
|
||||
self.textField?.endEditing(true)
|
||||
} else {
|
||||
self.cancelPressed()
|
||||
}
|
||||
}
|
||||
|
||||
public func textFieldDidBeginEditing(_ textField: UITextField) {
|
||||
}
|
||||
|
||||
public func textFieldDidEndEditing(_ textField: UITextField) {
|
||||
}
|
||||
|
||||
public func textFieldShouldReturn(_ textField: UITextField) -> Bool {
|
||||
textField.endEditing(true)
|
||||
return false
|
||||
}
|
||||
|
||||
@objc private func textFieldChanged(_ textField: UITextField) {
|
||||
self.update(transition: .immediate)
|
||||
|
||||
let text = textField.text ?? ""
|
||||
|
||||
var inputLanguage = textField.textInputMode?.primaryLanguage ?? "en"
|
||||
if let range = inputLanguage.range(of: "-") {
|
||||
inputLanguage = String(inputLanguage[inputLanguage.startIndex ..< range.lowerBound])
|
||||
}
|
||||
if let range = inputLanguage.range(of: "_") {
|
||||
inputLanguage = String(inputLanguage[inputLanguage.startIndex ..< range.lowerBound])
|
||||
}
|
||||
|
||||
self.clearIconView.isHidden = text.isEmpty
|
||||
self.clearIconTintView.isHidden = text.isEmpty
|
||||
self.clearIconButton.isHidden = text.isEmpty
|
||||
|
||||
self.currentPresetSearchTerm = nil
|
||||
self.updateQuery(.text(value: text, language: inputLanguage))
|
||||
}
|
||||
|
||||
private func update(transition: Transition) {
|
||||
guard let params = self.params else {
|
||||
return
|
||||
}
|
||||
self.params = nil
|
||||
self.update(context: params.context, theme: params.theme, forceNeedsVibrancy: params.forceNeedsVibrancy, strings: params.strings, text: params.text, useOpaqueTheme: params.useOpaqueTheme, isActive: params.isActive, size: params.size, canFocus: params.canFocus, searchCategories: params.searchCategories, searchState: params.searchState, transition: transition)
|
||||
}
|
||||
|
||||
public func update(context: AccountContext, theme: PresentationTheme, forceNeedsVibrancy: Bool, strings: PresentationStrings, text: String, useOpaqueTheme: Bool, isActive: Bool, size: CGSize, canFocus: Bool, searchCategories: EmojiSearchCategories?, searchState: EmojiPagerContentComponent.SearchState, transition: Transition) {
|
||||
let textInputState: EmojiSearchSearchBarComponent.TextInputState
|
||||
if let textField = self.textField {
|
||||
textInputState = .active(hasText: !(textField.text ?? "").isEmpty)
|
||||
} else {
|
||||
textInputState = .inactive
|
||||
}
|
||||
|
||||
let params = Params(
|
||||
context: context,
|
||||
theme: theme,
|
||||
forceNeedsVibrancy: forceNeedsVibrancy,
|
||||
strings: strings,
|
||||
text: text,
|
||||
useOpaqueTheme: useOpaqueTheme,
|
||||
isActive: isActive,
|
||||
hasPresetSearch: self.currentPresetSearchTerm == nil,
|
||||
textInputState: textInputState,
|
||||
searchState: searchState,
|
||||
size: size,
|
||||
canFocus: canFocus,
|
||||
searchCategories: searchCategories
|
||||
)
|
||||
|
||||
if self.params == params {
|
||||
return
|
||||
}
|
||||
|
||||
let isActiveWithText = isActive && self.currentPresetSearchTerm == nil
|
||||
|
||||
if self.params?.theme !== theme {
|
||||
/*self.searchIconView.image = generateTintedImage(image: UIImage(bundleImageName: "Components/Search Bar/Loupe"), color: .white)?.withRenderingMode(.alwaysTemplate)
|
||||
self.searchIconView.tintColor = useOpaqueTheme ? theme.chat.inputMediaPanel.panelContentOpaqueSearchOverlayColor : theme.chat.inputMediaPanel.panelContentVibrantSearchOverlayColor
|
||||
|
||||
self.searchIconTintView.image = generateTintedImage(image: UIImage(bundleImageName: "Components/Search Bar/Loupe"), color: .white)
|
||||
|
||||
self.backIconView.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: .white)?.withRenderingMode(.alwaysTemplate)
|
||||
self.backIconView.tintColor = useOpaqueTheme ? theme.chat.inputMediaPanel.panelContentOpaqueSearchOverlayColor : theme.chat.inputMediaPanel.panelContentVibrantSearchOverlayColor
|
||||
|
||||
self.backIconTintView.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: .white)*/
|
||||
|
||||
self.clearIconView.image = generateTintedImage(image: UIImage(bundleImageName: "Components/Search Bar/Clear"), color: .white)?.withRenderingMode(.alwaysTemplate)
|
||||
self.clearIconView.tintColor = useOpaqueTheme ? theme.chat.inputMediaPanel.panelContentOpaqueSearchOverlayColor : theme.chat.inputMediaPanel.panelContentVibrantSearchOverlayColor
|
||||
|
||||
self.clearIconTintView.image = generateTintedImage(image: UIImage(bundleImageName: "Components/Search Bar/Clear"), color: .white)
|
||||
}
|
||||
|
||||
self.params = params
|
||||
|
||||
let sideInset: CGFloat = 12.0
|
||||
let topInset: CGFloat = 8.0
|
||||
let inputHeight: CGFloat = 36.0
|
||||
|
||||
let sideTextInset: CGFloat = sideInset + 4.0 + 24.0
|
||||
|
||||
if theme.overallDarkAppearance && forceNeedsVibrancy {
|
||||
self.backgroundLayer.backgroundColor = theme.chat.inputMediaPanel.panelContentControlVibrantSelectionColor.withMultipliedAlpha(0.3).cgColor
|
||||
self.tintBackgroundLayer.backgroundColor = UIColor(white: 1.0, alpha: 0.2).cgColor
|
||||
} else if useOpaqueTheme {
|
||||
self.backgroundLayer.backgroundColor = theme.chat.inputMediaPanel.panelContentControlOpaqueSelectionColor.cgColor
|
||||
self.tintBackgroundLayer.backgroundColor = UIColor.white.cgColor
|
||||
} else {
|
||||
self.backgroundLayer.backgroundColor = theme.chat.inputMediaPanel.panelContentControlVibrantSelectionColor.cgColor
|
||||
self.tintBackgroundLayer.backgroundColor = UIColor(white: 1.0, alpha: 0.2).cgColor
|
||||
}
|
||||
|
||||
self.backgroundLayer.cornerRadius = inputHeight * 0.5
|
||||
self.tintBackgroundLayer.cornerRadius = inputHeight * 0.5
|
||||
|
||||
let cancelColor: UIColor
|
||||
if theme.overallDarkAppearance && forceNeedsVibrancy {
|
||||
cancelColor = theme.chat.inputMediaPanel.panelContentVibrantSearchOverlayColor.withMultipliedAlpha(0.3)
|
||||
} else {
|
||||
cancelColor = useOpaqueTheme ? theme.list.itemAccentColor : theme.chat.inputMediaPanel.panelContentVibrantSearchOverlayColor
|
||||
}
|
||||
|
||||
let cancelTextSize = self.cancelButtonTitle.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(Text(
|
||||
text: strings.Common_Cancel,
|
||||
font: Font.regular(17.0),
|
||||
color: cancelColor
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: size.width - 32.0, height: 100.0)
|
||||
)
|
||||
let _ = self.cancelButtonTintTitle.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(Text(
|
||||
text: strings.Common_Cancel,
|
||||
font: Font.regular(17.0),
|
||||
color: .white
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: size.width - 32.0, height: 100.0)
|
||||
)
|
||||
|
||||
let cancelButtonSpacing: CGFloat = 8.0
|
||||
|
||||
var backgroundFrame = CGRect(origin: CGPoint(x: sideInset, y: topInset), size: CGSize(width: size.width - sideInset * 2.0, height: inputHeight))
|
||||
if isActiveWithText {
|
||||
backgroundFrame.size.width -= cancelTextSize.width + cancelButtonSpacing
|
||||
}
|
||||
transition.setFrame(layer: self.backgroundLayer, frame: backgroundFrame)
|
||||
transition.setFrame(layer: self.tintBackgroundLayer, frame: backgroundFrame)
|
||||
|
||||
transition.setFrame(view: self.cancelButton, frame: CGRect(origin: CGPoint(x: backgroundFrame.maxX, y: 0.0), size: CGSize(width: cancelButtonSpacing + cancelTextSize.width, height: size.height)))
|
||||
|
||||
let textX: CGFloat = backgroundFrame.minX + sideTextInset
|
||||
let textFrame = CGRect(origin: CGPoint(x: textX, y: backgroundFrame.minY), size: CGSize(width: backgroundFrame.maxX - textX, height: backgroundFrame.height))
|
||||
self.textFrame = textFrame
|
||||
|
||||
let statusContent: EmojiSearchStatusComponent.Content
|
||||
switch searchState {
|
||||
case .empty:
|
||||
statusContent = .search
|
||||
case .searching:
|
||||
statusContent = .progress
|
||||
case .active:
|
||||
statusContent = .results
|
||||
}
|
||||
|
||||
let statusSize = CGSize(width: 24.0, height: 24.0)
|
||||
let _ = self.statusIcon.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(EmojiSearchStatusComponent(
|
||||
theme: theme,
|
||||
forceNeedsVibrancy: forceNeedsVibrancy,
|
||||
strings: strings,
|
||||
useOpaqueTheme: useOpaqueTheme,
|
||||
content: statusContent
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: statusSize
|
||||
)
|
||||
let iconFrame = CGRect(origin: CGPoint(x: textFrame.minX - statusSize.width - 4.0, y: backgroundFrame.minY + floor((backgroundFrame.height - statusSize.height) / 2.0)), size: statusSize)
|
||||
if let statusIconView = self.statusIcon.view as? EmojiSearchStatusComponent.View {
|
||||
if statusIconView.superview == nil {
|
||||
self.addSubview(statusIconView)
|
||||
self.tintContainerView.addSubview(statusIconView.tintContainerView)
|
||||
}
|
||||
|
||||
transition.setFrame(view: statusIconView, frame: iconFrame)
|
||||
transition.setFrame(view: statusIconView.tintContainerView, frame: iconFrame)
|
||||
}
|
||||
|
||||
let placeholderContentFrame = CGRect(origin: CGPoint(x: textFrame.minX - 6.0, y: backgroundFrame.minY), size: CGSize(width: backgroundFrame.maxX - (textFrame.minX - 6.0), height: backgroundFrame.height))
|
||||
let _ = self.placeholderContent.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(EmojiSearchSearchBarComponent(
|
||||
context: context,
|
||||
theme: theme,
|
||||
forceNeedsVibrancy: forceNeedsVibrancy,
|
||||
strings: strings,
|
||||
useOpaqueTheme: useOpaqueTheme,
|
||||
textInputState: textInputState,
|
||||
categories: searchCategories,
|
||||
searchTermUpdated: { [weak self] term in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
var shouldChangeActivation = false
|
||||
if (self.currentPresetSearchTerm == nil) != (term == nil) {
|
||||
shouldChangeActivation = true
|
||||
}
|
||||
self.currentPresetSearchTerm = term
|
||||
|
||||
if shouldChangeActivation {
|
||||
if let term {
|
||||
self.update(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)))
|
||||
|
||||
self.updateQuery(.category(value: term))
|
||||
self.activated(false)
|
||||
} else {
|
||||
self.deactivated(self.textField?.isFirstResponder ?? false)
|
||||
self.updateQuery(nil)
|
||||
}
|
||||
} else {
|
||||
if let term {
|
||||
self.updateQuery(.category(value: term))
|
||||
} else {
|
||||
self.updateQuery(nil)
|
||||
}
|
||||
}
|
||||
},
|
||||
activateTextInput: { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.activateTextInput()
|
||||
}
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: placeholderContentFrame.size
|
||||
)
|
||||
if let placeholderContentView = self.placeholderContent.view as? EmojiSearchSearchBarComponent.View {
|
||||
if placeholderContentView.superview == nil {
|
||||
self.addSubview(placeholderContentView)
|
||||
self.tintContainerView.addSubview(placeholderContentView.tintContainerView)
|
||||
}
|
||||
transition.setFrame(view: placeholderContentView, frame: placeholderContentFrame)
|
||||
transition.setFrame(view: placeholderContentView.tintContainerView, frame: placeholderContentFrame)
|
||||
}
|
||||
|
||||
/*if let searchCategories {
|
||||
let suggestedItemsView: ComponentView<Empty>
|
||||
var suggestedItemsTransition = transition
|
||||
if let current = self.suggestedItemsView {
|
||||
suggestedItemsView = current
|
||||
} else {
|
||||
suggestedItemsTransition = .immediate
|
||||
suggestedItemsView = ComponentView()
|
||||
self.suggestedItemsView = suggestedItemsView
|
||||
}
|
||||
|
||||
let itemsX: CGFloat = textFrame.maxX + 8.0
|
||||
let suggestedItemsFrame = CGRect(origin: CGPoint(x: itemsX, y: backgroundFrame.minY), size: CGSize(width: backgroundFrame.maxX - itemsX, height: backgroundFrame.height))
|
||||
|
||||
if let suggestedItemsComponentView = suggestedItemsView.view {
|
||||
if suggestedItemsComponentView.superview == nil {
|
||||
self.addSubview(suggestedItemsComponentView)
|
||||
}
|
||||
suggestedItemsTransition.setFrame(view: suggestedItemsComponentView, frame: suggestedItemsFrame)
|
||||
suggestedItemsTransition.setAlpha(view: suggestedItemsComponentView, alpha: isActiveWithText ? 0.0 : 1.0)
|
||||
}
|
||||
} else {
|
||||
if let suggestedItemsView = self.suggestedItemsView {
|
||||
self.suggestedItemsView = nil
|
||||
if let suggestedItemsComponentView = suggestedItemsView.view {
|
||||
transition.setAlpha(view: suggestedItemsComponentView, alpha: 0.0, completion: { [weak suggestedItemsComponentView] _ in
|
||||
suggestedItemsComponentView?.removeFromSuperview()
|
||||
})
|
||||
}
|
||||
}
|
||||
}*/
|
||||
|
||||
if let image = self.clearIconView.image {
|
||||
let iconFrame = CGRect(origin: CGPoint(x: backgroundFrame.maxX - image.size.width - 4.0, y: backgroundFrame.minY + floor((backgroundFrame.height - image.size.height) / 2.0)), size: image.size)
|
||||
transition.setFrame(view: self.clearIconView, frame: iconFrame)
|
||||
transition.setFrame(view: self.clearIconTintView, frame: iconFrame)
|
||||
transition.setFrame(view: self.clearIconButton, frame: iconFrame.insetBy(dx: -8.0, dy: -10.0))
|
||||
}
|
||||
|
||||
if let cancelButtonTitleComponentView = self.cancelButtonTitle.view {
|
||||
if cancelButtonTitleComponentView.superview == nil {
|
||||
self.addSubview(cancelButtonTitleComponentView)
|
||||
cancelButtonTitleComponentView.isUserInteractionEnabled = false
|
||||
}
|
||||
transition.setFrame(view: cancelButtonTitleComponentView, frame: CGRect(origin: CGPoint(x: backgroundFrame.maxX + cancelButtonSpacing, y: floor((size.height - cancelTextSize.height) / 2.0)), size: cancelTextSize))
|
||||
transition.setAlpha(view: cancelButtonTitleComponentView, alpha: isActiveWithText ? 1.0 : 0.0)
|
||||
}
|
||||
if let cancelButtonTintTitleComponentView = self.cancelButtonTintTitle.view {
|
||||
if cancelButtonTintTitleComponentView.superview == nil {
|
||||
self.tintContainerView.addSubview(cancelButtonTintTitleComponentView)
|
||||
cancelButtonTintTitleComponentView.isUserInteractionEnabled = false
|
||||
}
|
||||
transition.setFrame(view: cancelButtonTintTitleComponentView, frame: CGRect(origin: CGPoint(x: backgroundFrame.maxX + cancelButtonSpacing, y: floor((size.height - cancelTextSize.height) / 2.0)), size: cancelTextSize))
|
||||
transition.setAlpha(view: cancelButtonTintTitleComponentView, alpha: isActiveWithText ? 1.0 : 0.0)
|
||||
}
|
||||
|
||||
var hasText = false
|
||||
if let textField = self.textField {
|
||||
textField.textColor = theme.contextMenu.primaryColor
|
||||
transition.setFrame(view: textField, frame: CGRect(origin: CGPoint(x: backgroundFrame.minX + sideTextInset, y: backgroundFrame.minY), size: CGSize(width: backgroundFrame.width - sideTextInset - 32.0, height: backgroundFrame.height)))
|
||||
|
||||
if let text = textField.text, !text.isEmpty {
|
||||
hasText = true
|
||||
}
|
||||
}
|
||||
let _ = hasText
|
||||
|
||||
/*self.tintTextView.view?.isHidden = hasText
|
||||
self.textView.view?.isHidden = hasText*/
|
||||
}
|
||||
}
|
@ -420,6 +420,15 @@ final class EmojiSearchSearchBarComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
guard let component = self.component else {
|
||||
return nil
|
||||
}
|
||||
let _ = component
|
||||
|
||||
return super.hitTest(point, with: event)
|
||||
}
|
||||
|
||||
private func updateScrolling(transition: Transition, fromScrolling: Bool) {
|
||||
guard let component = self.component, let itemLayout = self.itemLayout else {
|
||||
return
|
||||
@ -427,8 +436,12 @@ final class EmojiSearchSearchBarComponent: Component {
|
||||
|
||||
let itemAlpha: CGFloat
|
||||
switch component.textInputState {
|
||||
case .active:
|
||||
itemAlpha = 0.0
|
||||
case let .active(hasText):
|
||||
if hasText {
|
||||
itemAlpha = 0.0
|
||||
} else {
|
||||
itemAlpha = 1.0
|
||||
}
|
||||
case .inactive:
|
||||
itemAlpha = 1.0
|
||||
}
|
||||
@ -674,7 +687,7 @@ final class EmojiSearchSearchBarComponent: Component {
|
||||
if self.scrollView.bounds.size != availableSize {
|
||||
transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(), size: availableSize))
|
||||
}
|
||||
if case .active = component.textInputState {
|
||||
if case .active(true) = component.textInputState {
|
||||
transition.setBoundsOrigin(view: self.scrollView, origin: CGPoint())
|
||||
}
|
||||
if self.scrollView.contentSize != itemLayout.contentSize {
|
||||
|
@ -0,0 +1,101 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import ComponentFlow
|
||||
import AccountContext
|
||||
import TelegramCore
|
||||
import TelegramPresentationData
|
||||
import EmojiStatusComponent
|
||||
|
||||
final class EmptySearchResultsView: UIView {
|
||||
override public static var layerClass: AnyClass {
|
||||
return PassthroughLayer.self
|
||||
}
|
||||
|
||||
let tintContainerView: UIView
|
||||
let titleLabel: ComponentView<Empty>
|
||||
let titleTintLabel: ComponentView<Empty>
|
||||
let icon: ComponentView<Empty>
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.tintContainerView = UIView()
|
||||
|
||||
self.titleLabel = ComponentView()
|
||||
self.titleTintLabel = ComponentView()
|
||||
self.icon = ComponentView()
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
(self.layer as? PassthroughLayer)?.mirrorLayer = self.tintContainerView.layer
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func update(context: AccountContext, theme: PresentationTheme, useOpaqueTheme: Bool, text: String, file: TelegramMediaFile?, size: CGSize, searchInitiallyHidden: Bool, transition: Transition) {
|
||||
let titleColor: UIColor
|
||||
if useOpaqueTheme {
|
||||
titleColor = theme.chat.inputMediaPanel.panelContentOpaqueSearchOverlayColor
|
||||
} else {
|
||||
titleColor = theme.chat.inputMediaPanel.panelContentVibrantSearchOverlayColor
|
||||
}
|
||||
|
||||
let iconSize: CGSize
|
||||
if let file = file {
|
||||
iconSize = self.icon.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(EmojiStatusComponent(
|
||||
context: context,
|
||||
animationCache: context.animationCache,
|
||||
animationRenderer: context.animationRenderer,
|
||||
content: .animation(content: .file(file: file), size: CGSize(width: 32.0, height: 32.0), placeholderColor: titleColor, themeColor: nil, loopMode: .forever),
|
||||
isVisibleForAnimations: context.sharedContext.energyUsageSettings.loopEmoji,
|
||||
action: nil
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: 32.0, height: 32.0)
|
||||
)
|
||||
} else {
|
||||
iconSize = CGSize()
|
||||
}
|
||||
|
||||
let titleSize = self.titleLabel.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(Text(text: text, font: Font.regular(15.0), color: titleColor)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: size.width, height: 100.0)
|
||||
)
|
||||
let _ = self.titleTintLabel.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(Text(text: text, font: Font.regular(15.0), color: .white)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: size.width, height: 100.0)
|
||||
)
|
||||
|
||||
let spacing: CGFloat = 4.0
|
||||
let contentHeight = iconSize.height + spacing + titleSize.height
|
||||
let contentOriginY = searchInitiallyHidden ? floor((size.height - contentHeight) / 2.0) : 10.0
|
||||
let iconFrame = CGRect(origin: CGPoint(x: floor((size.width - iconSize.width) / 2.0), y: contentOriginY), size: iconSize)
|
||||
let titleFrame = CGRect(origin: CGPoint(x: floor((size.width - titleSize.width) / 2.0), y: iconFrame.maxY + spacing), size: titleSize)
|
||||
|
||||
if let iconView = self.icon.view {
|
||||
if iconView.superview == nil {
|
||||
self.addSubview(iconView)
|
||||
}
|
||||
transition.setFrame(view: iconView, frame: iconFrame)
|
||||
}
|
||||
if let titleLabelView = self.titleLabel.view {
|
||||
if titleLabelView.superview == nil {
|
||||
self.addSubview(titleLabelView)
|
||||
}
|
||||
transition.setFrame(view: titleLabelView, frame: titleFrame)
|
||||
}
|
||||
if let titleTintLabelView = self.titleTintLabel.view {
|
||||
if titleTintLabelView.superview == nil {
|
||||
self.tintContainerView.addSubview(titleTintLabelView)
|
||||
}
|
||||
transition.setFrame(view: titleTintLabelView, frame: titleFrame)
|
||||
}
|
||||
}
|
||||
}
|
@ -85,7 +85,7 @@ final class EntityKeyboardAnimationTopPanelComponent: Component {
|
||||
}
|
||||
|
||||
final class View: UIView {
|
||||
var itemLayer: EmojiPagerContentComponent.View.ItemLayer?
|
||||
var itemLayer: EmojiKeyboardItemLayer?
|
||||
var placeholderView: EmojiPagerContentComponent.View.ItemPlaceholderView?
|
||||
var component: EntityKeyboardAnimationTopPanelComponent?
|
||||
var titleView: ComponentView<Empty>?
|
||||
@ -116,7 +116,7 @@ final class EntityKeyboardAnimationTopPanelComponent: Component {
|
||||
|
||||
if self.itemLayer == nil {
|
||||
let tintColor: EmojiPagerContentComponent.Item.TintMode = component.customTintColor.flatMap { .custom($0) } ?? .primary
|
||||
let itemLayer = EmojiPagerContentComponent.View.ItemLayer(
|
||||
let itemLayer = EmojiKeyboardItemLayer(
|
||||
item: EmojiPagerContentComponent.Item(
|
||||
animationData: component.item,
|
||||
content: .animation(component.item),
|
||||
@ -157,7 +157,7 @@ final class EntityKeyboardAnimationTopPanelComponent: Component {
|
||||
transition.setPosition(layer: itemLayer, position: CGPoint(x: iconFrame.midX, y: iconFrame.midY))
|
||||
transition.setBounds(layer: itemLayer, bounds: CGRect(origin: CGPoint(), size: iconFrame.size))
|
||||
|
||||
var badge: EmojiPagerContentComponent.View.ItemLayer.Badge?
|
||||
var badge: EmojiKeyboardItemLayer.Badge?
|
||||
if component.isPremiumLocked {
|
||||
badge = .locked
|
||||
} else if component.isFeatured {
|
||||
|
@ -0,0 +1,209 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import ComponentFlow
|
||||
import AccountContext
|
||||
import TelegramPresentationData
|
||||
import AnimationCache
|
||||
import MultiAnimationRenderer
|
||||
import PagerComponent
|
||||
|
||||
final class GroupEmbeddedView: UIScrollView, UIScrollViewDelegate, PagerExpandableScrollView {
|
||||
private struct ItemLayout {
|
||||
var itemSize: CGFloat
|
||||
var itemSpacing: CGFloat
|
||||
var sideInset: CGFloat
|
||||
var itemCount: Int
|
||||
var contentSize: CGSize
|
||||
|
||||
init(height: CGFloat, sideInset: CGFloat, itemCount: Int) {
|
||||
self.itemSize = 30.0
|
||||
self.itemSpacing = 20.0
|
||||
self.sideInset = sideInset
|
||||
self.itemCount = itemCount
|
||||
|
||||
self.contentSize = CGSize(width: self.sideInset * 2.0 + CGFloat(self.itemCount) * self.itemSize + CGFloat(self.itemCount - 1) * self.itemSpacing, height: height)
|
||||
}
|
||||
|
||||
func frame(at index: Int) -> CGRect {
|
||||
return CGRect(origin: CGPoint(x: sideInset + CGFloat(index) * (self.itemSize + self.itemSpacing), y: floor((self.contentSize.height - self.itemSize) / 2.0)), size: CGSize(width: self.itemSize, height: self.itemSize))
|
||||
}
|
||||
|
||||
func visibleItems(for rect: CGRect) -> Range<Int>? {
|
||||
let offsetRect = rect.offsetBy(dx: -self.sideInset, dy: 0.0)
|
||||
var minVisibleIndex = Int(floor((offsetRect.minX - self.itemSpacing) / (self.itemSize + self.itemSpacing)))
|
||||
minVisibleIndex = max(0, minVisibleIndex)
|
||||
var maxVisibleIndex = Int(ceil((offsetRect.maxX - self.itemSpacing) / (self.itemSize + self.itemSpacing)))
|
||||
maxVisibleIndex = min(maxVisibleIndex, self.itemCount - 1)
|
||||
|
||||
if minVisibleIndex <= maxVisibleIndex {
|
||||
return minVisibleIndex ..< (maxVisibleIndex + 1)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private let performItemAction: (EmojiPagerContentComponent.Item, UIView, CGRect, CALayer) -> Void
|
||||
|
||||
private var visibleItemLayers: [EmojiKeyboardItemLayer.Key: EmojiKeyboardItemLayer] = [:]
|
||||
private var ignoreScrolling: Bool = false
|
||||
|
||||
private var context: AccountContext?
|
||||
private var theme: PresentationTheme?
|
||||
private var cache: AnimationCache?
|
||||
private var renderer: MultiAnimationRenderer?
|
||||
private var currentInsets: UIEdgeInsets?
|
||||
private var currentSize: CGSize?
|
||||
private var items: [EmojiPagerContentComponent.Item]?
|
||||
private var isStickers: Bool = false
|
||||
|
||||
private var itemLayout: ItemLayout?
|
||||
|
||||
init(performItemAction: @escaping (EmojiPagerContentComponent.Item, UIView, CGRect, CALayer) -> Void) {
|
||||
self.performItemAction = performItemAction
|
||||
|
||||
super.init(frame: CGRect())
|
||||
|
||||
self.delaysContentTouches = false
|
||||
if #available(iOSApplicationExtension 11.0, iOS 11.0, *) {
|
||||
self.contentInsetAdjustmentBehavior = .never
|
||||
}
|
||||
if #available(iOS 13.0, *) {
|
||||
self.automaticallyAdjustsScrollIndicatorInsets = false
|
||||
}
|
||||
self.showsVerticalScrollIndicator = true
|
||||
self.showsHorizontalScrollIndicator = false
|
||||
self.delegate = self
|
||||
self.clipsToBounds = true
|
||||
self.scrollsToTop = false
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func tapGesture(point: CGPoint) -> Bool {
|
||||
guard let itemLayout = self.itemLayout else {
|
||||
return false
|
||||
}
|
||||
|
||||
for (_, itemLayer) in self.visibleItemLayers {
|
||||
if itemLayer.frame.inset(by: UIEdgeInsets(top: -6.0, left: -itemLayout.itemSpacing, bottom: -6.0, right: -itemLayout.itemSpacing)).contains(point) {
|
||||
self.performItemAction(itemLayer.item, self, itemLayer.frame, itemLayer)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||
if !self.ignoreScrolling {
|
||||
self.updateVisibleItems(transition: .immediate, attemptSynchronousLoad: false)
|
||||
}
|
||||
}
|
||||
|
||||
private func updateVisibleItems(transition: Transition, attemptSynchronousLoad: Bool) {
|
||||
guard let context = self.context, let theme = self.theme, let itemLayout = self.itemLayout, let items = self.items, let cache = self.cache, let renderer = self.renderer else {
|
||||
return
|
||||
}
|
||||
|
||||
var validIds = Set<EmojiKeyboardItemLayer.Key>()
|
||||
if let itemRange = itemLayout.visibleItems(for: self.bounds) {
|
||||
for index in itemRange.lowerBound ..< itemRange.upperBound {
|
||||
let item = items[index]
|
||||
let itemId = EmojiKeyboardItemLayer.Key(
|
||||
groupId: AnyHashable(0),
|
||||
itemId: item.content.id
|
||||
)
|
||||
validIds.insert(itemId)
|
||||
|
||||
let itemLayer: EmojiKeyboardItemLayer
|
||||
if let current = self.visibleItemLayers[itemId] {
|
||||
itemLayer = current
|
||||
} else {
|
||||
itemLayer = EmojiKeyboardItemLayer(
|
||||
item: item,
|
||||
context: context,
|
||||
attemptSynchronousLoad: attemptSynchronousLoad,
|
||||
content: item.content,
|
||||
cache: cache,
|
||||
renderer: renderer,
|
||||
placeholderColor: .clear,
|
||||
blurredBadgeColor: .clear,
|
||||
accentIconColor: theme.list.itemAccentColor,
|
||||
pointSize: CGSize(width: 32.0, height: 32.0),
|
||||
onUpdateDisplayPlaceholder: { _, _ in
|
||||
}
|
||||
)
|
||||
self.visibleItemLayers[itemId] = itemLayer
|
||||
self.layer.addSublayer(itemLayer)
|
||||
}
|
||||
|
||||
switch item.tintMode {
|
||||
case let .custom(color):
|
||||
itemLayer.layerTintColor = color.cgColor
|
||||
case .accent:
|
||||
itemLayer.layerTintColor = theme.list.itemAccentColor.cgColor
|
||||
case .primary:
|
||||
itemLayer.layerTintColor = theme.list.itemPrimaryTextColor.cgColor
|
||||
case .none:
|
||||
itemLayer.layerTintColor = nil
|
||||
}
|
||||
|
||||
let itemFrame = itemLayout.frame(at: index)
|
||||
itemLayer.frame = itemFrame
|
||||
|
||||
itemLayer.isVisibleForAnimations = self.isStickers ? context.sharedContext.energyUsageSettings.loopStickers : context.sharedContext.energyUsageSettings.loopEmoji
|
||||
}
|
||||
}
|
||||
|
||||
var removedIds: [EmojiKeyboardItemLayer.Key] = []
|
||||
for (id, itemLayer) in self.visibleItemLayers {
|
||||
if !validIds.contains(id) {
|
||||
removedIds.append(id)
|
||||
itemLayer.removeFromSuperlayer()
|
||||
}
|
||||
}
|
||||
for id in removedIds {
|
||||
self.visibleItemLayers.removeValue(forKey: id)
|
||||
}
|
||||
}
|
||||
|
||||
func update(
|
||||
context: AccountContext,
|
||||
theme: PresentationTheme,
|
||||
insets: UIEdgeInsets,
|
||||
size: CGSize,
|
||||
items: [EmojiPagerContentComponent.Item],
|
||||
isStickers: Bool,
|
||||
cache: AnimationCache,
|
||||
renderer: MultiAnimationRenderer,
|
||||
attemptSynchronousLoad: Bool
|
||||
) {
|
||||
if self.theme === theme && self.currentInsets == insets && self.currentSize == size && self.items == items {
|
||||
return
|
||||
}
|
||||
|
||||
self.context = context
|
||||
self.theme = theme
|
||||
self.currentInsets = insets
|
||||
self.currentSize = size
|
||||
self.items = items
|
||||
self.isStickers = isStickers
|
||||
self.cache = cache
|
||||
self.renderer = renderer
|
||||
|
||||
let itemLayout = ItemLayout(height: size.height, sideInset: insets.left, itemCount: items.count)
|
||||
self.itemLayout = itemLayout
|
||||
|
||||
self.ignoreScrolling = true
|
||||
if itemLayout.contentSize != self.contentSize {
|
||||
self.contentSize = itemLayout.contentSize
|
||||
}
|
||||
self.ignoreScrolling = false
|
||||
|
||||
self.updateVisibleItems(transition: .immediate, attemptSynchronousLoad: attemptSynchronousLoad)
|
||||
}
|
||||
}
|
@ -0,0 +1,128 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import ComponentFlow
|
||||
import TelegramPresentationData
|
||||
|
||||
final class GroupExpandActionButton: UIButton {
|
||||
override static var layerClass: AnyClass {
|
||||
return PassthroughLayer.self
|
||||
}
|
||||
|
||||
let tintContainerLayer: SimpleLayer
|
||||
|
||||
private var currentTextLayout: (string: String, color: UIColor, constrainedWidth: CGFloat, size: CGSize)?
|
||||
private let backgroundLayer: SimpleLayer
|
||||
private let tintBackgroundLayer: SimpleLayer
|
||||
private let textLayer: SimpleLayer
|
||||
private let pressed: () -> Void
|
||||
|
||||
init(pressed: @escaping () -> Void) {
|
||||
self.pressed = pressed
|
||||
|
||||
self.tintContainerLayer = SimpleLayer()
|
||||
|
||||
self.backgroundLayer = SimpleLayer()
|
||||
self.backgroundLayer.masksToBounds = true
|
||||
|
||||
self.tintBackgroundLayer = SimpleLayer()
|
||||
self.tintBackgroundLayer.masksToBounds = true
|
||||
|
||||
self.textLayer = SimpleLayer()
|
||||
|
||||
super.init(frame: CGRect())
|
||||
|
||||
(self.layer as? PassthroughLayer)?.mirrorLayer = self.tintContainerLayer
|
||||
|
||||
self.layer.addSublayer(self.backgroundLayer)
|
||||
|
||||
self.layer.addSublayer(self.textLayer)
|
||||
|
||||
self.addTarget(self, action: #selector(self.onPressed), for: .touchUpInside)
|
||||
}
|
||||
|
||||
required init(coder: NSCoder) {
|
||||
preconditionFailure()
|
||||
}
|
||||
|
||||
@objc private func onPressed() {
|
||||
self.pressed()
|
||||
}
|
||||
|
||||
override func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
|
||||
self.alpha = 0.6
|
||||
|
||||
return super.beginTracking(touch, with: event)
|
||||
}
|
||||
|
||||
override func endTracking(_ touch: UITouch?, with event: UIEvent?) {
|
||||
let alpha = self.alpha
|
||||
self.alpha = 1.0
|
||||
self.layer.animateAlpha(from: alpha, to: 1.0, duration: 0.25)
|
||||
|
||||
super.endTracking(touch, with: event)
|
||||
}
|
||||
|
||||
override func cancelTracking(with event: UIEvent?) {
|
||||
let alpha = self.alpha
|
||||
self.alpha = 1.0
|
||||
self.layer.animateAlpha(from: alpha, to: 1.0, duration: 0.25)
|
||||
|
||||
super.cancelTracking(with: event)
|
||||
}
|
||||
|
||||
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||
let alpha = self.alpha
|
||||
self.alpha = 1.0
|
||||
self.layer.animateAlpha(from: alpha, to: 1.0, duration: 0.25)
|
||||
|
||||
super.touchesCancelled(touches, with: event)
|
||||
}
|
||||
|
||||
func update(theme: PresentationTheme, title: String, useOpaqueTheme: Bool) -> CGSize {
|
||||
let textConstrainedWidth: CGFloat = 100.0
|
||||
let color = theme.list.itemCheckColors.foregroundColor
|
||||
|
||||
if useOpaqueTheme {
|
||||
self.backgroundLayer.backgroundColor = theme.chat.inputMediaPanel.panelContentControlOpaqueOverlayColor.cgColor
|
||||
} else {
|
||||
self.backgroundLayer.backgroundColor = theme.chat.inputMediaPanel.panelContentControlVibrantOverlayColor.cgColor
|
||||
}
|
||||
self.tintContainerLayer.backgroundColor = UIColor.white.cgColor
|
||||
|
||||
let textSize: CGSize
|
||||
if let currentTextLayout = self.currentTextLayout, currentTextLayout.string == title, currentTextLayout.color == color, currentTextLayout.constrainedWidth == textConstrainedWidth {
|
||||
textSize = currentTextLayout.size
|
||||
} else {
|
||||
let font: UIFont = Font.semibold(13.0)
|
||||
let string = NSAttributedString(string: title.uppercased(), font: font, textColor: color)
|
||||
let stringBounds = string.boundingRect(with: CGSize(width: textConstrainedWidth, height: 100.0), options: .usesLineFragmentOrigin, context: nil)
|
||||
textSize = CGSize(width: ceil(stringBounds.width), height: ceil(stringBounds.height))
|
||||
self.textLayer.contents = generateImage(textSize, opaque: false, scale: 0.0, rotatedContext: { size, context in
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
UIGraphicsPushContext(context)
|
||||
|
||||
string.draw(in: stringBounds)
|
||||
|
||||
UIGraphicsPopContext()
|
||||
})?.cgImage
|
||||
self.currentTextLayout = (title, color, textConstrainedWidth, textSize)
|
||||
}
|
||||
|
||||
var sideInset: CGFloat = 10.0
|
||||
if textSize.width > 24.0 {
|
||||
sideInset = 6.0
|
||||
}
|
||||
let size = CGSize(width: textSize.width + sideInset * 2.0, height: 28.0)
|
||||
|
||||
let textFrame = CGRect(origin: CGPoint(x: floor((size.width - textSize.width) / 2.0), y: floor((size.height - textSize.height) / 2.0)), size: textSize)
|
||||
self.textLayer.frame = textFrame
|
||||
|
||||
self.backgroundLayer.frame = CGRect(origin: CGPoint(), size: size)
|
||||
self.tintBackgroundLayer.frame = CGRect(origin: CGPoint(), size: size)
|
||||
self.backgroundLayer.cornerRadius = min(size.width, size.height) / 2.0
|
||||
self.tintContainerLayer.cornerRadius = min(size.width, size.height) / 2.0
|
||||
|
||||
return size
|
||||
}
|
||||
}
|
@ -0,0 +1,149 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import ComponentFlow
|
||||
import TelegramPresentationData
|
||||
|
||||
final class GroupHeaderActionButton: UIButton {
|
||||
override static var layerClass: AnyClass {
|
||||
return PassthroughLayer.self
|
||||
}
|
||||
|
||||
let tintContainerLayer: SimpleLayer
|
||||
|
||||
private var currentTextLayout: (string: String, color: UIColor, constrainedWidth: CGFloat, size: CGSize)?
|
||||
private let backgroundLayer: SimpleLayer
|
||||
private let tintBackgroundLayer: SimpleLayer
|
||||
private let textLayer: SimpleLayer
|
||||
private let tintTextLayer: SimpleLayer
|
||||
private let pressed: () -> Void
|
||||
|
||||
init(pressed: @escaping () -> Void) {
|
||||
self.pressed = pressed
|
||||
|
||||
self.tintContainerLayer = SimpleLayer()
|
||||
|
||||
self.backgroundLayer = SimpleLayer()
|
||||
self.backgroundLayer.masksToBounds = true
|
||||
|
||||
self.tintBackgroundLayer = SimpleLayer()
|
||||
self.tintBackgroundLayer.masksToBounds = true
|
||||
|
||||
self.textLayer = SimpleLayer()
|
||||
self.tintTextLayer = SimpleLayer()
|
||||
|
||||
super.init(frame: CGRect())
|
||||
|
||||
(self.layer as? PassthroughLayer)?.mirrorLayer = self.tintContainerLayer
|
||||
|
||||
self.layer.addSublayer(self.backgroundLayer)
|
||||
self.layer.addSublayer(self.textLayer)
|
||||
|
||||
self.addTarget(self, action: #selector(self.onPressed), for: .touchUpInside)
|
||||
|
||||
self.tintContainerLayer.addSublayer(self.tintBackgroundLayer)
|
||||
self.tintContainerLayer.addSublayer(self.tintTextLayer)
|
||||
}
|
||||
|
||||
required init(coder: NSCoder) {
|
||||
preconditionFailure()
|
||||
}
|
||||
|
||||
@objc private func onPressed() {
|
||||
self.pressed()
|
||||
}
|
||||
|
||||
override func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
|
||||
self.alpha = 0.6
|
||||
|
||||
return super.beginTracking(touch, with: event)
|
||||
}
|
||||
|
||||
override func endTracking(_ touch: UITouch?, with event: UIEvent?) {
|
||||
let alpha = self.alpha
|
||||
self.alpha = 1.0
|
||||
self.layer.animateAlpha(from: alpha, to: 1.0, duration: 0.25)
|
||||
|
||||
super.endTracking(touch, with: event)
|
||||
}
|
||||
|
||||
override func cancelTracking(with event: UIEvent?) {
|
||||
let alpha = self.alpha
|
||||
self.alpha = 1.0
|
||||
self.layer.animateAlpha(from: alpha, to: 1.0, duration: 0.25)
|
||||
|
||||
super.cancelTracking(with: event)
|
||||
}
|
||||
|
||||
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||
let alpha = self.alpha
|
||||
self.alpha = 1.0
|
||||
self.layer.animateAlpha(from: alpha, to: 1.0, duration: 0.25)
|
||||
|
||||
super.touchesCancelled(touches, with: event)
|
||||
}
|
||||
|
||||
func update(theme: PresentationTheme, title: String, compact: Bool) -> CGSize {
|
||||
let textConstrainedWidth: CGFloat = 100.0
|
||||
|
||||
let needsVibrancy = !theme.overallDarkAppearance && compact
|
||||
|
||||
let foregroundColor: UIColor
|
||||
let backgroundColor: UIColor
|
||||
|
||||
if compact {
|
||||
foregroundColor = theme.chat.inputMediaPanel.panelContentVibrantOverlayColor
|
||||
backgroundColor = foregroundColor.withMultipliedAlpha(0.2)
|
||||
} else {
|
||||
foregroundColor = theme.list.itemCheckColors.foregroundColor
|
||||
backgroundColor = theme.list.itemCheckColors.fillColor
|
||||
}
|
||||
|
||||
self.backgroundLayer.backgroundColor = backgroundColor.cgColor
|
||||
self.tintBackgroundLayer.backgroundColor = UIColor.white.withAlphaComponent(0.2).cgColor
|
||||
|
||||
self.tintContainerLayer.isHidden = !needsVibrancy
|
||||
|
||||
let textSize: CGSize
|
||||
if let currentTextLayout = self.currentTextLayout, currentTextLayout.string == title, currentTextLayout.color == foregroundColor, currentTextLayout.constrainedWidth == textConstrainedWidth {
|
||||
textSize = currentTextLayout.size
|
||||
} else {
|
||||
let font: UIFont = compact ? Font.medium(11.0) : Font.semibold(15.0)
|
||||
let string = NSAttributedString(string: title.uppercased(), font: font, textColor: foregroundColor)
|
||||
let tintString = NSAttributedString(string: title.uppercased(), font: font, textColor: .white)
|
||||
let stringBounds = string.boundingRect(with: CGSize(width: textConstrainedWidth, height: 100.0), options: .usesLineFragmentOrigin, context: nil)
|
||||
textSize = CGSize(width: ceil(stringBounds.width), height: ceil(stringBounds.height))
|
||||
self.textLayer.contents = generateImage(textSize, opaque: false, scale: 0.0, rotatedContext: { size, context in
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
UIGraphicsPushContext(context)
|
||||
|
||||
string.draw(in: stringBounds)
|
||||
|
||||
UIGraphicsPopContext()
|
||||
})?.cgImage
|
||||
self.tintTextLayer.contents = generateImage(textSize, opaque: false, scale: 0.0, rotatedContext: { size, context in
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
UIGraphicsPushContext(context)
|
||||
|
||||
tintString.draw(in: stringBounds)
|
||||
|
||||
UIGraphicsPopContext()
|
||||
})?.cgImage
|
||||
self.currentTextLayout = (title, foregroundColor, textConstrainedWidth, textSize)
|
||||
}
|
||||
|
||||
let size = CGSize(width: textSize.width + (compact ? 6.0 : 16.0) * 2.0, height: compact ? 16.0 : 28.0)
|
||||
|
||||
let textFrame = CGRect(origin: CGPoint(x: floor((size.width - textSize.width) / 2.0), y: floorToScreenPixels((size.height - textSize.height) / 2.0)), size: textSize)
|
||||
self.textLayer.frame = textFrame
|
||||
self.tintTextLayer.frame = textFrame
|
||||
|
||||
self.backgroundLayer.frame = CGRect(origin: CGPoint(), size: size)
|
||||
self.backgroundLayer.cornerRadius = min(size.width, size.height) / 2.0
|
||||
|
||||
self.tintBackgroundLayer.frame = self.backgroundLayer.frame
|
||||
self.tintBackgroundLayer.cornerRadius = self.backgroundLayer.cornerRadius
|
||||
|
||||
return size
|
||||
}
|
||||
}
|
@ -0,0 +1,525 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import ComponentFlow
|
||||
import AccountContext
|
||||
import TelegramCore
|
||||
import TelegramPresentationData
|
||||
import AnimationCache
|
||||
import MultiAnimationRenderer
|
||||
|
||||
final class GroupHeaderLayer: UIView {
|
||||
override static var layerClass: AnyClass {
|
||||
return PassthroughLayer.self
|
||||
}
|
||||
|
||||
private let actionPressed: () -> Void
|
||||
private let performItemAction: (EmojiPagerContentComponent.Item, UIView, CGRect, CALayer) -> Void
|
||||
|
||||
private let textLayer: SimpleLayer
|
||||
private let tintTextLayer: SimpleLayer
|
||||
|
||||
private var subtitleLayer: SimpleLayer?
|
||||
private var tintSubtitleLayer: SimpleLayer?
|
||||
private var lockIconLayer: SimpleLayer?
|
||||
private var tintLockIconLayer: SimpleLayer?
|
||||
private var badgeLayer: SimpleLayer?
|
||||
private var tintBadgeLayer: SimpleLayer?
|
||||
private(set) var clearIconLayer: SimpleLayer?
|
||||
private var tintClearIconLayer: SimpleLayer?
|
||||
private var separatorLayer: SimpleLayer?
|
||||
private var tintSeparatorLayer: SimpleLayer?
|
||||
private var actionButton: GroupHeaderActionButton?
|
||||
|
||||
private var groupEmbeddedView: GroupEmbeddedView?
|
||||
|
||||
private var theme: PresentationTheme?
|
||||
|
||||
private var currentTextLayout: (string: String, color: UIColor, constrainedWidth: CGFloat, size: CGSize)?
|
||||
private var currentSubtitleLayout: (string: String, color: UIColor, constrainedWidth: CGFloat, size: CGSize)?
|
||||
|
||||
let tintContentLayer: SimpleLayer
|
||||
|
||||
init(actionPressed: @escaping () -> Void, performItemAction: @escaping (EmojiPagerContentComponent.Item, UIView, CGRect, CALayer) -> Void) {
|
||||
self.actionPressed = actionPressed
|
||||
self.performItemAction = performItemAction
|
||||
|
||||
self.textLayer = SimpleLayer()
|
||||
self.tintTextLayer = SimpleLayer()
|
||||
|
||||
self.tintContentLayer = SimpleLayer()
|
||||
|
||||
super.init(frame: CGRect())
|
||||
|
||||
self.layer.addSublayer(self.textLayer)
|
||||
self.tintContentLayer.addSublayer(self.tintTextLayer)
|
||||
|
||||
(self.layer as? PassthroughLayer)?.mirrorLayer = self.tintContentLayer
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func update(
|
||||
context: AccountContext,
|
||||
theme: PresentationTheme,
|
||||
forceNeedsVibrancy: Bool,
|
||||
layoutType: EmojiPagerContentComponent.ItemLayoutType,
|
||||
hasTopSeparator: Bool,
|
||||
actionButtonTitle: String?,
|
||||
actionButtonIsCompact: Bool,
|
||||
title: String,
|
||||
subtitle: String?,
|
||||
badge: String?,
|
||||
isPremiumLocked: Bool,
|
||||
hasClear: Bool,
|
||||
embeddedItems: [EmojiPagerContentComponent.Item]?,
|
||||
isStickers: Bool,
|
||||
constrainedSize: CGSize,
|
||||
insets: UIEdgeInsets,
|
||||
cache: AnimationCache,
|
||||
renderer: MultiAnimationRenderer,
|
||||
attemptSynchronousLoad: Bool
|
||||
) -> (size: CGSize, centralContentWidth: CGFloat) {
|
||||
var themeUpdated = false
|
||||
if self.theme !== theme {
|
||||
self.theme = theme
|
||||
themeUpdated = true
|
||||
}
|
||||
|
||||
let needsVibrancy = !theme.overallDarkAppearance || forceNeedsVibrancy
|
||||
|
||||
let textOffsetY: CGFloat
|
||||
if hasTopSeparator {
|
||||
textOffsetY = 9.0
|
||||
} else {
|
||||
textOffsetY = 0.0
|
||||
}
|
||||
|
||||
let subtitleColor: UIColor
|
||||
if theme.overallDarkAppearance && forceNeedsVibrancy {
|
||||
subtitleColor = theme.chat.inputMediaPanel.panelContentVibrantOverlayColor.withMultipliedAlpha(0.2)
|
||||
} else {
|
||||
subtitleColor = theme.chat.inputMediaPanel.panelContentVibrantOverlayColor
|
||||
}
|
||||
|
||||
let color: UIColor
|
||||
let needsTintText: Bool
|
||||
if subtitle != nil {
|
||||
color = theme.chat.inputPanel.primaryTextColor
|
||||
needsTintText = false
|
||||
} else {
|
||||
color = subtitleColor
|
||||
needsTintText = true
|
||||
}
|
||||
|
||||
let titleHorizontalOffset: CGFloat
|
||||
if isPremiumLocked {
|
||||
titleHorizontalOffset = 10.0 + 2.0
|
||||
} else {
|
||||
titleHorizontalOffset = 0.0
|
||||
}
|
||||
|
||||
var actionButtonSize: CGSize?
|
||||
if let actionButtonTitle = actionButtonTitle {
|
||||
let actionButton: GroupHeaderActionButton
|
||||
if let current = self.actionButton {
|
||||
actionButton = current
|
||||
} else {
|
||||
actionButton = GroupHeaderActionButton(pressed: self.actionPressed)
|
||||
self.actionButton = actionButton
|
||||
self.addSubview(actionButton)
|
||||
self.tintContentLayer.addSublayer(actionButton.tintContainerLayer)
|
||||
}
|
||||
|
||||
actionButtonSize = actionButton.update(theme: theme, title: actionButtonTitle, compact: actionButtonIsCompact)
|
||||
} else {
|
||||
if let actionButton = self.actionButton {
|
||||
self.actionButton = nil
|
||||
actionButton.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
|
||||
var clearSize: CGSize = .zero
|
||||
var clearWidth: CGFloat = 0.0
|
||||
if hasClear {
|
||||
var updateImage = themeUpdated
|
||||
|
||||
let clearIconLayer: SimpleLayer
|
||||
if let current = self.clearIconLayer {
|
||||
clearIconLayer = current
|
||||
} else {
|
||||
updateImage = true
|
||||
clearIconLayer = SimpleLayer()
|
||||
self.clearIconLayer = clearIconLayer
|
||||
self.layer.addSublayer(clearIconLayer)
|
||||
}
|
||||
let tintClearIconLayer: SimpleLayer
|
||||
if let current = self.tintClearIconLayer {
|
||||
tintClearIconLayer = current
|
||||
} else {
|
||||
updateImage = true
|
||||
tintClearIconLayer = SimpleLayer()
|
||||
self.tintClearIconLayer = tintClearIconLayer
|
||||
self.tintContentLayer.addSublayer(tintClearIconLayer)
|
||||
}
|
||||
|
||||
tintClearIconLayer.isHidden = !needsVibrancy
|
||||
|
||||
clearSize = clearIconLayer.bounds.size
|
||||
if updateImage, let image = PresentationResourcesChat.chatInputMediaPanelGridDismissImage(theme, color: subtitleColor) {
|
||||
clearSize = image.size
|
||||
clearIconLayer.contents = image.cgImage
|
||||
}
|
||||
if updateImage, let image = PresentationResourcesChat.chatInputMediaPanelGridDismissImage(theme, color: .white) {
|
||||
tintClearIconLayer.contents = image.cgImage
|
||||
}
|
||||
|
||||
tintClearIconLayer.frame = clearIconLayer.frame
|
||||
clearWidth = 4.0 + clearSize.width
|
||||
} else {
|
||||
if let clearIconLayer = self.clearIconLayer {
|
||||
self.clearIconLayer = nil
|
||||
clearIconLayer.removeFromSuperlayer()
|
||||
}
|
||||
if let tintClearIconLayer = self.tintClearIconLayer {
|
||||
self.tintClearIconLayer = nil
|
||||
tintClearIconLayer.removeFromSuperlayer()
|
||||
}
|
||||
}
|
||||
|
||||
var textConstrainedWidth = constrainedSize.width - titleHorizontalOffset - 10.0
|
||||
if let actionButtonSize = actionButtonSize {
|
||||
if actionButtonIsCompact {
|
||||
textConstrainedWidth -= actionButtonSize.width * 2.0 + 10.0
|
||||
} else {
|
||||
textConstrainedWidth -= actionButtonSize.width + 10.0
|
||||
}
|
||||
}
|
||||
if clearWidth > 0.0 {
|
||||
textConstrainedWidth -= clearWidth + 8.0
|
||||
}
|
||||
|
||||
let textSize: CGSize
|
||||
if let currentTextLayout = self.currentTextLayout, currentTextLayout.string == title, currentTextLayout.color == color, currentTextLayout.constrainedWidth == textConstrainedWidth {
|
||||
textSize = currentTextLayout.size
|
||||
} else {
|
||||
let font: UIFont
|
||||
let stringValue: String
|
||||
if subtitle == nil {
|
||||
font = Font.medium(13.0)
|
||||
stringValue = title.uppercased()
|
||||
} else {
|
||||
font = Font.semibold(16.0)
|
||||
stringValue = title
|
||||
}
|
||||
let string = NSAttributedString(string: stringValue, font: font, textColor: color)
|
||||
let whiteString = NSAttributedString(string: stringValue, font: font, textColor: .white)
|
||||
let stringBounds = string.boundingRect(with: CGSize(width: textConstrainedWidth, height: 18.0), options: [.usesLineFragmentOrigin, .truncatesLastVisibleLine], context: nil)
|
||||
textSize = CGSize(width: ceil(stringBounds.width), height: ceil(stringBounds.height))
|
||||
self.textLayer.contents = generateImage(textSize, opaque: false, scale: 0.0, rotatedContext: { size, context in
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
UIGraphicsPushContext(context)
|
||||
|
||||
//string.draw(in: stringBounds)
|
||||
string.draw(with: stringBounds, options: [.usesLineFragmentOrigin, .truncatesLastVisibleLine], context: nil)
|
||||
|
||||
UIGraphicsPopContext()
|
||||
})?.cgImage
|
||||
self.tintTextLayer.contents = generateImage(textSize, opaque: false, scale: 0.0, rotatedContext: { size, context in
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
UIGraphicsPushContext(context)
|
||||
|
||||
//whiteString.draw(in: stringBounds)
|
||||
whiteString.draw(with: stringBounds, options: [.usesLineFragmentOrigin, .truncatesLastVisibleLine], context: nil)
|
||||
|
||||
UIGraphicsPopContext()
|
||||
})?.cgImage
|
||||
self.tintTextLayer.isHidden = !needsVibrancy
|
||||
self.currentTextLayout = (title, color, textConstrainedWidth, textSize)
|
||||
}
|
||||
|
||||
var badgeSize: CGSize = .zero
|
||||
if let badge {
|
||||
func generateBadgeImage(color: UIColor) -> UIImage? {
|
||||
let string = NSAttributedString(string: badge, font: Font.semibold(11.0), textColor: .white)
|
||||
let stringBounds = string.boundingRect(with: CGSize(width: 120, height: 18.0), options: [.usesLineFragmentOrigin, .truncatesLastVisibleLine], context: nil)
|
||||
|
||||
let badgeSize = CGSize(width: stringBounds.width + 8.0, height: 16.0)
|
||||
return generateImage(badgeSize, opaque: false, scale: 0.0, rotatedContext: { size, context in
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
|
||||
context.setFillColor(color.cgColor)
|
||||
context.addPath(UIBezierPath(roundedRect: CGRect(origin: .zero, size: badgeSize), cornerRadius: badgeSize.height / 2.0).cgPath)
|
||||
context.fillPath()
|
||||
|
||||
context.setBlendMode(.clear)
|
||||
|
||||
UIGraphicsPushContext(context)
|
||||
|
||||
string.draw(with: CGRect(origin: CGPoint(x: floorToScreenPixels((badgeSize.width - stringBounds.size.width) / 2.0), y: floorToScreenPixels((badgeSize.height - stringBounds.size.height) / 2.0)), size: stringBounds.size), options: [.usesLineFragmentOrigin, .truncatesLastVisibleLine], context: nil)
|
||||
|
||||
UIGraphicsPopContext()
|
||||
})
|
||||
}
|
||||
|
||||
let badgeLayer: SimpleLayer
|
||||
if let current = self.badgeLayer {
|
||||
badgeLayer = current
|
||||
} else {
|
||||
badgeLayer = SimpleLayer()
|
||||
self.badgeLayer = badgeLayer
|
||||
self.layer.addSublayer(badgeLayer)
|
||||
|
||||
if let image = generateBadgeImage(color: color.withMultipliedAlpha(0.66)) {
|
||||
badgeLayer.contents = image.cgImage
|
||||
badgeLayer.bounds = CGRect(origin: .zero, size: image.size)
|
||||
}
|
||||
}
|
||||
badgeSize = badgeLayer.bounds.size
|
||||
|
||||
let tintBadgeLayer: SimpleLayer
|
||||
if let current = self.tintBadgeLayer {
|
||||
tintBadgeLayer = current
|
||||
} else {
|
||||
tintBadgeLayer = SimpleLayer()
|
||||
self.tintBadgeLayer = tintBadgeLayer
|
||||
self.tintContentLayer.addSublayer(tintBadgeLayer)
|
||||
|
||||
if let image = generateBadgeImage(color: .white) {
|
||||
tintBadgeLayer.contents = image.cgImage
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if let badgeLayer = self.badgeLayer {
|
||||
self.badgeLayer = nil
|
||||
badgeLayer.removeFromSuperlayer()
|
||||
}
|
||||
if let tintBadgeLayer = self.tintBadgeLayer {
|
||||
self.tintBadgeLayer = nil
|
||||
tintBadgeLayer.removeFromSuperlayer()
|
||||
}
|
||||
}
|
||||
|
||||
let textFrame: CGRect
|
||||
if subtitle == nil {
|
||||
textFrame = CGRect(origin: CGPoint(x: titleHorizontalOffset + floor((constrainedSize.width - titleHorizontalOffset - (textSize.width + badgeSize.width)) / 2.0), y: textOffsetY), size: textSize)
|
||||
} else {
|
||||
textFrame = CGRect(origin: CGPoint(x: titleHorizontalOffset, y: textOffsetY), size: textSize)
|
||||
}
|
||||
self.textLayer.frame = textFrame
|
||||
self.tintTextLayer.frame = textFrame
|
||||
self.tintTextLayer.isHidden = !needsTintText
|
||||
|
||||
if let badgeLayer = self.badgeLayer, let tintBadgeLayer = self.tintBadgeLayer {
|
||||
badgeLayer.frame = CGRect(origin: CGPoint(x: textFrame.maxX + 4.0, y: 0.0), size: badgeLayer.frame.size)
|
||||
tintBadgeLayer.frame = badgeLayer.frame
|
||||
}
|
||||
|
||||
if isPremiumLocked {
|
||||
let lockIconLayer: SimpleLayer
|
||||
if let current = self.lockIconLayer {
|
||||
lockIconLayer = current
|
||||
} else {
|
||||
lockIconLayer = SimpleLayer()
|
||||
self.lockIconLayer = lockIconLayer
|
||||
self.layer.addSublayer(lockIconLayer)
|
||||
}
|
||||
if let image = PresentationResourcesChat.chatEntityKeyboardLock(theme, color: color) {
|
||||
let imageSize = image.size
|
||||
lockIconLayer.contents = image.cgImage
|
||||
lockIconLayer.frame = CGRect(origin: CGPoint(x: textFrame.minX - imageSize.width - 3.0, y: 2.0 + UIScreenPixel), size: imageSize)
|
||||
} else {
|
||||
lockIconLayer.contents = nil
|
||||
}
|
||||
|
||||
let tintLockIconLayer: SimpleLayer
|
||||
if let current = self.tintLockIconLayer {
|
||||
tintLockIconLayer = current
|
||||
} else {
|
||||
tintLockIconLayer = SimpleLayer()
|
||||
self.tintLockIconLayer = tintLockIconLayer
|
||||
self.tintContentLayer.addSublayer(tintLockIconLayer)
|
||||
}
|
||||
if let image = PresentationResourcesChat.chatEntityKeyboardLock(theme, color: .white) {
|
||||
tintLockIconLayer.contents = image.cgImage
|
||||
tintLockIconLayer.frame = lockIconLayer.frame
|
||||
tintLockIconLayer.isHidden = !needsVibrancy
|
||||
} else {
|
||||
tintLockIconLayer.contents = nil
|
||||
}
|
||||
} else {
|
||||
if let lockIconLayer = self.lockIconLayer {
|
||||
self.lockIconLayer = nil
|
||||
lockIconLayer.removeFromSuperlayer()
|
||||
}
|
||||
if let tintLockIconLayer = self.tintLockIconLayer {
|
||||
self.tintLockIconLayer = nil
|
||||
tintLockIconLayer.removeFromSuperlayer()
|
||||
}
|
||||
}
|
||||
|
||||
let subtitleSize: CGSize
|
||||
if let subtitle = subtitle {
|
||||
var updateSubtitleContents: UIImage?
|
||||
var updateTintSubtitleContents: UIImage?
|
||||
if let currentSubtitleLayout = self.currentSubtitleLayout, currentSubtitleLayout.string == subtitle, currentSubtitleLayout.color == subtitleColor, currentSubtitleLayout.constrainedWidth == textConstrainedWidth {
|
||||
subtitleSize = currentSubtitleLayout.size
|
||||
} else {
|
||||
let string = NSAttributedString(string: subtitle, font: Font.regular(15.0), textColor: subtitleColor)
|
||||
let whiteString = NSAttributedString(string: subtitle, font: Font.regular(15.0), textColor: .white)
|
||||
let stringBounds = string.boundingRect(with: CGSize(width: textConstrainedWidth, height: 100.0), options: .usesLineFragmentOrigin, context: nil)
|
||||
subtitleSize = CGSize(width: ceil(stringBounds.width), height: ceil(stringBounds.height))
|
||||
updateSubtitleContents = generateImage(subtitleSize, opaque: false, scale: 0.0, rotatedContext: { size, context in
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
UIGraphicsPushContext(context)
|
||||
|
||||
string.draw(in: stringBounds)
|
||||
|
||||
UIGraphicsPopContext()
|
||||
})
|
||||
updateTintSubtitleContents = generateImage(subtitleSize, opaque: false, scale: 0.0, rotatedContext: { size, context in
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
UIGraphicsPushContext(context)
|
||||
|
||||
whiteString.draw(in: stringBounds)
|
||||
|
||||
UIGraphicsPopContext()
|
||||
})
|
||||
self.currentSubtitleLayout = (subtitle, subtitleColor, textConstrainedWidth, subtitleSize)
|
||||
}
|
||||
|
||||
let subtitleLayer: SimpleLayer
|
||||
if let current = self.subtitleLayer {
|
||||
subtitleLayer = current
|
||||
} else {
|
||||
subtitleLayer = SimpleLayer()
|
||||
self.subtitleLayer = subtitleLayer
|
||||
self.layer.addSublayer(subtitleLayer)
|
||||
}
|
||||
|
||||
if let updateSubtitleContents = updateSubtitleContents {
|
||||
subtitleLayer.contents = updateSubtitleContents.cgImage
|
||||
}
|
||||
|
||||
let tintSubtitleLayer: SimpleLayer
|
||||
if let current = self.tintSubtitleLayer {
|
||||
tintSubtitleLayer = current
|
||||
} else {
|
||||
tintSubtitleLayer = SimpleLayer()
|
||||
self.tintSubtitleLayer = tintSubtitleLayer
|
||||
self.tintContentLayer.addSublayer(tintSubtitleLayer)
|
||||
}
|
||||
tintSubtitleLayer.isHidden = !needsVibrancy
|
||||
|
||||
if let updateTintSubtitleContents = updateTintSubtitleContents {
|
||||
tintSubtitleLayer.contents = updateTintSubtitleContents.cgImage
|
||||
}
|
||||
|
||||
let subtitleFrame = CGRect(origin: CGPoint(x: 0.0, y: textFrame.maxY + 1.0), size: subtitleSize)
|
||||
subtitleLayer.frame = subtitleFrame
|
||||
tintSubtitleLayer.frame = subtitleFrame
|
||||
} else {
|
||||
subtitleSize = CGSize()
|
||||
if let subtitleLayer = self.subtitleLayer {
|
||||
self.subtitleLayer = nil
|
||||
subtitleLayer.removeFromSuperlayer()
|
||||
}
|
||||
if let tintSubtitleLayer = self.tintSubtitleLayer {
|
||||
self.tintSubtitleLayer = nil
|
||||
tintSubtitleLayer.removeFromSuperlayer()
|
||||
}
|
||||
}
|
||||
|
||||
self.clearIconLayer?.frame = CGRect(origin: CGPoint(x: constrainedSize.width - clearSize.width, y: floorToScreenPixels((textSize.height - clearSize.height) / 2.0)), size: clearSize)
|
||||
|
||||
var size: CGSize
|
||||
size = CGSize(width: constrainedSize.width, height: constrainedSize.height)
|
||||
|
||||
if let embeddedItems = embeddedItems {
|
||||
let groupEmbeddedView: GroupEmbeddedView
|
||||
if let current = self.groupEmbeddedView {
|
||||
groupEmbeddedView = current
|
||||
} else {
|
||||
groupEmbeddedView = GroupEmbeddedView(performItemAction: self.performItemAction)
|
||||
self.groupEmbeddedView = groupEmbeddedView
|
||||
self.addSubview(groupEmbeddedView)
|
||||
}
|
||||
|
||||
let groupEmbeddedViewSize = CGSize(width: constrainedSize.width + insets.left + insets.right, height: 36.0)
|
||||
groupEmbeddedView.frame = CGRect(origin: CGPoint(x: -insets.left, y: size.height - groupEmbeddedViewSize.height), size: groupEmbeddedViewSize)
|
||||
groupEmbeddedView.update(
|
||||
context: context,
|
||||
theme: theme,
|
||||
insets: insets,
|
||||
size: groupEmbeddedViewSize,
|
||||
items: embeddedItems,
|
||||
isStickers: isStickers,
|
||||
cache: cache,
|
||||
renderer: renderer,
|
||||
attemptSynchronousLoad: attemptSynchronousLoad
|
||||
)
|
||||
} else {
|
||||
if let groupEmbeddedView = self.groupEmbeddedView {
|
||||
self.groupEmbeddedView = nil
|
||||
groupEmbeddedView.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
|
||||
if let actionButtonSize = actionButtonSize, let actionButton = self.actionButton {
|
||||
let actionButtonFrame = CGRect(origin: CGPoint(x: size.width - actionButtonSize.width, y: textFrame.minY + (actionButtonIsCompact ? 0.0 : 3.0)), size: actionButtonSize)
|
||||
actionButton.bounds = CGRect(origin: CGPoint(), size: actionButtonFrame.size)
|
||||
actionButton.center = actionButtonFrame.center
|
||||
}
|
||||
|
||||
if hasTopSeparator {
|
||||
let separatorLayer: SimpleLayer
|
||||
if let current = self.separatorLayer {
|
||||
separatorLayer = current
|
||||
} else {
|
||||
separatorLayer = SimpleLayer()
|
||||
self.separatorLayer = separatorLayer
|
||||
self.layer.addSublayer(separatorLayer)
|
||||
}
|
||||
separatorLayer.backgroundColor = subtitleColor.cgColor
|
||||
separatorLayer.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: UIScreenPixel))
|
||||
|
||||
let tintSeparatorLayer: SimpleLayer
|
||||
if let current = self.tintSeparatorLayer {
|
||||
tintSeparatorLayer = current
|
||||
} else {
|
||||
tintSeparatorLayer = SimpleLayer()
|
||||
self.tintSeparatorLayer = tintSeparatorLayer
|
||||
self.tintContentLayer.addSublayer(tintSeparatorLayer)
|
||||
}
|
||||
tintSeparatorLayer.backgroundColor = UIColor.white.cgColor
|
||||
tintSeparatorLayer.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: UIScreenPixel))
|
||||
|
||||
tintSeparatorLayer.isHidden = !needsVibrancy
|
||||
} else {
|
||||
if let separatorLayer = self.separatorLayer {
|
||||
self.separatorLayer = separatorLayer
|
||||
separatorLayer.removeFromSuperlayer()
|
||||
}
|
||||
if let tintSeparatorLayer = self.tintSeparatorLayer {
|
||||
self.tintSeparatorLayer = tintSeparatorLayer
|
||||
tintSeparatorLayer.removeFromSuperlayer()
|
||||
}
|
||||
}
|
||||
|
||||
return (size, titleHorizontalOffset + textSize.width + clearWidth)
|
||||
}
|
||||
|
||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
return super.hitTest(point, with: event)
|
||||
}
|
||||
|
||||
func tapGesture(point: CGPoint) -> Bool {
|
||||
if let groupEmbeddedView = self.groupEmbeddedView {
|
||||
return groupEmbeddedView.tapGesture(point: self.convert(point, to: groupEmbeddedView))
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,346 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import ComponentFlow
|
||||
|
||||
public class PassthroughLayer: CALayer {
|
||||
public var mirrorLayer: CALayer?
|
||||
|
||||
override init() {
|
||||
super.init()
|
||||
}
|
||||
|
||||
override init(layer: Any) {
|
||||
super.init(layer: layer)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override public var position: CGPoint {
|
||||
get {
|
||||
return super.position
|
||||
} set(value) {
|
||||
if let mirrorLayer = self.mirrorLayer {
|
||||
mirrorLayer.position = value
|
||||
}
|
||||
super.position = value
|
||||
}
|
||||
}
|
||||
|
||||
override public var bounds: CGRect {
|
||||
get {
|
||||
return super.bounds
|
||||
} set(value) {
|
||||
if let mirrorLayer = self.mirrorLayer {
|
||||
mirrorLayer.bounds = value
|
||||
}
|
||||
super.bounds = value
|
||||
}
|
||||
}
|
||||
|
||||
override public var opacity: Float {
|
||||
get {
|
||||
return super.opacity
|
||||
} set(value) {
|
||||
if let mirrorLayer = self.mirrorLayer {
|
||||
mirrorLayer.opacity = value
|
||||
}
|
||||
super.opacity = value
|
||||
}
|
||||
}
|
||||
|
||||
override public var sublayerTransform: CATransform3D {
|
||||
get {
|
||||
return super.sublayerTransform
|
||||
} set(value) {
|
||||
if let mirrorLayer = self.mirrorLayer {
|
||||
mirrorLayer.sublayerTransform = value
|
||||
}
|
||||
super.sublayerTransform = value
|
||||
}
|
||||
}
|
||||
|
||||
override public var transform: CATransform3D {
|
||||
get {
|
||||
return super.transform
|
||||
} set(value) {
|
||||
if let mirrorLayer = self.mirrorLayer {
|
||||
mirrorLayer.transform = value
|
||||
}
|
||||
super.transform = value
|
||||
}
|
||||
}
|
||||
|
||||
override public func add(_ animation: CAAnimation, forKey key: String?) {
|
||||
if let mirrorLayer = self.mirrorLayer {
|
||||
mirrorLayer.add(animation, forKey: key)
|
||||
}
|
||||
|
||||
super.add(animation, forKey: key)
|
||||
}
|
||||
|
||||
override public func removeAllAnimations() {
|
||||
if let mirrorLayer = self.mirrorLayer {
|
||||
mirrorLayer.removeAllAnimations()
|
||||
}
|
||||
|
||||
super.removeAllAnimations()
|
||||
}
|
||||
|
||||
override public func removeAnimation(forKey: String) {
|
||||
if let mirrorLayer = self.mirrorLayer {
|
||||
mirrorLayer.removeAnimation(forKey: forKey)
|
||||
}
|
||||
|
||||
super.removeAnimation(forKey: forKey)
|
||||
}
|
||||
}
|
||||
|
||||
open class PassthroughView: UIView {
|
||||
override public static var layerClass: AnyClass {
|
||||
return PassthroughLayer.self
|
||||
}
|
||||
|
||||
public let passthroughView: UIView
|
||||
|
||||
override public init(frame: CGRect) {
|
||||
self.passthroughView = UIView()
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
(self.layer as? PassthroughLayer)?.mirrorLayer = self.passthroughView.layer
|
||||
}
|
||||
|
||||
required public init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
}
|
||||
|
||||
class PassthroughShapeLayer: CAShapeLayer {
|
||||
var mirrorLayer: CAShapeLayer?
|
||||
|
||||
override init() {
|
||||
super.init()
|
||||
}
|
||||
|
||||
override init(layer: Any) {
|
||||
super.init(layer: layer)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override var position: CGPoint {
|
||||
get {
|
||||
return super.position
|
||||
} set(value) {
|
||||
if let mirrorLayer = self.mirrorLayer {
|
||||
mirrorLayer.position = value
|
||||
}
|
||||
super.position = value
|
||||
}
|
||||
}
|
||||
|
||||
override var bounds: CGRect {
|
||||
get {
|
||||
return super.bounds
|
||||
} set(value) {
|
||||
if let mirrorLayer = self.mirrorLayer {
|
||||
mirrorLayer.bounds = value
|
||||
}
|
||||
super.bounds = value
|
||||
}
|
||||
}
|
||||
|
||||
override var opacity: Float {
|
||||
get {
|
||||
return super.opacity
|
||||
} set(value) {
|
||||
if let mirrorLayer = self.mirrorLayer {
|
||||
mirrorLayer.opacity = value
|
||||
}
|
||||
super.opacity = value
|
||||
}
|
||||
}
|
||||
|
||||
override var sublayerTransform: CATransform3D {
|
||||
get {
|
||||
return super.sublayerTransform
|
||||
} set(value) {
|
||||
if let mirrorLayer = self.mirrorLayer {
|
||||
mirrorLayer.sublayerTransform = value
|
||||
}
|
||||
super.sublayerTransform = value
|
||||
}
|
||||
}
|
||||
|
||||
override var transform: CATransform3D {
|
||||
get {
|
||||
return super.transform
|
||||
} set(value) {
|
||||
if let mirrorLayer = self.mirrorLayer {
|
||||
mirrorLayer.transform = value
|
||||
}
|
||||
super.transform = value
|
||||
}
|
||||
}
|
||||
|
||||
override var path: CGPath? {
|
||||
get {
|
||||
return super.path
|
||||
} set(value) {
|
||||
if let mirrorLayer = self.mirrorLayer {
|
||||
mirrorLayer.path = value
|
||||
}
|
||||
super.path = value
|
||||
}
|
||||
}
|
||||
|
||||
override var fillColor: CGColor? {
|
||||
get {
|
||||
return super.fillColor
|
||||
} set(value) {
|
||||
if let mirrorLayer = self.mirrorLayer {
|
||||
mirrorLayer.fillColor = value
|
||||
}
|
||||
super.fillColor = value
|
||||
}
|
||||
}
|
||||
|
||||
override var fillRule: CAShapeLayerFillRule {
|
||||
get {
|
||||
return super.fillRule
|
||||
} set(value) {
|
||||
if let mirrorLayer = self.mirrorLayer {
|
||||
mirrorLayer.fillRule = value
|
||||
}
|
||||
super.fillRule = value
|
||||
}
|
||||
}
|
||||
|
||||
override var strokeColor: CGColor? {
|
||||
get {
|
||||
return super.strokeColor
|
||||
} set(value) {
|
||||
/*if let mirrorLayer = self.mirrorLayer {
|
||||
mirrorLayer.strokeColor = value
|
||||
}*/
|
||||
super.strokeColor = value
|
||||
}
|
||||
}
|
||||
|
||||
override var strokeStart: CGFloat {
|
||||
get {
|
||||
return super.strokeStart
|
||||
} set(value) {
|
||||
if let mirrorLayer = self.mirrorLayer {
|
||||
mirrorLayer.strokeStart = value
|
||||
}
|
||||
super.strokeStart = value
|
||||
}
|
||||
}
|
||||
|
||||
override var strokeEnd: CGFloat {
|
||||
get {
|
||||
return super.strokeEnd
|
||||
} set(value) {
|
||||
if let mirrorLayer = self.mirrorLayer {
|
||||
mirrorLayer.strokeEnd = value
|
||||
}
|
||||
super.strokeEnd = value
|
||||
}
|
||||
}
|
||||
|
||||
override var lineWidth: CGFloat {
|
||||
get {
|
||||
return super.lineWidth
|
||||
} set(value) {
|
||||
if let mirrorLayer = self.mirrorLayer {
|
||||
mirrorLayer.lineWidth = value
|
||||
}
|
||||
super.lineWidth = value
|
||||
}
|
||||
}
|
||||
|
||||
override var miterLimit: CGFloat {
|
||||
get {
|
||||
return super.miterLimit
|
||||
} set(value) {
|
||||
if let mirrorLayer = self.mirrorLayer {
|
||||
mirrorLayer.miterLimit = value
|
||||
}
|
||||
super.miterLimit = value
|
||||
}
|
||||
}
|
||||
|
||||
override var lineCap: CAShapeLayerLineCap {
|
||||
get {
|
||||
return super.lineCap
|
||||
} set(value) {
|
||||
if let mirrorLayer = self.mirrorLayer {
|
||||
mirrorLayer.lineCap = value
|
||||
}
|
||||
super.lineCap = value
|
||||
}
|
||||
}
|
||||
|
||||
override var lineJoin: CAShapeLayerLineJoin {
|
||||
get {
|
||||
return super.lineJoin
|
||||
} set(value) {
|
||||
if let mirrorLayer = self.mirrorLayer {
|
||||
mirrorLayer.lineJoin = value
|
||||
}
|
||||
super.lineJoin = value
|
||||
}
|
||||
}
|
||||
|
||||
override var lineDashPhase: CGFloat {
|
||||
get {
|
||||
return super.lineDashPhase
|
||||
} set(value) {
|
||||
if let mirrorLayer = self.mirrorLayer {
|
||||
mirrorLayer.lineDashPhase = value
|
||||
}
|
||||
super.lineDashPhase = value
|
||||
}
|
||||
}
|
||||
|
||||
override var lineDashPattern: [NSNumber]? {
|
||||
get {
|
||||
return super.lineDashPattern
|
||||
} set(value) {
|
||||
if let mirrorLayer = self.mirrorLayer {
|
||||
mirrorLayer.lineDashPattern = value
|
||||
}
|
||||
super.lineDashPattern = value
|
||||
}
|
||||
}
|
||||
|
||||
override func add(_ animation: CAAnimation, forKey key: String?) {
|
||||
if let mirrorLayer = self.mirrorLayer {
|
||||
mirrorLayer.add(animation, forKey: key)
|
||||
}
|
||||
|
||||
super.add(animation, forKey: key)
|
||||
}
|
||||
|
||||
override func removeAllAnimations() {
|
||||
if let mirrorLayer = self.mirrorLayer {
|
||||
mirrorLayer.removeAllAnimations()
|
||||
}
|
||||
|
||||
super.removeAllAnimations()
|
||||
}
|
||||
|
||||
override func removeAnimation(forKey: String) {
|
||||
if let mirrorLayer = self.mirrorLayer {
|
||||
mirrorLayer.removeAnimation(forKey: forKey)
|
||||
}
|
||||
|
||||
super.removeAnimation(forKey: forKey)
|
||||
}
|
||||
}
|
@ -0,0 +1,140 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import ComponentFlow
|
||||
import AccountContext
|
||||
import TelegramCore
|
||||
|
||||
private let premiumBadgeIcon: UIImage? = generateTintedImage(image: UIImage(bundleImageName: "Chat List/PeerPremiumIcon"), color: .white)
|
||||
private let featuredBadgeIcon: UIImage? = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Media/PanelBadgeAdd"), color: .white)
|
||||
private let lockedBadgeIcon: UIImage? = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Media/PanelBadgeLock"), color: .white)
|
||||
|
||||
private let itemBadgeTextFont: UIFont = {
|
||||
return Font.regular(10.0)
|
||||
}()
|
||||
|
||||
final class PremiumBadgeView: UIView {
|
||||
private let context: AccountContext
|
||||
|
||||
private var badge: EmojiKeyboardItemLayer.Badge?
|
||||
|
||||
let contentLayer: SimpleLayer
|
||||
private let overlayColorLayer: SimpleLayer
|
||||
private let iconLayer: SimpleLayer
|
||||
private var customFileLayer: InlineFileIconLayer?
|
||||
|
||||
init(context: AccountContext) {
|
||||
self.context = context
|
||||
|
||||
self.contentLayer = SimpleLayer()
|
||||
self.contentLayer.contentsGravity = .resize
|
||||
self.contentLayer.masksToBounds = true
|
||||
|
||||
self.overlayColorLayer = SimpleLayer()
|
||||
self.overlayColorLayer.masksToBounds = true
|
||||
|
||||
self.iconLayer = SimpleLayer()
|
||||
|
||||
super.init(frame: CGRect())
|
||||
|
||||
self.layer.addSublayer(self.contentLayer)
|
||||
self.layer.addSublayer(self.overlayColorLayer)
|
||||
self.layer.addSublayer(self.iconLayer)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func update(transition: Transition, badge: EmojiKeyboardItemLayer.Badge, backgroundColor: UIColor, size: CGSize) {
|
||||
if self.badge != badge {
|
||||
self.badge = badge
|
||||
|
||||
switch badge {
|
||||
case .premium:
|
||||
self.iconLayer.contents = premiumBadgeIcon?.cgImage
|
||||
case .featured:
|
||||
self.iconLayer.contents = featuredBadgeIcon?.cgImage
|
||||
case .locked:
|
||||
self.iconLayer.contents = lockedBadgeIcon?.cgImage
|
||||
case let .text(text):
|
||||
let string = NSAttributedString(string: text, font: itemBadgeTextFont)
|
||||
let size = CGSize(width: 12.0, height: 12.0)
|
||||
let stringBounds = string.boundingRect(with: CGSize(width: 100.0, height: 100.0), options: .usesLineFragmentOrigin, context: nil)
|
||||
let image = generateImage(size, rotatedContext: { size, context in
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
UIGraphicsPushContext(context)
|
||||
string.draw(at: CGPoint(x: floor((size.width - stringBounds.width) * 0.5), y: floor((size.height - stringBounds.height) * 0.5)))
|
||||
UIGraphicsPopContext()
|
||||
})
|
||||
self.iconLayer.contents = image?.cgImage
|
||||
case .customFile:
|
||||
self.iconLayer.contents = nil
|
||||
}
|
||||
|
||||
if case let .customFile(customFile) = badge {
|
||||
let customFileLayer: InlineFileIconLayer
|
||||
if let current = self.customFileLayer {
|
||||
customFileLayer = current
|
||||
} else {
|
||||
customFileLayer = InlineFileIconLayer(
|
||||
context: self.context,
|
||||
userLocation: .other,
|
||||
attemptSynchronousLoad: false,
|
||||
file: customFile,
|
||||
cache: self.context.animationCache,
|
||||
renderer: self.context.animationRenderer,
|
||||
unique: false,
|
||||
placeholderColor: .clear,
|
||||
pointSize: CGSize(width: 18.0, height: 18.0),
|
||||
dynamicColor: nil
|
||||
)
|
||||
self.customFileLayer = customFileLayer
|
||||
self.layer.addSublayer(customFileLayer)
|
||||
}
|
||||
let _ = customFileLayer
|
||||
} else {
|
||||
if let customFileLayer = self.customFileLayer {
|
||||
self.customFileLayer = nil
|
||||
customFileLayer.removeFromSuperlayer()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let iconInset: CGFloat
|
||||
switch badge {
|
||||
case .premium:
|
||||
iconInset = 2.0
|
||||
case .featured:
|
||||
iconInset = 0.0
|
||||
case .locked:
|
||||
iconInset = 0.0
|
||||
case .text, .customFile:
|
||||
iconInset = 0.0
|
||||
}
|
||||
|
||||
switch badge {
|
||||
case .text, .customFile:
|
||||
self.contentLayer.isHidden = true
|
||||
self.overlayColorLayer.isHidden = true
|
||||
default:
|
||||
self.contentLayer.isHidden = false
|
||||
self.overlayColorLayer.isHidden = false
|
||||
}
|
||||
|
||||
self.overlayColorLayer.backgroundColor = backgroundColor.cgColor
|
||||
|
||||
transition.setFrame(layer: self.contentLayer, frame: CGRect(origin: CGPoint(), size: size))
|
||||
transition.setCornerRadius(layer: self.contentLayer, cornerRadius: min(size.width / 2.0, size.height / 2.0))
|
||||
|
||||
transition.setFrame(layer: self.overlayColorLayer, frame: CGRect(origin: CGPoint(), size: size))
|
||||
transition.setCornerRadius(layer: self.overlayColorLayer, cornerRadius: min(size.width / 2.0, size.height / 2.0))
|
||||
|
||||
transition.setFrame(layer: self.iconLayer, frame: CGRect(origin: CGPoint(), size: size).insetBy(dx: iconInset, dy: iconInset))
|
||||
|
||||
if let customFileLayer = self.customFileLayer {
|
||||
let iconSize = CGSize(width: 18.0, height: 18.0)
|
||||
transition.setFrame(layer: customFileLayer, frame: CGRect(origin: CGPoint(), size: iconSize))
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,138 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import ComponentFlow
|
||||
import TelegramPresentationData
|
||||
|
||||
final class WarpView: UIView {
|
||||
private final class WarpPartView: UIView {
|
||||
let cloneView: PortalView
|
||||
|
||||
init?(contentView: PortalSourceView) {
|
||||
guard let cloneView = PortalView(matchPosition: false) else {
|
||||
return nil
|
||||
}
|
||||
self.cloneView = cloneView
|
||||
|
||||
super.init(frame: CGRect())
|
||||
|
||||
self.layer.anchorPoint = CGPoint(x: 0.5, y: 0.0)
|
||||
|
||||
self.clipsToBounds = true
|
||||
self.addSubview(cloneView.view)
|
||||
contentView.addPortal(view: cloneView)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func update(containerSize: CGSize, rect: CGRect, transition: Transition) {
|
||||
transition.setFrame(view: self.cloneView.view, frame: CGRect(origin: CGPoint(x: -rect.minX, y: -rect.minY), size: CGSize(width: containerSize.width, height: containerSize.height)))
|
||||
}
|
||||
}
|
||||
|
||||
let contentView: PortalSourceView
|
||||
|
||||
private let clippingView: UIView
|
||||
|
||||
private var warpViews: [WarpPartView] = []
|
||||
private let warpMaskContainer: UIView
|
||||
private let warpMaskGradientLayer: SimpleGradientLayer
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.contentView = PortalSourceView()
|
||||
self.clippingView = UIView()
|
||||
|
||||
self.warpMaskContainer = UIView()
|
||||
self.warpMaskGradientLayer = SimpleGradientLayer()
|
||||
self.warpMaskContainer.layer.mask = self.warpMaskGradientLayer
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.clippingView.addSubview(self.contentView)
|
||||
|
||||
self.clippingView.clipsToBounds = true
|
||||
self.addSubview(self.clippingView)
|
||||
self.addSubview(self.warpMaskContainer)
|
||||
|
||||
for _ in 0 ..< 8 {
|
||||
if let warpView = WarpPartView(contentView: self.contentView) {
|
||||
self.warpViews.append(warpView)
|
||||
self.warpMaskContainer.addSubview(warpView)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func update(size: CGSize, topInset: CGFloat, warpHeight: CGFloat, theme: PresentationTheme, transition: Transition) {
|
||||
transition.setFrame(view: self.contentView, frame: CGRect(origin: CGPoint(), size: size))
|
||||
|
||||
let allItemsHeight = warpHeight * 0.5
|
||||
for i in 0 ..< self.warpViews.count {
|
||||
let itemHeight = warpHeight / CGFloat(self.warpViews.count)
|
||||
let itemFraction = CGFloat(i + 1) / CGFloat(self.warpViews.count)
|
||||
let _ = itemHeight
|
||||
|
||||
let da = CGFloat.pi * 0.5 / CGFloat(self.warpViews.count)
|
||||
let alpha = CGFloat.pi * 0.5 - itemFraction * CGFloat.pi * 0.5
|
||||
let endPoint = CGPoint(x: cos(alpha), y: sin(alpha))
|
||||
let prevAngle = alpha + da
|
||||
let prevPt = CGPoint(x: cos(prevAngle), y: sin(prevAngle))
|
||||
var angle: CGFloat
|
||||
angle = -atan2(endPoint.y - prevPt.y, endPoint.x - prevPt.x)
|
||||
|
||||
let itemLengthVector = CGPoint(x: endPoint.x - prevPt.x, y: endPoint.y - prevPt.y)
|
||||
let itemLength = sqrt(itemLengthVector.x * itemLengthVector.x + itemLengthVector.y * itemLengthVector.y) * warpHeight * 0.5
|
||||
let _ = itemLength
|
||||
|
||||
var transform: CATransform3D
|
||||
transform = CATransform3DIdentity
|
||||
transform.m34 = 1.0 / 240.0
|
||||
|
||||
transform = CATransform3DTranslate(transform, 0.0, prevPt.x * allItemsHeight, (1.0 - prevPt.y) * allItemsHeight)
|
||||
transform = CATransform3DRotate(transform, angle, 1.0, 0.0, 0.0)
|
||||
|
||||
let positionY = size.height - allItemsHeight + 4.0 + CGFloat(i) * itemLength
|
||||
let rect = CGRect(origin: CGPoint(x: 0.0, y: positionY), size: CGSize(width: size.width, height: itemLength))
|
||||
transition.setPosition(view: self.warpViews[i], position: CGPoint(x: rect.midX, y: 4.0))
|
||||
transition.setBounds(view: self.warpViews[i], bounds: CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: itemLength)))
|
||||
transition.setTransform(view: self.warpViews[i], transform: transform)
|
||||
self.warpViews[i].update(containerSize: size, rect: rect, transition: transition)
|
||||
}
|
||||
|
||||
let clippingTopInset: CGFloat = topInset
|
||||
let frame = CGRect(origin: CGPoint(x: 0.0, y: clippingTopInset), size: CGSize(width: size.width, height: -clippingTopInset + size.height - 21.0))
|
||||
transition.setPosition(view: self.clippingView, position: frame.center)
|
||||
transition.setBounds(view: self.clippingView, bounds: CGRect(origin: CGPoint(x: 0.0, y: clippingTopInset), size: frame.size))
|
||||
self.clippingView.clipsToBounds = true
|
||||
|
||||
transition.setFrame(view: self.warpMaskContainer, frame: CGRect(origin: CGPoint(x: 0.0, y: size.height - allItemsHeight), size: CGSize(width: size.width, height: allItemsHeight)))
|
||||
|
||||
var locations: [NSNumber] = []
|
||||
var colors: [CGColor] = []
|
||||
let numStops = 6
|
||||
for i in 0 ..< numStops {
|
||||
let step = CGFloat(i) / CGFloat(numStops - 1)
|
||||
locations.append(step as NSNumber)
|
||||
colors.append(UIColor.black.withAlphaComponent(1.0 - step * step).cgColor)
|
||||
}
|
||||
|
||||
let gradientHeight: CGFloat = 6.0
|
||||
self.warpMaskGradientLayer.startPoint = CGPoint(x: 0.0, y: (allItemsHeight - gradientHeight) / allItemsHeight)
|
||||
self.warpMaskGradientLayer.endPoint = CGPoint(x: 0.0, y: 1.0)
|
||||
|
||||
self.warpMaskGradientLayer.locations = locations
|
||||
self.warpMaskGradientLayer.colors = colors
|
||||
self.warpMaskGradientLayer.type = .axial
|
||||
|
||||
transition.setFrame(layer: self.warpMaskGradientLayer, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: allItemsHeight)))
|
||||
}
|
||||
|
||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
return self.contentView.hitTest(point, with: event)
|
||||
}
|
||||
}
|
@ -105,7 +105,7 @@ final class EmojiListInputComponent: Component {
|
||||
private var component: EmojiListInputComponent?
|
||||
private weak var state: EmptyComponentState?
|
||||
|
||||
private var itemLayers: [Int64: EmojiPagerContentComponent.View.ItemLayer] = [:]
|
||||
private var itemLayers: [Int64: EmojiKeyboardItemLayer] = [:]
|
||||
private let trailingPlaceholder = ComponentView<Empty>()
|
||||
private let caretIndicator: CaretIndicatorView
|
||||
|
||||
@ -239,7 +239,7 @@ final class EmojiListInputComponent: Component {
|
||||
|
||||
var itemTransition = transition
|
||||
var animateIn = false
|
||||
let itemLayer: EmojiPagerContentComponent.View.ItemLayer
|
||||
let itemLayer: EmojiKeyboardItemLayer
|
||||
if let current = self.itemLayers[itemKey] {
|
||||
itemLayer = current
|
||||
} else {
|
||||
@ -249,7 +249,7 @@ final class EmojiListInputComponent: Component {
|
||||
let animationData = EntityKeyboardAnimationData(
|
||||
file: item.file
|
||||
)
|
||||
itemLayer = EmojiPagerContentComponent.View.ItemLayer(
|
||||
itemLayer = EmojiKeyboardItemLayer(
|
||||
item: EmojiPagerContentComponent.Item(
|
||||
animationData: animationData,
|
||||
content: .animation(animationData),
|
||||
|
Loading…
x
Reference in New Issue
Block a user