Merge commit '40780242fe8ef168b115e9b2b47914faecf3dea5'

This commit is contained in:
Isaac 2025-05-23 15:56:20 +08:00
commit 23e52bc1f7
13 changed files with 526 additions and 230 deletions

View File

@ -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 {
self.pollOptions.append(PollOption(id: self.nextPollOptionId))
self.nextPollOptionId += 1
@ -921,7 +921,7 @@ final class ComposePollScreenComponent: Component {
contentHeight += 7.0
let pollOptionsLimitReached = self.pollOptions.count >= 10
let pollOptionsLimitReached = self.pollOptions.count >= component.initialData.maxPollAnswersCount
var animatePollOptionsFooterIn = false
var pollOptionsFooterTransition = transition
if self.currentPollOptionsLimitReached != pollOptionsLimitReached {
@ -944,7 +944,7 @@ final class ComposePollScreenComponent: Component {
maximumNumberOfLines: 0
))
} else {
let remainingCount = 10 - self.pollOptions.count
let remainingCount = component.initialData.maxPollAnswersCount - self.pollOptions.count
let rawString = environment.strings.CreatePoll_OptionCountFooterFormat(Int32(remainingCount))
var pollOptionsFooterItems: [AnimatedTextComponent.Item] = []
@ -1476,13 +1476,16 @@ public class ComposePollScreen: ViewControllerComponentContainer, AttachmentCont
public final class InitialData {
fileprivate let maxPollTextLength: Int
fileprivate let maxPollOptionLength: Int
fileprivate let maxPollAnswersCount: Int
fileprivate init(
maxPollTextLength: Int,
maxPollOptionLength: Int
maxPollOptionLength: Int,
maxPollAnwsersCount: Int
) {
self.maxPollTextLength = maxPollTextLength
self.maxPollOptionLength = maxPollOptionLength
self.maxPollAnswersCount = maxPollAnwsersCount
}
}
@ -1577,9 +1580,14 @@ public class ComposePollScreen: ViewControllerComponentContainer, AttachmentCont
}
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(
maxPollTextLength: Int(200),
maxPollOptionLength: 100
maxPollOptionLength: 100,
maxPollAnwsersCount: maxPollAnwsersCount
)
}

View File

