Initial avatar editor implementation

This commit is contained in:
Ilya Laktyushin 2023-01-11 01:05:10 +04:00
parent 1bc6d85f0f
commit 89481bdef2
19 changed files with 3242 additions and 64 deletions

View File

@ -186,6 +186,7 @@ public final class PagerComponent<ChildEnvironmentType: Equatable, TopPanelEnvir
public let panelStateUpdated: ((PagerComponentPanelState, Transition) -> Void)?
public let isTopPanelExpandedUpdated: (Bool, Transition) -> Void
public let isTopPanelHiddenUpdated: (Bool, Transition) -> Void
public let contentIdUpdated: (AnyHashable) -> Void
public let panelHideBehavior: PagerComponentPanelHideBehavior
public let clipContentToTopPanel: Bool
@ -205,6 +206,7 @@ public final class PagerComponent<ChildEnvironmentType: Equatable, TopPanelEnvir
panelStateUpdated: ((PagerComponentPanelState, Transition) -> Void)?,
isTopPanelExpandedUpdated: @escaping (Bool, Transition) -> Void,
isTopPanelHiddenUpdated: @escaping (Bool, Transition) -> Void,
contentIdUpdated: @escaping (AnyHashable) -> Void,
panelHideBehavior: PagerComponentPanelHideBehavior,
clipContentToTopPanel: Bool
) {
@ -223,6 +225,7 @@ public final class PagerComponent<ChildEnvironmentType: Equatable, TopPanelEnvir
self.panelStateUpdated = panelStateUpdated
self.isTopPanelExpandedUpdated = isTopPanelExpandedUpdated
self.isTopPanelHiddenUpdated = isTopPanelHiddenUpdated
self.contentIdUpdated = contentIdUpdated
self.panelHideBehavior = panelHideBehavior
self.clipContentToTopPanel = clipContentToTopPanel
}
@ -397,12 +400,33 @@ public final class PagerComponent<ChildEnvironmentType: Equatable, TopPanelEnvir
} else {
self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)))
}
if let centralId = self.centralId {
self.component?.contentIdUpdated(centralId)
}
}
default:
break
}
}
public func navigateToContentId(_ id: AnyHashable) {
var updateTopPanelExpanded = false
if self.centralId != id {
self.centralId = id
if self.isTopPanelExpanded {
updateTopPanelExpanded = true
}
}
if updateTopPanelExpanded {
self.isTopPanelExpandedUpdated(isExpanded: false, transition: Transition(animation: .curve(duration: 0.4, curve: .spring)))
} else {
self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)))
}
self.component?.contentIdUpdated(id)
}
func update(component: PagerComponent<ChildEnvironmentType, TopPanelEnvironment>, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
let previousPanelHideBehavior = self.component?.panelHideBehavior
@ -418,21 +442,7 @@ public final class PagerComponent<ChildEnvironmentType: Equatable, TopPanelEnvir
guard let strongSelf = self else {
return
}
var updateTopPanelExpanded = false
if strongSelf.centralId != id {
strongSelf.centralId = id
if strongSelf.isTopPanelExpanded {
updateTopPanelExpanded = true
}
}
if updateTopPanelExpanded {
strongSelf.isTopPanelExpandedUpdated(isExpanded: false, transition: Transition(animation: .curve(duration: 0.4, curve: .spring)))
} else {
strongSelf.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)))
}
strongSelf.navigateToContentId(id)
}
var centralId: AnyHashable?

View File

@ -195,8 +195,8 @@ final class DrawingStickerEntityView: DrawingEntityView {
if let animationNode = animationNode {
let _ = (animationNode.status
|> take(1)
|> deliverOnMainQueue).start(next: { [weak self] status in
|> take(1)
|> deliverOnMainQueue).start(next: { [weak self] status in
self?.started?(status.duration)
})
}
@ -273,7 +273,7 @@ final class DrawingStickerEntityView: DrawingEntityView {
self.animationNode?.setup(source: source, width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), playbackMode: .loop, mode: .direct(cachePathPrefix: nil))
self.cachedDisposable.set((source.cachedDataPath(width: 384, height: 384)
|> deliverOn(Queue.concurrentDefaultQueue())).start())
|> deliverOn(Queue.concurrentDefaultQueue())).start())
}
}
self.animationNode?.visibility = isPlaying

View File

