Various improvements

This commit is contained in:
Isaac 2024-11-20 22:09:16 +04:00
parent 03ed993f80
commit 9a038722d3
26 changed files with 355 additions and 31 deletions

View File

@ -1182,6 +1182,27 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
self.openStarsTopup(amount: amount)
}
self.chatListDisplayNode.mainContainerNode.openWebApp = { [weak self] user in
guard let self else {
return
}
//TODO:localize
self.context.sharedContext.openWebApp(
context: self.context,
parentController: self,
updatedPresentationData: self.updatedPresentationData,
botPeer: .user(user),
chatPeer: nil,
threadId: nil,
buttonText: "",
url: "",
simple: true,
source: .generic,
skipTermsOfService: true,
payload: nil
)
}
self.chatListDisplayNode.mainContainerNode.openPremiumManagement = { [weak self] in
guard let self else {
return

View File

@ -351,6 +351,9 @@ public final class ChatListContainerNode: ASDisplayNode, ASGestureRecognizerDele
itemNode.listNode.openStarsTopup = { [weak self] amount in
self?.openStarsTopup?(amount)
}
itemNode.listNode.openWebApp = { [weak self] amount in
self?.openWebApp?(amount)
}
self.currentItemStateValue.set(itemNode.listNode.state |> map { state in
let filterId: Int32?
@ -417,6 +420,7 @@ public final class ChatListContainerNode: ASDisplayNode, ASGestureRecognizerDele
var openPremiumManagement: (() -> Void)?
var openStories: ((ChatListNode.OpenStoriesSubject, ASDisplayNode?) -> Void)?
var openStarsTopup: ((Int64?) -> Void)?
var openWebApp: ((TelegramUser) -> Void)?
var addedVisibleChatsWithPeerIds: (([EnginePeer.Id]) -> Void)?
var didBeginSelectingChats: (() -> Void)?
var canExpandHiddenItems: (() -> Bool)?

View File

@ -3013,6 +3013,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
}, openStarsTopup: { _ in
}, dismissNotice: { _ in
}, editPeer: { _ in
}, openWebApp: { _ in
})
chatListInteraction.isSearchMode = true
@ -4905,6 +4906,7 @@ public final class ChatListSearchShimmerNode: ASDisplayNode {
}, openStarsTopup: { _ in
}, dismissNotice: { _ in
}, editPeer: { _ in
}, openWebApp: { _ in
})
var isInlineMode = false
if case .topics = key {

View File

@ -159,6 +159,7 @@ public final class ChatListShimmerNode: ASDisplayNode {
}, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openPremiumGift: { _, _ in }, openPremiumManagement: {}, openActiveSessions: {}, openBirthdaySetup: {}, performActiveSessionAction: { _, _ in }, openChatFolderUpdates: {}, hideChatFolderUpdates: {}, openStories: { _, _ in }, openStarsTopup: { _ in
}, dismissNotice: { _ in
}, editPeer: { _ in
}, openWebApp: { _ in
})
interaction.isInlineMode = isInlineMode

View File

@ -1234,6 +1234,9 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
var credibilityIconComponent: EmojiStatusComponent?
let mutedIconNode: ASImageNode
var itemTagList: ComponentView<Empty>?
var actionButtonTitleNode: TextNode?
var actionButtonBackgroundView: UIImageView?
var actionButtonNode: HighlightableButtonNode?
private var hierarchyTrackingLayer: HierarchyTrackingLayer?
private var cachedDataDisposable = MetaDisposable()
@ -1890,6 +1893,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
let onlineLayout = self.onlineNode.asyncLayout()
let selectableControlLayout = ItemListSelectableControlNode.asyncLayout(self.selectableControlNode)
let reorderControlLayout = ItemListEditableReorderControlNode.asyncLayout(self.reorderControlNode)
let makeActionButtonTitleNodeLayout = TextNode.asyncLayout(self.actionButtonTitleNode)
let currentItem = self.layoutParams?.0
let currentChatListText = self.cachedChatListText
@ -3061,6 +3065,12 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
let (mentionBadgeLayout, mentionBadgeApply) = mentionBadgeLayout(CGSize(width: rawContentWidth, height: CGFloat.greatestFiniteMagnitude), badgeDiameter, badgeFont, currentMentionBadgeImage, mentionBadgeContent)
var actionButtonTitleNodeLayoutAndApply: (TextNodeLayout, () -> TextNode)?
if case .none = badgeContent, case .none = mentionBadgeContent, case let .chat(itemPeer) = contentPeer, case let .user(user) = itemPeer.chatMainPeer, let botInfo = user.botInfo, botInfo.flags.contains(.hasWebApp) {
//TODO:localize
actionButtonTitleNodeLayoutAndApply = makeActionButtonTitleNodeLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: "OPEN", font: Font.semibold(15.0), textColor: theme.unreadBadgeActiveTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: rawContentWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
}
var badgeSize: CGFloat = 0.0
if !badgeLayout.width.isZero {
badgeSize += badgeLayout.width + 5.0
@ -3081,6 +3091,14 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
}
badgeSize += currentPinnedIconImage.size.width
}
if let (actionButtonTitleNodeLayout, _) = actionButtonTitleNodeLayoutAndApply {
if !badgeSize.isZero {
badgeSize += 4.0
} else {
badgeSize += 5.0
}
badgeSize += actionButtonTitleNodeLayout.size.width + 12.0 * 2.0
}
badgeSize = max(badgeSize, reorderInset)
if !itemTags.isEmpty {
@ -3816,23 +3834,19 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
strongSelf.statusNode.fontSize = item.presentationData.fontSize.itemListBaseFontSize
let _ = strongSelf.statusNode.transitionToState(statusState, animated: animateContent)
var nextBadgeX: CGFloat = contentRect.maxX
if let _ = currentBadgeBackgroundImage {
let badgeFrame = CGRect(x: contentRect.maxX - badgeLayout.width, y: contentRect.maxY - badgeLayout.height - 2.0, width: badgeLayout.width, height: badgeLayout.height)
let badgeFrame = CGRect(x: nextBadgeX - badgeLayout.width, y: contentRect.maxY - badgeLayout.height - 2.0, width: badgeLayout.width, height: badgeLayout.height)
transition.updateFrame(node: strongSelf.badgeNode, frame: badgeFrame)
nextBadgeX -= badgeLayout.width + 6.0
}
if currentMentionBadgeImage != nil || currentBadgeBackgroundImage != nil {
let mentionBadgeOffset: CGFloat
if badgeLayout.width.isZero {
mentionBadgeOffset = contentRect.maxX - mentionBadgeLayout.width
} else {
mentionBadgeOffset = contentRect.maxX - badgeLayout.width - 6.0 - mentionBadgeLayout.width
}
let badgeFrame = CGRect(x: mentionBadgeOffset, y: contentRect.maxY - mentionBadgeLayout.height - 2.0, width: mentionBadgeLayout.width, height: mentionBadgeLayout.height)
let badgeFrame = CGRect(x: nextBadgeX - mentionBadgeLayout.width, y: contentRect.maxY - mentionBadgeLayout.height - 2.0, width: mentionBadgeLayout.width, height: mentionBadgeLayout.height)
transition.updateFrame(node: strongSelf.mentionBadgeNode, frame: badgeFrame)
nextBadgeX -= mentionBadgeLayout.width + 6.0
}
if let currentPinnedIconImage = currentPinnedIconImage {
@ -3840,14 +3854,71 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
strongSelf.pinnedIconNode.isHidden = false
let pinnedIconSize = currentPinnedIconImage.size
let pinnedIconFrame = CGRect(x: contentRect.maxX - pinnedIconSize.width, y: contentRect.maxY - pinnedIconSize.height - 2.0, width: pinnedIconSize.width, height: pinnedIconSize.height)
let pinnedIconFrame = CGRect(x: nextBadgeX - pinnedIconSize.width, y: contentRect.maxY - pinnedIconSize.height - 2.0, width: pinnedIconSize.width, height: pinnedIconSize.height)
strongSelf.pinnedIconNode.frame = pinnedIconFrame
nextBadgeX -= pinnedIconSize.width + 6.0
} else {
strongSelf.pinnedIconNode.image = nil
strongSelf.pinnedIconNode.isHidden = true
}
if let (actionButtonTitleNodeLayout, apply) = actionButtonTitleNodeLayoutAndApply {
let actionButtonSize = CGSize(width: actionButtonTitleNodeLayout.size.width + 12.0 * 2.0, height: actionButtonTitleNodeLayout.size.height + 5.0 + 4.0)
let actionButtonFrame = CGRect(x: nextBadgeX - actionButtonSize.width, y: contentRect.maxY - actionButtonSize.height, width: actionButtonSize.width, height: actionButtonSize.height)
let actionButtonNode: HighlightableButtonNode
if let current = strongSelf.actionButtonNode {
actionButtonNode = current
} else {
actionButtonNode = HighlightableButtonNode()
strongSelf.actionButtonNode = actionButtonNode
strongSelf.mainContentContainerNode.addSubnode(actionButtonNode)
actionButtonNode.addTarget(strongSelf, action: #selector(strongSelf.actionButtonPressed), forControlEvents: .touchUpInside)
}
let actionButtonBackgroundView: UIImageView
if let current = strongSelf.actionButtonBackgroundView {
actionButtonBackgroundView = current
} else {
actionButtonBackgroundView = UIImageView()
strongSelf.actionButtonBackgroundView = actionButtonBackgroundView
actionButtonNode.view.addSubview(actionButtonBackgroundView)
if actionButtonBackgroundView.image?.size.height != actionButtonSize.height {
actionButtonBackgroundView.image = generateStretchableFilledCircleImage(diameter: actionButtonSize.height, color: .white)?.withRenderingMode(.alwaysTemplate)
}
}
actionButtonBackgroundView.tintColor = theme.unreadBadgeActiveBackgroundColor
let actionButtonTitleNode = apply()
if strongSelf.actionButtonTitleNode !== actionButtonTitleNode {
strongSelf.actionButtonTitleNode?.removeFromSupernode()
strongSelf.actionButtonTitleNode = actionButtonTitleNode
actionButtonNode.addSubnode(actionButtonTitleNode)
}
actionButtonNode.frame = actionButtonFrame
actionButtonBackgroundView.frame = CGRect(origin: CGPoint(), size: actionButtonFrame.size)
actionButtonTitleNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((actionButtonFrame.width - actionButtonTitleNodeLayout.size.width) * 0.5), y: 5.0), size: actionButtonTitleNodeLayout.size)
nextBadgeX -= actionButtonSize.width + 6.0
} else {
if let actionButtonTitleNode = strongSelf.actionButtonTitleNode {
actionButtonTitleNode.removeFromSupernode()
strongSelf.actionButtonTitleNode = nil
}
if let actionButtonBackgroundView = strongSelf.actionButtonBackgroundView {
actionButtonBackgroundView.removeFromSuperview()
strongSelf.actionButtonBackgroundView = nil
}
if let actionButtonNode = strongSelf.actionButtonNode {
actionButtonNode.removeFromSupernode()
strongSelf.actionButtonNode = nil
}
}
var titleOffset: CGFloat = 0.0
if let currentSecretIconImage = currentSecretIconImage {
let iconNode: ASImageNode
@ -4495,6 +4566,18 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
item.interaction.openForumThread(index.messageIndex.id.peerId, topicItem.id)
}
@objc private func actionButtonPressed() {
guard let item else {
return
}
guard case let .peer(peerData) = item.content else {
return
}
if case let .user(user) = peerData.peer.peer, let botInfo = user.botInfo, botInfo.flags.contains(.hasWebApp) {
item.interaction.openWebApp(user)
}
}
override public func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
}

