mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-07-21 02:31:10 +00:00
Merge commit '40780242fe8ef168b115e9b2b47914faecf3dea5'
This commit is contained in:
commit
23e52bc1f7
@ -850,7 +850,7 @@ final class ComposePollScreenComponent: Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.pollOptions.count < 10, let lastOption = self.pollOptions.last {
|
if self.pollOptions.count < component.initialData.maxPollAnswersCount, let lastOption = self.pollOptions.last {
|
||||||
if lastOption.textInputState.text.length != 0 {
|
if lastOption.textInputState.text.length != 0 {
|
||||||
self.pollOptions.append(PollOption(id: self.nextPollOptionId))
|
self.pollOptions.append(PollOption(id: self.nextPollOptionId))
|
||||||
self.nextPollOptionId += 1
|
self.nextPollOptionId += 1
|
||||||
@ -921,7 +921,7 @@ final class ComposePollScreenComponent: Component {
|
|||||||
|
|
||||||
contentHeight += 7.0
|
contentHeight += 7.0
|
||||||
|
|
||||||
let pollOptionsLimitReached = self.pollOptions.count >= 10
|
let pollOptionsLimitReached = self.pollOptions.count >= component.initialData.maxPollAnswersCount
|
||||||
var animatePollOptionsFooterIn = false
|
var animatePollOptionsFooterIn = false
|
||||||
var pollOptionsFooterTransition = transition
|
var pollOptionsFooterTransition = transition
|
||||||
if self.currentPollOptionsLimitReached != pollOptionsLimitReached {
|
if self.currentPollOptionsLimitReached != pollOptionsLimitReached {
|
||||||
@ -944,7 +944,7 @@ final class ComposePollScreenComponent: Component {
|
|||||||
maximumNumberOfLines: 0
|
maximumNumberOfLines: 0
|
||||||
))
|
))
|
||||||
} else {
|
} else {
|
||||||
let remainingCount = 10 - self.pollOptions.count
|
let remainingCount = component.initialData.maxPollAnswersCount - self.pollOptions.count
|
||||||
let rawString = environment.strings.CreatePoll_OptionCountFooterFormat(Int32(remainingCount))
|
let rawString = environment.strings.CreatePoll_OptionCountFooterFormat(Int32(remainingCount))
|
||||||
|
|
||||||
var pollOptionsFooterItems: [AnimatedTextComponent.Item] = []
|
var pollOptionsFooterItems: [AnimatedTextComponent.Item] = []
|
||||||
@ -1476,13 +1476,16 @@ public class ComposePollScreen: ViewControllerComponentContainer, AttachmentCont
|
|||||||
public final class InitialData {
|
public final class InitialData {
|
||||||
fileprivate let maxPollTextLength: Int
|
fileprivate let maxPollTextLength: Int
|
||||||
fileprivate let maxPollOptionLength: Int
|
fileprivate let maxPollOptionLength: Int
|
||||||
|
fileprivate let maxPollAnswersCount: Int
|
||||||
|
|
||||||
fileprivate init(
|
fileprivate init(
|
||||||
maxPollTextLength: Int,
|
maxPollTextLength: Int,
|
||||||
maxPollOptionLength: Int
|
maxPollOptionLength: Int,
|
||||||
|
maxPollAnwsersCount: Int
|
||||||
) {
|
) {
|
||||||
self.maxPollTextLength = maxPollTextLength
|
self.maxPollTextLength = maxPollTextLength
|
||||||
self.maxPollOptionLength = maxPollOptionLength
|
self.maxPollOptionLength = maxPollOptionLength
|
||||||
|
self.maxPollAnswersCount = maxPollAnwsersCount
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1577,9 +1580,14 @@ public class ComposePollScreen: ViewControllerComponentContainer, AttachmentCont
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static func initialData(context: AccountContext) -> InitialData {
|
public static func initialData(context: AccountContext) -> InitialData {
|
||||||
|
var maxPollAnwsersCount: Int = 10
|
||||||
|
if let data = context.currentAppConfiguration.with({ $0 }).data, let value = data["poll_answers_max"] as? Double {
|
||||||
|
maxPollAnwsersCount = Int(value)
|
||||||
|
}
|
||||||
return InitialData(
|
return InitialData(
|
||||||
maxPollTextLength: Int(200),
|
maxPollTextLength: Int(200),
|
||||||
maxPollOptionLength: 100
|
maxPollOptionLength: 100,
|
||||||
|
maxPollAnwsersCount: maxPollAnwsersCount
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -426,7 +426,9 @@ private func contactListNodeEntries(accountPeer: EnginePeer?, peers: [ContactLis
|
|||||||
let text = strings.Contacts_PermissionsText
|
let text = strings.Contacts_PermissionsText
|
||||||
switch authorizationStatus {
|
switch authorizationStatus {
|
||||||
case .limited:
|
case .limited:
|
||||||
|
if displaySortOptions {
|
||||||
entries.append(.permissionLimited(theme, strings))
|
entries.append(.permissionLimited(theme, strings))
|
||||||
|
}
|
||||||
case .denied:
|
case .denied:
|
||||||
entries.append(.permissionInfo(theme, title, text, suppressed))
|
entries.append(.permissionInfo(theme, title, text, suppressed))
|
||||||
entries.append(.permissionEnable(theme, strings.Permissions_ContactsAllowInSettings_v0))
|
entries.append(.permissionEnable(theme, strings.Permissions_ContactsAllowInSettings_v0))
|
||||||
|
@ -116,6 +116,10 @@ public final class PresentationData: Equatable {
|
|||||||
return PresentationData(strings: self.strings, theme: self.theme, autoNightModeTriggered: self.autoNightModeTriggered, chatWallpaper: chatWallpaper, chatFontSize: self.chatFontSize, chatBubbleCorners: self.chatBubbleCorners, listsFontSize: self.listsFontSize, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, nameSortOrder: self.nameSortOrder, reduceMotion: self.reduceMotion, largeEmoji: self.largeEmoji)
|
return PresentationData(strings: self.strings, theme: self.theme, autoNightModeTriggered: self.autoNightModeTriggered, chatWallpaper: chatWallpaper, chatFontSize: self.chatFontSize, chatBubbleCorners: self.chatBubbleCorners, listsFontSize: self.listsFontSize, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, nameSortOrder: self.nameSortOrder, reduceMotion: self.reduceMotion, largeEmoji: self.largeEmoji)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func withUpdate(listsFontSize: PresentationFontSize) -> PresentationData {
|
||||||
|
return PresentationData(strings: self.strings, theme: self.theme, autoNightModeTriggered: self.autoNightModeTriggered, chatWallpaper: self.chatWallpaper, chatFontSize: self.chatFontSize, chatBubbleCorners: self.chatBubbleCorners, listsFontSize: listsFontSize, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, nameSortOrder: self.nameSortOrder, reduceMotion: self.reduceMotion, largeEmoji: self.largeEmoji)
|
||||||
|
}
|
||||||
|
|
||||||
public static func ==(lhs: PresentationData, rhs: PresentationData) -> Bool {
|
public static func ==(lhs: PresentationData, rhs: PresentationData) -> Bool {
|
||||||
return lhs.strings === rhs.strings && lhs.theme === rhs.theme && lhs.autoNightModeTriggered == rhs.autoNightModeTriggered && lhs.chatWallpaper == rhs.chatWallpaper && lhs.chatFontSize == rhs.chatFontSize && lhs.chatBubbleCorners == rhs.chatBubbleCorners && lhs.listsFontSize == rhs.listsFontSize && lhs.dateTimeFormat == rhs.dateTimeFormat && lhs.reduceMotion == rhs.reduceMotion && lhs.largeEmoji == rhs.largeEmoji
|
return lhs.strings === rhs.strings && lhs.theme === rhs.theme && lhs.autoNightModeTriggered == rhs.autoNightModeTriggered && lhs.chatWallpaper == rhs.chatWallpaper && lhs.chatFontSize == rhs.chatFontSize && lhs.chatBubbleCorners == rhs.chatBubbleCorners && lhs.listsFontSize == rhs.listsFontSize && lhs.dateTimeFormat == rhs.dateTimeFormat && lhs.reduceMotion == rhs.reduceMotion && lhs.largeEmoji == rhs.largeEmoji
|
||||||
}
|
}
|
||||||
|
@ -2,11 +2,13 @@ import Foundation
|
|||||||
import UIKit
|
import UIKit
|
||||||
import Display
|
import Display
|
||||||
import ComponentFlow
|
import ComponentFlow
|
||||||
|
import SwiftSignalKit
|
||||||
import PlainButtonComponent
|
import PlainButtonComponent
|
||||||
import MultilineTextWithEntitiesComponent
|
import MultilineTextComponent
|
||||||
import BundleIconComponent
|
import BundleIconComponent
|
||||||
import TextFormat
|
import TextFormat
|
||||||
import AccountContext
|
import AccountContext
|
||||||
|
import LottieComponent
|
||||||
|
|
||||||
public final class FilterSelectorComponent: Component {
|
public final class FilterSelectorComponent: Component {
|
||||||
public struct Colors: Equatable {
|
public struct Colors: Equatable {
|
||||||
@ -24,39 +26,45 @@ public final class FilterSelectorComponent: Component {
|
|||||||
|
|
||||||
public struct Item: Equatable {
|
public struct Item: Equatable {
|
||||||
public var id: AnyHashable
|
public var id: AnyHashable
|
||||||
|
public var index: Int
|
||||||
public var iconName: String?
|
public var iconName: String?
|
||||||
public var title: String
|
public var title: String
|
||||||
public var action: (UIView) -> Void
|
public var action: (UIView) -> Void
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
id: AnyHashable,
|
id: AnyHashable,
|
||||||
|
index: Int = 0,
|
||||||
iconName: String? = nil,
|
iconName: String? = nil,
|
||||||
title: String,
|
title: String,
|
||||||
action: @escaping (UIView) -> Void
|
action: @escaping (UIView) -> Void
|
||||||
) {
|
) {
|
||||||
self.id = id
|
self.id = id
|
||||||
|
self.index = index
|
||||||
self.iconName = iconName
|
self.iconName = iconName
|
||||||
self.title = title
|
self.title = title
|
||||||
self.action = action
|
self.action = action
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func ==(lhs: Item, rhs: Item) -> Bool {
|
public static func ==(lhs: Item, rhs: Item) -> Bool {
|
||||||
return lhs.id == rhs.id && lhs.iconName == rhs.iconName && lhs.title == rhs.title
|
return lhs.id == rhs.id && lhs.index == rhs.index && lhs.iconName == rhs.iconName && lhs.title == rhs.title
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public let context: AccountContext?
|
public let context: AccountContext?
|
||||||
public let colors: Colors
|
public let colors: Colors
|
||||||
public let items: [Item]
|
public let items: [Item]
|
||||||
|
public let selectedItemId: AnyHashable?
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
context: AccountContext? = nil,
|
context: AccountContext? = nil,
|
||||||
colors: Colors,
|
colors: Colors,
|
||||||
items: [Item]
|
items: [Item],
|
||||||
|
selectedItemId: AnyHashable?
|
||||||
) {
|
) {
|
||||||
self.context = context
|
self.context = context
|
||||||
self.colors = colors
|
self.colors = colors
|
||||||
self.items = items
|
self.items = items
|
||||||
|
self.selectedItemId = selectedItemId
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func ==(lhs: FilterSelectorComponent, rhs: FilterSelectorComponent) -> Bool {
|
public static func ==(lhs: FilterSelectorComponent, rhs: FilterSelectorComponent) -> Bool {
|
||||||
@ -69,6 +77,9 @@ public final class FilterSelectorComponent: Component {
|
|||||||
if lhs.items != rhs.items {
|
if lhs.items != rhs.items {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
if lhs.selectedItemId != rhs.selectedItemId {
|
||||||
|
return false
|
||||||
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -150,15 +161,17 @@ public final class FilterSelectorComponent: Component {
|
|||||||
validIds.append(itemId)
|
validIds.append(itemId)
|
||||||
|
|
||||||
let itemSize = itemView.title.update(
|
let itemSize = itemView.title.update(
|
||||||
transition: .immediate,
|
transition: transition,
|
||||||
component: AnyComponent(PlainButtonComponent(
|
component: AnyComponent(PlainButtonComponent(
|
||||||
content: AnyComponent(ItemComponent(
|
content: AnyComponent(ItemComponent(
|
||||||
context: component.context,
|
context: component.context,
|
||||||
|
index: item.index,
|
||||||
iconName: item.iconName,
|
iconName: item.iconName,
|
||||||
text: item.title,
|
text: item.title,
|
||||||
font: itemFont,
|
font: itemFont,
|
||||||
color: component.colors.foreground,
|
color: component.colors.foreground,
|
||||||
backgroundColor: component.colors.background
|
backgroundColor: component.colors.background,
|
||||||
|
isSelected: itemId == component.selectedItemId
|
||||||
)),
|
)),
|
||||||
effectAlignment: .center,
|
effectAlignment: .center,
|
||||||
minSize: nil,
|
minSize: nil,
|
||||||
@ -242,34 +255,43 @@ extension CGRect {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private final class ItemComponent: CombinedComponent {
|
private final class ItemComponent: Component {
|
||||||
let context: AccountContext?
|
let context: AccountContext?
|
||||||
|
let index: Int
|
||||||
let iconName: String?
|
let iconName: String?
|
||||||
let text: String
|
let text: String
|
||||||
let font: UIFont
|
let font: UIFont
|
||||||
let color: UIColor
|
let color: UIColor
|
||||||
let backgroundColor: UIColor
|
let backgroundColor: UIColor
|
||||||
|
let isSelected: Bool
|
||||||
|
|
||||||
init(
|
init(
|
||||||
context: AccountContext?,
|
context: AccountContext?,
|
||||||
|
index: Int,
|
||||||
iconName: String?,
|
iconName: String?,
|
||||||
text: String,
|
text: String,
|
||||||
font: UIFont,
|
font: UIFont,
|
||||||
color: UIColor,
|
color: UIColor,
|
||||||
backgroundColor: UIColor
|
backgroundColor: UIColor,
|
||||||
|
isSelected: Bool
|
||||||
) {
|
) {
|
||||||
self.context = context
|
self.context = context
|
||||||
|
self.index = index
|
||||||
self.iconName = iconName
|
self.iconName = iconName
|
||||||
self.text = text
|
self.text = text
|
||||||
self.font = font
|
self.font = font
|
||||||
self.color = color
|
self.color = color
|
||||||
self.backgroundColor = backgroundColor
|
self.backgroundColor = backgroundColor
|
||||||
|
self.isSelected = isSelected
|
||||||
}
|
}
|
||||||
|
|
||||||
static func ==(lhs: ItemComponent, rhs: ItemComponent) -> Bool {
|
static func ==(lhs: ItemComponent, rhs: ItemComponent) -> Bool {
|
||||||
if lhs.context !== rhs.context {
|
if lhs.context !== rhs.context {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
if lhs.index != rhs.index {
|
||||||
|
return false
|
||||||
|
}
|
||||||
if lhs.iconName != rhs.iconName {
|
if lhs.iconName != rhs.iconName {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@ -285,80 +307,166 @@ private final class ItemComponent: CombinedComponent {
|
|||||||
if lhs.backgroundColor != rhs.backgroundColor {
|
if lhs.backgroundColor != rhs.backgroundColor {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
if lhs.isSelected != rhs.isSelected {
|
||||||
|
return false
|
||||||
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
static var body: Body {
|
public final class View: UIView {
|
||||||
let background = Child(RoundedRectangle.self)
|
private var component: ItemComponent?
|
||||||
let title = Child(MultilineTextWithEntitiesComponent.self)
|
private weak var state: EmptyComponentState?
|
||||||
let icon = Child(BundleIconComponent.self)
|
|
||||||
|
|
||||||
return { context in
|
private let background = ComponentView<Empty>()
|
||||||
let component = context.component
|
private let title = ComponentView<Empty>()
|
||||||
|
private let icon = ComponentView<Empty>()
|
||||||
|
|
||||||
let attributedTitle = NSMutableAttributedString(string: component.text, font: component.font, textColor: component.color)
|
private var isSelected = false
|
||||||
let range = (attributedTitle.string as NSString).range(of: "⭐️")
|
private var iconName: String?
|
||||||
if range.location != NSNotFound {
|
|
||||||
attributedTitle.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .stars(tinted: false)), range: range)
|
private let playOnce = ActionSlot<Void>()
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
super.init(frame: frame)
|
||||||
}
|
}
|
||||||
|
|
||||||
let title = title.update(
|
required init?(coder: NSCoder) {
|
||||||
component: MultilineTextWithEntitiesComponent(
|
fatalError("init(coder:) has not been implemented")
|
||||||
context: component.context,
|
}
|
||||||
animationCache: component.context?.animationCache,
|
|
||||||
animationRenderer: component.context?.animationRenderer,
|
func update(component: ItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
||||||
placeholderColor: .white,
|
let previousComponent = self.component
|
||||||
|
self.component = component
|
||||||
|
self.state = state
|
||||||
|
|
||||||
|
var animateTitleInDirection: CGFloat?
|
||||||
|
if let previousComponent, previousComponent.text != component.text, !transition.animation.isImmediate, let titleView = self.title.view, let snapshotView = titleView.snapshotView(afterScreenUpdates: false) {
|
||||||
|
snapshotView.frame = titleView.frame
|
||||||
|
self.addSubview(snapshotView)
|
||||||
|
|
||||||
|
var direction: CGFloat = 1.0
|
||||||
|
if previousComponent.index < component.index {
|
||||||
|
direction = -1.0
|
||||||
|
}
|
||||||
|
|
||||||
|
snapshotView.layer.animatePosition(from: .zero, to: CGPoint(x: 0.0, y: 6.0 * direction), duration: 0.2, removeOnCompletion: false, additive: true)
|
||||||
|
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in
|
||||||
|
snapshotView.removeFromSuperview()
|
||||||
|
})
|
||||||
|
|
||||||
|
animateTitleInDirection = direction
|
||||||
|
}
|
||||||
|
|
||||||
|
let attributedTitle = NSAttributedString(string: component.text, font: component.font, textColor: component.color)
|
||||||
|
let titleSize = self.title.update(
|
||||||
|
transition: .immediate,
|
||||||
|
component: AnyComponent(MultilineTextComponent(
|
||||||
text: .plain(attributedTitle)
|
text: .plain(attributedTitle)
|
||||||
),
|
)),
|
||||||
availableSize: context.availableSize,
|
environment: {},
|
||||||
transition: .immediate
|
containerSize: availableSize
|
||||||
)
|
)
|
||||||
|
|
||||||
let icon = icon.update(
|
let animationName = component.iconName ?? (component.isSelected ? "GiftFilterMenuOpen" : "GiftFilterMenuClose")
|
||||||
component: BundleIconComponent(
|
let animationSize = component.iconName != nil ? CGSize(width: 22.0, height: 22.0) : CGSize(width: 10.0, height: 22.0)
|
||||||
name: component.iconName ?? "Item List/ExpandableSelectorArrows",
|
|
||||||
tintColor: component.color,
|
let iconSize = self.icon.update(
|
||||||
maxSize: component.iconName != nil ? CGSize(width: 22.0, height: 22.0) : nil
|
transition: transition,
|
||||||
),
|
component: AnyComponent(LottieComponent(
|
||||||
availableSize: CGSize(width: 100, height: 100),
|
content: LottieComponent.AppBundleContent(name: animationName),
|
||||||
transition: .immediate
|
color: component.color,
|
||||||
|
playOnce: self.playOnce
|
||||||
|
)),
|
||||||
|
environment: {},
|
||||||
|
containerSize: CGSize(width: 22.0, height: 22.0)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var playAnimation = false
|
||||||
|
if self.isSelected != component.isSelected || self.iconName != component.iconName {
|
||||||
|
if let iconName = component.iconName {
|
||||||
|
if component.isSelected {
|
||||||
|
playAnimation = true
|
||||||
|
} else if self.iconName != iconName {
|
||||||
|
playAnimation = true
|
||||||
|
}
|
||||||
|
self.iconName = iconName
|
||||||
|
} else {
|
||||||
|
playAnimation = true
|
||||||
|
}
|
||||||
|
self.isSelected = component.isSelected
|
||||||
|
}
|
||||||
|
if playAnimation {
|
||||||
|
self.playOnce.invoke(Void())
|
||||||
|
}
|
||||||
|
|
||||||
let padding: CGFloat = 12.0
|
let padding: CGFloat = 12.0
|
||||||
var leftPadding = padding
|
var leftPadding = padding
|
||||||
if let _ = component.iconName {
|
if let _ = component.iconName {
|
||||||
leftPadding -= 4.0
|
leftPadding -= 4.0
|
||||||
}
|
}
|
||||||
let spacing: CGFloat = 4.0
|
let spacing: CGFloat = 4.0
|
||||||
let totalWidth = title.size.width + icon.size.width + spacing
|
let totalWidth = titleSize.width + animationSize.width + spacing
|
||||||
let size = CGSize(width: totalWidth + leftPadding + padding, height: 28.0)
|
let size = CGSize(width: totalWidth + leftPadding + padding, height: 28.0)
|
||||||
let background = background.update(
|
|
||||||
component: RoundedRectangle(
|
let backgroundSize = self.background.update(
|
||||||
|
transition: transition,
|
||||||
|
component: AnyComponent(RoundedRectangle(
|
||||||
color: component.backgroundColor,
|
color: component.backgroundColor,
|
||||||
cornerRadius: 14.0
|
cornerRadius: 14.0
|
||||||
),
|
)),
|
||||||
availableSize: size,
|
environment: {},
|
||||||
transition: .immediate
|
containerSize: size
|
||||||
)
|
|
||||||
context.add(background
|
|
||||||
.position(CGPoint(x: size.width / 2.0, y: size.height / 2.0))
|
|
||||||
)
|
|
||||||
if let _ = component.iconName {
|
|
||||||
context.add(title
|
|
||||||
.position(CGPoint(x: size.width - padding - title.size.width / 2.0, y: size.height / 2.0))
|
|
||||||
)
|
|
||||||
context.add(icon
|
|
||||||
.position(CGPoint(x: leftPadding + icon.size.width / 2.0, y: size.height / 2.0))
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
context.add(title
|
|
||||||
.position(CGPoint(x: padding + title.size.width / 2.0, y: size.height / 2.0))
|
|
||||||
)
|
|
||||||
context.add(icon
|
|
||||||
.position(CGPoint(x: size.width - padding - icon.size.width / 2.0, y: size.height / 2.0))
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if let backgroundView = self.background.view {
|
||||||
|
if backgroundView.superview == nil {
|
||||||
|
self.addSubview(backgroundView)
|
||||||
}
|
}
|
||||||
|
transition.setPosition(view: backgroundView, position: CGPoint(x: size.width / 2.0, y: size.height / 2.0))
|
||||||
|
transition.setBounds(view: backgroundView, bounds: CGRect(origin: CGPoint(), size: backgroundSize))
|
||||||
|
}
|
||||||
|
|
||||||
|
if let titleView = self.title.view {
|
||||||
|
if titleView.superview == nil {
|
||||||
|
self.addSubview(titleView)
|
||||||
|
}
|
||||||
|
let titlePosition: CGPoint
|
||||||
|
if let _ = component.iconName {
|
||||||
|
titlePosition = CGPoint(x: size.width - padding - titleSize.width / 2.0, y: size.height / 2.0)
|
||||||
|
} else {
|
||||||
|
titlePosition = CGPoint(x: padding + titleSize.width / 2.0, y: size.height / 2.0)
|
||||||
|
}
|
||||||
|
if let animateTitleInDirection {
|
||||||
|
titleView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||||
|
titleView.center = CGPoint(x: titlePosition.x, y: titlePosition.y - 6.0 * animateTitleInDirection)
|
||||||
|
}
|
||||||
|
transition.setPosition(view: titleView, position: titlePosition)
|
||||||
|
titleView.bounds = CGRect(origin: CGPoint(), size: titleSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let iconView = self.icon.view {
|
||||||
|
if iconView.superview == nil {
|
||||||
|
self.addSubview(iconView)
|
||||||
|
}
|
||||||
|
let iconPosition: CGPoint
|
||||||
|
if let _ = component.iconName {
|
||||||
|
iconPosition = CGPoint(x: leftPadding + iconSize.width / 2.0, y: size.height / 2.0)
|
||||||
|
} else {
|
||||||
|
iconPosition = CGPoint(x: size.width - padding - animationSize.width / 2.0, y: size.height / 2.0)
|
||||||
|
}
|
||||||
|
transition.setPosition(view: iconView, position: iconPosition)
|
||||||
|
transition.setBounds(view: iconView, bounds: CGRect(origin: CGPoint(), size: iconSize))
|
||||||
|
}
|
||||||
|
|
||||||
return size
|
return size
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func makeView() -> View {
|
||||||
|
return View(frame: CGRect())
|
||||||
|
}
|
||||||
|
|
||||||
|
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
||||||
|
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -201,70 +201,27 @@ private final class GiftAttributeListContextItemNode: ASDisplayNode, ContextMenu
|
|||||||
private let actionSelected: (ContextMenuActionResult) -> Void
|
private let actionSelected: (ContextMenuActionResult) -> Void
|
||||||
|
|
||||||
private let scrollNode: ASScrollNode
|
private let scrollNode: ASScrollNode
|
||||||
private let actionNodes: [ContextControllerActionsListActionItemNode]
|
private var actionNodes: [AnyHashable: ContextControllerActionsListActionItemNode] = [:]
|
||||||
private let separatorNodes: [ASDisplayNode]
|
private var separatorNodes: [AnyHashable: ASDisplayNode] = [:]
|
||||||
|
|
||||||
private var searchDisposable: Disposable?
|
private var searchDisposable: Disposable?
|
||||||
private var searchQuery = ""
|
private var searchQuery = ""
|
||||||
|
|
||||||
|
private var itemHeights: [AnyHashable: CGFloat] = [:]
|
||||||
|
private var totalContentHeight: CGFloat = 0
|
||||||
|
private var itemFrames: [AnyHashable: CGRect] = [:]
|
||||||
|
|
||||||
init(presentationData: PresentationData, item: GiftAttributeListContextItem, getController: @escaping () -> ContextControllerProtocol?, actionSelected: @escaping (ContextMenuActionResult) -> Void) {
|
init(presentationData: PresentationData, item: GiftAttributeListContextItem, getController: @escaping () -> ContextControllerProtocol?, actionSelected: @escaping (ContextMenuActionResult) -> Void) {
|
||||||
self.item = item
|
self.item = item
|
||||||
self.presentationData = presentationData
|
self.presentationData = presentationData.withUpdate(listsFontSize: .regular)
|
||||||
self.getController = getController
|
self.getController = getController
|
||||||
self.actionSelected = actionSelected
|
self.actionSelected = actionSelected
|
||||||
|
|
||||||
self.scrollNode = ASScrollNode()
|
self.scrollNode = ASScrollNode()
|
||||||
|
|
||||||
var actionNodes: [ContextControllerActionsListActionItemNode] = []
|
|
||||||
var separatorNodes: [ASDisplayNode] = []
|
|
||||||
|
|
||||||
let selectedAttributes = Set(item.selectedAttributes)
|
|
||||||
|
|
||||||
let selectAllAction = ContextMenuActionItem(text: presentationData.strings.Gift_Store_SelectAll, icon: { theme in
|
|
||||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Select"), color: theme.contextMenu.primaryColor)
|
|
||||||
}, iconPosition: .left, action: { _, f in
|
|
||||||
getController()?.dismiss(result: .dismissWithoutContent, completion: nil)
|
|
||||||
|
|
||||||
item.selectAll()
|
|
||||||
})
|
|
||||||
|
|
||||||
let selectAllActionNode = ContextControllerActionsListActionItemNode(context: item.context, getController: getController, requestDismiss: actionSelected, requestUpdateAction: { _, _ in }, item: selectAllAction)
|
|
||||||
actionNodes.append(selectAllActionNode)
|
|
||||||
|
|
||||||
let separatorNode = ASDisplayNode()
|
|
||||||
separatorNode.backgroundColor = presentationData.theme.contextMenu.itemSeparatorColor
|
|
||||||
separatorNodes.append(separatorNode)
|
|
||||||
|
|
||||||
for attribute in item.attributes {
|
|
||||||
guard let action = actionForAttribute(attribute: attribute, presentationData: presentationData, selectedAttributes: selectedAttributes, searchQuery: self.searchQuery, item: item, getController: getController) else {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
let actionNode = ContextControllerActionsListActionItemNode(context: item.context, getController: getController, requestDismiss: actionSelected, requestUpdateAction: { _, _ in }, item: action)
|
|
||||||
actionNodes.append(actionNode)
|
|
||||||
if actionNodes.count != item.attributes.count {
|
|
||||||
let separatorNode = ASDisplayNode()
|
|
||||||
separatorNode.backgroundColor = presentationData.theme.contextMenu.itemSeparatorColor
|
|
||||||
separatorNodes.append(separatorNode)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let nopAction: ((ContextControllerProtocol?, @escaping (ContextMenuActionResult) -> Void) -> Void)? = nil
|
|
||||||
let emptyResultsAction = ContextMenuActionItem(text: presentationData.strings.Gift_Store_NoResults, textFont: .small, icon: { _ in return nil }, action: nopAction)
|
|
||||||
let emptyResultsActionNode = ContextControllerActionsListActionItemNode(context: item.context, getController: getController, requestDismiss: actionSelected, requestUpdateAction: { _, _ in }, item: emptyResultsAction)
|
|
||||||
actionNodes.append(emptyResultsActionNode)
|
|
||||||
|
|
||||||
self.actionNodes = actionNodes
|
|
||||||
self.separatorNodes = separatorNodes
|
|
||||||
|
|
||||||
super.init()
|
super.init()
|
||||||
|
|
||||||
self.addSubnode(self.scrollNode)
|
self.addSubnode(self.scrollNode)
|
||||||
for separatorNode in self.separatorNodes {
|
|
||||||
self.scrollNode.addSubnode(separatorNode)
|
|
||||||
}
|
|
||||||
for actionNode in self.actionNodes {
|
|
||||||
self.scrollNode.addSubnode(actionNode)
|
|
||||||
}
|
|
||||||
|
|
||||||
self.searchDisposable = (item.searchQuery
|
self.searchDisposable = (item.searchQuery
|
||||||
|> deliverOnMainQueue).start(next: { [weak self] searchQuery in
|
|> deliverOnMainQueue).start(next: { [weak self] searchQuery in
|
||||||
@ -272,15 +229,7 @@ private final class GiftAttributeListContextItemNode: ASDisplayNode, ContextMenu
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
self.searchQuery = searchQuery.trimmingCharacters(in: .whitespacesAndNewlines)
|
self.searchQuery = searchQuery.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
self.invalidateLayout()
|
||||||
var i = 1
|
|
||||||
for attribute in item.attributes {
|
|
||||||
guard let action = actionForAttribute(attribute: attribute, presentationData: presentationData, selectedAttributes: selectedAttributes, searchQuery: self.searchQuery, item: item, getController: getController) else {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
self.actionNodes[i].setItem(item: action)
|
|
||||||
i += 1
|
|
||||||
}
|
|
||||||
self.getController()?.requestLayout(transition: .immediate)
|
self.getController()?.requestLayout(transition: .immediate)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -298,95 +247,245 @@ private final class GiftAttributeListContextItemNode: ASDisplayNode, ContextMenu
|
|||||||
self.scrollNode.view.scrollIndicatorInsets = UIEdgeInsets(top: 0.0, left: 0.0, bottom: 5.0, right: 0.0)
|
self.scrollNode.view.scrollIndicatorInsets = UIEdgeInsets(top: 0.0, left: 0.0, bottom: 5.0, right: 0.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateLayout(constrainedWidth: CGFloat, constrainedHeight: CGFloat) -> (CGSize, (CGSize, ContainedViewLayoutTransition) -> Void) {
|
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||||
let minActionsWidth: CGFloat = 250.0
|
if let maxWidth = self.maxWidth {
|
||||||
let maxActionsWidth: CGFloat = 300.0
|
self.updateScrolling(maxWidth: maxWidth)
|
||||||
let constrainedWidth = min(constrainedWidth, maxActionsWidth)
|
}
|
||||||
var maxWidth: CGFloat = 0.0
|
}
|
||||||
var contentHeight: CGFloat = 0.0
|
|
||||||
var heightsAndCompletions: [(Int, CGFloat, (CGSize, ContainedViewLayoutTransition) -> Void)] = []
|
|
||||||
|
|
||||||
|
enum ItemType {
|
||||||
|
case selectAll
|
||||||
|
case attribute(StarGift.UniqueGift.Attribute)
|
||||||
|
case noResults
|
||||||
|
case separator
|
||||||
|
}
|
||||||
|
|
||||||
|
private func getVisibleItems(in scrollView: UIScrollView, constrainedWidth: CGFloat) -> [(itemId: AnyHashable, itemType: ItemType, frame: CGRect)] {
|
||||||
let effectiveAttributes: [StarGift.UniqueGift.Attribute]
|
let effectiveAttributes: [StarGift.UniqueGift.Attribute]
|
||||||
if self.searchQuery.isEmpty {
|
if self.searchQuery.isEmpty {
|
||||||
effectiveAttributes = self.item.attributes
|
effectiveAttributes = self.item.attributes
|
||||||
} else {
|
} else {
|
||||||
effectiveAttributes = filteredAttributes(attributes: self.item.attributes, query: self.searchQuery)
|
effectiveAttributes = filteredAttributes(attributes: self.item.attributes, query: self.searchQuery)
|
||||||
}
|
}
|
||||||
let visibleAttributes = Set(effectiveAttributes.map { attribute -> AnyHashable in
|
|
||||||
|
var items: [(itemId: AnyHashable, itemType: ItemType, frame: CGRect)] = []
|
||||||
|
var yOffset: CGFloat = 0
|
||||||
|
|
||||||
|
let defaultHeight: CGFloat = 42.0
|
||||||
|
if self.searchQuery.isEmpty {
|
||||||
|
let selectAllId = AnyHashable("selectAll")
|
||||||
|
let height = self.itemHeights[selectAllId] ?? defaultHeight
|
||||||
|
let frame = CGRect(x: 0, y: yOffset, width: constrainedWidth, height: height)
|
||||||
|
items.append((selectAllId, .selectAll, frame))
|
||||||
|
yOffset += height
|
||||||
|
|
||||||
|
let separatorId = AnyHashable("separator_selectAll")
|
||||||
|
let separatorFrame = CGRect(x: 0, y: yOffset, width: constrainedWidth, height: UIScreenPixel)
|
||||||
|
items.append((separatorId, .separator, separatorFrame))
|
||||||
|
yOffset += UIScreenPixel
|
||||||
|
}
|
||||||
|
|
||||||
|
for (index, attribute) in effectiveAttributes.enumerated() {
|
||||||
|
let attributeId = self.getAttributeId(from: attribute)
|
||||||
|
let height = self.itemHeights[attributeId] ?? defaultHeight
|
||||||
|
let frame = CGRect(x: 0, y: yOffset, width: constrainedWidth, height: height)
|
||||||
|
items.append((attributeId, .attribute(attribute), frame))
|
||||||
|
yOffset += height
|
||||||
|
|
||||||
|
if index < effectiveAttributes.count - 1 {
|
||||||
|
let separatorId = AnyHashable("separator_\(attributeId)")
|
||||||
|
let separatorFrame = CGRect(x: 0, y: yOffset, width: constrainedWidth, height: UIScreenPixel)
|
||||||
|
items.append((separatorId, .separator, separatorFrame))
|
||||||
|
yOffset += UIScreenPixel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !self.searchQuery.isEmpty && effectiveAttributes.isEmpty {
|
||||||
|
let noResultsId = AnyHashable("noResults")
|
||||||
|
let height = self.itemHeights[noResultsId] ?? defaultHeight
|
||||||
|
let frame = CGRect(x: 0, y: yOffset, width: constrainedWidth, height: height)
|
||||||
|
items.append((noResultsId, .noResults, frame))
|
||||||
|
yOffset += height
|
||||||
|
}
|
||||||
|
|
||||||
|
self.totalContentHeight = yOffset
|
||||||
|
|
||||||
|
for (itemId, _, frame) in items {
|
||||||
|
self.itemFrames[itemId] = frame
|
||||||
|
}
|
||||||
|
|
||||||
|
let visibleBounds = scrollView.bounds.insetBy(dx: 0.0, dy: -100.0)
|
||||||
|
return items.filter { visibleBounds.intersects($0.frame) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private func getAttributeId(from attribute: StarGift.UniqueGift.Attribute) -> AnyHashable {
|
||||||
switch attribute {
|
switch attribute {
|
||||||
case let .model(_, file, _):
|
case let .model(_, file, _):
|
||||||
return file.fileId.id
|
return AnyHashable("model_\(file.fileId.id)")
|
||||||
case let .pattern(_, file, _):
|
case let .pattern(_, file, _):
|
||||||
return file.fileId.id
|
return AnyHashable("pattern_\(file.fileId.id)")
|
||||||
case let .backdrop(_, id, _, _, _, _, _):
|
case let .backdrop(_, id, _, _, _, _, _):
|
||||||
return id
|
return AnyHashable("backdrop_\(id)")
|
||||||
default:
|
default:
|
||||||
fatalError()
|
return AnyHashable("unknown")
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var maxWidth: CGFloat?
|
||||||
|
private func updateScrolling(maxWidth: CGFloat) {
|
||||||
|
let scrollView = self.scrollNode.view
|
||||||
|
|
||||||
|
let constrainedWidth = scrollView.bounds.width
|
||||||
|
let visibleItems = self.getVisibleItems(in: scrollView, constrainedWidth: constrainedWidth)
|
||||||
|
|
||||||
|
var validNodeIds: Set<AnyHashable> = []
|
||||||
|
|
||||||
|
for (itemId, itemType, frame) in visibleItems {
|
||||||
|
validNodeIds.insert(itemId)
|
||||||
|
|
||||||
|
switch itemType {
|
||||||
|
case .selectAll:
|
||||||
|
if self.actionNodes[itemId] == nil {
|
||||||
|
let selectAllAction = ContextMenuActionItem(text: presentationData.strings.Gift_Store_SelectAll, icon: { theme in
|
||||||
|
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Select"), color: theme.contextMenu.primaryColor)
|
||||||
|
}, iconPosition: .left, action: { _, f in
|
||||||
|
self.getController()?.dismiss(result: .dismissWithoutContent, completion: nil)
|
||||||
|
self.item.selectAll()
|
||||||
})
|
})
|
||||||
|
|
||||||
for i in 0 ..< self.actionNodes.count {
|
let actionNode = ContextControllerActionsListActionItemNode(
|
||||||
let itemNode = self.actionNodes[i]
|
context: self.item.context,
|
||||||
if !self.searchQuery.isEmpty && i == 0 {
|
getController: self.getController,
|
||||||
itemNode.isHidden = true
|
requestDismiss: self.actionSelected,
|
||||||
continue
|
requestUpdateAction: { _, _ in },
|
||||||
|
item: selectAllAction
|
||||||
|
)
|
||||||
|
self.actionNodes[itemId] = actionNode
|
||||||
|
self.scrollNode.addSubnode(actionNode)
|
||||||
}
|
}
|
||||||
|
|
||||||
if i > 0 && i < self.actionNodes.count - 1 {
|
case .attribute(let attribute):
|
||||||
let attribute = self.item.attributes[i - 1]
|
if self.actionNodes[itemId] == nil {
|
||||||
let attributeId: AnyHashable
|
let selectedAttributes = Set(self.item.selectedAttributes)
|
||||||
switch attribute {
|
guard let action = actionForAttribute(
|
||||||
case let .model(_, file, _):
|
attribute: attribute,
|
||||||
attributeId = AnyHashable(file.fileId.id)
|
presentationData: self.presentationData,
|
||||||
case let .pattern(_, file, _):
|
selectedAttributes: selectedAttributes,
|
||||||
attributeId = AnyHashable(file.fileId.id)
|
searchQuery: self.searchQuery,
|
||||||
case let .backdrop(_, id, _, _, _, _, _):
|
item: self.item,
|
||||||
attributeId = AnyHashable(id)
|
getController: self.getController
|
||||||
default:
|
) else { continue }
|
||||||
fatalError()
|
|
||||||
}
|
let actionNode = ContextControllerActionsListActionItemNode(
|
||||||
if !visibleAttributes.contains(attributeId) {
|
context: self.item.context,
|
||||||
itemNode.isHidden = true
|
getController: self.getController,
|
||||||
continue
|
requestDismiss: self.actionSelected,
|
||||||
}
|
requestUpdateAction: { _, _ in },
|
||||||
}
|
item: action
|
||||||
if i == self.actionNodes.count - 1 {
|
)
|
||||||
if !visibleAttributes.isEmpty {
|
self.actionNodes[itemId] = actionNode
|
||||||
itemNode.isHidden = true
|
self.scrollNode.addSubnode(actionNode)
|
||||||
continue
|
|
||||||
} else {
|
} else {
|
||||||
|
let selectedAttributes = Set(self.item.selectedAttributes)
|
||||||
|
if let action = actionForAttribute(
|
||||||
|
attribute: attribute,
|
||||||
|
presentationData: self.presentationData,
|
||||||
|
selectedAttributes: selectedAttributes,
|
||||||
|
searchQuery: self.searchQuery,
|
||||||
|
item: self.item,
|
||||||
|
getController: self.getController
|
||||||
|
) {
|
||||||
|
self.actionNodes[itemId]?.setItem(item: action)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
itemNode.isHidden = false
|
|
||||||
|
|
||||||
let (minSize, complete) = itemNode.update(presentationData: self.presentationData, constrainedSize: CGSize(width: constrainedWidth, height: constrainedHeight))
|
|
||||||
maxWidth = max(maxWidth, minSize.width)
|
|
||||||
heightsAndCompletions.append((i, minSize.height, complete))
|
|
||||||
contentHeight += minSize.height
|
|
||||||
}
|
|
||||||
|
|
||||||
maxWidth = max(maxWidth, minActionsWidth)
|
case .noResults:
|
||||||
|
if self.actionNodes[itemId] == nil {
|
||||||
|
let nopAction: ((ContextControllerProtocol?, @escaping (ContextMenuActionResult) -> Void) -> Void)? = nil
|
||||||
|
let emptyResultsAction = ContextMenuActionItem(
|
||||||
|
text: presentationData.strings.Gift_Store_NoResults,
|
||||||
|
textFont: .small,
|
||||||
|
icon: { _ in return nil },
|
||||||
|
action: nopAction
|
||||||
|
)
|
||||||
|
let actionNode = ContextControllerActionsListActionItemNode(
|
||||||
|
context: self.item.context,
|
||||||
|
getController: self.getController,
|
||||||
|
requestDismiss: self.actionSelected,
|
||||||
|
requestUpdateAction: { _, _ in },
|
||||||
|
item: emptyResultsAction
|
||||||
|
)
|
||||||
|
self.actionNodes[itemId] = actionNode
|
||||||
|
self.scrollNode.addSubnode(actionNode)
|
||||||
|
}
|
||||||
|
case .separator:
|
||||||
|
if self.separatorNodes[itemId] == nil {
|
||||||
|
let separatorNode = ASDisplayNode()
|
||||||
|
separatorNode.backgroundColor = presentationData.theme.contextMenu.itemSeparatorColor
|
||||||
|
self.separatorNodes[itemId] = separatorNode
|
||||||
|
self.scrollNode.addSubnode(separatorNode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let actionNode = self.actionNodes[itemId] {
|
||||||
|
actionNode.frame = frame
|
||||||
|
|
||||||
|
let (minSize, complete) = actionNode.update(presentationData: self.presentationData, constrainedSize: frame.size)
|
||||||
|
self.itemHeights[itemId] = minSize.height
|
||||||
|
complete(CGSize(width: maxWidth, height: minSize.height), .immediate)
|
||||||
|
} else if let separatorNode = self.separatorNodes[itemId] {
|
||||||
|
separatorNode.frame = frame
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var nodesToRemove: [AnyHashable] = []
|
||||||
|
for (nodeId, node) in self.actionNodes {
|
||||||
|
if !validNodeIds.contains(nodeId) {
|
||||||
|
nodesToRemove.append(nodeId)
|
||||||
|
node.removeFromSupernode()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for nodeId in nodesToRemove {
|
||||||
|
self.actionNodes.removeValue(forKey: nodeId)
|
||||||
|
}
|
||||||
|
|
||||||
|
var separatorsToRemove: [AnyHashable] = []
|
||||||
|
for (separatorId, separatorNode) in self.separatorNodes {
|
||||||
|
if !validNodeIds.contains(separatorId) {
|
||||||
|
separatorsToRemove.append(separatorId)
|
||||||
|
separatorNode.removeFromSupernode()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for separatorId in separatorsToRemove {
|
||||||
|
self.separatorNodes.removeValue(forKey: separatorId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func invalidateLayout() {
|
||||||
|
self.itemHeights.removeAll()
|
||||||
|
self.itemFrames.removeAll()
|
||||||
|
self.totalContentHeight = 0.0
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateLayout(constrainedWidth: CGFloat, constrainedHeight: CGFloat) -> (CGSize, (CGSize, ContainedViewLayoutTransition) -> Void) {
|
||||||
|
let minActionsWidth: CGFloat = 250.0
|
||||||
|
let maxActionsWidth: CGFloat = 300.0
|
||||||
|
let constrainedWidth = min(constrainedWidth, maxActionsWidth)
|
||||||
|
let maxWidth = max(constrainedWidth, minActionsWidth)
|
||||||
|
|
||||||
let maxHeight: CGFloat = min(360.0, constrainedHeight - 108.0)
|
let maxHeight: CGFloat = min(360.0, constrainedHeight - 108.0)
|
||||||
|
|
||||||
return (CGSize(width: maxWidth, height: min(maxHeight, contentHeight)), { size, transition in
|
if self.totalContentHeight == 0 {
|
||||||
var verticalOffset: CGFloat = 0.0
|
let _ = self.getVisibleItems(in: UIScrollView(), constrainedWidth: constrainedWidth)
|
||||||
for (i, itemHeight, itemCompletion) in heightsAndCompletions {
|
|
||||||
let itemNode = self.actionNodes[i]
|
|
||||||
|
|
||||||
let itemSize = CGSize(width: maxWidth, height: itemHeight)
|
|
||||||
transition.updateFrame(node: itemNode, frame: CGRect(origin: CGPoint(x: 0.0, y: verticalOffset), size: itemSize))
|
|
||||||
itemCompletion(itemSize, transition)
|
|
||||||
verticalOffset += itemHeight
|
|
||||||
|
|
||||||
if i < self.actionNodes.count - 2 {
|
|
||||||
let separatorNode = self.separatorNodes[i]
|
|
||||||
separatorNode.frame = CGRect(x: 0, y: verticalOffset, width: size.width, height: UIScreenPixel)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return (CGSize(width: maxWidth, height: min(maxHeight, self.totalContentHeight)), { size, transition in
|
||||||
|
self.maxWidth = maxWidth
|
||||||
|
|
||||||
transition.updateFrame(node: self.scrollNode, frame: CGRect(origin: CGPoint(), size: size))
|
transition.updateFrame(node: self.scrollNode, frame: CGRect(origin: CGPoint(), size: size))
|
||||||
self.scrollNode.view.contentSize = CGSize(width: size.width, height: contentHeight)
|
self.scrollNode.view.contentSize = CGSize(width: size.width, height: self.totalContentHeight)
|
||||||
|
|
||||||
|
self.updateScrolling(maxWidth: maxWidth)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -417,7 +516,7 @@ private final class GiftAttributeListContextItemNode: ASDisplayNode, ContextMenu
|
|||||||
}
|
}
|
||||||
|
|
||||||
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
|
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
|
||||||
for actionNode in self.actionNodes {
|
for (_, actionNode) in self.actionNodes {
|
||||||
actionNode.updateIsHighlighted(isHighlighted: false)
|
actionNode.updateIsHighlighted(isHighlighted: false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -98,6 +98,8 @@ final class GiftStoreScreenComponent: Component {
|
|||||||
private var initialCount: Int32?
|
private var initialCount: Int32?
|
||||||
private var showLoading = true
|
private var showLoading = true
|
||||||
|
|
||||||
|
private var selectedFilterId: AnyHashable?
|
||||||
|
|
||||||
private var component: GiftStoreScreenComponent?
|
private var component: GiftStoreScreenComponent?
|
||||||
private(set) weak var state: State?
|
private(set) weak var state: State?
|
||||||
private var environment: EnvironmentType?
|
private var environment: EnvironmentType?
|
||||||
@ -502,6 +504,13 @@ final class GiftStoreScreenComponent: Component {
|
|||||||
})))
|
})))
|
||||||
|
|
||||||
let contextController = ContextController(presentationData: presentationData, source: .reference(GiftStoreReferenceContentSource(controller: controller, sourceView: sourceView)), items: .single(ContextController.Items(content: .list(items))), gesture: nil)
|
let contextController = ContextController(presentationData: presentationData, source: .reference(GiftStoreReferenceContentSource(controller: controller, sourceView: sourceView)), items: .single(ContextController.Items(content: .list(items))), gesture: nil)
|
||||||
|
contextController.dismissed = { [weak self] in
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.selectedFilterId = nil
|
||||||
|
self.state?.updated()
|
||||||
|
}
|
||||||
controller.presentInGlobalOverlay(contextController)
|
controller.presentInGlobalOverlay(contextController)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -603,6 +612,13 @@ final class GiftStoreScreenComponent: Component {
|
|||||||
items: .single(ContextController.Items(content: .list(items))),
|
items: .single(ContextController.Items(content: .list(items))),
|
||||||
gesture: nil
|
gesture: nil
|
||||||
)
|
)
|
||||||
|
contextController.dismissed = { [weak self] in
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.selectedFilterId = nil
|
||||||
|
self.state?.updated()
|
||||||
|
}
|
||||||
controller.presentInGlobalOverlay(contextController)
|
controller.presentInGlobalOverlay(contextController)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -704,6 +720,13 @@ final class GiftStoreScreenComponent: Component {
|
|||||||
items: .single(ContextController.Items(content: .list(items))),
|
items: .single(ContextController.Items(content: .list(items))),
|
||||||
gesture: nil
|
gesture: nil
|
||||||
)
|
)
|
||||||
|
contextController.dismissed = { [weak self] in
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.selectedFilterId = nil
|
||||||
|
self.state?.updated()
|
||||||
|
}
|
||||||
controller.presentInGlobalOverlay(contextController)
|
controller.presentInGlobalOverlay(contextController)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -805,6 +828,13 @@ final class GiftStoreScreenComponent: Component {
|
|||||||
items: .single(ContextController.Items(content: .list(items))),
|
items: .single(ContextController.Items(content: .list(items))),
|
||||||
gesture: nil
|
gesture: nil
|
||||||
)
|
)
|
||||||
|
contextController.dismissed = { [weak self] in
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.selectedFilterId = nil
|
||||||
|
self.state?.updated()
|
||||||
|
}
|
||||||
controller.presentInGlobalOverlay(contextController)
|
controller.presentInGlobalOverlay(contextController)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -996,29 +1026,43 @@ final class GiftStoreScreenComponent: Component {
|
|||||||
let optionWidth = (availableSize.width - sideInset * 2.0 - optionSpacing * 2.0) / 3.0
|
let optionWidth = (availableSize.width - sideInset * 2.0 - optionSpacing * 2.0) / 3.0
|
||||||
|
|
||||||
var sortingTitle = environment.strings.Gift_Store_Sort_Date
|
var sortingTitle = environment.strings.Gift_Store_Sort_Date
|
||||||
var sortingIcon: String = "Peer Info/SortDate"
|
var sortingIcon: String = "GiftFilterDate"
|
||||||
|
var sortingIndex: Int = 0
|
||||||
if let sorting = self.state?.starGiftsState?.sorting {
|
if let sorting = self.state?.starGiftsState?.sorting {
|
||||||
switch sorting {
|
switch sorting {
|
||||||
case .date:
|
|
||||||
sortingTitle = environment.strings.Gift_Store_Sort_Date
|
|
||||||
sortingIcon = "Peer Info/SortDate"
|
|
||||||
case .value:
|
case .value:
|
||||||
sortingTitle = environment.strings.Gift_Store_Sort_Price
|
sortingTitle = environment.strings.Gift_Store_Sort_Price
|
||||||
sortingIcon = "Peer Info/SortValue"
|
sortingIcon = "GiftFilterPrice"
|
||||||
|
sortingIndex = 0
|
||||||
|
case .date:
|
||||||
|
sortingTitle = environment.strings.Gift_Store_Sort_Date
|
||||||
|
sortingIcon = "GiftFilterDate"
|
||||||
|
sortingIndex = 1
|
||||||
case .number:
|
case .number:
|
||||||
sortingTitle = environment.strings.Gift_Store_Sort_Number
|
sortingTitle = environment.strings.Gift_Store_Sort_Number
|
||||||
sortingIcon = "Peer Info/SortNumber"
|
sortingIcon = "GiftFilterNumber"
|
||||||
|
sortingIndex = 2
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum FilterItemId: Int32 {
|
||||||
|
case sort
|
||||||
|
case model
|
||||||
|
case backdrop
|
||||||
|
case symbol
|
||||||
|
}
|
||||||
|
|
||||||
var filterItems: [FilterSelectorComponent.Item] = []
|
var filterItems: [FilterSelectorComponent.Item] = []
|
||||||
filterItems.append(FilterSelectorComponent.Item(
|
filterItems.append(FilterSelectorComponent.Item(
|
||||||
id: AnyHashable(0),
|
id: AnyHashable(FilterItemId.sort),
|
||||||
|
index: sortingIndex,
|
||||||
iconName: sortingIcon,
|
iconName: sortingIcon,
|
||||||
title: sortingTitle,
|
title: sortingTitle,
|
||||||
action: { [weak self] view in
|
action: { [weak self] view in
|
||||||
if let self {
|
if let self {
|
||||||
|
self.selectedFilterId = AnyHashable(FilterItemId.sort)
|
||||||
self.openSortContextMenu(sourceView: view)
|
self.openSortContextMenu(sourceView: view)
|
||||||
|
self.state?.updated()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
))
|
))
|
||||||
@ -1035,10 +1079,10 @@ final class GiftStoreScreenComponent: Component {
|
|||||||
switch attribute {
|
switch attribute {
|
||||||
case .model:
|
case .model:
|
||||||
modelCount += 1
|
modelCount += 1
|
||||||
case .pattern:
|
|
||||||
symbolCount += 1
|
|
||||||
case .backdrop:
|
case .backdrop:
|
||||||
backdropCount += 1
|
backdropCount += 1
|
||||||
|
case .pattern:
|
||||||
|
symbolCount += 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1054,29 +1098,35 @@ final class GiftStoreScreenComponent: Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
filterItems.append(FilterSelectorComponent.Item(
|
filterItems.append(FilterSelectorComponent.Item(
|
||||||
id: AnyHashable(1),
|
id: AnyHashable(FilterItemId.model),
|
||||||
title: modelTitle,
|
title: modelTitle,
|
||||||
action: { [weak self] view in
|
action: { [weak self] view in
|
||||||
if let self {
|
if let self {
|
||||||
|
self.selectedFilterId = AnyHashable(FilterItemId.model)
|
||||||
self.openModelContextMenu(sourceView: view)
|
self.openModelContextMenu(sourceView: view)
|
||||||
|
self.state?.updated()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
))
|
))
|
||||||
filterItems.append(FilterSelectorComponent.Item(
|
filterItems.append(FilterSelectorComponent.Item(
|
||||||
id: AnyHashable(2),
|
id: AnyHashable(FilterItemId.backdrop),
|
||||||
title: backdropTitle,
|
title: backdropTitle,
|
||||||
action: { [weak self] view in
|
action: { [weak self] view in
|
||||||
if let self {
|
if let self {
|
||||||
|
self.selectedFilterId = AnyHashable(FilterItemId.backdrop)
|
||||||
self.openBackdropContextMenu(sourceView: view)
|
self.openBackdropContextMenu(sourceView: view)
|
||||||
|
self.state?.updated()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
))
|
))
|
||||||
filterItems.append(FilterSelectorComponent.Item(
|
filterItems.append(FilterSelectorComponent.Item(
|
||||||
id: AnyHashable(3),
|
id: AnyHashable(FilterItemId.symbol),
|
||||||
title: symbolTitle,
|
title: symbolTitle,
|
||||||
action: { [weak self] view in
|
action: { [weak self] view in
|
||||||
if let self {
|
if let self {
|
||||||
|
self.selectedFilterId = AnyHashable(FilterItemId.symbol)
|
||||||
self.openSymbolContextMenu(sourceView: view)
|
self.openSymbolContextMenu(sourceView: view)
|
||||||
|
self.state?.updated()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
))
|
))
|
||||||
@ -1092,7 +1142,8 @@ final class GiftStoreScreenComponent: Component {
|
|||||||
foreground: theme.list.itemPrimaryTextColor.withMultipliedAlpha(0.65),
|
foreground: theme.list.itemPrimaryTextColor.withMultipliedAlpha(0.65),
|
||||||
background: theme.list.itemSecondaryTextColor.mixedWith(theme.list.blocksBackgroundColor, alpha: 0.85)
|
background: theme.list.itemSecondaryTextColor.mixedWith(theme.list.blocksBackgroundColor, alpha: 0.85)
|
||||||
),
|
),
|
||||||
items: filterItems
|
items: filterItems,
|
||||||
|
selectedItemId: self.selectedFilterId
|
||||||
)),
|
)),
|
||||||
environment: {},
|
environment: {},
|
||||||
containerSize: CGSize(width: availableSize.width - 10.0 * 2.0, height: 50.0)
|
containerSize: CGSize(width: availableSize.width - 10.0 * 2.0, height: 50.0)
|
||||||
@ -1193,8 +1244,17 @@ final class GiftStoreScreenComponent: Component {
|
|||||||
guard let self else {
|
guard let self else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
let previousFilterAttributes = self.starGiftsState?.filterAttributes
|
||||||
|
let previousSorting = self.starGiftsState?.sorting
|
||||||
self.starGiftsState = state
|
self.starGiftsState = state
|
||||||
self.updated()
|
|
||||||
|
var transition: ComponentTransition = .immediate
|
||||||
|
if let previousFilterAttributes, previousFilterAttributes != state.filterAttributes {
|
||||||
|
transition = .easeInOut(duration: 0.25)
|
||||||
|
} else if let previousSorting, previousSorting != state.sorting {
|
||||||
|
transition = .easeInOut(duration: 0.25)
|
||||||
|
}
|
||||||
|
self.updated(transition: transition)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
BIN
submodules/TelegramUI/Resources/Animations/GiftFilterDate.tgs
Normal file
BIN
submodules/TelegramUI/Resources/Animations/GiftFilterDate.tgs
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
submodules/TelegramUI/Resources/Animations/GiftFilterNumber.tgs
Normal file
BIN
submodules/TelegramUI/Resources/Animations/GiftFilterNumber.tgs
Normal file
Binary file not shown.
BIN
submodules/TelegramUI/Resources/Animations/GiftFilterPrice.tgs
Normal file
BIN
submodules/TelegramUI/Resources/Animations/GiftFilterPrice.tgs
Normal file
Binary file not shown.
@ -61,8 +61,10 @@ extension ChatControllerImpl {
|
|||||||
|
|
||||||
var canSendPolls = true
|
var canSendPolls = true
|
||||||
if let peer = self.presentationInterfaceState.renderedPeer?.peer {
|
if let peer = self.presentationInterfaceState.renderedPeer?.peer {
|
||||||
if let peer = peer as? TelegramUser, peer.botInfo == nil {
|
if let peer = peer as? TelegramUser {
|
||||||
|
if peer.botInfo == nil && peer.id != self.context.account.peerId {
|
||||||
canSendPolls = false
|
canSendPolls = false
|
||||||
|
}
|
||||||
} else if peer is TelegramSecretChat {
|
} else if peer is TelegramSecretChat {
|
||||||
canSendPolls = false
|
canSendPolls = false
|
||||||
} else if let channel = peer as? TelegramChannel {
|
} else if let channel = peer as? TelegramChannel {
|
||||||
|
@ -528,9 +528,22 @@ public final class WebAppMessagePreviewScreen: ViewControllerComponentContainer
|
|||||||
}
|
}
|
||||||
|
|
||||||
fileprivate func proceed() {
|
fileprivate func proceed() {
|
||||||
let requestPeerType = self.preparedMessage.peerTypes.requestPeerTypes
|
let peerTypes = self.preparedMessage.peerTypes
|
||||||
|
var types: [ReplyMarkupButtonRequestPeerType] = []
|
||||||
|
if peerTypes.contains(.users) {
|
||||||
|
types.append(.user(.init(isBot: false, isPremium: nil)))
|
||||||
|
}
|
||||||
|
if peerTypes.contains(.bots) {
|
||||||
|
types.append(.user(.init(isBot: true, isPremium: nil)))
|
||||||
|
}
|
||||||
|
if peerTypes.contains(.channels) {
|
||||||
|
types.append(.channel(.init(isCreator: false, hasUsername: nil, userAdminRights: TelegramChatAdminRights(rights: [.canPostMessages]), botAdminRights: nil)))
|
||||||
|
}
|
||||||
|
if peerTypes.contains(.groups) {
|
||||||
|
types.append(.group(.init(isCreator: false, hasUsername: nil, isForum: nil, botParticipant: false, userAdminRights: nil, botAdminRights: nil)))
|
||||||
|
}
|
||||||
|
|
||||||
let controller = self.context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: self.context, filter: [.excludeRecent, .doNotSearchMessages], requestPeerType: requestPeerType, hasContactSelector: false, multipleSelection: true, selectForumThreads: true, immediatelyActivateMultipleSelection: true))
|
let controller = self.context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: self.context, filter: [.excludeRecent, .doNotSearchMessages], requestPeerType: types, hasContactSelector: false, multipleSelection: true, selectForumThreads: true, immediatelyActivateMultipleSelection: true))
|
||||||
|
|
||||||
controller.multiplePeersSelected = { [weak self, weak controller] peers, _, _, _, _, _ in
|
controller.multiplePeersSelected = { [weak self, weak controller] peers, _, _, _, _, _ in
|
||||||
guard let self else {
|
guard let self else {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user