Various fixes

This commit is contained in:
Ilya Laktyushin 2025-02-27 03:20:51 +04:00
parent 3f12448474
commit c026f5af0b
4 changed files with 247 additions and 83 deletions

View File

@ -1150,9 +1150,33 @@ private final class ProfileGiftsContextImpl {
self.actionDisposable.set(
_internal_updateStarGiftAddedToProfile(account: self.account, reference: reference, added: added).startStrict()
)
if let index = self.gifts.firstIndex(where: { $0.reference == reference }) {
self.gifts[index] = self.gifts[index].withSavedToProfile(added)
if !added && self.gifts[index].pinnedToTop {
let pinnedGifts = self.gifts.filter { $0.pinnedToTop && $0.reference != reference }
let existingGifts = Set(pinnedGifts.compactMap { $0.reference })
var updatedGifts: [ProfileGiftsContext.State.StarGift] = []
for gift in self.gifts {
if let reference = gift.reference, existingGifts.contains(reference) {
continue
}
var gift = gift
if gift.reference == reference {
gift = gift.withPinnedToTop(false).withSavedToProfile(false)
}
updatedGifts.append(gift)
}
updatedGifts.sort { lhs, rhs in
lhs.date > rhs.date
}
updatedGifts.insert(contentsOf: pinnedGifts, at: 0)
self.gifts = updatedGifts
} else {
self.gifts[index] = self.gifts[index].withSavedToProfile(added)
}
}
if let index = self.filteredGifts.firstIndex(where: { $0.reference == reference }) {
self.filteredGifts[index] = self.filteredGifts[index].withSavedToProfile(added)
if !self.filter.contains(.hidden) && !added {
@ -1164,9 +1188,14 @@ private final class ProfileGiftsContextImpl {
func updateStarGiftPinnedToTop(reference: StarGiftReference, pinnedToTop: Bool) {
var pinnedGifts = self.gifts.filter { $0.pinnedToTop }
var saveToProfile = false
if var gift = self.gifts.first(where: { $0.reference == reference }) {
gift = gift.withPinnedToTop(pinnedToTop)
if pinnedToTop {
if !gift.savedToProfile {
gift = gift.withSavedToProfile(true)
saveToProfile = true
}
pinnedGifts.append(gift)
} else {
pinnedGifts.removeAll(where: { $0.reference == reference })
@ -1192,8 +1221,15 @@ private final class ProfileGiftsContextImpl {
}
self.pushState()
var signal = _internal_updateStarGiftsPinnedToTop(account: self.account, peerId: self.peerId, references: pinnedGifts.compactMap { $0.reference })
if saveToProfile {
signal = _internal_updateStarGiftAddedToProfile(account: self.account, reference: reference, added: true)
|> then(signal)
}
self.actionDisposable.set(
_internal_updateStarGiftsPinnedToTop(account: self.account, peerId: self.peerId, references: pinnedGifts.compactMap { $0.reference }).startStrict(completed: { [weak self] in
(signal |> deliverOn(self.queue)).startStrict(completed: { [weak self] in
self?.reload()
})
)
@ -1201,13 +1237,19 @@ private final class ProfileGiftsContextImpl {
public func updatePinnedToTopStarGifts(references: [StarGiftReference]) {
let existingGifts = Set(references)
var saveSignals: [Signal<Never, NoError>] = []
let currentPinnedGifts = self.gifts.filter { gift in
if let reference = gift.reference {
return existingGifts.contains(reference)
} else {
return false
}
}.map { $0.withPinnedToTop(true) }
}.map { gift in
if !gift.savedToProfile, let reference = gift.reference {
saveSignals.append(_internal_updateStarGiftAddedToProfile(account: self.account, reference: reference, added: true))
}
return gift.withPinnedToTop(true).withSavedToProfile(true)
}
var updatedGifts: [ProfileGiftsContext.State.StarGift] = []
for gift in self.gifts {
@ -1231,8 +1273,15 @@ private final class ProfileGiftsContextImpl {
self.pushState()
var signal = _internal_updateStarGiftsPinnedToTop(account: self.account, peerId: self.peerId, references: pinnedGifts.compactMap { $0.reference })
if !saveSignals.isEmpty {
signal = combineLatest(saveSignals)
|> ignoreValues
|> then(signal)
}
self.actionDisposable.set(
_internal_updateStarGiftsPinnedToTop(account: self.account, peerId: self.peerId, references: pinnedGifts.compactMap { $0.reference }).startStrict(completed: { [weak self] in
(signal |> deliverOn(self.queue)).startStrict(completed: { [weak self] in
self?.reload()
})
)

View File

@ -664,7 +664,7 @@ public final class GiftItemComponent: Component {
var iconBackgroundSize: CGSize?
if component.isEditing {
if !component.isPinned {
if !component.isPinned && backgroundColor != nil {
iconBackgroundSize = CGSize(width: 48.0, height: 48.0)
}
} else {
@ -710,7 +710,7 @@ public final class GiftItemComponent: Component {
iconBackground.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false)
}
if component.isPinned || component.isEditing {
if component.isPinned || (component.isEditing && backgroundColor != nil) {
let pinnedIcon: UIImageView
if let currentIcon = self.pinnedIcon {
pinnedIcon = currentIcon

View File

@ -43,9 +43,11 @@ final class PeerInfoPaneTabsContainerPaneNode: ASDisplayNode {
private let titleNode: ImmediateTextNode
private let buttonNode: HighlightTrackingButtonNode
private var iconLayers: [InlineStickerItemLayer] = []
private var iconLayers: [AnyHashable: InlineStickerItemLayer] = [:]
private var isSelected: Bool = false
private var icons: [ProfileGiftsContext.State.StarGift] = []
private var titleWidth: CGFloat?
init(pressed: @escaping () -> Void) {
self.pressed = pressed
@ -67,27 +69,53 @@ final class PeerInfoPaneTabsContainerPaneNode: ASDisplayNode {
self.pressed()
}
func updateText(context: AccountContext, title: String, icons: [TelegramMediaFile] = [], isSelected: Bool, presentationData: PresentationData) {
func updateText(context: AccountContext, title: String, icons: [ProfileGiftsContext.State.StarGift] = [], isSelected: Bool, presentationData: PresentationData) {
self.isSelected = isSelected
self.titleNode.attributedText = NSAttributedString(string: title, font: Font.medium(14.0), textColor: isSelected ? presentationData.theme.list.itemAccentColor : presentationData.theme.list.itemSecondaryTextColor)
self.icons = icons
if !icons.isEmpty {
if self.iconLayers.isEmpty {
for icon in icons {
let iconSize = CGSize(width: 18.0, height: 18.0)
var validIds = Set<AnyHashable>()
var index = 0
for icon in icons {
let id: AnyHashable
if let reference = icon.reference {
id = reference
} else {
id = index
}
validIds.insert(id)
let iconSize = CGSize(width: 18.0, height: 18.0)
if let _ = self.iconLayers[id] {
} else {
var file: TelegramMediaFile?
switch icon.gift {
case let .generic(gift):
file = gift.file
case let .unique(gift):
for attribute in gift.attributes {
if case let .model(_, fileValue, _) = attribute {
file = fileValue
}
}
}
guard let file else {
continue
}
let emoji = ChatTextInputTextCustomEmojiAttribute(
interactivelySelectedFromPackId: nil,
fileId: icon.fileId.id,
file: icon
fileId: file.fileId.id,
file: file
)
let animationLayer = InlineStickerItemLayer(
context: .account(context),
userLocation: .other,
attemptSynchronousLoad: false,
emoji: emoji,
file: icon,
file: file,
cache: context.animationCache,
renderer: context.animationRenderer,
unique: true,
@ -96,12 +124,30 @@ final class PeerInfoPaneTabsContainerPaneNode: ASDisplayNode {
loopCount: 1
)
animationLayer.isVisibleForAnimations = true
self.iconLayers.append(animationLayer)
self.iconLayers[id] = animationLayer
self.layer.addSublayer(animationLayer)
animationLayer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
animationLayer.animateScale(from: 0.01, to: 1.0, duration: 0.2)
}
index += 1
}
var removeIds: [AnyHashable] = []
for (id, layer) in self.iconLayers {
if !validIds.contains(id) {
removeIds.append(id)
layer.animateScale(from: 1.0, to: 0.01, duration: 0.25, removeOnCompletion: false)
layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in
layer.removeFromSuperlayer()
})
}
}
for id in removeIds {
self.iconLayers.removeValue(forKey: id)
}
} else {
for layer in self.iconLayers {
for (_, layer) in self.iconLayers {
layer.removeFromSuperlayer()
}
self.iconLayers.removeAll()
@ -115,24 +161,54 @@ final class PeerInfoPaneTabsContainerPaneNode: ASDisplayNode {
}
func updateLayout(height: CGFloat) -> CGFloat {
var totalWidth: CGFloat = 0.0
let titleSize = self.titleNode.updateLayout(CGSize(width: 200.0, height: .greatestFiniteMagnitude))
self.titleNode.frame = CGRect(origin: CGPoint(x: 0.0, y: floor((height - titleSize.height) / 2.0)), size: titleSize)
totalWidth = titleSize.width
let iconSize = CGSize(width: 18.0, height: 18.0)
let spacing: CGFloat = 1.0
self.titleNode.frame = CGRect(origin: CGPoint(x: 0.0, y: floor((height - titleSize.height) / 2.0)), size: titleSize)
self.titleWidth = titleSize.width
var totalWidth = titleSize.width
if !self.iconLayers.isEmpty {
totalWidth += 2.0
let iconSize = CGSize(width: 18.0, height: 18.0)
let spacing: CGFloat = 1.0
for iconlayer in self.iconLayers {
iconlayer.frame = CGRect(origin: CGPoint(x: totalWidth, y: 15.0), size: iconSize)
totalWidth += iconSize.width + spacing
}
totalWidth += (iconSize.width + spacing) * CGFloat(self.iconLayers.count)
totalWidth -= spacing
}
self.layoutIcons(transition: .animated(duration: 0.3, curve: .spring))
return totalWidth
}
func layoutIcons(transition: ContainedViewLayoutTransition) {
guard let titleWidth = self.titleWidth else {
return
}
let iconSize = CGSize(width: 18.0, height: 18.0)
let spacing: CGFloat = 1.0
var origin = CGPoint(x: titleWidth + 2.0, y: 15.0)
var index = 0
for icon in self.icons {
let id: AnyHashable
if let reference = icon.reference {
id = reference
} else {
id = index
}
if let layer = self.iconLayers[id] {
var iconTransition = transition
if layer.frame.width.isZero {
iconTransition = .immediate
}
iconTransition.updateFrame(layer: layer, frame: CGRect(origin: origin, size: iconSize))
}
origin.x += iconSize.width + spacing
index += 1
}
}
func updateArea(size: CGSize, sideInset: CGFloat) {
self.buttonNode.frame = CGRect(origin: CGPoint(x: -sideInset, y: 0.0), size: CGSize(width: size.width + sideInset * 2.0, height: size.height))
}
@ -141,7 +217,7 @@ final class PeerInfoPaneTabsContainerPaneNode: ASDisplayNode {
struct PeerInfoPaneSpecifier: Equatable {
var key: PeerInfoPaneKey
var title: String
var icons: [TelegramMediaFile]
var icons: [ProfileGiftsContext.State.StarGift]
}
private func interpolateFrame(from fromValue: CGRect, to toValue: CGRect, t: CGFloat) -> CGRect {
@ -1189,7 +1265,7 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, ASGestureRecognizerDelegat
self.tabsContainerNode.update(size: CGSize(width: size.width - sideInset * 2.0, height: tabsHeight), presentationData: presentationData, paneList: availablePanes.map { key in
let title: String
var icons: [TelegramMediaFile] = []
var icons: [ProfileGiftsContext.State.StarGift] = []
switch key {
case .stories:
title = presentationData.strings.PeerInfo_PaneStories
@ -1223,19 +1299,9 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, ASGestureRecognizerDelegat
title = presentationData.strings.PeerInfo_SavedMessagesTabTitle
case .gifts:
title = presentationData.strings.PeerInfo_PaneGifts
icons = data?.profileGiftsContext?.currentState?.gifts.prefix(3).compactMap { gift in
switch gift.gift {
case let .generic(gift):
return gift.file
case let .unique(gift):
for attribute in gift.attributes {
if case let .model(_, file, _) = attribute {
return file
}
}
return nil
}
} ?? []
if let gifts = data?.profileGiftsContext?.currentState?.gifts.prefix(3) {
icons = Array(gifts)
}
}
return PeerInfoPaneSpecifier(key: key, title: title, icons: icons)
}, selectedPane: self.currentPaneKey, disableSwitching: disableTabSwitching, transitionFraction: self.transitionFraction, transition: transition)

View File

@ -76,7 +76,7 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
}
private var starsProducts: [ProfileGiftsContext.State.StarGift]?
private var starsItems: [AnyHashable: (ProfileGiftsContext.State.StarGift, ComponentView<Empty>)] = [:]
private var starsItems: [AnyHashable: (StarGiftReference?, ComponentView<Empty>)] = [:]
private var resultsAreFiltered = false
private var resultsAreEmpty = false
@ -90,6 +90,13 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
}
private var reorderedReferencesPromise = ValuePromise<[StarGiftReference]?>(nil)
private var reorderedPinnedReferences: Set<StarGiftReference>? {
didSet {
self.reorderedPinnedReferencesPromise.set(self.reorderedPinnedReferences)
}
}
private var reorderedPinnedReferencesPromise = ValuePromise<Set<StarGiftReference>?>(nil)
private var reorderRecognizer: ReorderGestureRecognizer?
private let maxPinnedCount: Int
@ -136,17 +143,24 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
for reference in reorderedReferences {
if let index = stateItems.firstIndex(where: { $0.reference == reference }) {
seenIds.insert(reference)
fixedStateItems.append(stateItems[index])
var item = stateItems[index]
if self.reorderedPinnedReferences?.contains(reference) == true, !item.pinnedToTop {
item = item.withPinnedToTop(true)
}
fixedStateItems.append(item)
}
}
for item in stateItems {
if let reference = item.reference, !seenIds.contains(reference) {
var item = item
if self.reorderedPinnedReferences?.contains(reference) == true, !item.pinnedToTop {
item = item.withPinnedToTop(true)
}
fixedStateItems.append(item)
}
}
stateItems = fixedStateItems
//self.reorderedReferences = fixedStateItems.compactMap(\.reference)
}
self.starsProducts = stateItems
self.pinnedReferences = Array(stateItems.filter { $0.pinnedToTop }.compactMap { $0.reference })
@ -218,7 +232,7 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
private func item(at point: CGPoint) -> (AnyHashable, ComponentView<Empty>)? {
let localPoint = self.scrollNode.view.convert(point, from: self.view)
for (id, visibleItem) in self.starsItems {
if let view = visibleItem.1.view, view.frame.contains(localPoint) {
if let view = visibleItem.1.view, view.frame.contains(localPoint), let reference = visibleItem.0, self.pinnedReferences.contains(reference) {
return (id, visibleItem.1)
}
}
@ -258,6 +272,7 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
Queue.mainQueue().after(1.0) {
self.reorderedReferences = nil
self.reorderedPinnedReferences = nil
}
}
@ -281,7 +296,7 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
} else {
self.reorderingItem = nil
}
self.updateScrolling(transition: .spring(duration: 0.3))
self.updateScrolling(transition: item == nil ? .spring(duration: 0.3) : .immediate)
}
}
@ -296,9 +311,9 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
if visibleItem.1 === visibleReorderingItem.1 {
continue
}
if let view = visibleItem.1.view, view.frame.contains(targetPosition), let reorderItem = self.starsItems[id]?.0 {
if let targetIndex = starsProducts.firstIndex(where: { $0.reference == visibleItem.0.reference }) {
self.reorderIfPossible(item: reorderItem, toIndex: targetIndex)
if let view = visibleItem.1.view, view.frame.contains(targetPosition), let reorderItemReference = self.starsItems[id]?.0 {
if let targetIndex = starsProducts.firstIndex(where: { $0.reference == visibleItem.0 }) {
self.reorderIfPossible(reference: reorderItemReference, toIndex: targetIndex)
}
break
}
@ -307,7 +322,7 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
}
}
private func reorderIfPossible(item: ProfileGiftsContext.State.StarGift, toIndex: Int) {
private func reorderIfPossible(reference: StarGiftReference, toIndex: Int) {
if let items = self.starsProducts {
var toIndex = toIndex
@ -322,7 +337,7 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
return item.reference
}
if let reference = item.reference, let fromIndex = ids.firstIndex(of: reference) {
if let fromIndex = ids.firstIndex(of: reference) {
if fromIndex < toIndex {
ids.insert(reference, at: toIndex + 1)
ids.remove(at: fromIndex)
@ -401,7 +416,7 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
visibleItem = current
} else {
visibleItem = ComponentView()
self.starsItems[itemId] = (product, visibleItem)
self.starsItems[itemId] = (product.reference, visibleItem)
itemTransition = .immediate
}
@ -450,11 +465,38 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
isEditing: self.isReordering,
mode: .profile,
action: { [weak self] in
guard let self else {
guard let self, let presentationData = self.currentParams?.presentationData else {
return
}
if self.isReordering {
if !product.pinnedToTop, let reference = product.reference, let items = self.starsProducts {
if self.pinnedReferences.count >= self.maxPinnedCount {
self.parentController?.present(UndoOverlayController(presentationData: presentationData, content: .info(title: nil, text: presentationData.strings.PeerInfo_Gifts_ToastPinLimit_Text(Int32(self.maxPinnedCount)), timeout: nil, customUndoText: nil), elevatedLayout: true, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root))
return
}
var reorderedPinnedReferences = Set<StarGiftReference>()
if let current = self.reorderedPinnedReferences {
reorderedPinnedReferences = current
}
reorderedPinnedReferences.insert(reference)
self.reorderedPinnedReferences = reorderedPinnedReferences
if let maxPinnedIndex = items.lastIndex(where: { $0.pinnedToTop }) {
var reorderedReferences: [StarGiftReference]
if let current = self.reorderedReferences {
reorderedReferences = current
} else {
let ids = items.compactMap { item -> StarGiftReference? in
return item.reference
}
reorderedReferences = ids
}
reorderedReferences.removeAll(where: { $0 == reference })
reorderedReferences.insert(reference, at: maxPinnedIndex + 1)
self.reorderedReferences = reorderedReferences
}
}
} else {
let controller = GiftViewScreen(
context: self.context,
@ -517,10 +559,15 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
}
}
var itemFrame = itemFrame
var isReordering = false
if let reorderingItem = self.reorderingItem, itemId == reorderingItem.id {
itemFrame = itemFrame.size.centered(around: reorderingItem.position)
isReordering = true
}
if itemView.layer.animation(forKey: "position") != nil && !isReordering {
} else {
itemTransition.setFrame(view: itemView, frame: itemFrame)
}
itemTransition.setFrame(view: itemView, frame: itemFrame)
if self.isReordering && product.pinnedToTop {
if itemView.layer.animation(forKey: "shaking_position") == nil {
@ -933,34 +980,36 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
var items: [ContextMenuItem] = []
if canManage {
items.append(.action(ContextMenuActionItem(text: gift.pinnedToTop ? strings.PeerInfo_Gifts_Context_Unpin : strings.PeerInfo_Gifts_Context_Pin , icon: { theme in generateTintedImage(image: UIImage(bundleImageName: gift.pinnedToTop ? "Chat/Context Menu/Unpin" : "Chat/Context Menu/Pin"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, f in
c?.dismiss(completion: { [weak self] in
guard let self else {
return
}
let pinnedToTop = !gift.pinnedToTop
if pinnedToTop && self.pinnedReferences.count >= self.maxPinnedCount {
self.parentController?.present(UndoOverlayController(presentationData: presentationData, content: .info(title: nil, text: presentationData.strings.PeerInfo_Gifts_ToastPinLimit_Text(Int32(self.maxPinnedCount)), timeout: nil, customUndoText: nil), elevatedLayout: true, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root))
return
}
if let reference = gift.reference {
self.profileGifts.updateStarGiftPinnedToTop(reference: reference, pinnedToTop: pinnedToTop)
}
let toastTitle: String?
let toastText: String
if !pinnedToTop {
toastTitle = nil
toastText = presentationData.strings.PeerInfo_Gifts_ToastUnpinned_Text
} else {
toastTitle = presentationData.strings.PeerInfo_Gifts_ToastPinned_Title
toastText = presentationData.strings.PeerInfo_Gifts_ToastPinned_Text
}
self.parentController?.present(UndoOverlayController(presentationData: presentationData, content: .universal(animation: !pinnedToTop ? "anim_toastunpin" : "anim_toastpin", scale: 0.06, colors: [:], title: toastTitle, text: toastText, customUndoText: nil, timeout: 5), elevatedLayout: true, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root))
})
})))
if case .unique = gift.gift {
items.append(.action(ContextMenuActionItem(text: gift.pinnedToTop ? strings.PeerInfo_Gifts_Context_Unpin : strings.PeerInfo_Gifts_Context_Pin , icon: { theme in generateTintedImage(image: UIImage(bundleImageName: gift.pinnedToTop ? "Chat/Context Menu/Unpin" : "Chat/Context Menu/Pin"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, f in
c?.dismiss(completion: { [weak self] in
guard let self else {
return
}
let pinnedToTop = !gift.pinnedToTop
if pinnedToTop && self.pinnedReferences.count >= self.maxPinnedCount {
self.parentController?.present(UndoOverlayController(presentationData: presentationData, content: .info(title: nil, text: presentationData.strings.PeerInfo_Gifts_ToastPinLimit_Text(Int32(self.maxPinnedCount)), timeout: nil, customUndoText: nil), elevatedLayout: true, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root))
return
}
if let reference = gift.reference {
self.profileGifts.updateStarGiftPinnedToTop(reference: reference, pinnedToTop: pinnedToTop)
}
let toastTitle: String?
let toastText: String
if !pinnedToTop {
toastTitle = nil
toastText = presentationData.strings.PeerInfo_Gifts_ToastUnpinned_Text
} else {
toastTitle = presentationData.strings.PeerInfo_Gifts_ToastPinned_Title
toastText = presentationData.strings.PeerInfo_Gifts_ToastPinned_Text
}
self.parentController?.present(UndoOverlayController(presentationData: presentationData, content: .universal(animation: !pinnedToTop ? "anim_toastunpin" : "anim_toastpin", scale: 0.06, colors: [:], title: toastTitle, text: toastText, customUndoText: nil, timeout: 5), elevatedLayout: true, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root))
})
})))
}
if canReorder {
items.append(.action(ContextMenuActionItem(text: strings.PeerInfo_Gifts_Context_Reorder, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/ReorderItems"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, f in