View File

@ -112,6 +112,7 @@ public final class ChatListNodeInteraction {
let openStarsTopup: (Int64?) -> Void
let dismissNotice: (ChatListNotice) -> Void
let editPeer: (ChatListItem) -> Void
let openWebApp: (TelegramUser) -> Void
public var searchTextHighightState: String?
var highlightedChatLocation: ChatListHighlightedLocation?
@ -167,7 +168,8 @@ public final class ChatListNodeInteraction {
openStories: @escaping (ChatListNode.OpenStoriesSubject, ASDisplayNode?) -> Void,
openStarsTopup: @escaping (Int64?) -> Void,
dismissNotice: @escaping (ChatListNotice) -> Void,
editPeer: @escaping (ChatListItem) -> Void
editPeer: @escaping (ChatListItem) -> Void,
openWebApp: @escaping (TelegramUser) -> Void
) {
self.activateSearch = activateSearch
self.peerSelected = peerSelected
@ -211,6 +213,7 @@ public final class ChatListNodeInteraction {
self.openStarsTopup = openStarsTopup
self.dismissNotice = dismissNotice
self.editPeer = editPeer
self.openWebApp = openWebApp
}
}
@ -1220,6 +1223,7 @@ public final class ChatListNode: ListView {
public var openBirthdaySetup: (() -> Void)?
public var openPremiumManagement: (() -> Void)?
public var openStarsTopup: ((Int64?) -> Void)?
public var openWebApp: ((TelegramUser) -> Void)?
private var theme: PresentationTheme
@ -1867,6 +1871,11 @@ public final class ChatListNode: ListView {
break
}
}, editPeer: { _ in
}, openWebApp: { [weak self] user in
guard let self else {
return
}
self.openWebApp?(user)
})
nodeInteraction.isInlineMode = isInlineMode