@ -154,6 +154,7 @@ private final class StickerSelectionComponent: Component {
switchToGifSubject: { _ in },
reorderItems: { _, _ in },
makeSearchContainerNode: { _ in return nil },
contentIdUpdated: { _ in },
deviceMetrics: component.deviceMetrics,
hiddenInputHeight: 0.0,
inputHeight: 0.0,

View File

@ -26,6 +26,8 @@ typedef void (^TGMediaAvatarPresentImpl)(id<LegacyComponentsContext>, void (^)(U
@property (nonatomic, copy) void (^requestSearchController)(TGMediaAssetsController *);
@property (nonatomic, copy) CGRect (^sourceRect)(void);
@property (nonatomic, copy) void (^requestAvatarEditor)(void (^)(UIImage *image, NSURL *asset, TGVideoEditAdjustments *adjustments, void(^commit)(void)));
@property (nonatomic, strong) id<TGPhotoPaintStickersContext> stickersContext;
- (instancetype)initWithContext:(id<LegacyComponentsContext>)context parentController:(TGViewController *)parentController hasDeleteButton:(bool)hasDeleteButton saveEditedPhotos:(bool)saveEditedPhotos saveCapturedMedia:(bool)saveCapturedMedia;

View File

@ -193,24 +193,60 @@
}];
[itemViews addObject:galleryItem];
// if (_hasSearchButton)
// {
// TGMenuSheetButtonItemView *viewItem = [[TGMenuSheetButtonItemView alloc] initWithTitle:TGLocalized(@"ProfilePhoto.SearchWeb") type:TGMenuSheetButtonTypeDefault fontSize:20.0 action:^
// {
// __strong TGMediaAvatarMenuMixin *strongSelf = weakSelf;
// if (strongSelf == nil)
// return;
//
// __strong TGMenuSheetController *strongController = weakController;
// if (strongController == nil)
// return;
//
// [strongController dismissAnimated:true];
// if (strongSelf != nil)
// strongSelf.requestSearchController(nil);
// }];
// [itemViews addObject:viewItem];
// }
if (!_signup) {
TGMenuSheetButtonItemView *viewItem = [[TGMenuSheetButtonItemView alloc] initWithTitle:@"Emoji or Sticker" type:TGMenuSheetButtonTypeDefault fontSize:20.0 action:^
{
__strong TGMediaAvatarMenuMixin *strongSelf = weakSelf;
if (strongSelf == nil)
return;
__strong TGMenuSheetController *strongController = weakController;
if (strongController == nil)
return;
[strongController dismissAnimated:true];
if (strongSelf != nil)
strongSelf.requestAvatarEditor(^(UIImage *image, NSURL *asset, TGVideoEditAdjustments *adjustments, void (^commit)(void)) {
__strong TGMediaAvatarMenuMixin *strongSelf = weakSelf;
if (strongSelf == nil)
return;
if (strongSelf.willFinishWithVideo != nil) {
strongSelf.willFinishWithVideo(image, ^{
if (strongSelf.didFinishWithVideo != nil)
strongSelf.didFinishWithVideo(image, asset, adjustments);
commit();
});
} else {
if (strongSelf.didFinishWithVideo != nil)
strongSelf.didFinishWithVideo(image, asset, adjustments);
commit();
}
});
}];
[itemViews addObject:viewItem];
}
if (_hasSearchButton)
{
TGMenuSheetButtonItemView *viewItem = [[TGMenuSheetButtonItemView alloc] initWithTitle:TGLocalized(@"ProfilePhoto.SearchWeb") type:TGMenuSheetButtonTypeDefault fontSize:20.0 action:^
{
__strong TGMediaAvatarMenuMixin *strongSelf = weakSelf;
if (strongSelf == nil)
return;
__strong TGMenuSheetController *strongController = weakController;
if (strongController == nil)
return;
[strongController dismissAnimated:true];
if (strongSelf != nil)
strongSelf.requestSearchController(nil);
}];
[itemViews addObject:viewItem];
}
if (_hasViewButton)
{

View File

@ -351,6 +351,7 @@ swift_library(
"//submodules/TelegramUI/Components/ChatInputNode",
"//submodules/TelegramUI/Components/ChatEntityKeyboardInputNode",
"//submodules/TelegramUI/Components/StorageUsageScreen",
"//submodules/TelegramUI/Components/AvatarEditorScreen",
"//submodules/MediaPasteboardUI:MediaPasteboardUI",
"//submodules/DrawingUI:DrawingUI",
"//submodules/FeaturedStickersScreen:FeaturedStickersScreen",

View File

@ -0,0 +1,43 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "AvatarEditorScreen",
module_name = "AvatarEditorScreen",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
"//submodules/Display:Display",
"//submodules/Postbox:Postbox",
"//submodules/TelegramCore:TelegramCore",
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
"//submodules/ComponentFlow:ComponentFlow",
"//submodules/Components/ViewControllerComponent:ViewControllerComponent",
"//submodules/Components/ComponentDisplayAdapters:ComponentDisplayAdapters",
"//submodules/Components/MultilineTextComponent:MultilineTextComponent",
"//submodules/Components/SolidRoundedButtonComponent",
"//submodules/TelegramPresentationData:TelegramPresentationData",
"//submodules/AccountContext:AccountContext",
"//submodules/AppBundle:AppBundle",
"//submodules/TelegramStringFormatting:TelegramStringFormatting",
"//submodules/PresentationDataUtils:PresentationDataUtils",
"//submodules/TelegramUI/Components/EmojiStatusComponent",
"//submodules/ContextUI",
"//submodules/UndoUI",
"//submodules/AnimatedStickerNode",
"//submodules/TelegramAnimatedStickerNode",
"//submodules/TelegramNotices:TelegramNotices",
"//submodules/Markdown:Markdown",
"//submodules/GradientBackground:GradientBackground",
"//submodules/LegacyComponents:LegacyComponents",
"//submodules/DrawingUI:DrawingUI",
"//submodules/StickerResources:StickerResources",
],
visibility = [
"//visibility:public",
],
)

View File

@ -0,0 +1,215 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import ComponentFlow
import SwiftSignalKit
import ViewControllerComponent
import ComponentDisplayAdapters
import TelegramPresentationData
import AccountContext
import TelegramCore
import MultilineTextComponent
import EmojiStatusComponent
import Postbox
import AnimatedStickerNode
import TelegramAnimatedStickerNode
import StickerResources
final class AvatarPreviewComponent: Component {
typealias EnvironmentType = Empty
let context: AccountContext
let background: AvatarBackground
let file: TelegramMediaFile?
let tapped: () -> Void
init(
context: AccountContext,
background: AvatarBackground,
file: TelegramMediaFile?,
tapped: @escaping () -> Void
) {
self.context = context
self.background = background
self.file = file
self.tapped = tapped
}
static func ==(lhs: AvatarPreviewComponent, rhs: AvatarPreviewComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.background != rhs.background {
return false
}
if lhs.file != rhs.file {
return false
}
return true
}
final class View: UIView, UITextFieldDelegate {
private let imageView: UIImageView
private let imageNode: TransformImageNode
private var animationNode: AnimatedStickerNode?
private var component: AvatarPreviewComponent?
private weak var state: EmptyComponentState?
private let stickerFetchedDisposable = MetaDisposable()
override init(frame: CGRect) {
self.imageView = UIImageView()
self.imageView.isUserInteractionEnabled = false
self.imageNode = TransformImageNode()
super.init(frame: frame)
self.disablesInteractiveModalDismiss = true
self.disablesInteractiveKeyboardGestureRecognizer = true
self.addSubview(self.imageView)
self.addSubnode(self.imageNode)
self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapped)))
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.stickerFetchedDisposable.dispose()
}
@objc func tapped() {
self.animationNode?.playOnce()
self.component?.tapped()
}
func update(component: AvatarPreviewComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
let previousBackground = self.component?.background
let hadFile = self.component?.file != nil
var fileUpdated = false
if self.component?.file?.fileId != component.file?.fileId {
self.imageNode.isHidden = false
fileUpdated = true
}
self.component = component
self.state = state
let size = CGSize(width: availableSize.width * 0.66, height: availableSize.width * 0.66)
var dimensions: CGSize?
if let file = component.file, fileUpdated, let fileDimensions = file.dimensions?.cgSize {
dimensions = fileDimensions
if !self.imageNode.isHidden && hadFile, let snapshotView = self.imageNode.view.snapshotContentTree() {
self.imageNode.view.superview?.addSubview(snapshotView)
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
snapshotView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false, completion: { [weak snapshotView] _ in
snapshotView?.removeFromSuperview()
})
}
if let animationNode = self.animationNode {
self.animationNode = nil
animationNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
animationNode.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false, completion: { [weak animationNode] _ in
animationNode?.removeFromSupernode()
})
}
if file.isAnimatedSticker || file.isVideoSticker || file.mimeType == "video/webm" {
self.imageNode.isHidden = false
if self.animationNode == nil {
let animationNode = DefaultAnimatedStickerNodeImpl()
animationNode.autoplay = false
self.animationNode = animationNode
animationNode.started = { [weak self] in
self?.imageNode.isHidden = true
}
self.addSubnode(animationNode)
}
self.imageNode.setSignal(chatMessageAnimatedSticker(postbox: component.context.account.postbox, userLocation: .other, file: file, small: false, size: fileDimensions.aspectFitted(CGSize(width: 256.0, height: 256.0))))
self.stickerFetchedDisposable.set(freeMediaFileResourceInteractiveFetched(account: component.context.account, userLocation: .other, fileReference: stickerPackFileReference(file), resource: file.resource).start())
} else {
if let animationNode = self.animationNode {
animationNode.visibility = false
self.animationNode = nil
animationNode.removeFromSupernode()
self.imageNode.isHidden = false
}
self.imageNode.setSignal(chatMessageSticker(account: component.context.account, userLocation: .other, file: file, small: false, synchronousLoad: false))
self.stickerFetchedDisposable.set(freeMediaFileResourceInteractiveFetched(account: component.context.account, userLocation: .other, fileReference: stickerPackFileReference(file), resource: chatMessageStickerResource(file: file, small: false)).start())
}
if fileUpdated && hadFile {
self.imageNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
self.imageNode.layer.animateScale(from: 0.01, to: 1.0, duration: 0.2)
if let animationNode = self.animationNode {
animationNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
animationNode.layer.animateScale(from: 0.01, to: 1.0, duration: 0.2)
}
}
}
if let dimensions {
let imageSize = dimensions.aspectFitted(size)
self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets()))()
self.imageNode.frame = CGRect(origin: CGPoint(x: floor((availableSize.width - imageSize.width) / 2.0), y: (availableSize.height - imageSize.height) / 2.0), size: imageSize)
if let animationNode = self.animationNode {
animationNode.frame = CGRect(origin: CGPoint(x: floor((availableSize.width - imageSize.width) / 2.0), y: (availableSize.height - imageSize.height) / 2.0), size: imageSize)
animationNode.updateLayout(size: imageSize)
}
if fileUpdated {
self.updateVisibility()
}
}
self.imageView.frame = CGRect(origin: .zero, size: availableSize)
if previousBackground != component.background {
if let _ = previousBackground, !transition.animation.isImmediate, let snapshotView = self.imageView.snapshotContentTree() {
self.insertSubview(snapshotView, aboveSubview: self.imageView)
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak snapshotView] _ in
snapshotView?.removeFromSuperview()
})
}
self.imageView.image = component.background.generateImage(size: availableSize)
}
return availableSize
}
private func updateVisibility() {
guard let component = self.component, let file = component.file else {
return
}
let dimensions = file.dimensions ?? PixelDimensions(width: 512, height: 512)
let fittedDimensions = dimensions.cgSize.aspectFitted(CGSize(width: 384.0, height: 384.0))
let source = AnimatedStickerResourceSource(account: component.context.account, resource: file.resource, isVideo: file.isVideoSticker || file.mimeType == "video/webm")
self.animationNode?.setup(source: source, width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), playbackMode: .count(2), mode: .direct(cachePathPrefix: nil))
self.animationNode?.visibility = true
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: State, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}