@ -426,7 +426,9 @@ private func contactListNodeEntries(accountPeer: EnginePeer?, peers: [ContactLis
let text = strings.Contacts_PermissionsText
switch authorizationStatus {
case .limited:
if displaySortOptions {
entries.append(.permissionLimited(theme, strings))
}
case .denied:
entries.append(.permissionInfo(theme, title, text, suppressed))
entries.append(.permissionEnable(theme, strings.Permissions_ContactsAllowInSettings_v0))

View File

@ -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)
}
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 {
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
}

View File

@ -2,11 +2,13 @@ import Foundation
import UIKit
import Display
import ComponentFlow
import SwiftSignalKit
import PlainButtonComponent
import MultilineTextWithEntitiesComponent
import MultilineTextComponent
import BundleIconComponent
import TextFormat
import AccountContext
import LottieComponent
public final class FilterSelectorComponent: Component {
public struct Colors: Equatable {
@ -24,39 +26,45 @@ public final class FilterSelectorComponent: Component {
public struct Item: Equatable {
public var id: AnyHashable
public var index: Int
public var iconName: String?
public var title: String
public var action: (UIView) -> Void
public init(
id: AnyHashable,
index: Int = 0,
iconName: String? = nil,
title: String,
action: @escaping (UIView) -> Void
) {
self.id = id
self.index = index
self.iconName = iconName
self.title = title
self.action = action
}
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 colors: Colors
public let items: [Item]
public let selectedItemId: AnyHashable?
public init(
context: AccountContext? = nil,
colors: Colors,
items: [Item]
items: [Item],
selectedItemId: AnyHashable?
) {
self.context = context
self.colors = colors
self.items = items
self.selectedItemId = selectedItemId
}
public static func ==(lhs: FilterSelectorComponent, rhs: FilterSelectorComponent) -> Bool {
@ -69,6 +77,9 @@ public final class FilterSelectorComponent: Component {
if lhs.items != rhs.items {
return false
}
if lhs.selectedItemId != rhs.selectedItemId {
return false
}
return true
}
@ -150,15 +161,17 @@ public final class FilterSelectorComponent: Component {
validIds.append(itemId)
let itemSize = itemView.title.update(
transition: .immediate,
transition: transition,
component: AnyComponent(PlainButtonComponent(
content: AnyComponent(ItemComponent(
context: component.context,
index: item.index,
iconName: item.iconName,
text: item.title,
font: itemFont,
color: component.colors.foreground,
backgroundColor: component.colors.background
backgroundColor: component.colors.background,
isSelected: itemId == component.selectedItemId
)),
effectAlignment: .center,
minSize: nil,
@ -242,34 +255,43 @@ extension CGRect {
}
}
private final class ItemComponent: CombinedComponent {
private final class ItemComponent: Component {
let context: AccountContext?
let index: Int
let iconName: String?
let text: String
let font: UIFont
let color: UIColor
let backgroundColor: UIColor
let isSelected: Bool
init(
context: AccountContext?,
index: Int,
iconName: String?,
text: String,
font: UIFont,
color: UIColor,
backgroundColor: UIColor
backgroundColor: UIColor,
isSelected: Bool
) {
self.context = context
self.index = index
self.iconName = iconName
self.text = text
self.font = font
self.color = color
self.backgroundColor = backgroundColor
self.isSelected = isSelected
}
static func ==(lhs: ItemComponent, rhs: ItemComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.index != rhs.index {
return false
}
if lhs.iconName != rhs.iconName {
return false
}
@ -285,80 +307,166 @@ private final class ItemComponent: CombinedComponent {
if lhs.backgroundColor != rhs.backgroundColor {
return false
}
if lhs.isSelected != rhs.isSelected {
return false
}
return true
}
static var body: Body {
let background = Child(RoundedRectangle.self)
let title = Child(MultilineTextWithEntitiesComponent.self)
let icon = Child(BundleIconComponent.self)
public final class View: UIView {
private var component: ItemComponent?
private weak var state: EmptyComponentState?
return { context in
let component = context.component
private let background = ComponentView<Empty>()
private let title = ComponentView<Empty>()
private let icon = ComponentView<Empty>()
let attributedTitle = NSMutableAttributedString(string: component.text, font: component.font, textColor: component.color)
let range = (attributedTitle.string as NSString).range(of: "⭐️")
if range.location != NSNotFound {
attributedTitle.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .stars(tinted: false)), range: range)
private var isSelected = false
private var iconName: String?
private let playOnce = ActionSlot<Void>()
override init(frame: CGRect) {
super.init(frame: frame)
}
let title = title.update(
component: MultilineTextWithEntitiesComponent(
context: component.context,
animationCache: component.context?.animationCache,
animationRenderer: component.context?.animationRenderer,
placeholderColor: .white,
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(component: ItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
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)
),
availableSize: context.availableSize,
transition: .immediate
)),
environment: {},
containerSize: availableSize
)
let icon = icon.update(
component: BundleIconComponent(
name: component.iconName ?? "Item List/ExpandableSelectorArrows",
tintColor: component.color,
maxSize: component.iconName != nil ? CGSize(width: 22.0, height: 22.0) : nil
),
availableSize: CGSize(width: 100, height: 100),
transition: .immediate
let animationName = component.iconName ?? (component.isSelected ? "GiftFilterMenuOpen" : "GiftFilterMenuClose")
let animationSize = component.iconName != nil ? CGSize(width: 22.0, height: 22.0) : CGSize(width: 10.0, height: 22.0)
let iconSize = self.icon.update(
transition: transition,
component: AnyComponent(LottieComponent(
content: LottieComponent.AppBundleContent(name: animationName),
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
var leftPadding = padding
if let _ = component.iconName {
leftPadding -= 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 background = background.update(
component: RoundedRectangle(
let backgroundSize = self.background.update(
transition: transition,
component: AnyComponent(RoundedRectangle(
color: component.backgroundColor,
cornerRadius: 14.0
),
availableSize: size,
transition: .immediate
)
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))
)),
environment: {},
containerSize: size
)
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
}
}
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)
}
}

View File

@ -201,70 +201,27 @@ private final class GiftAttributeListContextItemNode: ASDisplayNode, ContextMenu
private let actionSelected: (ContextMenuActionResult) -> Void
private let scrollNode: ASScrollNode
private let actionNodes: [ContextControllerActionsListActionItemNode]
private let separatorNodes: [ASDisplayNode]
private var actionNodes: [AnyHashable: ContextControllerActionsListActionItemNode] = [:]
private var separatorNodes: [AnyHashable: ASDisplayNode] = [:]
private var searchDisposable: Disposable?
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) {
self.item = item
self.presentationData = presentationData
self.presentationData = presentationData.withUpdate(listsFontSize: .regular)
self.getController = getController
self.actionSelected = actionSelected
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()
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
|> deliverOnMainQueue).start(next: { [weak self] searchQuery in
@ -272,15 +229,7 @@ private final class GiftAttributeListContextItemNode: ASDisplayNode, ContextMenu
return
}
self.searchQuery = searchQuery.trimmingCharacters(in: .whitespacesAndNewlines)
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.invalidateLayout()
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)
}
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)
var maxWidth: CGFloat = 0.0
var contentHeight: CGFloat = 0.0
var heightsAndCompletions: [(Int, CGFloat, (CGSize, ContainedViewLayoutTransition) -> Void)] = []
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if let maxWidth = self.maxWidth {
self.updateScrolling(maxWidth: maxWidth)
}
}
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]
if self.searchQuery.isEmpty {
effectiveAttributes = self.item.attributes
} else {
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 {
case let .model(_, file, _):
return file.fileId.id
return AnyHashable("model_\(file.fileId.id)")
case let .pattern(_, file, _):
return file.fileId.id
return AnyHashable("pattern_\(file.fileId.id)")
case let .backdrop(_, id, _, _, _, _, _):
return id
return AnyHashable("backdrop_\(id)")
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 itemNode = self.actionNodes[i]
if !self.searchQuery.isEmpty && i == 0 {
itemNode.isHidden = true
continue
let actionNode = ContextControllerActionsListActionItemNode(
context: self.item.context,
getController: self.getController,
requestDismiss: self.actionSelected,
requestUpdateAction: { _, _ in },
item: selectAllAction
)
self.actionNodes[itemId] = actionNode
self.scrollNode.addSubnode(actionNode)
}
if i > 0 && i < self.actionNodes.count - 1 {
let attribute = self.item.attributes[i - 1]
let attributeId: AnyHashable
switch attribute {
case let .model(_, file, _):
attributeId = AnyHashable(file.fileId.id)
case let .pattern(_, file, _):
attributeId = AnyHashable(file.fileId.id)
case let .backdrop(_, id, _, _, _, _, _):
attributeId = AnyHashable(id)
default:
fatalError()
}
if !visibleAttributes.contains(attributeId) {
itemNode.isHidden = true
continue
}
}
if i == self.actionNodes.count - 1 {
if !visibleAttributes.isEmpty {
itemNode.isHidden = true
continue
case .attribute(let attribute):
if self.actionNodes[itemId] == nil {
let selectedAttributes = Set(self.item.selectedAttributes)
guard let action = actionForAttribute(
attribute: attribute,
presentationData: self.presentationData,
selectedAttributes: selectedAttributes,
searchQuery: self.searchQuery,
item: self.item,
getController: self.getController
) else { continue }
let actionNode = ContextControllerActionsListActionItemNode(
context: self.item.context,
getController: self.getController,
requestDismiss: self.actionSelected,
requestUpdateAction: { _, _ in },
item: action
)
self.actionNodes[itemId] = actionNode
self.scrollNode.addSubnode(actionNode)
} 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)
return (CGSize(width: maxWidth, height: min(maxHeight, contentHeight)), { size, transition in
var verticalOffset: CGFloat = 0.0
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)
}
if self.totalContentHeight == 0 {
let _ = self.getVisibleItems(in: UIScrollView(), constrainedWidth: constrainedWidth)
}
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))
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) {
for actionNode in self.actionNodes {
for (_, actionNode) in self.actionNodes {
actionNode.updateIsHighlighted(isHighlighted: false)
}
}

View File

@ -98,6 +98,8 @@ final class GiftStoreScreenComponent: Component {
private var initialCount: Int32?
private var showLoading = true
private var selectedFilterId: AnyHashable?
private var component: GiftStoreScreenComponent?
private(set) weak var state: State?
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)
contextController.dismissed = { [weak self] in
guard let self else {
return
}
self.selectedFilterId = nil
self.state?.updated()
}
controller.presentInGlobalOverlay(contextController)
}
@ -603,6 +612,13 @@ final class GiftStoreScreenComponent: Component {
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)
}
@ -704,6 +720,13 @@ final class GiftStoreScreenComponent: Component {
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)
}
@ -805,6 +828,13 @@ final class GiftStoreScreenComponent: Component {
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)
}
@ -996,29 +1026,43 @@ final class GiftStoreScreenComponent: Component {
let optionWidth = (availableSize.width - sideInset * 2.0 - optionSpacing * 2.0) / 3.0
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 {
switch sorting {
case .date:
sortingTitle = environment.strings.Gift_Store_Sort_Date
sortingIcon = "Peer Info/SortDate"
case .value:
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:
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] = []
filterItems.append(FilterSelectorComponent.Item(
id: AnyHashable(0),
id: AnyHashable(FilterItemId.sort),
index: sortingIndex,
iconName: sortingIcon,
title: sortingTitle,
action: { [weak self] view in
if let self {
self.selectedFilterId = AnyHashable(FilterItemId.sort)
self.openSortContextMenu(sourceView: view)
self.state?.updated()
}
}
))
@ -1035,10 +1079,10 @@ final class GiftStoreScreenComponent: Component {
switch attribute {
case .model:
modelCount += 1
case .pattern:
symbolCount += 1
case .backdrop:
backdropCount += 1
case .pattern:
symbolCount += 1
}
}
@ -1054,29 +1098,35 @@ final class GiftStoreScreenComponent: Component {
}
filterItems.append(FilterSelectorComponent.Item(
id: AnyHashable(1),
id: AnyHashable(FilterItemId.model),
title: modelTitle,
action: { [weak self] view in
if let self {
self.selectedFilterId = AnyHashable(FilterItemId.model)
self.openModelContextMenu(sourceView: view)
self.state?.updated()
}
}
))
filterItems.append(FilterSelectorComponent.Item(
id: AnyHashable(2),
id: AnyHashable(FilterItemId.backdrop),
title: backdropTitle,
action: { [weak self] view in
if let self {
self.selectedFilterId = AnyHashable(FilterItemId.backdrop)
self.openBackdropContextMenu(sourceView: view)
self.state?.updated()
}
}
))
filterItems.append(FilterSelectorComponent.Item(
id: AnyHashable(3),
id: AnyHashable(FilterItemId.symbol),
title: symbolTitle,
action: { [weak self] view in
if let self {
self.selectedFilterId = AnyHashable(FilterItemId.symbol)
self.openSymbolContextMenu(sourceView: view)
self.state?.updated()
}
}
))
@ -1092,7 +1142,8 @@ final class GiftStoreScreenComponent: Component {
foreground: theme.list.itemPrimaryTextColor.withMultipliedAlpha(0.65),
background: theme.list.itemSecondaryTextColor.mixedWith(theme.list.blocksBackgroundColor, alpha: 0.85)
),
items: filterItems
items: filterItems,
selectedItemId: self.selectedFilterId
)),
environment: {},
containerSize: CGSize(width: availableSize.width - 10.0 * 2.0, height: 50.0)
@ -1193,8 +1244,17 @@ final class GiftStoreScreenComponent: Component {
guard let self else {
return
}
let previousFilterAttributes = self.starGiftsState?.filterAttributes
let previousSorting = self.starGiftsState?.sorting
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)
})
}

View File

@ -61,8 +61,10 @@ extension ChatControllerImpl {
var canSendPolls = true
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
}
} else if peer is TelegramSecretChat {
canSendPolls = false
} else if let channel = peer as? TelegramChannel {

View File

@ -528,9 +528,22 @@ public final class WebAppMessagePreviewScreen: ViewControllerComponentContainer
}
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
guard let self else {