View File

@ -21,7 +21,7 @@ public enum ChatMessageGalleryControllerData {
case pass(TelegramMediaFile)
case instantPage(InstantPageGalleryController, Int, Media)
case map(TelegramMediaMap)
case stickerPack(StickerPackReference)
case stickerPack(StickerPackReference, TelegramMediaFile?)
case audio(TelegramMediaFile)
case document(TelegramMediaFile, Bool)
case gallery(Signal<GalleryController, NoError>)
@ -104,7 +104,7 @@ public func chatMessageGalleryControllerData(context: AccountContext, chatLocati
for attribute in file.attributes {
if case let .CustomEmoji(_, _, _, reference) = attribute {
if let reference = reference {
return .stickerPack(reference)
return .stickerPack(reference, file)
}
break
}
@ -214,7 +214,7 @@ public func chatMessageGalleryControllerData(context: AccountContext, chatLocati
for attribute in file.attributes {
if case let .Sticker(_, reference, _) = attribute {
if let reference = reference {
return .stickerPack(reference)
return .stickerPack(reference, file)
}
break
}

View File

@ -230,6 +230,7 @@ private final class TextSizeSelectionControllerNode: ASDisplayNode, ASScrollView
}, openStarsTopup: { _ in
}, dismissNotice: { _ in
}, editPeer: { _ in
}, openWebApp: { _ in
})
let chatListPresentationData = ChatListPresentationData(theme: self.presentationData.theme, fontSize: self.presentationData.listsFontSize, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameSortOrder: self.presentationData.nameSortOrder, nameDisplayOrder: self.presentationData.nameDisplayOrder, disableAnimations: true)

View File

@ -379,6 +379,7 @@ final class ThemePreviewControllerNode: ASDisplayNode, ASScrollViewDelegate {
}, openStarsTopup: { _ in
}, dismissNotice: { _ in
}, editPeer: { _ in
}, openWebApp: { _ in
})
func makeChatListItem(

View File

@ -42,6 +42,7 @@ swift_library(
"//submodules/Pasteboard:Pasteboard",
"//submodules/TelegramUI/Components/Stickers/StickerPackEditTitleController",
"//submodules/TelegramUI/Components/CameraScreen",
"//submodules/TelegramUI/Components/EmojiStatusComponent",
],
visibility = [
"//visibility:public",

View File

@ -25,6 +25,8 @@ import Pasteboard
import StickerPackEditTitleController
import EntityKeyboard
import CameraScreen
import ComponentFlow
import EmojiStatusComponent
private let maxStickersCount = 120
@ -134,6 +136,8 @@ private final class StickerPackContainer: ASDisplayNode {
private let sendSticker: ((FileMediaReference, UIView, CGRect) -> Bool)?
private let sendEmoji: ((String, ChatTextInputTextCustomEmojiAttribute) -> Void)?
private let backgroundNode: ASImageNode
private let previewIconFile: TelegramMediaFile?
private var mainPreviewIcon: ComponentView<Empty>?
private let gridNode: GridNode
private let actionAreaBackgroundNode: NavigationBackgroundNode
private let actionAreaSeparatorNode: ASDisplayNode
@ -190,6 +194,7 @@ private final class StickerPackContainer: ASDisplayNode {
presentationData: PresentationData,
stickerPacks: [StickerPackReference],
loadedStickerPacks: [LoadedStickerPack],
previewIconFile: TelegramMediaFile?,
decideNextAction: @escaping (StickerPackContainer, StickerPackAction) -> StickerPackNextAction,
requestDismiss: @escaping () -> Void,
expandProgressUpdated: @escaping (StickerPackContainer, ContainedViewLayoutTransition, ContainedViewLayoutTransition) -> Void,
@ -218,6 +223,11 @@ private final class StickerPackContainer: ASDisplayNode {
self.backgroundNode.displayWithoutProcessing = true
self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 20.0, color: self.presentationData.theme.actionSheet.opaqueItemBackgroundColor)
self.previewIconFile = previewIconFile
if self.previewIconFile != nil {
self.mainPreviewIcon = ComponentView()
}
self.gridNode = GridNode()
self.gridNode.scrollView.alwaysBounceVertical = true
self.gridNode.scrollView.showsVerticalScrollIndicator = false
@ -292,6 +302,24 @@ private final class StickerPackContainer: ASDisplayNode {
self?.gridPresentationLayoutUpdated(presentationLayout, transition: transition)
}
self.gridNode.scrollingInitiated = { [weak self] in
guard let self else {
return
}
if let mainPreviewIconView = self.mainPreviewIcon?.view {
mainPreviewIconView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak self] _ in
guard let self else {
return
}
if let mainPreviewIconView = self.mainPreviewIcon?.view {
self.mainPreviewIcon = nil
mainPreviewIconView.removeFromSuperview()
}
})
mainPreviewIconView.layer.animateScale(from: 1.0, to: 0.5, duration: 0.2, removeOnCompletion: false)
}
}
self.gridNode.interactiveScrollingEnded = { [weak self] in
guard let strongSelf = self, !strongSelf.isDismissed else {
return
@ -1273,7 +1301,7 @@ private final class StickerPackContainer: ASDisplayNode {
|> deliverOnMainQueue).start(completed: {
commit()
let packController = StickerPackScreen(context: context, updatedPresentationData: updatedPresentationData, mainStickerPack: packReference, stickerPacks: [packReference], loadedStickerPacks: [], expandIfNeeded: true, parentNavigationController: navigationController, sendSticker: sendSticker, sendEmoji: nil, actionPerformed: nil, dismissed: nil, getSourceRect: nil)
let packController = StickerPackScreen(context: context, updatedPresentationData: updatedPresentationData, mainStickerPack: packReference, stickerPacks: [packReference], loadedStickerPacks: [], previewIconFile: nil, expandIfNeeded: true, parentNavigationController: navigationController, sendSticker: sendSticker, sendEmoji: nil, actionPerformed: nil, dismissed: nil, getSourceRect: nil)
(navigationController?.viewControllers.last as? ViewController)?.present(packController, in: .window(.root))
Queue.mainQueue().after(0.1) {
@ -1329,7 +1357,7 @@ private final class StickerPackContainer: ASDisplayNode {
let packReference: StickerPackReference = .id(id: info.id.id, accessHash: info.accessHash)
let _ = (context.engine.stickers.addStickerToStickerSet(packReference: packReference, sticker: sticker)
|> deliverOnMainQueue).start(completed: {
let packController = StickerPackScreen(context: context, updatedPresentationData: updatedPresentationData, mainStickerPack: packReference, stickerPacks: [packReference], loadedStickerPacks: [], expandIfNeeded: true, parentNavigationController: navigationController, sendSticker: sendSticker, sendEmoji: nil, actionPerformed: nil, dismissed: nil, getSourceRect: nil)
let packController = StickerPackScreen(context: context, updatedPresentationData: updatedPresentationData, mainStickerPack: packReference, stickerPacks: [packReference], loadedStickerPacks: [], previewIconFile: nil, expandIfNeeded: true, parentNavigationController: navigationController, sendSticker: sendSticker, sendEmoji: nil, actionPerformed: nil, dismissed: nil, getSourceRect: nil)
(navigationController?.viewControllers.last as? ViewController)?.present(packController, in: .window(.root))
Queue.mainQueue().after(0.1) {
@ -1378,7 +1406,7 @@ private final class StickerPackContainer: ASDisplayNode {
|> deliverOnMainQueue).start(completed: {
commit()
let packController = StickerPackScreen(context: context, updatedPresentationData: updatedPresentationData, mainStickerPack: packReference, stickerPacks: [packReference], loadedStickerPacks: [], expandIfNeeded: true, parentNavigationController: navigationController, sendSticker: sendSticker, sendEmoji: nil, actionPerformed: nil, dismissed: nil, getSourceRect: nil)
let packController = StickerPackScreen(context: context, updatedPresentationData: updatedPresentationData, mainStickerPack: packReference, stickerPacks: [packReference], loadedStickerPacks: [], previewIconFile: nil, expandIfNeeded: true, parentNavigationController: navigationController, sendSticker: sendSticker, sendEmoji: nil, actionPerformed: nil, dismissed: nil, getSourceRect: nil)
(navigationController?.viewControllers.last as? ViewController)?.present(packController, in: .window(.root))
Queue.mainQueue().after(0.1) {
@ -1956,6 +1984,20 @@ private final class StickerPackContainer: ASDisplayNode {
self.modalProgress = modalProgress
}
func animateIn() {
if let mainPreviewIconView = self.mainPreviewIcon?.view {
mainPreviewIconView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1)
mainPreviewIconView.layer.animateScale(from: 0.5, to: 1.0, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring)
}
}
func animateOut() {
if let mainPreviewIconView = self.mainPreviewIcon?.view {
mainPreviewIconView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
mainPreviewIconView.layer.animateScale(from: 1.0, to: 0.5, duration: 0.2, removeOnCompletion: false)
}
}
func updateLayout(layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
var insets = layout.insets(options: [.statusBar])
if case .regular = layout.metrics.widthClass {
@ -2140,6 +2182,38 @@ private final class StickerPackContainer: ASDisplayNode {
titleContainerFrame = CGRect(origin: CGPoint(x: backgroundFrame.minX + floor((backgroundFrame.width) / 2.0), y: backgroundFrame.minY + floor((56.0) / 2.0)), size: CGSize())
}
transition.updateFrame(node: self.backgroundNode, frame: backgroundFrame)
if let previewIconFile = self.previewIconFile, let mainPreviewIcon = self.mainPreviewIcon {
let iconFitSize = CGSize(width: 90.0, height: 90.0)
let iconSize = mainPreviewIcon.update(
transition: .immediate,
component: AnyComponent(EmojiStatusComponent(
context: self.context,
animationCache: self.context.animationCache,
animationRenderer: self.context.animationRenderer,
content: .animation(
content: .file(file: previewIconFile),
size: iconFitSize,
placeholderColor: .clear,
themeColor: self.presentationData.theme.list.itemPrimaryTextColor,
loopMode: .forever
),
isVisibleForAnimations: true,
action: nil
)),
environment: {},
containerSize: iconFitSize
)
let iconFrame = CGRect(origin: CGPoint(x: backgroundFrame.minX + floor((backgroundFrame.width - iconSize.width) * 0.5), y: backgroundFrame.minY - 50.0 - iconSize.height), size: iconSize)
if let iconView = mainPreviewIcon.view {
if iconView.superview == nil {
self.backgroundNode.view.superview?.addSubview(iconView)
}
transition.updatePosition(layer: iconView.layer, position: iconFrame.center)
iconView.bounds = CGRect(origin: CGPoint(), size: iconFrame.size)
}
}
transition.updateFrame(node: self.titleContainer, frame: titleContainerFrame)
transition.updateFrame(node: self.titleSeparatorNode, frame: CGRect(origin: CGPoint(x: backgroundFrame.minX, y: backgroundFrame.minY + 56.0 - UIScreenPixel), size: CGSize(width: backgroundFrame.width, height: UIScreenPixel)))
transition.updateFrame(node: self.titleBackgroundnode, frame: CGRect(origin: CGPoint(x: backgroundFrame.minX, y: backgroundFrame.minY), size: CGSize(width: backgroundFrame.width, height: 56.0)))
@ -2218,6 +2292,7 @@ private final class StickerPackScreenNode: ViewControllerTracingNode {
private weak var controller: StickerPackScreenImpl?
private var presentationData: PresentationData
private let stickerPacks: [StickerPackReference]
private let previewIconFile: TelegramMediaFile?
private let modalProgressUpdated: (CGFloat, ContainedViewLayoutTransition) -> Void
private let dismissed: () -> Void
private let presentInGlobalOverlay: (ViewController, Any?) -> Void
@ -2251,6 +2326,7 @@ private final class StickerPackScreenNode: ViewControllerTracingNode {
context: AccountContext,
controller: StickerPackScreenImpl,
stickerPacks: [StickerPackReference],
previewIconFile: TelegramMediaFile?,
initialSelectedStickerPackIndex: Int,
modalProgressUpdated: @escaping (CGFloat, ContainedViewLayoutTransition) -> Void,
dismissed: @escaping () -> Void,
@ -2264,6 +2340,7 @@ private final class StickerPackScreenNode: ViewControllerTracingNode {
self.controller = controller
self.presentationData = controller.presentationData
self.stickerPacks = stickerPacks
self.previewIconFile = previewIconFile
self.selectedStickerPackIndex = initialSelectedStickerPackIndex
self.modalProgressUpdated = modalProgressUpdated
self.dismissed = dismissed
@ -2398,7 +2475,7 @@ private final class StickerPackScreenNode: ViewControllerTracingNode {
wasAdded = true
containerTransition = .immediate
let index = i
container = StickerPackContainer(index: index, context: self.context, presentationData: self.presentationData, stickerPacks: self.stickerPacks, loadedStickerPacks: self.controller?.loadedStickerPacks ?? [], decideNextAction: { [weak self] container, action in
container = StickerPackContainer(index: index, context: self.context, presentationData: self.presentationData, stickerPacks: self.stickerPacks, loadedStickerPacks: self.controller?.loadedStickerPacks ?? [], previewIconFile: self.previewIconFile, decideNextAction: { [weak self] container, action in
guard let strongSelf = self, let layout = strongSelf.validLayout else {
return .dismiss
}
@ -2565,6 +2642,10 @@ private final class StickerPackScreenNode: ViewControllerTracingNode {
let minInset: CGFloat = (self.containers.map { (_, container) -> CGFloat in container.topContentInset }).max() ?? 0.0
self.containerContainingNode.layer.animatePosition(from: CGPoint(x: 0.0, y: self.containerContainingNode.bounds.height - minInset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
}
for (_, container) in self.containers {
container.animateIn()
}
}
func animateOut(completion: @escaping () -> Void) {
@ -2587,6 +2668,10 @@ private final class StickerPackScreenNode: ViewControllerTracingNode {
self.modalProgressUpdated(0.0, .animated(duration: 0.2, curve: .easeInOut))
}
for (_, container) in self.containers {
container.animateOut()
}
}
func dismiss() {
@ -2659,6 +2744,7 @@ public final class StickerPackScreenImpl: ViewController, StickerPackScreen {
private let stickerPacks: [StickerPackReference]
fileprivate let loadedStickerPacks: [LoadedStickerPack]
let previewIconFile: TelegramMediaFile?
private let initialSelectedStickerPackIndex: Int
fileprivate weak var parentNavigationController: NavigationController?
@ -2698,6 +2784,7 @@ public final class StickerPackScreenImpl: ViewController, StickerPackScreen {
updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil,
stickerPacks: [StickerPackReference],
loadedStickerPacks: [LoadedStickerPack],
previewIconFile: TelegramMediaFile?,
selectedStickerPackIndex: Int = 0,
mainActionTitle: String? = nil,
actionTitle: String? = nil,
@ -2714,6 +2801,7 @@ public final class StickerPackScreenImpl: ViewController, StickerPackScreen {
self.updatedPresentationData = updatedPresentationData
self.stickerPacks = stickerPacks
self.loadedStickerPacks = loadedStickerPacks
self.previewIconFile = previewIconFile
self.initialSelectedStickerPackIndex = selectedStickerPackIndex
self.mainActionTitle = mainActionTitle
self.actionTitle = actionTitle
@ -2751,7 +2839,7 @@ public final class StickerPackScreenImpl: ViewController, StickerPackScreen {
}
override public func loadDisplayNode() {
self.displayNode = StickerPackScreenNode(context: self.context, controller: self, stickerPacks: self.stickerPacks, initialSelectedStickerPackIndex: self.initialSelectedStickerPackIndex, modalProgressUpdated: { [weak self] value, transition in
self.displayNode = StickerPackScreenNode(context: self.context, controller: self, stickerPacks: self.stickerPacks, previewIconFile: self.previewIconFile, initialSelectedStickerPackIndex: self.initialSelectedStickerPackIndex, modalProgressUpdated: { [weak self] value, transition in
DispatchQueue.main.async {
guard let strongSelf = self else {
return
@ -2950,6 +3038,7 @@ public func StickerPackScreen(
mainStickerPack: StickerPackReference,
stickerPacks: [StickerPackReference],
loadedStickerPacks: [LoadedStickerPack] = [],
previewIconFile: TelegramMediaFile? = nil,
mainActionTitle: String? = nil,
actionTitle: String? = nil,
isEditing: Bool = false,
@ -2967,6 +3056,7 @@ public func StickerPackScreen(
updatedPresentationData: updatedPresentationData,
stickerPacks: stickerPacks,
loadedStickerPacks: loadedStickerPacks,
previewIconFile: previewIconFile,
selectedStickerPackIndex: stickerPacks.firstIndex(of: mainStickerPack) ?? 0,
mainActionTitle: mainActionTitle,
actionTitle: actionTitle,

View File

@ -20,6 +20,7 @@ final class VideoChatParticipantThumbnailComponent: Component {
let isPresentation: Bool
let isSelected: Bool
let isSpeaking: Bool
let displayVideo: Bool
let interfaceOrientation: UIInterfaceOrientation
let action: (() -> Void)?
let contextAction: ((EnginePeer, ContextExtractedContentContainingView, ContextGesture) -> Void)?
@ -31,6 +32,7 @@ final class VideoChatParticipantThumbnailComponent: Component {
isPresentation: Bool,
isSelected: Bool,
isSpeaking: Bool,
displayVideo: Bool,
interfaceOrientation: UIInterfaceOrientation,
action: (() -> Void)?,
contextAction: ((EnginePeer, ContextExtractedContentContainingView, ContextGesture) -> Void)?
@ -41,6 +43,7 @@ final class VideoChatParticipantThumbnailComponent: Component {
self.isPresentation = isPresentation
self.isSelected = isSelected
self.isSpeaking = isSpeaking
self.displayVideo = displayVideo
self.interfaceOrientation = interfaceOrientation
self.action = action
self.contextAction = contextAction
@ -65,6 +68,9 @@ final class VideoChatParticipantThumbnailComponent: Component {
if lhs.isSpeaking != rhs.isSpeaking {
return false
}
if lhs.displayVideo != rhs.displayVideo {
return false
}
if lhs.interfaceOrientation != rhs.interfaceOrientation {
return false
}
@ -251,7 +257,7 @@ final class VideoChatParticipantThumbnailComponent: Component {
titleView.bounds = CGRect(origin: CGPoint(), size: titleFrame.size)
}
if let videoDescription = component.isPresentation ? component.participant.presentationDescription : component.participant.videoDescription {
if component.displayVideo, let videoDescription = component.isPresentation ? component.participant.presentationDescription : component.participant.videoDescription {
let videoBackgroundLayer: SimpleLayer
if let current = self.videoBackgroundLayer {
videoBackgroundLayer = current
@ -470,6 +476,7 @@ final class VideoChatExpandedParticipantThumbnailsComponent: Component {
let call: PresentationGroupCall
let theme: PresentationTheme
let displayVideo: Bool
let participants: [Participant]
let selectedParticipant: Participant.Key?
let speakingParticipants: Set<EnginePeer.Id>
@ -480,6 +487,7 @@ final class VideoChatExpandedParticipantThumbnailsComponent: Component {
init(
call: PresentationGroupCall,
theme: PresentationTheme,
displayVideo: Bool,
participants: [Participant],
selectedParticipant: Participant.Key?,
speakingParticipants: Set<EnginePeer.Id>,
@ -489,6 +497,7 @@ final class VideoChatExpandedParticipantThumbnailsComponent: Component {
) {
self.call = call
self.theme = theme
self.displayVideo = displayVideo
self.participants = participants
self.selectedParticipant = selectedParticipant
self.speakingParticipants = speakingParticipants
@ -504,6 +513,9 @@ final class VideoChatExpandedParticipantThumbnailsComponent: Component {
if lhs.theme !== rhs.theme {
return false
}
if lhs.displayVideo != rhs.displayVideo {
return false
}
if lhs.participants != rhs.participants {
return false
}
@ -654,6 +666,7 @@ final class VideoChatExpandedParticipantThumbnailsComponent: Component {
isPresentation: participant.isPresentation,
isSelected: component.selectedParticipant == participant.key,
isSpeaking: component.speakingParticipants.contains(participant.participant.peer.id),
displayVideo: component.displayVideo,
interfaceOrientation: component.interfaceOrientation,
action: { [weak self] in
guard let self, let component = self.component else {

View File

@ -45,6 +45,7 @@ final class VideoChatParticipantVideoComponent: Component {
let isMyPeer: Bool
let isPresentation: Bool
let isSpeaking: Bool
let maxVideoQuality: Int
let isExpanded: Bool
let isUIHidden: Bool
let contentInsets: UIEdgeInsets
@ -63,6 +64,7 @@ final class VideoChatParticipantVideoComponent: Component {
isMyPeer: Bool,
isPresentation: Bool,
isSpeaking: Bool,
maxVideoQuality: Int,
isExpanded: Bool,
isUIHidden: Bool,
contentInsets: UIEdgeInsets,
@ -80,6 +82,7 @@ final class VideoChatParticipantVideoComponent: Component {
self.isMyPeer = isMyPeer
self.isPresentation = isPresentation
self.isSpeaking = isSpeaking
self.maxVideoQuality = maxVideoQuality
self.isExpanded = isExpanded
self.isUIHidden = isUIHidden
self.contentInsets = contentInsets
@ -104,6 +107,9 @@ final class VideoChatParticipantVideoComponent: Component {
if lhs.isSpeaking != rhs.isSpeaking {
return false
}
if lhs.maxVideoQuality != rhs.maxVideoQuality {
return false
}
if lhs.isExpanded != rhs.isExpanded {
return false
}
@ -413,7 +419,7 @@ final class VideoChatParticipantVideoComponent: Component {
alphaTransition.setAlpha(view: titleView, alpha: controlsAlpha)
}
let videoDescription = component.isPresentation ? component.participant.presentationDescription : component.participant.videoDescription
let videoDescription: GroupCallParticipantsContext.Participant.VideoDescription? = component.maxVideoQuality == 0 ? nil : (component.isPresentation ? component.participant.presentationDescription : component.participant.videoDescription)
var isEffectivelyPaused = false
if let videoDescription, videoDescription.isPaused {

View File

@ -129,6 +129,7 @@ final class VideoChatParticipantsComponent: Component {
let participants: Participants?
let speakingParticipants: Set<EnginePeer.Id>
let expandedVideoState: ExpandedVideoState?
let maxVideoQuality: Int
let theme: PresentationTheme
let strings: PresentationStrings
let layout: Layout
@ -147,6 +148,7 @@ final class VideoChatParticipantsComponent: Component {
participants: Participants?,
speakingParticipants: Set<EnginePeer.Id>,
expandedVideoState: ExpandedVideoState?,
maxVideoQuality: Int,
theme: PresentationTheme,
strings: PresentationStrings,
layout: Layout,
@ -164,6 +166,7 @@ final class VideoChatParticipantsComponent: Component {
self.participants = participants
self.speakingParticipants = speakingParticipants
self.expandedVideoState = expandedVideoState
self.maxVideoQuality = maxVideoQuality
self.theme = theme
self.strings = strings
self.layout = layout
@ -188,6 +191,9 @@ final class VideoChatParticipantsComponent: Component {
if lhs.expandedVideoState != rhs.expandedVideoState {
return false
}
if lhs.maxVideoQuality != rhs.maxVideoQuality {
return false
}
if lhs.theme !== rhs.theme {
return false
}
@ -1022,6 +1028,7 @@ final class VideoChatParticipantsComponent: Component {
isMyPeer: videoParticipant.participant.peer.id == component.participants?.myPeerId,
isPresentation: videoParticipant.isPresentation,
isSpeaking: component.speakingParticipants.contains(videoParticipant.participant.peer.id),
maxVideoQuality: component.maxVideoQuality,
isExpanded: isItemExpanded,
isUIHidden: isItemUIHidden || self.isPinchToZoomActive,
contentInsets: itemContentInsets,
@ -1376,6 +1383,7 @@ final class VideoChatParticipantsComponent: Component {
component: AnyComponent(VideoChatExpandedParticipantThumbnailsComponent(
call: component.call,
theme: component.theme,
displayVideo: component.maxVideoQuality != 0,
participants: thumbnailParticipants,
selectedParticipant: component.expandedVideoState.flatMap { expandedVideoState in
return VideoChatExpandedParticipantThumbnailsComponent.Participant.Key(id: expandedVideoState.mainParticipant.id, isPresentation: expandedVideoState.mainParticipant.isPresentation)
@ -1762,12 +1770,18 @@ final class VideoChatParticipantsComponent: Component {
}
var requestedVideo: [PresentationGroupCallRequestedVideo] = []
if let participants = component.participants {
if let participants = component.participants, component.maxVideoQuality != 0 {
for participant in participants.participants {
var maxVideoQuality: PresentationGroupCallRequestedVideo.Quality = .medium
if let expandedVideoState = component.expandedVideoState {
if expandedVideoState.mainParticipant.id == participant.peer.id, !expandedVideoState.mainParticipant.isPresentation {
maxVideoQuality = .full
if component.maxVideoQuality == Int.max {
maxVideoQuality = .full
} else if component.maxVideoQuality == 360 {
maxVideoQuality = .medium
} else {
maxVideoQuality = .thumbnail
}
} else {
maxVideoQuality = .thumbnail
}
@ -1776,15 +1790,27 @@ final class VideoChatParticipantsComponent: Component {
var maxPresentationQuality: PresentationGroupCallRequestedVideo.Quality = .medium
if let expandedVideoState = component.expandedVideoState {
if expandedVideoState.mainParticipant.id == participant.peer.id, expandedVideoState.mainParticipant.isPresentation {
maxPresentationQuality = .full
if component.maxVideoQuality == Int.max {
maxVideoQuality = .full
} else if component.maxVideoQuality == 360 {
maxVideoQuality = .medium
} else {
maxVideoQuality = .thumbnail
}
} else {
maxPresentationQuality = .thumbnail
}
}
if component.layout.videoColumn != nil && gridParticipants.count == 1 {
maxVideoQuality = .full
maxPresentationQuality = .full
if component.maxVideoQuality == Int.max {
maxVideoQuality = .full
} else if component.maxVideoQuality == 360 {
maxVideoQuality = .medium
} else {
maxVideoQuality = .thumbnail
}
maxPresentationQuality = maxVideoQuality
}
if let videoChannel = participant.requestedVideoChannel(minQuality: .thumbnail, maxQuality: maxVideoQuality) {

View File

@ -119,6 +119,8 @@ final class VideoChatScreenComponent: Component {
let updateAvatarDisposable = MetaDisposable()
var currentUpdatingAvatar: (TelegramMediaImageRepresentation, Float)?
var maxVideoQuality: Int = Int.max
override init(frame: CGRect) {
self.containerView = UIView()
self.containerView.clipsToBounds = true
@ -1506,6 +1508,7 @@ final class VideoChatScreenComponent: Component {
participants: mappedParticipants,
speakingParticipants: self.members?.speakingParticipants ?? Set(),
expandedVideoState: self.expandedParticipantsVideoState,
maxVideoQuality: self.maxVideoQuality,
theme: environment.theme,
strings: environment.strings,
layout: participantsLayout,

View File

@ -152,6 +152,56 @@ extension VideoChatScreenComponent.View {
}
}
//TODO:localize
let qualityList: [(Int, String)] = [
(0, "Audio Only"),
(180, "180p"),
(360, "360p"),
(Int.max, "720p")
]
let videoQualityTitle = qualityList.first(where: { $0.0 == self.maxVideoQuality })?.1 ?? ""
items.append(.action(ContextMenuActionItem(text: "Receive Video Quality", textColor: .primary, textLayout: .secondLineWithValue(videoQualityTitle), icon: { _ in
return nil
}, action: { [weak self] c, _ in
guard let self else {
c?.dismiss(completion: nil)
return
}
var items: [ContextMenuItem] = []
items.append(.action(ContextMenuActionItem(text: environment.strings.Common_Back, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.actionSheet.primaryTextColor)
}, iconPosition: .left, action: { (c, _) in
c?.popItems()
})))
items.append(.separator)
for (quality, title) in qualityList {
let isSelected = self.maxVideoQuality == quality
items.append(.action(ContextMenuActionItem(text: title, icon: { _ in
if isSelected {
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: .white)
} else {
return nil
}
}, action: { [weak self] _, f in
f(.default)
guard let self else {
return
}
if self.maxVideoQuality != quality {
self.maxVideoQuality = quality
self.state?.updated(transition: .immediate)
}
})))
}
c?.pushItems(items: .single(ContextController.Items(content: .list(items))))
})))
if callState.isVideoEnabled && (callState.muteState?.canUnmute ?? true) {
if component.call.hasScreencast {
items.append(.action(ContextMenuActionItem(text: environment.strings.VoiceChat_StopScreenSharing, icon: { theme in

View File

@ -669,6 +669,8 @@ public final class ChatInlineSearchResultsListComponent: Component {
dismissNotice: { _ in
},
editPeer: { _ in
},
openWebApp: { _ in
}
)
self.chatListNodeInteraction = chatListNodeInteraction

View File

@ -185,6 +185,7 @@ public final class LoadingOverlayNode: ASDisplayNode {
}, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openPremiumGift: { _, _ in }, openPremiumManagement: {}, openActiveSessions: {}, openBirthdaySetup: {}, performActiveSessionAction: { _, _ in }, openChatFolderUpdates: {}, hideChatFolderUpdates: {}, openStories: { _, _ in }, openStarsTopup: { _ in
}, dismissNotice: { _ in
}, editPeer: { _ in
}, openWebApp: { _ in
})
let items = (0 ..< 1).map { _ -> ChatListItem in
@ -539,6 +540,8 @@ private final class PeerInfoScreenPersonalChannelItemNode: PeerInfoScreenItemNod
dismissNotice: { _ in
},
editPeer: { _ in
},
openWebApp: { _ in
}
)

View File

@ -207,6 +207,8 @@ final class GreetingMessageListItemComponent: Component {
dismissNotice: { _ in
},
editPeer: { _ in
},
openWebApp: { _ in
}
)
self.chatListNodeInteraction = chatListNodeInteraction

View File

@ -228,6 +228,8 @@ final class QuickReplySetupScreenComponent: Component {
if let itemId = item.id {
parentView.openEditShortcut(id: itemId, currentValue: item.shortcut)
}
},
openWebApp: { _ in
}
)

View File

@ -873,6 +873,7 @@ final class ThemeAccentColorControllerNode: ASDisplayNode, ASScrollViewDelegate
}, openStarsTopup: { _ in
}, dismissNotice: { _ in
}, editPeer: { _ in
}, openWebApp: { _ in
})
let chatListPresentationData = ChatListPresentationData(theme: self.presentationData.theme, fontSize: self.presentationData.listsFontSize, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameSortOrder: self.presentationData.nameSortOrder, nameDisplayOrder: self.presentationData.nameDisplayOrder, disableAnimations: true)

View File

@ -294,14 +294,14 @@ extension ChatControllerImpl {
})
}
func presentEmojiList(references: [StickerPackReference]) {
func presentEmojiList(references: [StickerPackReference], previewIconFile: TelegramMediaFile? = nil) {
guard let packReference = references.first else {
return
}
self.chatDisplayNode.dismissTextInput()
let presentationData = self.presentationData
let controller = StickerPackScreen(context: self.context, updatedPresentationData: self.updatedPresentationData, mainStickerPack: packReference, stickerPacks: Array(references), parentNavigationController: self.effectiveNavigationController, sendEmoji: canSendMessagesToChat(self.presentationInterfaceState) ? { [weak self] text, attribute in
let controller = StickerPackScreen(context: self.context, updatedPresentationData: self.updatedPresentationData, mainStickerPack: packReference, stickerPacks: Array(references), previewIconFile: previewIconFile, parentNavigationController: self.effectiveNavigationController, sendEmoji: canSendMessagesToChat(self.presentationInterfaceState) ? { [weak self] text, attribute in
if let strongSelf = self {
strongSelf.controllerInteraction?.sendEmoji(text, attribute, false)
}

View File

@ -9026,7 +9026,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}
if let stickerPackReference = stickerPackReference {
self.presentEmojiList(references: [stickerPackReference])
self.presentEmojiList(references: [stickerPackReference], previewIconFile: file)
/*let _ = (self.context.engine.stickers.loadedStickerPack(reference: stickerPackReference, forceActualized: false)
|> deliverOnMainQueue).startStandalone(next: { [weak self] stickerPack in

View File

@ -293,6 +293,7 @@ class ChatSearchResultsControllerNode: ViewControllerTracingNode, ASScrollViewDe
}, openStarsTopup: { _ in
}, dismissNotice: { _ in
}, editPeer: { _ in
}, openWebApp: { _ in
})
interaction.searchTextHighightState = searchQuery
self.interaction = interaction

View File

@ -177,6 +177,8 @@ private struct CommandChatInputContextPanelEntry: Comparable, Identifiable {
dismissNotice: { _ in
},
editPeer: { _ in
},
openWebApp: { _ in
}
)

View File

@ -181,8 +181,8 @@ func openChatMessageImpl(_ params: OpenChatMessageParams) -> Bool {
controller.navigationPresentation = .modal
params.navigationController?.pushViewController(controller)
return true
case let .stickerPack(reference):
let controller = StickerPackScreen(context: params.context, updatedPresentationData: params.updatedPresentationData, mainStickerPack: reference, stickerPacks: [reference], parentNavigationController: params.navigationController, sendSticker: params.sendSticker, sendEmoji: params.sendEmoji, actionPerformed: { actions in
case let .stickerPack(reference, previewIconFile):
let controller = StickerPackScreen(context: params.context, updatedPresentationData: params.updatedPresentationData, mainStickerPack: reference, stickerPacks: [reference], previewIconFile: previewIconFile, parentNavigationController: params.navigationController, sendSticker: params.sendSticker, sendEmoji: params.sendEmoji, actionPerformed: { actions in
let presentationData = params.context.sharedContext.currentPresentationData.with { $0 }
if actions.count > 1, let first = actions.first {