View File

@ -0,0 +1,313 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import ComponentFlow
import ViewControllerComponent
import ComponentDisplayAdapters
import TelegramPresentationData
final class BackgroundColorComponent: Component {
let theme: PresentationTheme
let values: [AvatarBackground]
let selectedValue: AvatarBackground
let updateValue: (AvatarBackground) -> Void
let openColorPicker: () -> Void
init(
theme: PresentationTheme,
values: [AvatarBackground],
selectedValue: AvatarBackground,
updateValue: @escaping (AvatarBackground) -> Void,
openColorPicker: @escaping () -> Void
) {
self.theme = theme
self.values = values
self.selectedValue = selectedValue
self.updateValue = updateValue
self.openColorPicker = openColorPicker
}
static func ==(lhs: BackgroundColorComponent, rhs: BackgroundColorComponent) -> Bool {
if lhs.theme !== rhs.theme {
return false
}
if lhs.values != rhs.values {
return false
}
if lhs.selectedValue != rhs.selectedValue {
return false
}
return true
}
class View: UIView {
private var views: [Int: ComponentView<Empty>] = [:]
private var component: BackgroundColorComponent?
private weak var state: EmptyComponentState?
override init(frame: CGRect) {
super.init(frame: frame)
self.clipsToBounds = true
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(component: BackgroundColorComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
self.component = component
self.state = state
var values: [(AvatarBackground?, Bool)] = component.values.map { ($0, false) }
if !values.contains(where: { $0.0 == component.selectedValue }) {
values.append((component.selectedValue, true))
} else {
values.append((nil, true))
}
let itemSize = CGSize(width: 30.0, height: 30.0)
let sideInset: CGFloat = 12.0
let height: CGFloat = 50.0
let delta = (availableSize.width - sideInset * 2.0 - CGFloat(values.count) * itemSize.width) / CGFloat(values.count - 1)
for i in 0 ..< values.count {
let view: ComponentView<Empty>
if let current = self.views[i] {
view = current
} else {
view = ComponentView<Empty>()
self.views[i] = view
}
let itemSize = view.update(
transition: transition,
component: AnyComponent(
BackgroundSwatchComponent(
theme: component.theme,
background: values[i].0,
isCustom: values[i].1,
isSelected: component.selectedValue == values[i].0,
action: {
if !values[i].1, let value = values[i].0 {
component.updateValue(value)
} else {
component.openColorPicker()
}
}
)
),
environment: {},
containerSize: itemSize
)
if let itemView = view.view {
if itemView.superview == nil {
self.addSubview(itemView)
}
let position: CGFloat = sideInset + (delta + itemSize.width) * CGFloat(i)
transition.setFrame(view: itemView, frame: CGRect(origin: CGPoint(x: position, y: 10.0), size: itemSize))
}
}
return CGSize(width: availableSize.width, height: height)
}
}
func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
private func generateAddIcon(color: UIColor) -> UIImage? {
return generateImage(CGSize(width: 30.0, height: 30.0), contextGenerator: { size, context in
context.clear(CGRect(origin: .zero, size: size))
context.setStrokeColor(color.cgColor)
context.setLineWidth(2.0)
context.setLineCap(.round)
context.move(to: CGPoint(x: 15.0, y: 9.0))
context.addLine(to: CGPoint(x: 15.0, y: 21.0))
context.strokePath()
context.move(to: CGPoint(x: 9.0, y: 15.0))
context.addLine(to: CGPoint(x: 21.0, y: 15.0))
context.strokePath()
})
}
private func generateMoreIcon() -> UIImage? {
return generateImage(CGSize(width: 30.0, height: 30.0), contextGenerator: { size, context in
context.clear(CGRect(origin: .zero, size: size))
context.setFillColor(UIColor.white.cgColor)
context.addEllipse(in: CGRect(x: 8.5, y: 13.5, width: 3.0, height: 3.0))
context.fillPath()
context.addEllipse(in: CGRect(x: 13.5, y: 13.5, width: 3.0, height: 3.0))
context.fillPath()
context.addEllipse(in: CGRect(x: 18.5, y: 13.5, width: 3.0, height: 3.0))
context.fillPath()
})
}
final class BackgroundSwatchComponent: Component {
let theme: PresentationTheme
let background: AvatarBackground?
let isCustom: Bool
let isSelected: Bool
let action: () -> Void
init(
theme: PresentationTheme,
background: AvatarBackground?,
isCustom: Bool,
isSelected: Bool,
action: @escaping () -> Void
) {
self.theme = theme
self.background = background
self.isCustom = isCustom
self.isSelected = isSelected
self.action = action
}
static func == (lhs: BackgroundSwatchComponent, rhs: BackgroundSwatchComponent) -> Bool {
return lhs.theme === rhs.theme && lhs.background == rhs.background && lhs.isCustom == rhs.isCustom && lhs.isSelected == rhs.isSelected
}
final class View: UIButton {
private var component: BackgroundSwatchComponent?
private let maskLayer: SimpleLayer
private let ringMaskLayer: SimpleShapeLayer
private let circleMaskLayer: SimpleShapeLayer
private let iconLayer: SimpleLayer
private var currentIsHighlighted: Bool = false {
didSet {
if self.currentIsHighlighted != oldValue {
self.alpha = self.currentIsHighlighted ? 0.6 : 1.0
}
}
}
override init(frame: CGRect) {
self.maskLayer = SimpleLayer()
self.ringMaskLayer = SimpleShapeLayer()
self.circleMaskLayer = SimpleShapeLayer()
self.iconLayer = SimpleLayer()
super.init(frame: frame)
self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc private func pressed() {
self.component?.action()
}
override public func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
self.currentIsHighlighted = true
return super.beginTracking(touch, with: event)
}
override public func endTracking(_ touch: UITouch?, with event: UIEvent?) {
self.currentIsHighlighted = false
super.endTracking(touch, with: event)
}
override public func cancelTracking(with event: UIEvent?) {
self.currentIsHighlighted = false
super.cancelTracking(with: event)
}
func update(component: BackgroundSwatchComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
let previousBackground = self.component?.background
self.component = component
let contentSize = availableSize
let bounds = CGRect(origin: .zero, size: contentSize)
self.layer.allowsGroupOpacity = true
if self.layer.mask == nil {
self.layer.mask = self.maskLayer
self.maskLayer.frame = bounds
self.maskLayer.addSublayer(self.circleMaskLayer)
self.maskLayer.addSublayer(self.ringMaskLayer)
self.circleMaskLayer.frame = bounds
if self.circleMaskLayer.path == nil {
self.circleMaskLayer.path = UIBezierPath(ovalIn: bounds.insetBy(dx: 3.0, dy: 3.0)).cgPath
}
let ringFrame = bounds
self.ringMaskLayer.frame = CGRect(origin: .zero, size: ringFrame.size)
self.ringMaskLayer.strokeColor = UIColor.white.cgColor
self.ringMaskLayer.fillColor = UIColor.clear.cgColor
self.ringMaskLayer.lineWidth = 2.0 - UIScreenPixel
self.ringMaskLayer.path = UIBezierPath(ovalIn: CGRect(origin: .zero, size: ringFrame.size).insetBy(dx: 1.0, dy: 1.0)).cgPath
self.layer.addSublayer(self.iconLayer)
}
self.iconLayer.frame = bounds
if component.isCustom {
if previousBackground != component.background || self.iconLayer.contents == nil {
if component.background != nil {
self.iconLayer.contents = generateMoreIcon()?.cgImage
} else {
self.iconLayer.contents = generateAddIcon(color: component.theme.list.itemAccentColor)?.cgImage
}
}
} else {
self.iconLayer.contents = nil
}
if component.isSelected {
transition.setShapeLayerPath(layer: self.circleMaskLayer, path: CGPath(ellipseIn: bounds.insetBy(dx: 3.0, dy: 3.0), transform: nil))
} else {
transition.setShapeLayerPath(layer: self.circleMaskLayer, path: CGPath(ellipseIn: bounds, transform: nil))
}
if previousBackground != component.background {
if let background = component.background {
self.layer.backgroundColor = nil
self.layer.contents = background.generateImage(size: availableSize).cgImage
} else {
self.layer.backgroundColor = component.theme.list.itemAccentColor.withAlphaComponent(0.1).cgColor
self.layer.contents = nil
}
} else if component.background == nil {
self.layer.backgroundColor = component.theme.list.itemAccentColor.withAlphaComponent(0.1).cgColor
self.layer.contents = nil
}
return availableSize
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}

View File

@ -1495,6 +1495,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
}
)
},
contentIdUpdated: { _ in },
deviceMetrics: deviceMetrics,
hiddenInputHeight: hiddenInputHeight,
inputHeight: inputHeight,

View File

@ -185,6 +185,7 @@ public final class EmojiStatusSelectionComponent: Component {
switchToGifSubject: { _ in },
reorderItems: { _, _ in },
makeSearchContainerNode: { _ in return nil },
contentIdUpdated: { _ in },
deviceMetrics: component.deviceMetrics,
hiddenInputHeight: 0.0,
inputHeight: 0.0,

View File

@ -2534,6 +2534,13 @@ public final class EmojiPagerContentComponent: Component {
let frame: CGRect
let supergroupId: AnyHashable
let groupId: AnyHashable
let itemsPerRow: Int
let nativeItemSize: CGFloat
let visibleItemSize: CGFloat
let playbackItemSize: CGFloat
let horizontalSpacing: CGFloat
let verticalSpacing: CGFloat
let itemInsets: UIEdgeInsets
let headerHeight: CGFloat
let itemTopOffset: CGFloat
let itemCount: Int
@ -2642,6 +2649,30 @@ public final class EmojiPagerContentComponent: Component {
var verticalGroupOrigin: CGFloat = self.itemInsets.top
self.itemGroupLayouts = []
for itemGroup in itemGroups {
var itemsPerRow = self.itemsPerRow
var nativeItemSize = self.nativeItemSize
var visibleItemSize = self.visibleItemSize
var playbackItemSize = self.playbackItemSize
var horizontalSpacing = self.horizontalSpacing
var verticalSpacing = self.verticalSpacing
var itemInsets = self.itemInsets
if itemGroup.groupId == AnyHashable("stickers") {
let minItemsPerRow = 5
nativeItemSize = 70.0
playbackItemSize = 96.0
verticalSpacing = 2.0
let minSpacing = 12.0
itemInsets = UIEdgeInsets(top: containerInsets.top, left: containerInsets.left + 10.0, bottom: containerInsets.bottom, right: containerInsets.right + 10.0)
let itemHorizontalSpace = width - itemInsets.left - itemInsets.right
itemsPerRow = max(minItemsPerRow, Int((itemHorizontalSpace + minSpacing) / (nativeItemSize + minSpacing)))
let proposedItemSize = floor((itemHorizontalSpace - minSpacing * (CGFloat(itemsPerRow) - 1.0)) / CGFloat(itemsPerRow))
visibleItemSize = proposedItemSize < nativeItemSize ? proposedItemSize : nativeItemSize
horizontalSpacing = floorToScreenPixels((itemHorizontalSpace - visibleItemSize * CGFloat(itemsPerRow)) / CGFloat(itemsPerRow - 1))
}
var itemTopOffset: CGFloat = 0.0
var headerHeight: CGFloat = 0.0
var groupSpacing = self.verticalGroupDefaultSpacing
@ -2663,7 +2694,7 @@ public final class EmojiPagerContentComponent: Component {
if itemGroup.isEmbedded {
numRowsInGroup = 0
} else {
numRowsInGroup = (itemGroup.itemCount + (self.itemsPerRow - 1)) / self.itemsPerRow
numRowsInGroup = (itemGroup.itemCount + (itemsPerRow - 1)) / itemsPerRow
}
var collapsedItemIndex: Int?
@ -2674,7 +2705,7 @@ public final class EmojiPagerContentComponent: Component {
} else if let collapsedLineCount = itemGroup.collapsedLineCount, !expandedGroupIds.contains(itemGroup.groupId) {
let maxLines: Int = collapsedLineCount
if numRowsInGroup > maxLines {
visibleItemCount = self.itemsPerRow * maxLines - 1
visibleItemCount = itemsPerRow * maxLines - 1
collapsedItemIndex = visibleItemCount
collapsedItemText = "+\(itemGroup.itemCount - visibleItemCount)"
} else {
@ -2685,10 +2716,10 @@ public final class EmojiPagerContentComponent: Component {
}
if !itemGroup.isEmbedded {
numRowsInGroup = (visibleItemCount + (self.itemsPerRow - 1)) / self.itemsPerRow
numRowsInGroup = (visibleItemCount + (itemsPerRow - 1)) / itemsPerRow
}
var groupContentSize = CGSize(width: width, height: itemTopOffset + CGFloat(numRowsInGroup) * self.visibleItemSize + CGFloat(max(0, numRowsInGroup - 1)) * self.verticalSpacing)
var groupContentSize = CGSize(width: width, height: itemTopOffset + CGFloat(numRowsInGroup) * visibleItemSize + CGFloat(max(0, numRowsInGroup - 1)) * verticalSpacing)
if (itemGroup.isPremiumLocked || itemGroup.isFeatured), case .compact = layoutType {
groupContentSize.height += self.premiumButtonInset + self.premiumButtonHeight
}
@ -2697,6 +2728,13 @@ public final class EmojiPagerContentComponent: Component {
frame: CGRect(origin: CGPoint(x: 0.0, y: verticalGroupOrigin), size: groupContentSize),
supergroupId: itemGroup.supergroupId,
groupId: itemGroup.groupId,
itemsPerRow: itemsPerRow,
nativeItemSize: nativeItemSize,
visibleItemSize: visibleItemSize,
playbackItemSize: playbackItemSize,
horizontalSpacing: horizontalSpacing,
verticalSpacing: verticalSpacing,
itemInsets: itemInsets,
headerHeight: headerHeight,
itemTopOffset: itemTopOffset,
itemCount: visibleItemCount,
@ -2705,24 +2743,24 @@ public final class EmojiPagerContentComponent: Component {
))
verticalGroupOrigin += groupContentSize.height + groupSpacing
}
verticalGroupOrigin += self.itemInsets.bottom
verticalGroupOrigin += itemInsets.bottom
self.contentSize = CGSize(width: width, height: verticalGroupOrigin)
}
func frame(groupIndex: Int, itemIndex: Int) -> CGRect {
let groupLayout = self.itemGroupLayouts[groupIndex]
let row = itemIndex / self.itemsPerRow
let column = itemIndex % self.itemsPerRow
let row = itemIndex / groupLayout.itemsPerRow
let column = itemIndex % groupLayout.itemsPerRow
return CGRect(
origin: CGPoint(
x: self.itemInsets.left + CGFloat(column) * (self.visibleItemSize + self.horizontalSpacing),
y: groupLayout.frame.minY + groupLayout.itemTopOffset + CGFloat(row) * (self.visibleItemSize + self.verticalSpacing)
x: groupLayout.itemInsets.left + CGFloat(column) * (groupLayout.visibleItemSize + groupLayout.horizontalSpacing),
y: groupLayout.frame.minY + groupLayout.itemTopOffset + CGFloat(row) * (groupLayout.visibleItemSize + groupLayout.verticalSpacing)
),
size: CGSize(
width: self.visibleItemSize,
height: self.visibleItemSize
width: groupLayout.visibleItemSize,
height: groupLayout.visibleItemSize
)
)
}
@ -2731,22 +2769,22 @@ public final class EmojiPagerContentComponent: Component {
var result: [(supergroupId: AnyHashable, groupId: AnyHashable, groupIndex: Int, groupItems: Range<Int>?)] = []
for groupIndex in 0 ..< self.itemGroupLayouts.count {
let group = self.itemGroupLayouts[groupIndex]
let groupLayout = self.itemGroupLayouts[groupIndex]
if !rect.intersects(group.frame) {
if !rect.intersects(groupLayout.frame) {
continue
}
let offsetRect = rect.offsetBy(dx: -self.itemInsets.left, dy: -group.frame.minY - group.itemTopOffset)
var minVisibleRow = Int(floor((offsetRect.minY - self.verticalSpacing) / (self.visibleItemSize + self.verticalSpacing)))
let offsetRect = rect.offsetBy(dx: -groupLayout.itemInsets.left, dy: -groupLayout.frame.minY - groupLayout.itemTopOffset)
var minVisibleRow = Int(floor((offsetRect.minY - groupLayout.verticalSpacing) / (groupLayout.visibleItemSize + groupLayout.verticalSpacing)))
minVisibleRow = max(0, minVisibleRow)
let maxVisibleRow = Int(ceil((offsetRect.maxY - self.verticalSpacing) / (self.visibleItemSize + self.verticalSpacing)))
let maxVisibleRow = Int(ceil((offsetRect.maxY - groupLayout.verticalSpacing) / (groupLayout.visibleItemSize + groupLayout.verticalSpacing)))
let minVisibleIndex = minVisibleRow * self.itemsPerRow
let maxVisibleIndex = min(group.itemCount - 1, (maxVisibleRow + 1) * self.itemsPerRow - 1)
let minVisibleIndex = minVisibleRow * groupLayout.itemsPerRow
let maxVisibleIndex = min(groupLayout.itemCount - 1, (maxVisibleRow + 1) * groupLayout.itemsPerRow - 1)
result.append((
supergroupId: group.supergroupId,
groupId: group.groupId,
supergroupId: groupLayout.supergroupId,
groupId: groupLayout.groupId,
groupIndex: groupIndex,
groupItems: maxVisibleIndex >= minVisibleIndex ? (minVisibleIndex ..< (maxVisibleIndex + 1)) : nil
))
@ -5204,6 +5242,7 @@ public final class EmojiPagerContentComponent: Component {
if !itemGroup.isEmbedded, let groupItemRange = groupItems.groupItems {
for index in groupItemRange.lowerBound ..< groupItemRange.upperBound {
let item = itemGroup.items[index]
if assignTopVisibleSubgroupId {
if let subgroupId = item.subgroupId {
@ -5219,9 +5258,9 @@ public final class EmojiPagerContentComponent: Component {
let itemDimensions: CGSize = item.animationData?.dimensions ?? CGSize(width: 512.0, height: 512.0)
let itemNativeFitSize = itemDimensions.aspectFitted(CGSize(width: itemLayout.nativeItemSize, height: itemLayout.nativeItemSize))
let itemVisibleFitSize = itemDimensions.aspectFitted(CGSize(width: itemLayout.visibleItemSize, height: itemLayout.visibleItemSize))
let itemPlaybackSize = itemDimensions.aspectFitted(CGSize(width: itemLayout.playbackItemSize, height: itemLayout.playbackItemSize))
let itemNativeFitSize = itemDimensions.aspectFitted(CGSize(width: itemGroupLayout.nativeItemSize, height: itemGroupLayout.nativeItemSize))
let itemVisibleFitSize = itemDimensions.aspectFitted(CGSize(width: itemGroupLayout.visibleItemSize, height: itemGroupLayout.visibleItemSize))
let itemPlaybackSize = itemDimensions.aspectFitted(CGSize(width: itemGroupLayout.playbackItemSize, height: itemGroupLayout.playbackItemSize))
var animateItemIn = false
var updateItemLayerPlaceholder = false
@ -6309,6 +6348,8 @@ public final class EmojiPagerContentComponent: Component {
isEmojiSelection: Bool,
isTopicIconSelection: Bool = false,
isQuickReactionSelection: Bool = false,
isProfilePhotoEmojiSelection: Bool = false,
isGroupPhotoEmojiSelection: Bool = false,
topReactionItems: [EmojiComponentReactionItem],
areUnicodeEmojiEnabled: Bool,
areCustomEmojiEnabled: Bool,
@ -6359,6 +6400,8 @@ public final class EmojiPagerContentComponent: Component {
}
}
|> take(1)
} else if isProfilePhotoEmojiSelection {
//orderedItemListCollectionIds.append(Namespaces.OrderedItemList.CloudFeaturedProfilePhotoEmoji)
}
let availableReactions: Signal<AvailableReactions?, NoError>
@ -7071,7 +7114,7 @@ public final class EmojiPagerContentComponent: Component {
}
var displaySearchWithPlaceholder: String?
var searchInitiallyHidden = true
let searchInitiallyHidden = true
if hasSearch {
if isReactionSelection {
displaySearchWithPlaceholder = strings.EmojiSearch_SearchReactionsPlaceholder
@ -7079,7 +7122,8 @@ public final class EmojiPagerContentComponent: Component {
displaySearchWithPlaceholder = strings.EmojiSearch_SearchStatusesPlaceholder
} else if isEmojiSelection {
displaySearchWithPlaceholder = strings.EmojiSearch_SearchEmojiPlaceholder
searchInitiallyHidden = false
} else if isProfilePhotoEmojiSelection || isGroupPhotoEmojiSelection {
displaySearchWithPlaceholder = "Search"
}
}
@ -7147,7 +7191,8 @@ public final class EmojiPagerContentComponent: Component {
chatPeerId: EnginePeer.Id?,
hasSearch: Bool,
hasTrending: Bool,
forceHasPremium: Bool
forceHasPremium: Bool,
searchIsPlaceholderOnly: Bool = true
) -> Signal<EmojiPagerContentComponent, NoError> {
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 })
let isPremiumDisabled = premiumConfiguration.isPremiumDisabled
@ -7637,7 +7682,7 @@ public final class EmojiPagerContentComponent: Component {
warpContentsOnEdges: false,
displaySearchWithPlaceholder: hasSearch ? strings.StickersSearch_SearchStickersPlaceholder : nil,
searchInitiallyHidden: true,
searchIsPlaceholderOnly: true,
searchIsPlaceholderOnly: searchIsPlaceholderOnly,
emptySearchResults: nil,
enableLongPress: false,
selectedItems: Set()

View File

@ -108,6 +108,7 @@ public final class EntityKeyboardComponent: Component {
public let switchToGifSubject: (GifPagerContentComponent.Subject) -> Void
public let reorderItems: (ReorderCategory, [EntityKeyboardTopPanelComponent.Item]) -> Void
public let makeSearchContainerNode: (EntitySearchContentType) -> EntitySearchContainerNode?
public let contentIdUpdated: (AnyHashable) -> Void
public let deviceMetrics: DeviceMetrics
public let hiddenInputHeight: CGFloat
public let inputHeight: CGFloat
@ -138,6 +139,7 @@ public final class EntityKeyboardComponent: Component {
switchToGifSubject: @escaping (GifPagerContentComponent.Subject) -> Void,
reorderItems: @escaping (ReorderCategory, [EntityKeyboardTopPanelComponent.Item]) -> Void,
makeSearchContainerNode: @escaping (EntitySearchContentType) -> EntitySearchContainerNode?,
contentIdUpdated: @escaping (AnyHashable) -> Void,
deviceMetrics: DeviceMetrics,
hiddenInputHeight: CGFloat,
inputHeight: CGFloat,
@ -167,6 +169,7 @@ public final class EntityKeyboardComponent: Component {
self.switchToGifSubject = switchToGifSubject
self.reorderItems = reorderItems
self.makeSearchContainerNode = makeSearchContainerNode
self.contentIdUpdated = contentIdUpdated
self.deviceMetrics = deviceMetrics
self.hiddenInputHeight = hiddenInputHeight
self.inputHeight = inputHeight
@ -744,6 +747,12 @@ public final class EntityKeyboardComponent: Component {
}
strongSelf.isTopPanelHiddenUpdated(isTopPanelHidden: isTopPanelHidden, transition: transition)
},
contentIdUpdated: { [weak self] id in
guard let strongSelf = self, let component = strongSelf.component else {
return
}
component.contentIdUpdated(id)
},
panelHideBehavior: panelHideBehavior,
clipContentToTopPanel: component.clipContentToTopPanel
)),
@ -862,6 +871,15 @@ public final class EntityKeyboardComponent: Component {
component.hideTopPanelUpdated(self.isTopPanelHidden, transition)
}
public func scrollToContentId(_ contentId: AnyHashable) {
guard let _ = self.component else {
return
}
if let pagerView = self.pagerView.findTaggedView(tag: PagerComponentViewTag()) as? PagerComponent<EntityKeyboardChildEnvironment, EntityKeyboardTopContainerPanelEnvironment>.View {
pagerView.navigateToContentId(contentId)
}
}
public func openSearch() {
guard let component = self.component else {
return

View File

@ -416,6 +416,7 @@ private final class TopicIconSelectionComponent: Component {
switchToGifSubject: { _ in },
reorderItems: { _, _ in },
makeSearchContainerNode: { _ in return nil },
contentIdUpdated: { _ in },
deviceMetrics: component.deviceMetrics,
hiddenInputHeight: 0.0,
inputHeight: 0.0,

View File

@ -84,6 +84,7 @@ import StickerPackPreviewUI
import ChatListHeaderComponent
import ChatControllerInteraction
import StorageUsageScreen
import AvatarEditorScreen
enum PeerInfoAvatarEditingMode {
case generic
@ -7208,6 +7209,16 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
completion(nil)
}
}
mixin.requestAvatarEditor = { [weak self] completion in
guard let strongSelf = self, let completion else {
return
}
let controller = AvatarEditorScreen(context: strongSelf.context)
controller.completion = completion
(strongSelf.controller?.navigationController?.topViewController as? ViewController)?.push(controller)
}
if let confirmationTextPhoto, let confirmationAction {
mixin.willFinishWithImage = { [weak self] image, commit in
if let strongSelf = self, let image {

View File

@ -15,7 +15,6 @@ import AppBundle
import DatePickerNode
import DebugSettingsUI
import TabBarUI
import DrawingUI
public final class TelegramRootController: NavigationController {
private let context: AccountContext

View File

@ -8,6 +8,7 @@ import Display
import TelegramPresentationData
import AccountContext
import LegacyUI
import LegacyMediaPickerUI
func presentLegacyWebSearchEditor(context: AccountContext, theme: PresentationTheme, result: ChatContextResult, initialLayout: ContainerViewLayout?, updateHiddenMedia: @escaping (String?) -> Void, transitionHostView: @escaping () -> UIView?, transitionView: @escaping (ChatContextResult) -> UIView?, completed: @escaping (UIImage) -> Void, present: @escaping (ViewController, Any?) -> Void) {
guard let item = legacyWebSearchItem(account: context.account, result: result) else {
@ -33,7 +34,10 @@ func presentLegacyWebSearchEditor(context: AccountContext, theme: PresentationTh
let legacyController = LegacyController(presentation: .custom, theme: theme, initialLayout: initialLayout)
legacyController.statusBar.statusBarStyle = theme.rootController.statusBarStyle.style
let paintStickersContext = LegacyPaintStickersContext(context: context)
let controller = TGPhotoEditorController(context: legacyController.context, item: item, intent: TGPhotoEditorControllerAvatarIntent, adjustments: nil, caption: nil, screenImage: screenImage ?? UIImage(), availableTabs: TGPhotoEditorController.defaultTabsForAvatarIntent(), selectedTab: .cropTab)!
controller.stickersContext = paintStickersContext
legacyController.bind(controller: controller)
controller.editingContext = TGMediaEditingContext()