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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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