mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-12-24 07:05:35 +00:00
Various improvements
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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)))
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user