Various improvements

This commit is contained in:
Ilya Laktyushin
2025-02-22 19:17:36 +04:00
parent eecb2ef5c0
commit 8d7f9bf372
122 changed files with 3454 additions and 1121 deletions

View File

@@ -19,7 +19,7 @@ swift_library(
"//submodules/Components/ViewControllerComponent:ViewControllerComponent",
"//submodules/Components/ComponentDisplayAdapters:ComponentDisplayAdapters",
"//submodules/Components/MultilineTextComponent:MultilineTextComponent",
"//submodules/Components/SolidRoundedButtonComponent",
"//submodules/TelegramUI/Components/ButtonComponent",
"//submodules/TelegramPresentationData:TelegramPresentationData",
"//submodules/AccountContext:AccountContext",
"//submodules/AppBundle:AppBundle",
@@ -40,6 +40,8 @@ swift_library(
"//submodules/TelegramUI/Components/EmojiTextAttachmentView:EmojiTextAttachmentView",
"//submodules/TelegramUI/Components/MediaEditor",
"//submodules/TelegramUI/Components/AvatarBackground",
"//submodules/TelegramUI/Components/LottieComponent",
"//submodules/TelegramUI/Components/Premium/PremiumStarComponent",
],
visibility = [
"//visibility:public",

View File

@@ -20,11 +20,13 @@ import Markdown
import GradientBackground
import LegacyComponents
import DrawingUI
import SolidRoundedButtonComponent
import ButtonComponent
import AnimationCache
import EmojiTextAttachmentView
import MediaEditor
import AvatarBackground
import LottieComponent
import UndoUI
public struct AvatarKeyboardInputData: Equatable {
var emoji: EmojiPagerContentComponent
@@ -126,7 +128,14 @@ final class AvatarEditorScreenComponent: Component {
})
}
self.selectedBackground = .gradient(markup.backgroundColors.map { UInt32(bitPattern: $0) })
var isPremium = false
let colorsValue = markup.backgroundColors.map { UInt32(bitPattern: $0) }
if let defaultColor = AvatarBackground.defaultBackgrounds.first(where: { $0.colors == colorsValue}) {
if defaultColor.isPremium {
isPremium = true
}
}
self.selectedBackground = .gradient(colorsValue, isPremium)
self.previousColor = self.selectedBackground
} else {
self.selectedBackground = AvatarBackground.defaultBackgrounds.first!
@@ -188,6 +197,8 @@ final class AvatarEditorScreenComponent: Component {
private let buttonView = ComponentView<Empty>()
private var component: AvatarEditorScreenComponent?
private var environment: EnvironmentType?
private weak var state: State?
private var navigationMetrics: (navigationHeight: CGFloat, statusBarHeight: CGFloat)?
@@ -783,12 +794,15 @@ final class AvatarEditorScreenComponent: Component {
private var isExpanded = false
func update(component: AvatarEditorScreenComponent, availableSize: CGSize, state: State, environment: Environment<ViewControllerComponentContainer.Environment>, transition: ComponentTransition) -> CGSize {
func update(component: AvatarEditorScreenComponent, availableSize: CGSize, state: State, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
self.component = component
let environment = environment[EnvironmentType.self].value
self.environment = environment
self.state = state
let environment = environment[ViewControllerComponentContainer.Environment.self].value
let strings = environment.strings
let theme = environment.theme
let controller = environment.controller
self.controller = {
@@ -990,6 +1004,7 @@ final class AvatarEditorScreenComponent: Component {
transition: transition,
component: AnyComponent(BackgroundColorComponent(
theme: environment.theme,
isPremium: component.context.isPremium,
values: AvatarBackground.defaultBackgrounds,
selectedValue: state.selectedBackground,
customValue: state.customColor,
@@ -1037,8 +1052,8 @@ final class AvatarEditorScreenComponent: Component {
colors: state.selectedBackground.colors,
colorsChanged: { [weak state] colors in
if let state {
state.customColor = .gradient(colors)
state.selectedBackground = .gradient(colors)
state.customColor = .gradient(colors, true)
state.selectedBackground = .gradient(colors, true)
state.updated(transition: .immediate)
}
},
@@ -1268,29 +1283,65 @@ final class AvatarEditorScreenComponent: Component {
case .suggest:
buttonText = strings.AvatarEditor_SuggestProfilePhoto
case .user:
buttonText = strings.AvatarEditor_SetProfilePhoto
//TODO:localize
buttonText = "Set My Photo" //strings.AvatarEditor_SetProfilePhoto
case .group, .forum:
buttonText = strings.AvatarEditor_SetGroupPhoto
case .channel:
buttonText = strings.AvatarEditor_SetChannelPhoto
}
var isLocked = false
if component.peerType != .suggest, !component.context.isPremium {
if state.selectedBackground.isPremium {
isLocked = true
}
if let selectedFile = state.selectedFile {
if selectedFile.isSticker {
isLocked = true
}
}
}
var buttonContents: [AnyComponentWithIdentity<Empty>] = []
buttonContents.append(AnyComponentWithIdentity(id: AnyHashable(buttonText), component: AnyComponent(
Text(text: buttonText, font: Font.semibold(17.0), color: theme.list.itemCheckColors.foregroundColor)
)))
if !component.context.isPremium && isLocked {
buttonContents.append(AnyComponentWithIdentity(id: AnyHashable(0 as Int), component: AnyComponent(LottieComponent(
content: LottieComponent.AppBundleContent(name: "premium_unlock"),
color: theme.list.itemCheckColors.foregroundColor,
startingPosition: .begin,
size: CGSize(width: 30.0, height: 30.0),
loop: true
))))
}
let buttonSize = self.buttonView.update(
transition: transition,
component: AnyComponent(
SolidRoundedButtonComponent(
title: buttonText,
theme: SolidRoundedButtonComponent.Theme(theme: environment.theme),
fontSize: 17.0,
height: 50.0,
cornerRadius: 10.0,
ButtonComponent(
background: ButtonComponent.Background(
color: theme.list.itemCheckColors.fillColor,
foreground: theme.list.itemCheckColors.foregroundColor,
pressedColor: theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.8)
),
content: AnyComponentWithIdentity(id: AnyHashable(0 as Int), component: AnyComponent(
HStack(buttonContents, spacing: 3.0)
)),
isEnabled: true,
displaysProgress: false,
action: { [weak self] in
self?.complete()
if isLocked {
self?.presentPremiumToast()
} else {
self?.complete()
}
}
)
),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: environment.navigationHeight - environment.statusBarHeight)
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 50.0)
)
if let buttonView = self.buttonView.view {
if buttonView.superview == nil {
@@ -1298,10 +1349,41 @@ final class AvatarEditorScreenComponent: Component {
}
transition.setFrame(view: buttonView, frame: CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: buttonSize))
}
let bottomPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: contentHeight - 4.0), size: CGSize(width: availableSize.width, height: availableSize.height - contentHeight + 4.0))
if let controller = environment.controller(), !controller.automaticallyControlPresentationContextLayout {
let layout = ContainerViewLayout(
size: availableSize,
metrics: environment.metrics,
deviceMetrics: environment.deviceMetrics,
intrinsicInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: bottomPanelFrame.height, right: 0.0),
safeInsets: UIEdgeInsets(top: 0.0, left: environment.safeInsets.left, bottom: 0.0, right: environment.safeInsets.right),
additionalInsets: .zero,
statusBarHeight: environment.statusBarHeight,
inputHeight: nil,
inputHeightIsInteractivellyChanging: false,
inVoiceOver: false
)
controller.presentationContext.containerLayoutUpdated(layout, transition: transition.containedViewLayoutTransition)
}
return availableSize
}
private func presentPremiumToast() {
guard let environment = self.environment, let component = self.component, let parentController = environment.controller() else {
return
}
HapticFeedback().impact(.light)
let controller = premiumAlertController(
context: component.context,
parentController: parentController,
text: environment.strings.AvatarEditor_PremiumNeeded_Background
)
parentController.present(controller, in: .window(.root))
}
private let queue = Queue()
func complete() {
guard let state = self.state, let file = state.selectedFile, let controller = self.controller?() else {
@@ -1531,6 +1613,9 @@ public final class AvatarEditorScreen: ViewControllerComponentContainer {
let componentReady = Promise<Bool>()
super.init(context: context, component: AvatarEditorScreenComponent(context: context, ready: componentReady, peerType: peerType, markup: markup), navigationBarAppearance: .transparent)
self.automaticallyControlPresentationContextLayout = false
self.navigationPresentation = .modal
self.readyValue.set(componentReady.get() |> timeout(0.3, queue: .mainQueue(), alternate: .single(true)))

View File

@@ -10,6 +10,7 @@ import AvatarBackground
final class BackgroundColorComponent: Component {
let theme: PresentationTheme
let isPremium: Bool
let values: [AvatarBackground]
let selectedValue: AvatarBackground
let customValue: AvatarBackground?
@@ -18,6 +19,7 @@ final class BackgroundColorComponent: Component {
init(
theme: PresentationTheme,
isPremium: Bool,
values: [AvatarBackground],
selectedValue: AvatarBackground,
customValue: AvatarBackground?,
@@ -25,6 +27,7 @@ final class BackgroundColorComponent: Component {
openColorPicker: @escaping () -> Void
) {
self.theme = theme
self.isPremium = isPremium
self.values = values
self.selectedValue = selectedValue
self.customValue = customValue
@@ -36,6 +39,9 @@ final class BackgroundColorComponent: Component {
if lhs.theme !== rhs.theme {
return false
}
if lhs.isPremium != rhs.isPremium {
return false
}
if lhs.values != rhs.values {
return false
}
@@ -48,25 +54,45 @@ final class BackgroundColorComponent: Component {
return true
}
class View: UIView {
private var views: [Int: ComponentView<Empty>] = [:]
class View: UIView, UIScrollViewDelegate {
private var views: [AnyHashable: ComponentView<Empty>] = [:]
private var scrollView: UIScrollView
private var component: BackgroundColorComponent?
private weak var state: EmptyComponentState?
override init(frame: CGRect) {
super.init(frame: frame)
self.scrollView = UIScrollView()
self.scrollView.contentInsetAdjustmentBehavior = .never
self.scrollView.showsHorizontalScrollIndicator = false
self.scrollView.showsVerticalScrollIndicator = false
super.init(frame: frame)
self.clipsToBounds = true
self.scrollView.delegate = self
self.addSubview(self.scrollView)
self.scrollView.disablesInteractiveTransitionGestureRecognizer = true
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(component: BackgroundColorComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
self.component = component
self.state = state
func scrollViewDidScroll(_ scrollView: UIScrollView) {
self.updateScrolling(transition: .immediate)
}
func updateScrolling(transition: ComponentTransition) {
guard let component = self.component else {
return
}
let itemSize = CGSize(width: 30.0, height: 30.0)
let sideInset: CGFloat = 12.0
let spacing: CGFloat = 13.0
var values: [(AvatarBackground?, Bool)] = component.values.map { ($0, false) }
if let customValue = component.customValue {
@@ -75,50 +101,97 @@ final class BackgroundColorComponent: Component {
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 = floorToScreenPixels((availableSize.width - sideInset * 2.0 - CGFloat(values.count) * itemSize.width) / CGFloat(values.count - 1))
let visibleBounds = self.scrollView.bounds.insetBy(dx: 0.0, dy: -10.0)
var validIds: [AnyHashable] = []
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 position: CGFloat = sideInset + (spacing + itemSize.width) * CGFloat(i)
let itemFrame = CGRect(origin: CGPoint(x: position, y: 10.0), size: itemSize)
var isVisible = false
if visibleBounds.intersects(itemFrame) {
isVisible = true
}
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 let value = values[i].0, component.selectedValue != value {
component.updateValue(value)
} else if values[i].1 {
component.openColorPicker()
}
}
)
),
environment: {},
containerSize: itemSize
)
if let itemView = view.view {
if itemView.superview == nil {
self.addSubview(itemView)
if isVisible {
let itemId = AnyHashable(i)
validIds.append(itemId)
let view: ComponentView<Empty>
if let current = self.views[itemId] {
view = current
} else {
view = ComponentView<Empty>()
self.views[itemId] = view
}
let position: CGFloat = sideInset + (delta + itemSize.width) * CGFloat(i)
transition.setFrame(view: itemView, frame: CGRect(origin: CGPoint(x: position, y: 10.0), size: itemSize))
let _ = view.update(
transition: transition,
component: AnyComponent(
BackgroundSwatchComponent(
theme: component.theme,
background: values[i].0,
isCustom: values[i].1,
isSelected: component.selectedValue == values[i].0,
isLocked: i >= 7 && !values[i].1,
action: {
if let value = values[i].0, component.selectedValue != value {
component.updateValue(value)
} else if values[i].1 {
component.openColorPicker()
}
}
)
),
environment: {},
containerSize: itemSize
)
if let itemView = view.view {
if itemView.superview == nil {
self.scrollView.addSubview(itemView)
}
transition.setFrame(view: itemView, frame: itemFrame)
}
}
}
return CGSize(width: availableSize.width, height: height)
var removeIds: [AnyHashable] = []
for (id, item) in self.views {
if !validIds.contains(id) {
removeIds.append(id)
if let itemView = item.view {
itemView.removeFromSuperview()
}
}
}
for id in removeIds {
self.views.removeValue(forKey: id)
}
}
func update(component: BackgroundColorComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
self.component = component
self.state = state
let height: CGFloat = 50.0
let size = CGSize(width: availableSize.width, height: height)
let scrollFrame = CGRect(origin: .zero, size: size)
let itemSize = CGSize(width: 30.0, height: 30.0)
let sideInset: CGFloat = 12.0
let spacing: CGFloat = 13.0
let count = component.values.count + 1
let contentSize = CGSize(width: sideInset * 2.0 + CGFloat(count) * itemSize.width + CGFloat(count - 1) * spacing, height: height)
if self.scrollView.frame != scrollFrame {
self.scrollView.frame = scrollFrame
}
if self.scrollView.contentSize != contentSize {
self.scrollView.contentSize = contentSize
}
self.updateScrolling(transition: .immediate)
return size
}
}
@@ -164,11 +237,22 @@ private func generateMoreIcon() -> UIImage? {
})
}
private var lockIcon: UIImage? = {
let icon = generateTintedImage(image: UIImage(bundleImageName: "Chat/Stickers/SmallLock"), color: .white)
return generateImage(CGSize(width: 30.0, height: 30.0), contextGenerator: { size, context in
context.clear(CGRect(origin: .zero, size: size))
if let icon, let cgImage = icon.cgImage {
context.draw(cgImage, in: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - icon.size.width) / 2.0), y: floorToScreenPixels((size.height - icon.size.height) / 2.0)), size: icon.size), byTiling: false)
}
})
}()
final class BackgroundSwatchComponent: Component {
let theme: PresentationTheme
let background: AvatarBackground?
let isCustom: Bool
let isSelected: Bool
let isLocked: Bool
let action: () -> Void
init(
@@ -176,17 +260,19 @@ final class BackgroundSwatchComponent: Component {
background: AvatarBackground?,
isCustom: Bool,
isSelected: Bool,
isLocked: Bool,
action: @escaping () -> Void
) {
self.theme = theme
self.background = background
self.isCustom = isCustom
self.isSelected = isSelected
self.isLocked = isLocked
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
return lhs.theme === rhs.theme && lhs.background == rhs.background && lhs.isCustom == rhs.isCustom && lhs.isSelected == rhs.isSelected && lhs.isLocked == rhs.isLocked
}
final class View: UIButton {
@@ -283,6 +369,8 @@ final class BackgroundSwatchComponent: Component {
self.iconLayer.contents = generateAddIcon(color: component.theme.list.itemAccentColor)?.cgImage
}
}
} else if component.isLocked {
self.iconLayer.contents = lockIcon?.cgImage
} else {
self.iconLayer.contents = nil
}