mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Emoji status and reaction improvements
This commit is contained in:
parent
29933dd7d5
commit
3b636d5cb9
@ -29,8 +29,9 @@ public final class ChatMessageItemAssociatedData: Equatable {
|
|||||||
public let availableReactions: AvailableReactions?
|
public let availableReactions: AvailableReactions?
|
||||||
public let defaultReaction: MessageReaction.Reaction?
|
public let defaultReaction: MessageReaction.Reaction?
|
||||||
public let isPremium: Bool
|
public let isPremium: Bool
|
||||||
|
public let forceInlineReactions: Bool
|
||||||
|
|
||||||
public init(automaticDownloadPeerType: MediaAutoDownloadPeerType, automaticDownloadNetworkType: MediaAutoDownloadNetworkType, isRecentActions: Bool = false, subject: ChatControllerSubject? = nil, contactsPeerIds: Set<EnginePeer.Id> = Set(), channelDiscussionGroup: ChannelDiscussionGroupStatus = .unknown, animatedEmojiStickers: [String: [StickerPackItem]] = [:], additionalAnimatedEmojiStickers: [String: [Int: StickerPackItem]] = [:], forcedResourceStatus: FileMediaResourceStatus? = nil, currentlyPlayingMessageId: EngineMessage.Index? = nil, isCopyProtectionEnabled: Bool = false, availableReactions: AvailableReactions?, defaultReaction: MessageReaction.Reaction?, isPremium: Bool) {
|
public init(automaticDownloadPeerType: MediaAutoDownloadPeerType, automaticDownloadNetworkType: MediaAutoDownloadNetworkType, isRecentActions: Bool = false, subject: ChatControllerSubject? = nil, contactsPeerIds: Set<EnginePeer.Id> = Set(), channelDiscussionGroup: ChannelDiscussionGroupStatus = .unknown, animatedEmojiStickers: [String: [StickerPackItem]] = [:], additionalAnimatedEmojiStickers: [String: [Int: StickerPackItem]] = [:], forcedResourceStatus: FileMediaResourceStatus? = nil, currentlyPlayingMessageId: EngineMessage.Index? = nil, isCopyProtectionEnabled: Bool = false, availableReactions: AvailableReactions?, defaultReaction: MessageReaction.Reaction?, isPremium: Bool, forceInlineReactions: Bool = false) {
|
||||||
self.automaticDownloadPeerType = automaticDownloadPeerType
|
self.automaticDownloadPeerType = automaticDownloadPeerType
|
||||||
self.automaticDownloadNetworkType = automaticDownloadNetworkType
|
self.automaticDownloadNetworkType = automaticDownloadNetworkType
|
||||||
self.isRecentActions = isRecentActions
|
self.isRecentActions = isRecentActions
|
||||||
@ -45,6 +46,7 @@ public final class ChatMessageItemAssociatedData: Equatable {
|
|||||||
self.availableReactions = availableReactions
|
self.availableReactions = availableReactions
|
||||||
self.defaultReaction = defaultReaction
|
self.defaultReaction = defaultReaction
|
||||||
self.isPremium = isPremium
|
self.isPremium = isPremium
|
||||||
|
self.forceInlineReactions = forceInlineReactions
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func == (lhs: ChatMessageItemAssociatedData, rhs: ChatMessageItemAssociatedData) -> Bool {
|
public static func == (lhs: ChatMessageItemAssociatedData, rhs: ChatMessageItemAssociatedData) -> Bool {
|
||||||
@ -87,6 +89,9 @@ public final class ChatMessageItemAssociatedData: Equatable {
|
|||||||
if lhs.isPremium != rhs.isPremium {
|
if lhs.isPremium != rhs.isPremium {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
if lhs.forceInlineReactions != rhs.forceInlineReactions {
|
||||||
|
return false
|
||||||
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -849,6 +849,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
|||||||
private func openStatusSetup(sourceView: UIView) {
|
private func openStatusSetup(sourceView: UIView) {
|
||||||
self.present(EmojiStatusSelectionController(
|
self.present(EmojiStatusSelectionController(
|
||||||
context: self.context,
|
context: self.context,
|
||||||
|
mode: .statusSelection,
|
||||||
sourceView: sourceView,
|
sourceView: sourceView,
|
||||||
emojiContent: EmojiPagerContentComponent.emojiInputData(
|
emojiContent: EmojiPagerContentComponent.emojiInputData(
|
||||||
context: self.context,
|
context: self.context,
|
||||||
|
@ -118,7 +118,7 @@ final class ChatListTitleView: UIView, NavigationBarTitleView, NavigationBarTitl
|
|||||||
case .premium:
|
case .premium:
|
||||||
statusContent = .premium(color: self.theme.list.itemAccentColor)
|
statusContent = .premium(color: self.theme.list.itemAccentColor)
|
||||||
case let .emoji(emoji):
|
case let .emoji(emoji):
|
||||||
statusContent = .emojiStatus(status: emoji, size: CGSize(width: 22.0, height: 22.0), placeholderColor: self.theme.list.mediaPlaceholderColor)
|
statusContent = .animation(content: .customEmoji(fileId: emoji.fileId), size: CGSize(width: 22.0, height: 22.0), placeholderColor: self.theme.list.mediaPlaceholderColor)
|
||||||
}
|
}
|
||||||
|
|
||||||
var titleCredibilityIconTransition: Transition
|
var titleCredibilityIconTransition: Transition
|
||||||
@ -351,7 +351,7 @@ final class ChatListTitleView: UIView, NavigationBarTitleView, NavigationBarTitl
|
|||||||
case .premium:
|
case .premium:
|
||||||
statusContent = .premium(color: self.theme.list.itemAccentColor)
|
statusContent = .premium(color: self.theme.list.itemAccentColor)
|
||||||
case let .emoji(emoji):
|
case let .emoji(emoji):
|
||||||
statusContent = .emojiStatus(status: emoji, size: CGSize(width: 22.0, height: 22.0), placeholderColor: self.theme.list.mediaPlaceholderColor)
|
statusContent = .animation(content: .customEmoji(fileId: emoji.fileId), size: CGSize(width: 22.0, height: 22.0), placeholderColor: self.theme.list.mediaPlaceholderColor)
|
||||||
}
|
}
|
||||||
|
|
||||||
var titleCredibilityIconTransition = Transition(transition)
|
var titleCredibilityIconTransition = Transition(transition)
|
||||||
|
@ -1524,7 +1524,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
|||||||
if let peer = messages.last?.author {
|
if let peer = messages.last?.author {
|
||||||
if case let .user(user) = peer, let emojiStatus = user.emojiStatus {
|
if case let .user(user) = peer, let emojiStatus = user.emojiStatus {
|
||||||
currentCredibilityIconImage = PresentationResourcesChatList.premiumIcon(item.presentationData.theme)
|
currentCredibilityIconImage = PresentationResourcesChatList.premiumIcon(item.presentationData.theme)
|
||||||
currentCredibilityIconContent = .emojiStatus(status: emojiStatus, size: CGSize(width: 32.0, height: 32.0), placeholderColor: item.presentationData.theme.list.mediaPlaceholderColor)
|
currentCredibilityIconContent = .animation(content: .customEmoji(fileId: emojiStatus.fileId), size: CGSize(width: 32.0, height: 32.0), placeholderColor: item.presentationData.theme.list.mediaPlaceholderColor)
|
||||||
} else if peer.isScam {
|
} else if peer.isScam {
|
||||||
currentCredibilityIconImage = PresentationResourcesChatList.scamIcon(item.presentationData.theme, strings: item.presentationData.strings, type: .regular)
|
currentCredibilityIconImage = PresentationResourcesChatList.scamIcon(item.presentationData.theme, strings: item.presentationData.strings, type: .regular)
|
||||||
currentCredibilityIconContent = .scam(color: item.presentationData.theme.chat.message.incoming.scamColor)
|
currentCredibilityIconContent = .scam(color: item.presentationData.theme.chat.message.incoming.scamColor)
|
||||||
@ -1545,7 +1545,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
|||||||
} else if case let .chat(itemPeer) = contentPeer, let peer = itemPeer.chatMainPeer {
|
} else if case let .chat(itemPeer) = contentPeer, let peer = itemPeer.chatMainPeer {
|
||||||
if case let .user(user) = peer, let emojiStatus = user.emojiStatus {
|
if case let .user(user) = peer, let emojiStatus = user.emojiStatus {
|
||||||
currentCredibilityIconImage = PresentationResourcesChatList.premiumIcon(item.presentationData.theme)
|
currentCredibilityIconImage = PresentationResourcesChatList.premiumIcon(item.presentationData.theme)
|
||||||
currentCredibilityIconContent = .emojiStatus(status: emojiStatus, size: CGSize(width: 32.0, height: 32.0), placeholderColor: item.presentationData.theme.list.mediaPlaceholderColor)
|
currentCredibilityIconContent = .animation(content: .customEmoji(fileId: emojiStatus.fileId), size: CGSize(width: 32.0, height: 32.0), placeholderColor: item.presentationData.theme.list.mediaPlaceholderColor)
|
||||||
} else if peer.isScam {
|
} else if peer.isScam {
|
||||||
currentCredibilityIconImage = PresentationResourcesChatList.scamIcon(item.presentationData.theme, strings: item.presentationData.strings, type: .regular)
|
currentCredibilityIconImage = PresentationResourcesChatList.scamIcon(item.presentationData.theme, strings: item.presentationData.strings, type: .regular)
|
||||||
currentCredibilityIconContent = .scam(color: item.presentationData.theme.chat.message.incoming.scamColor)
|
currentCredibilityIconContent = .scam(color: item.presentationData.theme.chat.message.incoming.scamColor)
|
||||||
|
@ -616,7 +616,7 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
|
|||||||
} else if peer.isFake {
|
} else if peer.isFake {
|
||||||
credibilityIcon = .fake(color: item.presentationData.theme.chat.message.incoming.scamColor)
|
credibilityIcon = .fake(color: item.presentationData.theme.chat.message.incoming.scamColor)
|
||||||
} else if case let .user(user) = peer, let emojiStatus = user.emojiStatus {
|
} else if case let .user(user) = peer, let emojiStatus = user.emojiStatus {
|
||||||
credibilityIcon = .emojiStatus(status: emojiStatus, size: CGSize(width: 20.0, height: 20.0), placeholderColor: item.presentationData.theme.list.mediaPlaceholderColor)
|
credibilityIcon = .animation(content: .customEmoji(fileId: emojiStatus.fileId), size: CGSize(width: 20.0, height: 20.0), placeholderColor: item.presentationData.theme.list.mediaPlaceholderColor)
|
||||||
} else if peer.isVerified {
|
} else if peer.isVerified {
|
||||||
credibilityIcon = .verified(fillColor: item.presentationData.theme.list.itemCheckColors.fillColor, foregroundColor: item.presentationData.theme.list.itemCheckColors.foregroundColor)
|
credibilityIcon = .verified(fillColor: item.presentationData.theme.list.itemCheckColors.fillColor, foregroundColor: item.presentationData.theme.list.itemCheckColors.foregroundColor)
|
||||||
} else if peer.isPremium && !premiumConfiguration.isPremiumDisabled {
|
} else if peer.isPremium && !premiumConfiguration.isPremiumDisabled {
|
||||||
|
@ -614,7 +614,7 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
|
|||||||
} else if item.peer.isFake {
|
} else if item.peer.isFake {
|
||||||
credibilityIcon = .fake(color: item.presentationData.theme.chat.message.incoming.scamColor)
|
credibilityIcon = .fake(color: item.presentationData.theme.chat.message.incoming.scamColor)
|
||||||
} else if case let .user(user) = item.peer, let emojiStatus = user.emojiStatus {
|
} else if case let .user(user) = item.peer, let emojiStatus = user.emojiStatus {
|
||||||
credibilityIcon = .emojiStatus(status: emojiStatus, size: CGSize(width: 20.0, height: 20.0), placeholderColor: item.presentationData.theme.list.mediaPlaceholderColor)
|
credibilityIcon = .animation(content: .customEmoji(fileId: emojiStatus.fileId), size: CGSize(width: 20.0, height: 20.0), placeholderColor: item.presentationData.theme.list.mediaPlaceholderColor)
|
||||||
} else if item.peer.isVerified {
|
} else if item.peer.isVerified {
|
||||||
credibilityIcon = .verified(fillColor: item.presentationData.theme.list.itemCheckColors.fillColor, foregroundColor: item.presentationData.theme.list.itemCheckColors.foregroundColor)
|
credibilityIcon = .verified(fillColor: item.presentationData.theme.list.itemCheckColors.fillColor, foregroundColor: item.presentationData.theme.list.itemCheckColors.foregroundColor)
|
||||||
} else if item.peer.isPremium && !premiumConfiguration.isPremiumDisabled {
|
} else if item.peer.isPremium && !premiumConfiguration.isPremiumDisabled {
|
||||||
|
@ -295,8 +295,8 @@ class EmojiHeaderComponent: Component {
|
|||||||
context: component.context,
|
context: component.context,
|
||||||
animationCache: component.animationCache,
|
animationCache: component.animationCache,
|
||||||
animationRenderer: component.animationRenderer,
|
animationRenderer: component.animationRenderer,
|
||||||
content: .emojiStatus(
|
content: .animation(
|
||||||
status: PeerEmojiStatus(fileId: component.fileId),
|
content: .customEmoji(fileId: component.fileId),
|
||||||
size: CGSize(width: 100.0, height: 100.0),
|
size: CGSize(width: 100.0, height: 100.0),
|
||||||
placeholderColor: component.placeholderColor
|
placeholderColor: component.placeholderColor
|
||||||
),
|
),
|
||||||
|
@ -103,6 +103,7 @@ swift_library(
|
|||||||
"//submodules/PremiumUI:PremiumUI",
|
"//submodules/PremiumUI:PremiumUI",
|
||||||
"//submodules/InviteLinksUI:InviteLinksUI",
|
"//submodules/InviteLinksUI:InviteLinksUI",
|
||||||
"//submodules/HorizontalPeerItem:HorizontalPeerItem",
|
"//submodules/HorizontalPeerItem:HorizontalPeerItem",
|
||||||
|
"//submodules/TelegramUI/Components/EntityKeyboard:EntityKeyboard",
|
||||||
],
|
],
|
||||||
visibility = [
|
visibility = [
|
||||||
"//visibility:public",
|
"//visibility:public",
|
||||||
|
@ -0,0 +1,361 @@
|
|||||||
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
import Display
|
||||||
|
import AsyncDisplayKit
|
||||||
|
import SwiftSignalKit
|
||||||
|
import TelegramPresentationData
|
||||||
|
import Postbox
|
||||||
|
import TelegramCore
|
||||||
|
import ItemListUI
|
||||||
|
import EmojiStatusComponent
|
||||||
|
import ComponentFlow
|
||||||
|
import AccountContext
|
||||||
|
|
||||||
|
public class ItemListReactionItem: ListViewItem, ItemListItem {
|
||||||
|
let context: AccountContext
|
||||||
|
let presentationData: ItemListPresentationData
|
||||||
|
let title: String
|
||||||
|
let reaction: MessageReaction.Reaction
|
||||||
|
let availableReactions: AvailableReactions?
|
||||||
|
public let sectionId: ItemListSectionId
|
||||||
|
let style: ItemListStyle
|
||||||
|
let action: (() -> Void)?
|
||||||
|
public let tag: ItemListItemTag?
|
||||||
|
|
||||||
|
public init(context: AccountContext, presentationData: ItemListPresentationData, title: String, reaction: MessageReaction.Reaction, availableReactions: AvailableReactions?, sectionId: ItemListSectionId, style: ItemListStyle, action: (() -> Void)?, tag: ItemListItemTag? = nil) {
|
||||||
|
self.context = context
|
||||||
|
self.presentationData = presentationData
|
||||||
|
self.title = title
|
||||||
|
self.reaction = reaction
|
||||||
|
self.availableReactions = availableReactions
|
||||||
|
self.sectionId = sectionId
|
||||||
|
self.style = style
|
||||||
|
self.action = action
|
||||||
|
self.tag = tag
|
||||||
|
}
|
||||||
|
|
||||||
|
public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
|
||||||
|
async {
|
||||||
|
let node = ItemListReactionItemNode()
|
||||||
|
let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
|
||||||
|
|
||||||
|
node.contentSize = layout.contentSize
|
||||||
|
node.insets = layout.insets
|
||||||
|
|
||||||
|
Queue.mainQueue().async {
|
||||||
|
completion(node, {
|
||||||
|
return (nil, { _ in apply() })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
|
||||||
|
Queue.mainQueue().async {
|
||||||
|
if let nodeValue = node() as? ItemListReactionItemNode {
|
||||||
|
let makeLayout = nodeValue.asyncLayout()
|
||||||
|
|
||||||
|
async {
|
||||||
|
let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
|
||||||
|
Queue.mainQueue().async {
|
||||||
|
completion(layout, { _ in
|
||||||
|
apply()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public var selectable: Bool = true
|
||||||
|
|
||||||
|
public func selected(listView: ListView){
|
||||||
|
listView.clearHighlightAnimated(true)
|
||||||
|
self.action?()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private let badgeFont = Font.regular(15.0)
|
||||||
|
|
||||||
|
public class ItemListReactionItemNode: ListViewItemNode, ItemListItemNode {
|
||||||
|
private let backgroundNode: ASDisplayNode
|
||||||
|
private let topStripeNode: ASDisplayNode
|
||||||
|
private let bottomStripeNode: ASDisplayNode
|
||||||
|
private let highlightedBackgroundNode: ASDisplayNode
|
||||||
|
private let maskNode: ASImageNode
|
||||||
|
|
||||||
|
let titleNode: TextNode
|
||||||
|
let iconView: ComponentHostView<Empty>
|
||||||
|
|
||||||
|
private let activateArea: AccessibilityAreaNode
|
||||||
|
|
||||||
|
private var item: ItemListReactionItem?
|
||||||
|
private var fileDisposable: Disposable?
|
||||||
|
private var file: TelegramMediaFile?
|
||||||
|
|
||||||
|
override public var canBeSelected: Bool {
|
||||||
|
if let item = self.item, let _ = item.action {
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public var tag: ItemListItemTag? {
|
||||||
|
return self.item?.tag
|
||||||
|
}
|
||||||
|
|
||||||
|
public init() {
|
||||||
|
self.backgroundNode = ASDisplayNode()
|
||||||
|
self.backgroundNode.isLayerBacked = true
|
||||||
|
self.backgroundNode.backgroundColor = .white
|
||||||
|
|
||||||
|
self.maskNode = ASImageNode()
|
||||||
|
self.maskNode.isUserInteractionEnabled = false
|
||||||
|
|
||||||
|
self.topStripeNode = ASDisplayNode()
|
||||||
|
self.topStripeNode.isLayerBacked = true
|
||||||
|
|
||||||
|
self.bottomStripeNode = ASDisplayNode()
|
||||||
|
self.bottomStripeNode.isLayerBacked = true
|
||||||
|
|
||||||
|
self.titleNode = TextNode()
|
||||||
|
self.titleNode.isUserInteractionEnabled = false
|
||||||
|
|
||||||
|
self.iconView = ComponentHostView<Empty>()
|
||||||
|
|
||||||
|
self.highlightedBackgroundNode = ASDisplayNode()
|
||||||
|
self.highlightedBackgroundNode.isLayerBacked = true
|
||||||
|
|
||||||
|
self.activateArea = AccessibilityAreaNode()
|
||||||
|
|
||||||
|
super.init(layerBacked: false, dynamicBounce: false)
|
||||||
|
|
||||||
|
self.addSubnode(self.titleNode)
|
||||||
|
self.view.addSubview(self.iconView)
|
||||||
|
|
||||||
|
self.addSubnode(self.activateArea)
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
self.fileDisposable?.dispose()
|
||||||
|
}
|
||||||
|
|
||||||
|
public func asyncLayout() -> (_ item: ItemListReactionItem, _ params: ListViewItemLayoutParams, _ insets: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
|
||||||
|
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
|
||||||
|
|
||||||
|
let currentItem = self.item
|
||||||
|
|
||||||
|
return { item, params, neighbors in
|
||||||
|
var rightInset: CGFloat
|
||||||
|
rightInset = 34.0 + params.rightInset
|
||||||
|
let _ = rightInset
|
||||||
|
|
||||||
|
let contentSize: CGSize
|
||||||
|
let insets: UIEdgeInsets
|
||||||
|
let separatorHeight = UIScreenPixel
|
||||||
|
let itemBackgroundColor: UIColor
|
||||||
|
let itemSeparatorColor: UIColor
|
||||||
|
|
||||||
|
let leftInset = 16.0 + params.leftInset
|
||||||
|
|
||||||
|
var additionalTextRightInset: CGFloat = 0.0
|
||||||
|
additionalTextRightInset += 44.0
|
||||||
|
|
||||||
|
let titleColor: UIColor = item.presentationData.theme.list.itemPrimaryTextColor
|
||||||
|
|
||||||
|
let titleFont = Font.regular(item.presentationData.fontSize.itemListBaseFontSize)
|
||||||
|
|
||||||
|
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.title, font: titleFont, textColor: titleColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - params.rightInset - 20.0 - leftInset - additionalTextRightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||||
|
|
||||||
|
let verticalInset: CGFloat = 11.0
|
||||||
|
|
||||||
|
let height: CGFloat
|
||||||
|
height = verticalInset * 2.0 + titleLayout.size.height
|
||||||
|
|
||||||
|
switch item.style {
|
||||||
|
case .plain:
|
||||||
|
itemBackgroundColor = item.presentationData.theme.list.plainBackgroundColor
|
||||||
|
itemSeparatorColor = item.presentationData.theme.list.itemPlainSeparatorColor
|
||||||
|
contentSize = CGSize(width: params.width, height: height)
|
||||||
|
insets = itemListNeighborsPlainInsets(neighbors)
|
||||||
|
case .blocks:
|
||||||
|
itemBackgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor
|
||||||
|
itemSeparatorColor = item.presentationData.theme.list.itemBlocksSeparatorColor
|
||||||
|
contentSize = CGSize(width: params.width, height: height)
|
||||||
|
insets = itemListNeighborsGroupedInsets(neighbors, params)
|
||||||
|
}
|
||||||
|
|
||||||
|
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
|
||||||
|
|
||||||
|
return (ListViewItemNodeLayout(contentSize: contentSize, insets: insets), { [weak self] in
|
||||||
|
if let strongSelf = self {
|
||||||
|
strongSelf.item = item
|
||||||
|
|
||||||
|
strongSelf.activateArea.frame = CGRect(origin: CGPoint(x: params.leftInset, y: 0.0), size: CGSize(width: params.width - params.leftInset - params.rightInset, height: layout.contentSize.height))
|
||||||
|
strongSelf.activateArea.accessibilityLabel = item.title
|
||||||
|
|
||||||
|
strongSelf.activateArea.accessibilityTraits = []
|
||||||
|
|
||||||
|
if currentItem?.presentationData.theme !== item.presentationData.theme {
|
||||||
|
strongSelf.topStripeNode.backgroundColor = itemSeparatorColor
|
||||||
|
strongSelf.bottomStripeNode.backgroundColor = itemSeparatorColor
|
||||||
|
strongSelf.backgroundNode.backgroundColor = itemBackgroundColor
|
||||||
|
strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.list.itemHighlightedBackgroundColor
|
||||||
|
}
|
||||||
|
|
||||||
|
let _ = titleApply()
|
||||||
|
|
||||||
|
switch item.style {
|
||||||
|
case .plain:
|
||||||
|
if strongSelf.backgroundNode.supernode != nil {
|
||||||
|
strongSelf.backgroundNode.removeFromSupernode()
|
||||||
|
}
|
||||||
|
if strongSelf.topStripeNode.supernode != nil {
|
||||||
|
strongSelf.topStripeNode.removeFromSupernode()
|
||||||
|
}
|
||||||
|
if strongSelf.bottomStripeNode.supernode == nil {
|
||||||
|
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 0)
|
||||||
|
}
|
||||||
|
if strongSelf.maskNode.supernode != nil {
|
||||||
|
strongSelf.maskNode.removeFromSupernode()
|
||||||
|
}
|
||||||
|
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: leftInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - leftInset, height: separatorHeight))
|
||||||
|
case .blocks:
|
||||||
|
if strongSelf.backgroundNode.supernode == nil {
|
||||||
|
strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
|
||||||
|
}
|
||||||
|
if strongSelf.topStripeNode.supernode == nil {
|
||||||
|
strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1)
|
||||||
|
}
|
||||||
|
if strongSelf.bottomStripeNode.supernode == nil {
|
||||||
|
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2)
|
||||||
|
}
|
||||||
|
if strongSelf.maskNode.supernode == nil {
|
||||||
|
strongSelf.insertSubnode(strongSelf.maskNode, at: 3)
|
||||||
|
}
|
||||||
|
|
||||||
|
let hasCorners = itemListHasRoundedBlockLayout(params)
|
||||||
|
var hasTopCorners = false
|
||||||
|
var hasBottomCorners = false
|
||||||
|
switch neighbors.top {
|
||||||
|
case .sameSection(false):
|
||||||
|
strongSelf.topStripeNode.isHidden = true
|
||||||
|
default:
|
||||||
|
hasTopCorners = true
|
||||||
|
strongSelf.topStripeNode.isHidden = hasCorners
|
||||||
|
}
|
||||||
|
let bottomStripeInset: CGFloat
|
||||||
|
switch neighbors.bottom {
|
||||||
|
case .sameSection(false):
|
||||||
|
bottomStripeInset = leftInset
|
||||||
|
strongSelf.bottomStripeNode.isHidden = false
|
||||||
|
default:
|
||||||
|
bottomStripeInset = 0.0
|
||||||
|
hasBottomCorners = true
|
||||||
|
strongSelf.bottomStripeNode.isHidden = hasCorners
|
||||||
|
}
|
||||||
|
|
||||||
|
strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil
|
||||||
|
|
||||||
|
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
|
||||||
|
strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0)
|
||||||
|
strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: separatorHeight))
|
||||||
|
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - bottomStripeInset, height: separatorHeight))
|
||||||
|
}
|
||||||
|
|
||||||
|
let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: 11.0), size: titleLayout.size)
|
||||||
|
strongSelf.titleNode.frame = titleFrame
|
||||||
|
|
||||||
|
var animationContent: EmojiStatusComponent.AnimationContent?
|
||||||
|
switch item.reaction {
|
||||||
|
case .builtin:
|
||||||
|
if let availableReactions = item.availableReactions {
|
||||||
|
for reaction in availableReactions.reactions {
|
||||||
|
if reaction.value == item.reaction {
|
||||||
|
animationContent = .file(file: reaction.selectAnimation)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case let .custom(fileId):
|
||||||
|
animationContent = .customEmoji(fileId: fileId)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let animationContent = animationContent {
|
||||||
|
let iconBoundingSize = CGSize(width: 28.0, height: 28.0)
|
||||||
|
let iconOffsetX: CGFloat = 0.0
|
||||||
|
let iconSize = strongSelf.iconView.update(
|
||||||
|
transition: .immediate,
|
||||||
|
component: AnyComponent(EmojiStatusComponent(
|
||||||
|
context: item.context,
|
||||||
|
animationCache: item.context.animationCache,
|
||||||
|
animationRenderer: item.context.animationRenderer,
|
||||||
|
content: .animation(content: animationContent, size: iconBoundingSize, placeholderColor: item.presentationData.theme.list.mediaPlaceholderColor), action: nil, longTapAction: nil
|
||||||
|
)),
|
||||||
|
environment: {},
|
||||||
|
containerSize: iconBoundingSize
|
||||||
|
)
|
||||||
|
strongSelf.iconView.isUserInteractionEnabled = false
|
||||||
|
strongSelf.iconView.frame = CGRect(origin: CGPoint(x: params.width - params.rightInset - 7.0 - iconSize.width + iconOffsetX, y: floorToScreenPixels((height - iconSize.height) / 2.0)), size: iconSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*if let arrowImage = strongSelf.arrowNode.image {
|
||||||
|
strongSelf.arrowNode.frame = CGRect(origin: CGPoint(x: params.width - params.rightInset - 7.0 - arrowImage.size.width, y: floorToScreenPixels((height - arrowImage.size.height) / 2.0)), size: arrowImage.size)
|
||||||
|
}*/
|
||||||
|
strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: height + UIScreenPixel))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override public func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) {
|
||||||
|
super.setHighlighted(highlighted, at: point, animated: animated)
|
||||||
|
|
||||||
|
if highlighted {
|
||||||
|
self.highlightedBackgroundNode.alpha = 1.0
|
||||||
|
if self.highlightedBackgroundNode.supernode == nil {
|
||||||
|
var anchorNode: ASDisplayNode?
|
||||||
|
if self.bottomStripeNode.supernode != nil {
|
||||||
|
anchorNode = self.bottomStripeNode
|
||||||
|
} else if self.topStripeNode.supernode != nil {
|
||||||
|
anchorNode = self.topStripeNode
|
||||||
|
} else if self.backgroundNode.supernode != nil {
|
||||||
|
anchorNode = self.backgroundNode
|
||||||
|
}
|
||||||
|
if let anchorNode = anchorNode {
|
||||||
|
self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: anchorNode)
|
||||||
|
} else {
|
||||||
|
self.addSubnode(self.highlightedBackgroundNode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if self.highlightedBackgroundNode.supernode != nil {
|
||||||
|
if animated {
|
||||||
|
self.highlightedBackgroundNode.layer.animateAlpha(from: self.highlightedBackgroundNode.alpha, to: 0.0, duration: 0.4, completion: { [weak self] completed in
|
||||||
|
if let strongSelf = self {
|
||||||
|
if completed {
|
||||||
|
strongSelf.highlightedBackgroundNode.removeFromSupernode()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
self.highlightedBackgroundNode.alpha = 0.0
|
||||||
|
} else {
|
||||||
|
self.highlightedBackgroundNode.removeFromSupernode()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override public func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) {
|
||||||
|
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
|
||||||
|
}
|
||||||
|
|
||||||
|
override public func animateAdded(_ currentTimestamp: Double, duration: Double) {
|
||||||
|
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||||
|
}
|
||||||
|
|
||||||
|
override public func animateRemoved(_ currentTimestamp: Double, duration: Double) {
|
||||||
|
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
|
||||||
|
}
|
||||||
|
}
|
@ -12,19 +12,21 @@ import PresentationDataUtils
|
|||||||
import AccountContext
|
import AccountContext
|
||||||
import ReactionImageComponent
|
import ReactionImageComponent
|
||||||
import WebPBinding
|
import WebPBinding
|
||||||
|
import EmojiStatusSelectionComponent
|
||||||
|
import EntityKeyboard
|
||||||
|
|
||||||
private final class QuickReactionSetupControllerArguments {
|
private final class QuickReactionSetupControllerArguments {
|
||||||
let context: AccountContext
|
let context: AccountContext
|
||||||
let selectItem: (MessageReaction.Reaction) -> Void
|
let openQuickReaction: () -> Void
|
||||||
let toggleReaction: () -> Void
|
let toggleReaction: () -> Void
|
||||||
|
|
||||||
init(
|
init(
|
||||||
context: AccountContext,
|
context: AccountContext,
|
||||||
selectItem: @escaping (MessageReaction.Reaction) -> Void,
|
openQuickReaction: @escaping () -> Void,
|
||||||
toggleReaction: @escaping () -> Void
|
toggleReaction: @escaping () -> Void
|
||||||
) {
|
) {
|
||||||
self.context = context
|
self.context = context
|
||||||
self.selectItem = selectItem
|
self.openQuickReaction = openQuickReaction
|
||||||
self.toggleReaction = toggleReaction
|
self.toggleReaction = toggleReaction
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -39,21 +41,21 @@ private enum QuickReactionSetupControllerEntry: ItemListNodeEntry {
|
|||||||
case demoHeader
|
case demoHeader
|
||||||
case demoMessage
|
case demoMessage
|
||||||
case demoDescription
|
case demoDescription
|
||||||
case itemsHeader
|
case quickReaction
|
||||||
case item(MessageReaction.Reaction)
|
case quickReactionDescription
|
||||||
}
|
}
|
||||||
|
|
||||||
case demoHeader(String)
|
case demoHeader(String)
|
||||||
case demoMessage(wallpaper: TelegramWallpaper, fontSize: PresentationFontSize, bubbleCorners: PresentationChatBubbleCorners, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, availableReactions: AvailableReactions?, reaction: MessageReaction.Reaction?)
|
case demoMessage(wallpaper: TelegramWallpaper, fontSize: PresentationFontSize, bubbleCorners: PresentationChatBubbleCorners, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, availableReactions: AvailableReactions?, reaction: MessageReaction.Reaction?)
|
||||||
case demoDescription(String)
|
case demoDescription(String)
|
||||||
case itemsHeader(String)
|
case quickReaction(String, MessageReaction.Reaction, AvailableReactions)
|
||||||
case item(index: Int, value: MessageReaction.Reaction, image: UIImage?, imageIsAnimation: Bool, text: String, isSelected: Bool)
|
case quickReactionDescription(String)
|
||||||
|
|
||||||
var section: ItemListSectionId {
|
var section: ItemListSectionId {
|
||||||
switch self {
|
switch self {
|
||||||
case .demoHeader, .demoMessage, .demoDescription:
|
case .demoHeader, .demoMessage, .demoDescription:
|
||||||
return QuickReactionSetupControllerSection.demo.rawValue
|
return QuickReactionSetupControllerSection.demo.rawValue
|
||||||
case .itemsHeader, .item:
|
case .quickReaction, .quickReactionDescription:
|
||||||
return QuickReactionSetupControllerSection.items.rawValue
|
return QuickReactionSetupControllerSection.items.rawValue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -66,10 +68,10 @@ private enum QuickReactionSetupControllerEntry: ItemListNodeEntry {
|
|||||||
return .demoMessage
|
return .demoMessage
|
||||||
case .demoDescription:
|
case .demoDescription:
|
||||||
return .demoDescription
|
return .demoDescription
|
||||||
case .itemsHeader:
|
case .quickReaction:
|
||||||
return .itemsHeader
|
return .quickReaction
|
||||||
case let .item(_, value, _, _, _, _):
|
case .quickReactionDescription:
|
||||||
return .item(value)
|
return .quickReactionDescription
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -81,10 +83,10 @@ private enum QuickReactionSetupControllerEntry: ItemListNodeEntry {
|
|||||||
return 1
|
return 1
|
||||||
case .demoDescription:
|
case .demoDescription:
|
||||||
return 2
|
return 2
|
||||||
case .itemsHeader:
|
case .quickReaction:
|
||||||
return 3
|
return 3
|
||||||
case let .item(index, _, _, _, _, _):
|
case .quickReactionDescription:
|
||||||
return 100 + index
|
return 4
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -108,14 +110,14 @@ private enum QuickReactionSetupControllerEntry: ItemListNodeEntry {
|
|||||||
} else {
|
} else {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
case let .itemsHeader(text):
|
case let .quickReaction(lhsText, lhsReaction, lhsAvailableReactions):
|
||||||
if case .itemsHeader(text) = rhs {
|
if case let .quickReaction(rhsText, rhsReaction, rhsAvailableReactions) = rhs, lhsText == rhsText, lhsReaction == rhsReaction, lhsAvailableReactions == rhsAvailableReactions {
|
||||||
return true
|
return true
|
||||||
} else {
|
} else {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
case let .item(index, value, file, imageIsAnimation, text, isEnabled):
|
case let .quickReactionDescription(text):
|
||||||
if case .item(index, value, file, imageIsAnimation, text, isEnabled) = rhs {
|
if case .quickReactionDescription(text) = rhs {
|
||||||
return true
|
return true
|
||||||
} else {
|
} else {
|
||||||
return false
|
return false
|
||||||
@ -151,28 +153,12 @@ private enum QuickReactionSetupControllerEntry: ItemListNodeEntry {
|
|||||||
)
|
)
|
||||||
case let .demoDescription(text):
|
case let .demoDescription(text):
|
||||||
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
|
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
|
||||||
case let .itemsHeader(text):
|
case let .quickReaction(title, reaction, availableReactions):
|
||||||
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
|
return ItemListReactionItem(context: arguments.context, presentationData: presentationData, title: title, reaction: reaction, availableReactions: availableReactions, sectionId: self.section, style: .blocks, action: {
|
||||||
case let .item(_, value, image, imageIsAnimation, text, isSelected):
|
arguments.openQuickReaction()
|
||||||
var imageFitSize = CGSize(width: 30.0, height: 30.0)
|
})
|
||||||
if imageIsAnimation {
|
case let .quickReactionDescription(text):
|
||||||
imageFitSize.width = floor(imageFitSize.width * 2.0)
|
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
|
||||||
imageFitSize.height = floor(imageFitSize.height * 2.0)
|
|
||||||
}
|
|
||||||
return ItemListCheckboxItem(
|
|
||||||
presentationData: presentationData,
|
|
||||||
icon: image,
|
|
||||||
iconSize: image?.size.aspectFitted(imageFitSize),
|
|
||||||
title: text,
|
|
||||||
style: .right,
|
|
||||||
color: .accent,
|
|
||||||
checked: isSelected,
|
|
||||||
zeroSeparatorInsets: false,
|
|
||||||
sectionId: self.section,
|
|
||||||
action: {
|
|
||||||
arguments.selectItem(value)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -184,7 +170,6 @@ private struct QuickReactionSetupControllerState: Equatable {
|
|||||||
private func quickReactionSetupControllerEntries(
|
private func quickReactionSetupControllerEntries(
|
||||||
presentationData: PresentationData,
|
presentationData: PresentationData,
|
||||||
availableReactions: AvailableReactions?,
|
availableReactions: AvailableReactions?,
|
||||||
images: [MessageReaction.Reaction: (image: UIImage, isAnimation: Bool)],
|
|
||||||
reactionSettings: ReactionSettings,
|
reactionSettings: ReactionSettings,
|
||||||
state: QuickReactionSetupControllerState,
|
state: QuickReactionSetupControllerState,
|
||||||
isPremium: Bool
|
isPremium: Bool
|
||||||
@ -204,27 +189,11 @@ private func quickReactionSetupControllerEntries(
|
|||||||
))
|
))
|
||||||
entries.append(.demoDescription(presentationData.strings.Settings_QuickReactionSetup_DemoInfo))
|
entries.append(.demoDescription(presentationData.strings.Settings_QuickReactionSetup_DemoInfo))
|
||||||
|
|
||||||
entries.append(.itemsHeader(presentationData.strings.Settings_QuickReactionSetup_ReactionListHeader))
|
//TODO:localize
|
||||||
var index = 0
|
entries.append(.quickReaction("Choose Your Quick Reaction", reactionSettings.quickReaction, availableReactions))
|
||||||
for availableReaction in availableReactions.reactions {
|
|
||||||
if !availableReaction.isEnabled {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if !isPremium && availableReaction.isPremium {
|
//TODO:localize
|
||||||
continue
|
entries.append(.quickReactionDescription("You can set any emoji as your quick reaction."))
|
||||||
}
|
|
||||||
|
|
||||||
entries.append(.item(
|
|
||||||
index: index,
|
|
||||||
value: availableReaction.value,
|
|
||||||
image: images[availableReaction.value]?.image,
|
|
||||||
imageIsAnimation: images[availableReaction.value]?.isAnimation ?? false,
|
|
||||||
text: availableReaction.title,
|
|
||||||
isSelected: reactionSettings.quickReaction == availableReaction.value
|
|
||||||
))
|
|
||||||
index += 1
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return entries
|
return entries
|
||||||
@ -243,17 +212,14 @@ public func quickReactionSetupController(
|
|||||||
var dismissImpl: (() -> Void)?
|
var dismissImpl: (() -> Void)?
|
||||||
let _ = dismissImpl
|
let _ = dismissImpl
|
||||||
|
|
||||||
|
var openQuickReactionImpl: (() -> Void)?
|
||||||
|
|
||||||
let actionsDisposable = DisposableSet()
|
let actionsDisposable = DisposableSet()
|
||||||
|
|
||||||
let arguments = QuickReactionSetupControllerArguments(
|
let arguments = QuickReactionSetupControllerArguments(
|
||||||
context: context,
|
context: context,
|
||||||
selectItem: { reaction in
|
openQuickReaction: {
|
||||||
updateState { state in
|
openQuickReactionImpl?()
|
||||||
var state = state
|
|
||||||
state.hasReaction = false
|
|
||||||
return state
|
|
||||||
}
|
|
||||||
let _ = context.engine.stickers.updateQuickReaction(reaction: reaction).start()
|
|
||||||
},
|
},
|
||||||
toggleReaction: {
|
toggleReaction: {
|
||||||
updateState { state in
|
updateState { state in
|
||||||
@ -275,80 +241,22 @@ public func quickReactionSetupController(
|
|||||||
return reactionSettings
|
return reactionSettings
|
||||||
}
|
}
|
||||||
|
|
||||||
let images: Signal<[MessageReaction.Reaction: (image: UIImage, isAnimation: Bool)], NoError> = context.engine.stickers.availableReactions()
|
|
||||||
|> mapToSignal { availableReactions -> Signal<[MessageReaction.Reaction: (image: UIImage, isAnimation: Bool)], NoError> in
|
|
||||||
var signals: [Signal<(MessageReaction.Reaction, (image: UIImage, isAnimation: Bool)?), NoError>] = []
|
|
||||||
|
|
||||||
if let availableReactions = availableReactions {
|
|
||||||
for availableReaction in availableReactions.reactions {
|
|
||||||
if !availableReaction.isEnabled {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if let centerAnimation = availableReaction.centerAnimation {
|
|
||||||
let signal: Signal<(MessageReaction.Reaction, (image: UIImage, isAnimation: Bool)?), NoError> = reactionStaticImage(context: context, animation: centerAnimation, pixelSize: CGSize(width: 72.0 * 2.0, height: 72.0 * 2.0), queue: sharedReactionStaticImage)
|
|
||||||
|> map { data -> (MessageReaction.Reaction, (image: UIImage, isAnimation: Bool)?) in
|
|
||||||
guard data.isComplete else {
|
|
||||||
return (availableReaction.value, nil)
|
|
||||||
}
|
|
||||||
guard let dataValue = try? Data(contentsOf: URL(fileURLWithPath: data.path)) else {
|
|
||||||
return (availableReaction.value, nil)
|
|
||||||
}
|
|
||||||
guard let image = UIImage(data: dataValue) else {
|
|
||||||
return (availableReaction.value, nil)
|
|
||||||
}
|
|
||||||
return (availableReaction.value, (image, true))
|
|
||||||
}
|
|
||||||
signals.append(signal)
|
|
||||||
} else {
|
|
||||||
let signal: Signal<(MessageReaction.Reaction, (image: UIImage, isAnimation: Bool)?), NoError> = context.account.postbox.mediaBox.resourceData(availableReaction.staticIcon.resource)
|
|
||||||
|> map { data -> (MessageReaction.Reaction, (image: UIImage, isAnimation: Bool)?) in
|
|
||||||
guard data.complete else {
|
|
||||||
return (availableReaction.value, nil)
|
|
||||||
}
|
|
||||||
guard let dataValue = try? Data(contentsOf: URL(fileURLWithPath: data.path)) else {
|
|
||||||
return (availableReaction.value, nil)
|
|
||||||
}
|
|
||||||
guard let image = WebP.convert(fromWebP: dataValue) else {
|
|
||||||
return (availableReaction.value, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (availableReaction.value, (image, false))
|
|
||||||
}
|
|
||||||
signals.append(signal)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return combineLatest(queue: .mainQueue(), signals)
|
|
||||||
|> map { values -> [MessageReaction.Reaction: (image: UIImage, isAnimation: Bool)] in
|
|
||||||
var dict: [MessageReaction.Reaction: (image: UIImage, isAnimation: Bool)] = [:]
|
|
||||||
for (key, image) in values {
|
|
||||||
if let image = image {
|
|
||||||
dict[key] = image
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return dict
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let presentationData = updatedPresentationData?.signal ?? context.sharedContext.presentationData
|
let presentationData = updatedPresentationData?.signal ?? context.sharedContext.presentationData
|
||||||
let signal = combineLatest(queue: .mainQueue(),
|
let signal = combineLatest(queue: .mainQueue(),
|
||||||
presentationData,
|
presentationData,
|
||||||
statePromise.get(),
|
statePromise.get(),
|
||||||
context.engine.stickers.availableReactions(),
|
context.engine.stickers.availableReactions(),
|
||||||
settings,
|
settings,
|
||||||
images,
|
|
||||||
context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId))
|
context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId))
|
||||||
)
|
)
|
||||||
|> deliverOnMainQueue
|
|> deliverOnMainQueue
|
||||||
|> map { presentationData, state, availableReactions, settings, images, accountPeer -> (ItemListControllerState, (ItemListNodeState, Any)) in
|
|> map { presentationData, state, availableReactions, settings, accountPeer -> (ItemListControllerState, (ItemListNodeState, Any)) in
|
||||||
let isPremium = accountPeer?.isPremium ?? false
|
let isPremium = accountPeer?.isPremium ?? false
|
||||||
let title: String = presentationData.strings.Settings_QuickReactionSetup_Title
|
let title: String = presentationData.strings.Settings_QuickReactionSetup_Title
|
||||||
|
|
||||||
let entries = quickReactionSetupControllerEntries(
|
let entries = quickReactionSetupControllerEntries(
|
||||||
presentationData: presentationData,
|
presentationData: presentationData,
|
||||||
availableReactions: availableReactions,
|
availableReactions: availableReactions,
|
||||||
images: images,
|
|
||||||
reactionSettings: settings,
|
reactionSettings: settings,
|
||||||
state: state,
|
state: state,
|
||||||
isPremium: isPremium
|
isPremium: isPremium
|
||||||
@ -388,6 +296,74 @@ public func quickReactionSetupController(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
openQuickReactionImpl = { [weak controller] in
|
||||||
|
let _ = (combineLatest(queue: .mainQueue(),
|
||||||
|
settings,
|
||||||
|
context.engine.stickers.availableReactions()
|
||||||
|
)
|
||||||
|
|> take(1)
|
||||||
|
|> deliverOnMainQueue).start(next: { settings, availableReactions in
|
||||||
|
var currentSelectedFileId: MediaId?
|
||||||
|
switch settings.quickReaction {
|
||||||
|
case .builtin:
|
||||||
|
if let availableReactions = availableReactions {
|
||||||
|
if let reaction = availableReactions.reactions.first(where: { $0.value == settings.quickReaction }) {
|
||||||
|
currentSelectedFileId = reaction.selectAnimation.fileId
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case let .custom(fileId):
|
||||||
|
currentSelectedFileId = MediaId(namespace: Namespaces.Media.CloudFile, id: fileId)
|
||||||
|
}
|
||||||
|
|
||||||
|
var selectedItems = Set<MediaId>()
|
||||||
|
if let currentSelectedFileId = currentSelectedFileId {
|
||||||
|
selectedItems.insert(currentSelectedFileId)
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let controller = controller else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var sourceItemNode: ItemListReactionItemNode?
|
||||||
|
controller.forEachItemNode { itemNode in
|
||||||
|
if let itemNode = itemNode as? ItemListReactionItemNode {
|
||||||
|
sourceItemNode = itemNode
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let sourceItemNode = sourceItemNode {
|
||||||
|
controller.present(EmojiStatusSelectionController(
|
||||||
|
context: context,
|
||||||
|
mode: .quickReactionSelection(completion: {
|
||||||
|
updateState { state in
|
||||||
|
var state = state
|
||||||
|
state.hasReaction = false
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
sourceView: sourceItemNode.iconView,
|
||||||
|
emojiContent: EmojiPagerContentComponent.emojiInputData(
|
||||||
|
context: context,
|
||||||
|
animationCache: context.animationCache,
|
||||||
|
animationRenderer: context.animationRenderer,
|
||||||
|
isStandalone: false,
|
||||||
|
isStatusSelection: false,
|
||||||
|
isReactionSelection: true,
|
||||||
|
isQuickReactionSelection: true,
|
||||||
|
topReactionItems: [],
|
||||||
|
areUnicodeEmojiEnabled: false,
|
||||||
|
areCustomEmojiEnabled: true,
|
||||||
|
chatPeerId: context.account.peerId,
|
||||||
|
selectedItems: selectedItems
|
||||||
|
),
|
||||||
|
destinationItemView: { [weak sourceItemNode] in
|
||||||
|
return sourceItemNode?.iconView
|
||||||
|
}
|
||||||
|
), in: .window(.root))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
dismissImpl = { [weak controller] in
|
dismissImpl = { [weak controller] in
|
||||||
guard let controller = controller else {
|
guard let controller = controller else {
|
||||||
return
|
return
|
||||||
|
@ -147,7 +147,9 @@ class ReactionChatPreviewItemNode: ListViewItemNode {
|
|||||||
|
|
||||||
private func beginReactionAnimation() {
|
private func beginReactionAnimation() {
|
||||||
if let item = self.item, let updatedReaction = item.reaction, let availableReactions = item.availableReactions, let messageNode = self.messageNode as? ChatMessageItemNodeProtocol {
|
if let item = self.item, let updatedReaction = item.reaction, let availableReactions = item.availableReactions, let messageNode = self.messageNode as? ChatMessageItemNodeProtocol {
|
||||||
if let targetView = messageNode.targetReactionView(value: updatedReaction) {
|
if let _ = messageNode.targetReactionView(value: updatedReaction) {
|
||||||
|
switch updatedReaction {
|
||||||
|
case .builtin:
|
||||||
for reaction in availableReactions.reactions {
|
for reaction in availableReactions.reactions {
|
||||||
guard let centerAnimation = reaction.centerAnimation else {
|
guard let centerAnimation = reaction.centerAnimation else {
|
||||||
continue
|
continue
|
||||||
@ -157,6 +159,49 @@ class ReactionChatPreviewItemNode: ListViewItemNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if reaction.value == updatedReaction {
|
if reaction.value == updatedReaction {
|
||||||
|
let reactionItem = ReactionItem(
|
||||||
|
reaction: ReactionItem.Reaction(rawValue: reaction.value),
|
||||||
|
appearAnimation: reaction.appearAnimation,
|
||||||
|
stillAnimation: reaction.selectAnimation,
|
||||||
|
listAnimation: centerAnimation,
|
||||||
|
largeListAnimation: reaction.activateAnimation,
|
||||||
|
applicationAnimation: aroundAnimation,
|
||||||
|
largeApplicationAnimation: reaction.effectAnimation,
|
||||||
|
isCustom: false
|
||||||
|
)
|
||||||
|
self.beginReactionAnimation(reactionItem: reactionItem)
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case let .custom(fileId):
|
||||||
|
let _ = (item.context.engine.stickers.resolveInlineStickers(fileIds: [fileId])
|
||||||
|
|> deliverOnMainQueue).start(next: { [weak self] files in
|
||||||
|
guard let strongSelf = self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if let itemFile = files[fileId] {
|
||||||
|
let reactionItem = ReactionItem(
|
||||||
|
reaction: ReactionItem.Reaction(rawValue: .custom(itemFile.fileId.id)),
|
||||||
|
appearAnimation: itemFile,
|
||||||
|
stillAnimation: itemFile,
|
||||||
|
listAnimation: itemFile,
|
||||||
|
largeListAnimation: itemFile,
|
||||||
|
applicationAnimation: nil,
|
||||||
|
largeApplicationAnimation: nil,
|
||||||
|
isCustom: true
|
||||||
|
)
|
||||||
|
strongSelf.beginReactionAnimation(reactionItem: reactionItem)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func beginReactionAnimation(reactionItem: ReactionItem) {
|
||||||
|
if let item = self.item, let updatedReaction = item.reaction, let messageNode = self.messageNode as? ChatMessageItemNodeProtocol {
|
||||||
|
if let targetView = messageNode.targetReactionView(value: updatedReaction) {
|
||||||
if let standaloneReactionAnimation = self.standaloneReactionAnimation {
|
if let standaloneReactionAnimation = self.standaloneReactionAnimation {
|
||||||
standaloneReactionAnimation.cancel()
|
standaloneReactionAnimation.cancel()
|
||||||
standaloneReactionAnimation.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak standaloneReactionAnimation] _ in
|
standaloneReactionAnimation.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak standaloneReactionAnimation] _ in
|
||||||
@ -174,16 +219,7 @@ class ReactionChatPreviewItemNode: ListViewItemNode {
|
|||||||
supernode.addSubnode(standaloneReactionAnimation)
|
supernode.addSubnode(standaloneReactionAnimation)
|
||||||
standaloneReactionAnimation.frame = supernode.bounds
|
standaloneReactionAnimation.frame = supernode.bounds
|
||||||
standaloneReactionAnimation.animateReactionSelection(
|
standaloneReactionAnimation.animateReactionSelection(
|
||||||
context: item.context, theme: item.theme, animationCache: animationCache, reaction: ReactionItem(
|
context: item.context, theme: item.theme, animationCache: animationCache, reaction: reactionItem,
|
||||||
reaction: ReactionItem.Reaction(rawValue: reaction.value),
|
|
||||||
appearAnimation: reaction.appearAnimation,
|
|
||||||
stillAnimation: reaction.selectAnimation,
|
|
||||||
listAnimation: centerAnimation,
|
|
||||||
largeListAnimation: reaction.activateAnimation,
|
|
||||||
applicationAnimation: aroundAnimation,
|
|
||||||
largeApplicationAnimation: reaction.effectAnimation,
|
|
||||||
isCustom: false
|
|
||||||
),
|
|
||||||
avatarPeers: [],
|
avatarPeers: [],
|
||||||
playHaptic: false,
|
playHaptic: false,
|
||||||
isLarge: false,
|
isLarge: false,
|
||||||
@ -194,10 +230,6 @@ class ReactionChatPreviewItemNode: ListViewItemNode {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -19,7 +19,7 @@ import WebPBinding
|
|||||||
import ReactionImageComponent
|
import ReactionImageComponent
|
||||||
|
|
||||||
private final class InstalledStickerPacksControllerArguments {
|
private final class InstalledStickerPacksControllerArguments {
|
||||||
let account: Account
|
let context: AccountContext
|
||||||
|
|
||||||
let openStickerPack: (StickerPackCollectionInfo) -> Void
|
let openStickerPack: (StickerPackCollectionInfo) -> Void
|
||||||
let setPackIdWithRevealedOptions: (ItemCollectionId?, ItemCollectionId?) -> Void
|
let setPackIdWithRevealedOptions: (ItemCollectionId?, ItemCollectionId?) -> Void
|
||||||
@ -37,8 +37,8 @@ private final class InstalledStickerPacksControllerArguments {
|
|||||||
let expandTrendingPacks: () -> Void
|
let expandTrendingPacks: () -> Void
|
||||||
let addPack: (StickerPackCollectionInfo) -> Void
|
let addPack: (StickerPackCollectionInfo) -> Void
|
||||||
|
|
||||||
init(account: Account, openStickerPack: @escaping (StickerPackCollectionInfo) -> Void, setPackIdWithRevealedOptions: @escaping (ItemCollectionId?, ItemCollectionId?) -> Void, removePack: @escaping (ArchivedStickerPackItem) -> Void, openStickersBot: @escaping () -> Void, openMasks: @escaping () -> Void, openEmoji: @escaping () -> Void, openQuickReaction: @escaping () -> Void, openFeatured: @escaping () -> Void, openArchived: @escaping ([ArchivedStickerPackItem]?) -> Void, openSuggestOptions: @escaping () -> Void, toggleAnimatedStickers: @escaping (Bool) -> Void, toggleSuggestAnimatedEmoji: @escaping (Bool) -> Void, togglePackSelected: @escaping (ItemCollectionId) -> Void, expandTrendingPacks: @escaping () -> Void, addPack: @escaping (StickerPackCollectionInfo) -> Void) {
|
init(context: AccountContext, openStickerPack: @escaping (StickerPackCollectionInfo) -> Void, setPackIdWithRevealedOptions: @escaping (ItemCollectionId?, ItemCollectionId?) -> Void, removePack: @escaping (ArchivedStickerPackItem) -> Void, openStickersBot: @escaping () -> Void, openMasks: @escaping () -> Void, openEmoji: @escaping () -> Void, openQuickReaction: @escaping () -> Void, openFeatured: @escaping () -> Void, openArchived: @escaping ([ArchivedStickerPackItem]?) -> Void, openSuggestOptions: @escaping () -> Void, toggleAnimatedStickers: @escaping (Bool) -> Void, toggleSuggestAnimatedEmoji: @escaping (Bool) -> Void, togglePackSelected: @escaping (ItemCollectionId) -> Void, expandTrendingPacks: @escaping () -> Void, addPack: @escaping (StickerPackCollectionInfo) -> Void) {
|
||||||
self.account = account
|
self.context = context
|
||||||
self.openStickerPack = openStickerPack
|
self.openStickerPack = openStickerPack
|
||||||
self.setPackIdWithRevealedOptions = setPackIdWithRevealedOptions
|
self.setPackIdWithRevealedOptions = setPackIdWithRevealedOptions
|
||||||
self.removePack = removePack
|
self.removePack = removePack
|
||||||
@ -88,7 +88,7 @@ private indirect enum InstalledStickerPacksEntry: ItemListNodeEntry {
|
|||||||
case archived(PresentationTheme, String, Int32, [ArchivedStickerPackItem]?)
|
case archived(PresentationTheme, String, Int32, [ArchivedStickerPackItem]?)
|
||||||
case masks(PresentationTheme, String)
|
case masks(PresentationTheme, String)
|
||||||
case emoji(PresentationTheme, String)
|
case emoji(PresentationTheme, String)
|
||||||
case quickReaction(String, UIImage?)
|
case quickReaction(String, MessageReaction.Reaction, AvailableReactions)
|
||||||
case animatedStickers(PresentationTheme, String, Bool)
|
case animatedStickers(PresentationTheme, String, Bool)
|
||||||
case animatedStickersInfo(PresentationTheme, String)
|
case animatedStickersInfo(PresentationTheme, String)
|
||||||
case suggestAnimatedEmoji(String, Bool)
|
case suggestAnimatedEmoji(String, Bool)
|
||||||
@ -171,8 +171,8 @@ private indirect enum InstalledStickerPacksEntry: ItemListNodeEntry {
|
|||||||
} else {
|
} else {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
case let .quickReaction(lhsText, lhsImage):
|
case let .quickReaction(lhsText, lhsReaction, lhsAvailableReactions):
|
||||||
if case let .quickReaction(rhsText, rhsImage) = rhs, lhsText == rhsText, lhsImage === rhsImage {
|
if case let .quickReaction(rhsText, rhsReaction, rhsAvailableReactions) = rhs, lhsText == rhsText, lhsReaction == rhsReaction, lhsAvailableReactions == rhsAvailableReactions {
|
||||||
return true
|
return true
|
||||||
} else {
|
} else {
|
||||||
return false
|
return false
|
||||||
@ -430,14 +430,8 @@ private indirect enum InstalledStickerPacksEntry: ItemListNodeEntry {
|
|||||||
return ItemListDisclosureItem(presentationData: presentationData, title: text, label: "", sectionId: self.section, style: .blocks, action: {
|
return ItemListDisclosureItem(presentationData: presentationData, title: text, label: "", sectionId: self.section, style: .blocks, action: {
|
||||||
arguments.openEmoji()
|
arguments.openEmoji()
|
||||||
})
|
})
|
||||||
case let .quickReaction(title, image):
|
case let .quickReaction(title, reaction, availableReactions):
|
||||||
let labelStyle: ItemListDisclosureLabelStyle
|
return ItemListReactionItem(context: arguments.context, presentationData: presentationData, title: title, reaction: reaction, availableReactions: availableReactions, sectionId: self.section, style: .blocks, action: {
|
||||||
if let image = image {
|
|
||||||
labelStyle = .image(image: image, size: image.size.aspectFitted(CGSize(width: 30.0, height: 30.0)))
|
|
||||||
} else {
|
|
||||||
labelStyle = .text
|
|
||||||
}
|
|
||||||
return ItemListDisclosureItem(presentationData: presentationData, title: title, label: "", labelStyle: labelStyle, sectionId: self.section, style: .blocks, action: {
|
|
||||||
arguments.openQuickReaction()
|
arguments.openQuickReaction()
|
||||||
})
|
})
|
||||||
case let .archived(_, text, count, archived):
|
case let .archived(_, text, count, archived):
|
||||||
@ -457,7 +451,7 @@ private indirect enum InstalledStickerPacksEntry: ItemListNodeEntry {
|
|||||||
case let .trendingPacksTitle(_, text):
|
case let .trendingPacksTitle(_, text):
|
||||||
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
|
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
|
||||||
case let .trendingPack(_, _, _, info, topItem, count, animatedStickers, unread, installed):
|
case let .trendingPack(_, _, _, info, topItem, count, animatedStickers, unread, installed):
|
||||||
return ItemListStickerPackItem(presentationData: presentationData, account: arguments.account, packInfo: info, itemCount: count, topItem: topItem, unread: unread, control: .installation(installed: installed), editing: ItemListStickerPackItemEditing(editable: false, editing: false, revealed: false, reorderable: false, selectable: false), enabled: true, playAnimatedStickers: animatedStickers, sectionId: self.section, action: {
|
return ItemListStickerPackItem(presentationData: presentationData, account: arguments.context.account, packInfo: info, itemCount: count, topItem: topItem, unread: unread, control: .installation(installed: installed), editing: ItemListStickerPackItemEditing(editable: false, editing: false, revealed: false, reorderable: false, selectable: false), enabled: true, playAnimatedStickers: animatedStickers, sectionId: self.section, action: {
|
||||||
arguments.openStickerPack(info)
|
arguments.openStickerPack(info)
|
||||||
}, setPackIdWithRevealedOptions: { _, _ in
|
}, setPackIdWithRevealedOptions: { _, _ in
|
||||||
}, addPack: {
|
}, addPack: {
|
||||||
@ -472,7 +466,7 @@ private indirect enum InstalledStickerPacksEntry: ItemListNodeEntry {
|
|||||||
case let .packsTitle(_, text):
|
case let .packsTitle(_, text):
|
||||||
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
|
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
|
||||||
case let .pack(_, _, _, info, topItem, count, animatedStickers, enabled, editing, selected):
|
case let .pack(_, _, _, info, topItem, count, animatedStickers, enabled, editing, selected):
|
||||||
return ItemListStickerPackItem(presentationData: presentationData, account: arguments.account, packInfo: info, itemCount: count, topItem: topItem, unread: false, control: editing.editing ? .check(checked: selected ?? false) : .none, editing: editing, enabled: enabled, playAnimatedStickers: animatedStickers, sectionId: self.section, action: {
|
return ItemListStickerPackItem(presentationData: presentationData, account: arguments.context.account, packInfo: info, itemCount: count, topItem: topItem, unread: false, control: editing.editing ? .check(checked: selected ?? false) : .none, editing: editing, enabled: enabled, playAnimatedStickers: animatedStickers, sectionId: self.section, action: {
|
||||||
arguments.openStickerPack(info)
|
arguments.openStickerPack(info)
|
||||||
}, setPackIdWithRevealedOptions: { current, previous in
|
}, setPackIdWithRevealedOptions: { current, previous in
|
||||||
arguments.setPackIdWithRevealedOptions(current, previous)
|
arguments.setPackIdWithRevealedOptions(current, previous)
|
||||||
@ -556,7 +550,7 @@ private func namespaceForMode(_ mode: InstalledStickerPacksControllerMode) -> It
|
|||||||
|
|
||||||
private let maxTrendingPacksDisplayedLimit: Int32 = 3
|
private let maxTrendingPacksDisplayedLimit: Int32 = 3
|
||||||
|
|
||||||
private func installedStickerPacksControllerEntries(presentationData: PresentationData, state: InstalledStickerPacksControllerState, mode: InstalledStickerPacksControllerMode, view: CombinedView, temporaryPackOrder: [ItemCollectionId]?, featured: [FeaturedStickerPackItem], archived: [ArchivedStickerPackItem]?, stickerSettings: StickerSettings, quickReactionImage: UIImage?) -> [InstalledStickerPacksEntry] {
|
private func installedStickerPacksControllerEntries(presentationData: PresentationData, state: InstalledStickerPacksControllerState, mode: InstalledStickerPacksControllerMode, view: CombinedView, temporaryPackOrder: [ItemCollectionId]?, featured: [FeaturedStickerPackItem], archived: [ArchivedStickerPackItem]?, stickerSettings: StickerSettings, quickReaction: MessageReaction.Reaction?, availableReactions: AvailableReactions?) -> [InstalledStickerPacksEntry] {
|
||||||
var entries: [InstalledStickerPacksEntry] = []
|
var entries: [InstalledStickerPacksEntry] = []
|
||||||
|
|
||||||
var installedPacks = Set<ItemCollectionId>()
|
var installedPacks = Set<ItemCollectionId>()
|
||||||
@ -592,7 +586,9 @@ private func installedStickerPacksControllerEntries(presentationData: Presentati
|
|||||||
|
|
||||||
entries.append(.emoji(presentationData.theme, presentationData.strings.StickersList_EmojiItem))
|
entries.append(.emoji(presentationData.theme, presentationData.strings.StickersList_EmojiItem))
|
||||||
|
|
||||||
entries.append(.quickReaction(presentationData.strings.Settings_QuickReactionSetup_NavigationTitle, quickReactionImage))
|
if let quickReaction = quickReaction, let availableReactions = availableReactions {
|
||||||
|
entries.append(.quickReaction(presentationData.strings.Settings_QuickReactionSetup_NavigationTitle, quickReaction, availableReactions))
|
||||||
|
}
|
||||||
|
|
||||||
entries.append(.animatedStickers(presentationData.theme, presentationData.strings.StickerPacksSettings_AnimatedStickers, stickerSettings.loopAnimatedStickers))
|
entries.append(.animatedStickers(presentationData.theme, presentationData.strings.StickerPacksSettings_AnimatedStickers, stickerSettings.loopAnimatedStickers))
|
||||||
entries.append(.animatedStickersInfo(presentationData.theme, presentationData.strings.StickerPacksSettings_AnimatedStickersInfo))
|
entries.append(.animatedStickersInfo(presentationData.theme, presentationData.strings.StickerPacksSettings_AnimatedStickersInfo))
|
||||||
@ -733,7 +729,7 @@ public func installedStickerPacksController(context: AccountContext, mode: Insta
|
|||||||
var presentStickerPackController: ((StickerPackCollectionInfo) -> Void)?
|
var presentStickerPackController: ((StickerPackCollectionInfo) -> Void)?
|
||||||
var navigationControllerImpl: (() -> NavigationController?)?
|
var navigationControllerImpl: (() -> NavigationController?)?
|
||||||
|
|
||||||
let arguments = InstalledStickerPacksControllerArguments(account: context.account, openStickerPack: { info in
|
let arguments = InstalledStickerPacksControllerArguments(context: context, openStickerPack: { info in
|
||||||
presentStickerPackController?(info)
|
presentStickerPackController?(info)
|
||||||
}, setPackIdWithRevealedOptions: { packId, fromPackId in
|
}, setPackIdWithRevealedOptions: { packId, fromPackId in
|
||||||
updateState { state in
|
updateState { state in
|
||||||
@ -925,21 +921,14 @@ public func installedStickerPacksController(context: AccountContext, mode: Insta
|
|||||||
let temporaryPackOrder = Promise<[ItemCollectionId]?>(nil)
|
let temporaryPackOrder = Promise<[ItemCollectionId]?>(nil)
|
||||||
|
|
||||||
let featured = Promise<[FeaturedStickerPackItem]>()
|
let featured = Promise<[FeaturedStickerPackItem]>()
|
||||||
let quickReactionImage: Signal<UIImage?, NoError>
|
let quickReaction: Signal<MessageReaction.Reaction?, NoError>
|
||||||
|
|
||||||
switch mode {
|
switch mode {
|
||||||
case .general, .modal:
|
case .general, .modal:
|
||||||
featured.set(context.account.viewTracker.featuredStickerPacks())
|
featured.set(context.account.viewTracker.featuredStickerPacks())
|
||||||
archivedPromise.set(.single(archivedPacks) |> then(context.engine.stickers.archivedStickerPacks() |> map(Optional.init)))
|
archivedPromise.set(.single(archivedPacks) |> then(context.engine.stickers.archivedStickerPacks() |> map(Optional.init)))
|
||||||
quickReactionImage = combineLatest(
|
quickReaction = context.account.postbox.preferencesView(keys: [PreferencesKeys.reactionSettings])
|
||||||
context.engine.stickers.availableReactions(),
|
|> map { preferencesView -> MessageReaction.Reaction? in
|
||||||
context.account.postbox.preferencesView(keys: [PreferencesKeys.reactionSettings])
|
|
||||||
)
|
|
||||||
|> map { availableReactions, preferencesView -> TelegramMediaFile? in
|
|
||||||
guard let availableReactions = availableReactions else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
let reactionSettings: ReactionSettings
|
let reactionSettings: ReactionSettings
|
||||||
if let entry = preferencesView.values[PreferencesKeys.reactionSettings], let value = entry.get(ReactionSettings.self) {
|
if let entry = preferencesView.values[PreferencesKeys.reactionSettings], let value = entry.get(ReactionSettings.self) {
|
||||||
reactionSettings = value
|
reactionSettings = value
|
||||||
@ -947,45 +936,17 @@ public func installedStickerPacksController(context: AccountContext, mode: Insta
|
|||||||
reactionSettings = .default
|
reactionSettings = .default
|
||||||
}
|
}
|
||||||
|
|
||||||
for reaction in availableReactions.reactions {
|
return reactionSettings.quickReaction
|
||||||
if reaction.value == reactionSettings.quickReaction {
|
|
||||||
return reaction.staticIcon
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|> distinctUntilChanged
|
|> distinctUntilChanged
|
||||||
|> mapToSignal { file -> Signal<UIImage?, NoError> in
|
|
||||||
guard let file = file else {
|
|
||||||
return .single(nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
return context.account.postbox.mediaBox.resourceData(file.resource)
|
|
||||||
|> distinctUntilChanged(isEqual: { lhs, rhs in
|
|
||||||
return lhs.complete == rhs.complete
|
|
||||||
})
|
|
||||||
|> map { data -> UIImage? in
|
|
||||||
guard data.complete else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
guard let dataValue = try? Data(contentsOf: URL(fileURLWithPath: data.path)) else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
guard let image = WebP.convert(fromWebP: dataValue) else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return image
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case .masks:
|
case .masks:
|
||||||
featured.set(.single([]))
|
featured.set(.single([]))
|
||||||
archivedPromise.set(.single(nil) |> then(context.engine.stickers.archivedStickerPacks(namespace: .masks) |> map(Optional.init)))
|
archivedPromise.set(.single(nil) |> then(context.engine.stickers.archivedStickerPacks(namespace: .masks) |> map(Optional.init)))
|
||||||
quickReactionImage = .single(nil)
|
quickReaction = .single(nil)
|
||||||
case .emoji:
|
case .emoji:
|
||||||
featured.set(.single([]))
|
featured.set(.single([]))
|
||||||
archivedPromise.set(.single(nil) |> then(context.engine.stickers.archivedStickerPacks(namespace: .emoji) |> map(Optional.init)))
|
archivedPromise.set(.single(nil) |> then(context.engine.stickers.archivedStickerPacks(namespace: .emoji) |> map(Optional.init)))
|
||||||
quickReactionImage = .single(nil)
|
quickReaction = .single(nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
var previousPackCount: Int?
|
var previousPackCount: Int?
|
||||||
@ -995,10 +956,11 @@ public func installedStickerPacksController(context: AccountContext, mode: Insta
|
|||||||
temporaryPackOrder.get(),
|
temporaryPackOrder.get(),
|
||||||
combineLatest(queue: .mainQueue(), featured.get(), archivedPromise.get()),
|
combineLatest(queue: .mainQueue(), featured.get(), archivedPromise.get()),
|
||||||
context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.stickerSettings]),
|
context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.stickerSettings]),
|
||||||
quickReactionImage
|
quickReaction,
|
||||||
|
context.engine.stickers.availableReactions()
|
||||||
)
|
)
|
||||||
|> deliverOnMainQueue
|
|> deliverOnMainQueue
|
||||||
|> map { presentationData, state, view, temporaryPackOrder, featuredAndArchived, sharedData, quickReactionImage -> (ItemListControllerState, (ItemListNodeState, Any)) in
|
|> map { presentationData, state, view, temporaryPackOrder, featuredAndArchived, sharedData, quickReaction, availableReactions -> (ItemListControllerState, (ItemListNodeState, Any)) in
|
||||||
var stickerSettings = StickerSettings.defaultSettings
|
var stickerSettings = StickerSettings.defaultSettings
|
||||||
if let value = sharedData.entries[ApplicationSpecificSharedDataKeys.stickerSettings]?.get(StickerSettings.self) {
|
if let value = sharedData.entries[ApplicationSpecificSharedDataKeys.stickerSettings]?.get(StickerSettings.self) {
|
||||||
stickerSettings = value
|
stickerSettings = value
|
||||||
@ -1152,7 +1114,7 @@ public func installedStickerPacksController(context: AccountContext, mode: Insta
|
|||||||
|
|
||||||
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(title), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true)
|
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(title), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true)
|
||||||
|
|
||||||
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: installedStickerPacksControllerEntries(presentationData: presentationData, state: state, mode: mode, view: view, temporaryPackOrder: temporaryPackOrder, featured: featuredAndArchived.0, archived: featuredAndArchived.1, stickerSettings: stickerSettings, quickReactionImage: quickReactionImage), style: .blocks, ensureVisibleItemTag: focusOnItemTag, toolbarItem: toolbarItem, animateChanges: previous != nil && packCount != nil && (previous! != 0 && previous! >= packCount! - 10))
|
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: installedStickerPacksControllerEntries(presentationData: presentationData, state: state, mode: mode, view: view, temporaryPackOrder: temporaryPackOrder, featured: featuredAndArchived.0, archived: featuredAndArchived.1, stickerSettings: stickerSettings, quickReaction: quickReaction, availableReactions: availableReactions), style: .blocks, ensureVisibleItemTag: focusOnItemTag, toolbarItem: toolbarItem, animateChanges: previous != nil && packCount != nil && (previous! != 0 && previous! >= packCount! - 10))
|
||||||
return (controllerState, (listState, arguments))
|
return (controllerState, (listState, arguments))
|
||||||
}
|
}
|
||||||
|> afterDisposed {
|
|> afterDisposed {
|
||||||
|
@ -15,13 +15,27 @@ import TextFormat
|
|||||||
public final class EmojiStatusComponent: Component {
|
public final class EmojiStatusComponent: Component {
|
||||||
public typealias EnvironmentType = Empty
|
public typealias EnvironmentType = Empty
|
||||||
|
|
||||||
|
public enum AnimationContent: Equatable {
|
||||||
|
case file(file: TelegramMediaFile)
|
||||||
|
case customEmoji(fileId: Int64)
|
||||||
|
|
||||||
|
var fileId: MediaId {
|
||||||
|
switch self {
|
||||||
|
case let .file(file):
|
||||||
|
return file.fileId
|
||||||
|
case let .customEmoji(fileId):
|
||||||
|
return MediaId(namespace: Namespaces.Media.CloudFile, id: fileId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public enum Content: Equatable {
|
public enum Content: Equatable {
|
||||||
case none
|
case none
|
||||||
case premium(color: UIColor)
|
case premium(color: UIColor)
|
||||||
case verified(fillColor: UIColor, foregroundColor: UIColor)
|
case verified(fillColor: UIColor, foregroundColor: UIColor)
|
||||||
case fake(color: UIColor)
|
case fake(color: UIColor)
|
||||||
case scam(color: UIColor)
|
case scam(color: UIColor)
|
||||||
case emojiStatus(status: PeerEmojiStatus, size: CGSize, placeholderColor: UIColor)
|
case animation(content: AnimationContent, size: CGSize, placeholderColor: UIColor)
|
||||||
}
|
}
|
||||||
|
|
||||||
public let context: AccountContext
|
public let context: AccountContext
|
||||||
@ -153,14 +167,14 @@ public final class EmojiStatusComponent: Component {
|
|||||||
iconImage = nil
|
iconImage = nil
|
||||||
case .scam:
|
case .scam:
|
||||||
iconImage = nil
|
iconImage = nil
|
||||||
case let .emojiStatus(emojiStatus, size, placeholderColor):
|
case let .animation(animationContent, size, placeholderColor):
|
||||||
iconImage = nil
|
iconImage = nil
|
||||||
emojiFileId = emojiStatus.fileId
|
emojiFileId = animationContent.fileId.id
|
||||||
emojiPlaceholderColor = placeholderColor
|
emojiPlaceholderColor = placeholderColor
|
||||||
emojiSize = size
|
emojiSize = size
|
||||||
|
|
||||||
if case let .emojiStatus(previousEmojiStatus, _, _) = self.component?.content {
|
if case let .animation(previousAnimationContent, _, _) = self.component?.content {
|
||||||
if previousEmojiStatus.fileId != emojiStatus.fileId {
|
if previousAnimationContent.fileId != animationContent.fileId {
|
||||||
self.emojiFileDisposable?.dispose()
|
self.emojiFileDisposable?.dispose()
|
||||||
self.emojiFileDisposable = nil
|
self.emojiFileDisposable = nil
|
||||||
|
|
||||||
@ -180,11 +194,18 @@ public final class EmojiStatusComponent: Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
switch animationContent {
|
||||||
|
case let .file(file):
|
||||||
|
self.emojiFile = file
|
||||||
|
case .customEmoji:
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
iconImage = self.iconView?.image
|
iconImage = self.iconView?.image
|
||||||
if case let .emojiStatus(status, size, placeholderColor) = component.content {
|
if case let .animation(animationContent, size, placeholderColor) = component.content {
|
||||||
emojiFileId = status.fileId
|
emojiFileId = animationContent.fileId.id
|
||||||
emojiPlaceholderColor = placeholderColor
|
emojiPlaceholderColor = placeholderColor
|
||||||
emojiSize = size
|
emojiSize = size
|
||||||
}
|
}
|
||||||
|
@ -548,7 +548,13 @@ public final class EmojiStatusSelectionController: ViewController {
|
|||||||
|
|
||||||
let cloudOffset0: CGFloat = 30.0
|
let cloudOffset0: CGFloat = 30.0
|
||||||
let cloudSize0: CGFloat = 16.0
|
let cloudSize0: CGFloat = 16.0
|
||||||
let cloudFrame0 = CGRect(origin: CGPoint(x: floor(sourceOrigin.x + cloudOffset0 - cloudSize0 / 2.0), y: componentFrame.minY - cloudSize0 / 2.0), size: CGSize(width: cloudSize0, height: cloudSize0))
|
var cloudFrame0 = CGRect(origin: CGPoint(x: floor(sourceOrigin.x + cloudOffset0 - cloudSize0 / 2.0), y: componentFrame.minY - cloudSize0 / 2.0), size: CGSize(width: cloudSize0, height: cloudSize0))
|
||||||
|
var invertX = false
|
||||||
|
if cloudFrame0.maxX >= layout.size.width - layout.safeInsets.right - 32.0 {
|
||||||
|
cloudFrame0.origin.x = floor(sourceOrigin.x - cloudSize0 - cloudOffset0 + cloudSize0 / 2.0)
|
||||||
|
invertX = true
|
||||||
|
}
|
||||||
|
|
||||||
transition.setFrame(layer: self.cloudLayer0, frame: cloudFrame0)
|
transition.setFrame(layer: self.cloudLayer0, frame: cloudFrame0)
|
||||||
if self.cloudShadowLayer0.bounds.size != cloudFrame0.size {
|
if self.cloudShadowLayer0.bounds.size != cloudFrame0.size {
|
||||||
let cloudShadowPath = UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: cloudFrame0.size), cornerRadius: 24.0).cgPath
|
let cloudShadowPath = UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: cloudFrame0.size), cornerRadius: 24.0).cgPath
|
||||||
@ -559,7 +565,10 @@ public final class EmojiStatusSelectionController: ViewController {
|
|||||||
|
|
||||||
let cloudOffset1 = CGPoint(x: -9.0, y: -14.0)
|
let cloudOffset1 = CGPoint(x: -9.0, y: -14.0)
|
||||||
let cloudSize1: CGFloat = 8.0
|
let cloudSize1: CGFloat = 8.0
|
||||||
let cloudFrame1 = CGRect(origin: CGPoint(x: floor(cloudFrame0.midX + cloudOffset1.x - cloudSize1 / 2.0), y: floor(cloudFrame0.midY + cloudOffset1.y - cloudSize1 / 2.0)), size: CGSize(width: cloudSize1, height: cloudSize1))
|
var cloudFrame1 = CGRect(origin: CGPoint(x: floor(cloudFrame0.midX + cloudOffset1.x - cloudSize1 / 2.0), y: floor(cloudFrame0.midY + cloudOffset1.y - cloudSize1 / 2.0)), size: CGSize(width: cloudSize1, height: cloudSize1))
|
||||||
|
if invertX {
|
||||||
|
cloudFrame1.origin.x = floor(cloudFrame0.midX - cloudSize1 - cloudOffset1.x + cloudSize1 / 2.0)
|
||||||
|
}
|
||||||
transition.setFrame(layer: self.cloudLayer1, frame: cloudFrame1)
|
transition.setFrame(layer: self.cloudLayer1, frame: cloudFrame1)
|
||||||
if self.cloudShadowLayer1.bounds.size != cloudFrame1.size {
|
if self.cloudShadowLayer1.bounds.size != cloudFrame1.size {
|
||||||
let cloudShadowPath = UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: cloudFrame1.size), cornerRadius: 24.0).cgPath
|
let cloudShadowPath = UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: cloudFrame1.size), cornerRadius: 24.0).cgPath
|
||||||
@ -622,8 +631,34 @@ public final class EmojiStatusSelectionController: ViewController {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
switch controller.mode {
|
||||||
|
case .statusSelection:
|
||||||
let _ = (self.context.engine.accountData.setEmojiStatus(file: item?.itemFile)
|
let _ = (self.context.engine.accountData.setEmojiStatus(file: item?.itemFile)
|
||||||
|> deliverOnMainQueue).start()
|
|> deliverOnMainQueue).start()
|
||||||
|
case let .quickReactionSelection(completion):
|
||||||
|
if let item = item, let itemFile = item.itemFile {
|
||||||
|
var selectedReaction: MessageReaction.Reaction?
|
||||||
|
|
||||||
|
if let availableReactions = self.availableReactions {
|
||||||
|
for reaction in availableReactions.reactions {
|
||||||
|
if reaction.selectAnimation.fileId == itemFile.fileId {
|
||||||
|
selectedReaction = reaction.value
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if selectedReaction == nil {
|
||||||
|
selectedReaction = .custom(itemFile.fileId.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let selectedReaction = selectedReaction {
|
||||||
|
let _ = context.engine.stickers.updateQuickReaction(reaction: selectedReaction).start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
completion()
|
||||||
|
}
|
||||||
|
|
||||||
if let item = item, let destinationView = controller.destinationItemView() {
|
if let item = item, let destinationView = controller.destinationItemView() {
|
||||||
self.animateOutToStatus(groupId: groupId, item: item, destinationView: destinationView)
|
self.animateOutToStatus(groupId: groupId, item: item, destinationView: destinationView)
|
||||||
@ -633,9 +668,15 @@ public final class EmojiStatusSelectionController: ViewController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public enum Mode {
|
||||||
|
case statusSelection
|
||||||
|
case quickReactionSelection(completion: () -> Void)
|
||||||
|
}
|
||||||
|
|
||||||
private let context: AccountContext
|
private let context: AccountContext
|
||||||
private weak var sourceView: UIView?
|
private weak var sourceView: UIView?
|
||||||
private let emojiContent: Signal<EmojiPagerContentComponent, NoError>
|
private let emojiContent: Signal<EmojiPagerContentComponent, NoError>
|
||||||
|
private let mode: Mode
|
||||||
private let destinationItemView: () -> UIView?
|
private let destinationItemView: () -> UIView?
|
||||||
|
|
||||||
fileprivate let _ready = Promise<Bool>()
|
fileprivate let _ready = Promise<Bool>()
|
||||||
@ -643,8 +684,9 @@ public final class EmojiStatusSelectionController: ViewController {
|
|||||||
return self._ready
|
return self._ready
|
||||||
}
|
}
|
||||||
|
|
||||||
public init(context: AccountContext, sourceView: UIView, emojiContent: Signal<EmojiPagerContentComponent, NoError>, destinationItemView: @escaping () -> UIView?) {
|
public init(context: AccountContext, mode: Mode, sourceView: UIView, emojiContent: Signal<EmojiPagerContentComponent, NoError>, destinationItemView: @escaping () -> UIView?) {
|
||||||
self.context = context
|
self.context = context
|
||||||
|
self.mode = mode
|
||||||
self.sourceView = sourceView
|
self.sourceView = sourceView
|
||||||
self.emojiContent = emojiContent
|
self.emojiContent = emojiContent
|
||||||
self.destinationItemView = destinationItemView
|
self.destinationItemView = destinationItemView
|
||||||
|
@ -255,7 +255,7 @@ public final class EntityKeyboardAnimationData: Equatable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public final class PassthroughLayer: CALayer {
|
public class PassthroughLayer: CALayer {
|
||||||
public var mirrorLayer: CALayer?
|
public var mirrorLayer: CALayer?
|
||||||
|
|
||||||
override init() {
|
override init() {
|
||||||
@ -1778,6 +1778,7 @@ public final class EmojiPagerContentComponent: Component {
|
|||||||
public let itemLayoutType: ItemLayoutType
|
public let itemLayoutType: ItemLayoutType
|
||||||
public let warpContentsOnEdges: Bool
|
public let warpContentsOnEdges: Bool
|
||||||
public let enableLongPress: Bool
|
public let enableLongPress: Bool
|
||||||
|
public let selectedItems: Set<MediaId>
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
id: AnyHashable,
|
id: AnyHashable,
|
||||||
@ -1789,7 +1790,8 @@ public final class EmojiPagerContentComponent: Component {
|
|||||||
itemGroups: [ItemGroup],
|
itemGroups: [ItemGroup],
|
||||||
itemLayoutType: ItemLayoutType,
|
itemLayoutType: ItemLayoutType,
|
||||||
warpContentsOnEdges: Bool,
|
warpContentsOnEdges: Bool,
|
||||||
enableLongPress: Bool
|
enableLongPress: Bool,
|
||||||
|
selectedItems: Set<MediaId>
|
||||||
) {
|
) {
|
||||||
self.id = id
|
self.id = id
|
||||||
self.context = context
|
self.context = context
|
||||||
@ -1801,6 +1803,7 @@ public final class EmojiPagerContentComponent: Component {
|
|||||||
self.itemLayoutType = itemLayoutType
|
self.itemLayoutType = itemLayoutType
|
||||||
self.warpContentsOnEdges = warpContentsOnEdges
|
self.warpContentsOnEdges = warpContentsOnEdges
|
||||||
self.enableLongPress = enableLongPress
|
self.enableLongPress = enableLongPress
|
||||||
|
self.selectedItems = selectedItems
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func ==(lhs: EmojiPagerContentComponent, rhs: EmojiPagerContentComponent) -> Bool {
|
public static func ==(lhs: EmojiPagerContentComponent, rhs: EmojiPagerContentComponent) -> Bool {
|
||||||
@ -1837,6 +1840,9 @@ public final class EmojiPagerContentComponent: Component {
|
|||||||
if lhs.enableLongPress != rhs.enableLongPress {
|
if lhs.enableLongPress != rhs.enableLongPress {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
if lhs.selectedItems != rhs.selectedItems {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@ -2441,6 +2447,32 @@ public final class EmojiPagerContentComponent: Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private final class ItemSelectionLayer: PassthroughLayer {
|
||||||
|
let tintContainerLayer: SimpleLayer
|
||||||
|
|
||||||
|
override init() {
|
||||||
|
self.tintContainerLayer = SimpleLayer()
|
||||||
|
|
||||||
|
super.init()
|
||||||
|
|
||||||
|
self.mirrorLayer = self.tintContainerLayer
|
||||||
|
}
|
||||||
|
|
||||||
|
override func action(forKey event: String) -> CAAction? {
|
||||||
|
return nullAction
|
||||||
|
}
|
||||||
|
|
||||||
|
override init(layer: Any) {
|
||||||
|
self.tintContainerLayer = SimpleLayer()
|
||||||
|
|
||||||
|
super.init(layer: layer)
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private final class ContentScrollLayer: CALayer {
|
private final class ContentScrollLayer: CALayer {
|
||||||
var mirrorLayer: CALayer?
|
var mirrorLayer: CALayer?
|
||||||
|
|
||||||
@ -2546,6 +2578,7 @@ public final class EmojiPagerContentComponent: Component {
|
|||||||
|
|
||||||
private let placeholdersContainerView: UIView
|
private let placeholdersContainerView: UIView
|
||||||
private var visibleItemPlaceholderViews: [ItemLayer.Key: ItemPlaceholderView] = [:]
|
private var visibleItemPlaceholderViews: [ItemLayer.Key: ItemPlaceholderView] = [:]
|
||||||
|
private var visibleItemSelectionLayers: [ItemLayer.Key: ItemSelectionLayer] = [:]
|
||||||
private var visibleItemLayers: [ItemLayer.Key: ItemLayer] = [:]
|
private var visibleItemLayers: [ItemLayer.Key: ItemLayer] = [:]
|
||||||
private var visibleGroupHeaders: [AnyHashable: GroupHeaderLayer] = [:]
|
private var visibleGroupHeaders: [AnyHashable: GroupHeaderLayer] = [:]
|
||||||
private var visibleGroupBorders: [AnyHashable: GroupBorderLayer] = [:]
|
private var visibleGroupBorders: [AnyHashable: GroupBorderLayer] = [:]
|
||||||
@ -2674,13 +2707,17 @@ public final class EmojiPagerContentComponent: Component {
|
|||||||
|
|
||||||
public func animateIn(fromLocation: CGPoint) {
|
public func animateIn(fromLocation: CGPoint) {
|
||||||
let scrollLocation = self.convert(fromLocation, to: self.scrollView)
|
let scrollLocation = self.convert(fromLocation, to: self.scrollView)
|
||||||
for (_, itemLayer) in self.visibleItemLayers {
|
for (key, itemLayer) in self.visibleItemLayers {
|
||||||
let distanceVector = CGPoint(x: scrollLocation.x - itemLayer.position.x, y: scrollLocation.y - itemLayer.position.y)
|
let distanceVector = CGPoint(x: scrollLocation.x - itemLayer.position.x, y: scrollLocation.y - itemLayer.position.y)
|
||||||
let distance = sqrt(distanceVector.x * distanceVector.x + distanceVector.y * distanceVector.y)
|
let distance = sqrt(distanceVector.x * distanceVector.x + distanceVector.y * distanceVector.y)
|
||||||
|
|
||||||
let distanceNorm = min(1.0, max(0.0, distance / self.bounds.width))
|
let distanceNorm = min(1.0, max(0.0, distance / self.bounds.width))
|
||||||
let delay = 0.05 + (distanceNorm) * 0.3
|
let delay = 0.05 + (distanceNorm) * 0.3
|
||||||
itemLayer.animateScale(from: 0.01, to: 1.0, duration: 0.18, delay: delay, timingFunction: kCAMediaTimingFunctionSpring)
|
itemLayer.animateScale(from: 0.01, to: 1.0, duration: 0.18, delay: delay, timingFunction: kCAMediaTimingFunctionSpring)
|
||||||
|
|
||||||
|
if let itemSelectionLayer = self.visibleItemSelectionLayers[key] {
|
||||||
|
itemSelectionLayer.animateScale(from: 0.01, to: 1.0, duration: 0.18, delay: delay, timingFunction: kCAMediaTimingFunctionSpring)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2689,7 +2726,7 @@ public final class EmojiPagerContentComponent: Component {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
for (_, itemLayer) in self.visibleItemLayers {
|
for (key, itemLayer) in self.visibleItemLayers {
|
||||||
guard case let .animation(animationData) = itemLayer.item.content else {
|
guard case let .animation(animationData) = itemLayer.item.content else {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@ -2699,6 +2736,10 @@ public final class EmojiPagerContentComponent: Component {
|
|||||||
if let sourceItem = sourceItems[file.fileId] {
|
if let sourceItem = sourceItems[file.fileId] {
|
||||||
itemLayer.animatePosition(from: CGPoint(x: sourceItem.position.x - itemLayer.position.x, y: 0.0), to: CGPoint(), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
|
itemLayer.animatePosition(from: CGPoint(x: sourceItem.position.x - itemLayer.position.x, y: 0.0), to: CGPoint(), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
|
||||||
|
|
||||||
|
if let itemSelectionLayer = self.visibleItemSelectionLayers[key] {
|
||||||
|
itemSelectionLayer.animatePosition(from: CGPoint(x: sourceItem.position.x - itemLayer.position.x, y: 0.0), to: CGPoint(), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
|
||||||
|
}
|
||||||
|
|
||||||
component.animationRenderer.setFrameIndex(itemId: animationData.resource.resource.id.stringRepresentation, size: itemLayer.pixelSize, frameIndex: sourceItem.frameIndex, placeholder: sourceItem.placeholder)
|
component.animationRenderer.setFrameIndex(itemId: animationData.resource.resource.id.stringRepresentation, size: itemLayer.pixelSize, frameIndex: sourceItem.frameIndex, placeholder: sourceItem.placeholder)
|
||||||
} else {
|
} else {
|
||||||
let distance = itemLayer.position.y - itemLayout.frame(groupIndex: 0, itemIndex: 0).midY
|
let distance = itemLayer.position.y - itemLayout.frame(groupIndex: 0, itemIndex: 0).midY
|
||||||
@ -2710,6 +2751,11 @@ public final class EmojiPagerContentComponent: Component {
|
|||||||
|
|
||||||
itemLayer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15, delay: delay)
|
itemLayer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15, delay: delay)
|
||||||
itemLayer.animateSpring(from: 0.01 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.6, delay: delay)
|
itemLayer.animateSpring(from: 0.01 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.6, delay: delay)
|
||||||
|
|
||||||
|
if let itemSelectionLayer = self.visibleItemSelectionLayers[key] {
|
||||||
|
itemSelectionLayer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15, delay: delay)
|
||||||
|
itemSelectionLayer.animateSpring(from: 0.01 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.6, delay: delay)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2786,6 +2832,10 @@ public final class EmojiPagerContentComponent: Component {
|
|||||||
for (id, layer) in self.visibleItemLayers {
|
for (id, layer) in self.visibleItemLayers {
|
||||||
previousVisibleLayers[id] = (layer, layer.frame.offsetBy(dx: 0.0, dy: -self.scrollView.bounds.minY))
|
previousVisibleLayers[id] = (layer, layer.frame.offsetBy(dx: 0.0, dy: -self.scrollView.bounds.minY))
|
||||||
}
|
}
|
||||||
|
var previousVisibleItemSelectionLayers: [ItemLayer.Key: (CALayer, CGRect)] = [:]
|
||||||
|
for (id, layer) in self.visibleItemSelectionLayers {
|
||||||
|
previousVisibleItemSelectionLayers[id] = (layer, layer.frame.offsetBy(dx: 0.0, dy: -self.scrollView.bounds.minY))
|
||||||
|
}
|
||||||
var previousVisiblePlaceholderViews: [ItemLayer.Key: (UIView, CGRect)] = [:]
|
var previousVisiblePlaceholderViews: [ItemLayer.Key: (UIView, CGRect)] = [:]
|
||||||
for (id, view) in self.visibleItemPlaceholderViews {
|
for (id, view) in self.visibleItemPlaceholderViews {
|
||||||
previousVisiblePlaceholderViews[id] = (view, view.frame.offsetBy(dx: 0.0, dy: -self.scrollView.bounds.minY))
|
previousVisiblePlaceholderViews[id] = (view, view.frame.offsetBy(dx: 0.0, dy: -self.scrollView.bounds.minY))
|
||||||
@ -3084,6 +3134,9 @@ public final class EmojiPagerContentComponent: Component {
|
|||||||
for (_, layer) in self.visibleItemLayers {
|
for (_, layer) in self.visibleItemLayers {
|
||||||
layer.animatePosition(from: CGPoint(x: 0.0, y: commonItemOffset), to: CGPoint(), duration: duration, timingFunction: timingFunction, additive: true)
|
layer.animatePosition(from: CGPoint(x: 0.0, y: commonItemOffset), to: CGPoint(), duration: duration, timingFunction: timingFunction, additive: true)
|
||||||
}
|
}
|
||||||
|
for (_, layer) in self.visibleItemSelectionLayers {
|
||||||
|
layer.animatePosition(from: CGPoint(x: 0.0, y: commonItemOffset), to: CGPoint(), duration: duration, timingFunction: timingFunction, additive: true)
|
||||||
|
}
|
||||||
for (id, layerAndFrame) in previousVisibleLayers {
|
for (id, layerAndFrame) in previousVisibleLayers {
|
||||||
if self.visibleItemLayers[id] != nil {
|
if self.visibleItemLayers[id] != nil {
|
||||||
continue
|
continue
|
||||||
@ -3095,6 +3148,17 @@ public final class EmojiPagerContentComponent: Component {
|
|||||||
layer?.removeFromSuperlayer()
|
layer?.removeFromSuperlayer()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
for (id, layerAndFrame) in previousVisibleItemSelectionLayers {
|
||||||
|
if self.visibleItemSelectionLayers[id] != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
let layer = layerAndFrame.0
|
||||||
|
layer.frame = layerAndFrame.1.offsetBy(dx: 0.0, dy: self.scrollView.bounds.minY)
|
||||||
|
self.scrollView.layer.addSublayer(layer)
|
||||||
|
layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: -commonItemOffset), duration: duration, timingFunction: timingFunction, removeOnCompletion: false, additive: true, completion: { [weak layer] _ in
|
||||||
|
layer?.removeFromSuperlayer()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
for (_, view) in self.visibleItemPlaceholderViews {
|
for (_, view) in self.visibleItemPlaceholderViews {
|
||||||
view.layer.animatePosition(from: CGPoint(x: 0.0, y: commonItemOffset), to: CGPoint(), duration: duration, timingFunction: timingFunction, additive: true)
|
view.layer.animatePosition(from: CGPoint(x: 0.0, y: commonItemOffset), to: CGPoint(), duration: duration, timingFunction: timingFunction, additive: true)
|
||||||
@ -3794,10 +3858,6 @@ public final class EmojiPagerContentComponent: Component {
|
|||||||
} else {
|
} else {
|
||||||
pointSize = itemPlaybackSize
|
pointSize = itemPlaybackSize
|
||||||
}
|
}
|
||||||
/*if case let .animation(animationData) = item.content, animationData.isReaction {
|
|
||||||
pointSize.width += pointSize.width
|
|
||||||
pointSize.height += pointSize.height
|
|
||||||
}*/
|
|
||||||
|
|
||||||
let placeholderColor = keyboardChildEnvironment.theme.chat.inputPanel.primaryTextColor.withMultipliedAlpha(0.1)
|
let placeholderColor = keyboardChildEnvironment.theme.chat.inputPanel.primaryTextColor.withMultipliedAlpha(0.1)
|
||||||
itemLayer = ItemLayer(
|
itemLayer = ItemLayer(
|
||||||
@ -3858,12 +3918,13 @@ public final class EmojiPagerContentComponent: Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
//itemLayer.backgroundColor = UIColor.lightGray.cgColor
|
|
||||||
self.scrollView.layer.addSublayer(itemLayer)
|
self.scrollView.layer.addSublayer(itemLayer)
|
||||||
self.visibleItemLayers[itemId] = itemLayer
|
self.visibleItemLayers[itemId] = itemLayer
|
||||||
}
|
}
|
||||||
|
|
||||||
var itemFrame = itemLayout.frame(groupIndex: groupItems.groupIndex, itemIndex: index)
|
var itemFrame = itemLayout.frame(groupIndex: groupItems.groupIndex, itemIndex: index)
|
||||||
|
let baseItemFrame = itemFrame
|
||||||
|
|
||||||
itemFrame.origin.x += floor((itemFrame.width - itemVisibleFitSize.width) / 2.0)
|
itemFrame.origin.x += floor((itemFrame.width - itemVisibleFitSize.width) / 2.0)
|
||||||
itemFrame.origin.y += floor((itemFrame.height - itemVisibleFitSize.height) / 2.0)
|
itemFrame.origin.y += floor((itemFrame.height - itemVisibleFitSize.height) / 2.0)
|
||||||
@ -3889,33 +3950,6 @@ public final class EmojiPagerContentComponent: Component {
|
|||||||
let itemPosition = CGPoint(x: itemFrame.midX, y: itemFrame.midY)
|
let itemPosition = CGPoint(x: itemFrame.midX, y: itemFrame.midY)
|
||||||
itemTransition.setPosition(layer: itemLayer, position: itemPosition)
|
itemTransition.setPosition(layer: itemLayer, position: itemPosition)
|
||||||
|
|
||||||
if itemLayout.curveNearBounds && !"".isEmpty {
|
|
||||||
let fractionLength: CGFloat = itemFrame.height
|
|
||||||
let rotationX: CGFloat = 1.0 - max(0.0, min(1.0, (effectiveVisibleBounds.maxY + 4.0 + itemFrame.height / 2.0 - itemPosition.y) / fractionLength))
|
|
||||||
if rotationX.isZero {
|
|
||||||
itemTransition.setTransform(layer: itemLayer, transform: CATransform3DIdentity)
|
|
||||||
} else {
|
|
||||||
var transform = CATransform3DIdentity
|
|
||||||
|
|
||||||
let centralOffset = itemFrame.midX - effectiveVisibleBounds.width / 2.0
|
|
||||||
transform = CATransform3DTranslate(transform, centralOffset * 2.0, 0.0, 0.0)
|
|
||||||
|
|
||||||
var projectionMatrix = CATransform3DIdentity
|
|
||||||
projectionMatrix.m34 = -1.0 / 500.0
|
|
||||||
transform = CATransform3DConcat(projectionMatrix, transform)
|
|
||||||
|
|
||||||
transform = CATransform3DTranslate(transform, -centralOffset * 2.0, 0.0, 0.0)
|
|
||||||
|
|
||||||
let rotationAngle = CGFloat.pi * 0.5 * rotationX
|
|
||||||
let rotationOffset = sin(rotationAngle)
|
|
||||||
let zOffset: CGFloat = 0.0
|
|
||||||
|
|
||||||
transform = CATransform3DTranslate(transform, 0.0, -rotationOffset * itemFrame.height * 0.5, -zOffset * itemFrame.height * 0.5)
|
|
||||||
transform = CATransform3DRotate(transform, rotationAngle, 1.0, 0.0, 0.0)
|
|
||||||
itemTransition.setTransform(layer: itemLayer, transform: transform)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var badge: ItemLayer.Badge?
|
var badge: ItemLayer.Badge?
|
||||||
if itemGroup.displayPremiumBadges, let file = item.itemFile, file.isPremiumSticker {
|
if itemGroup.displayPremiumBadges, let file = item.itemFile, file.isPremiumSticker {
|
||||||
badge = .premium
|
badge = .premium
|
||||||
@ -3933,6 +3967,24 @@ public final class EmojiPagerContentComponent: Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let itemFile = item.itemFile, component.selectedItems.contains(itemFile.fileId) {
|
||||||
|
let itemSelectionLayer: ItemSelectionLayer
|
||||||
|
if let current = self.visibleItemSelectionLayers[itemId] {
|
||||||
|
itemSelectionLayer = current
|
||||||
|
} else {
|
||||||
|
itemSelectionLayer = ItemSelectionLayer()
|
||||||
|
itemSelectionLayer.cornerRadius = 8.0
|
||||||
|
itemSelectionLayer.tintContainerLayer.cornerRadius = 8.0
|
||||||
|
self.scrollView.layer.insertSublayer(itemSelectionLayer, below: itemLayer)
|
||||||
|
self.mirrorContentScrollView.layer.addSublayer(itemSelectionLayer.tintContainerLayer)
|
||||||
|
self.visibleItemSelectionLayers[itemId] = itemSelectionLayer
|
||||||
|
}
|
||||||
|
|
||||||
|
itemSelectionLayer.backgroundColor = keyboardChildEnvironment.theme.chat.inputMediaPanel.panelContentVibrantOverlayColor.cgColor
|
||||||
|
itemSelectionLayer.tintContainerLayer.backgroundColor = UIColor.white.cgColor
|
||||||
|
itemSelectionLayer.frame = baseItemFrame
|
||||||
|
}
|
||||||
|
|
||||||
if animateItemIn, !transition.animation.isImmediate, let contentAnimation = contentAnimation, case .groupExpanded(id: itemGroup.groupId) = contentAnimation.type, let placeholderView = self.visibleItemPlaceholderViews[itemId] {
|
if animateItemIn, !transition.animation.isImmediate, let contentAnimation = contentAnimation, case .groupExpanded(id: itemGroup.groupId) = contentAnimation.type, let placeholderView = self.visibleItemPlaceholderViews[itemId] {
|
||||||
placeholderView.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.4)
|
placeholderView.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.4)
|
||||||
placeholderView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1)
|
placeholderView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1)
|
||||||
@ -3949,6 +4001,8 @@ public final class EmojiPagerContentComponent: Component {
|
|||||||
if !validIds.contains(id) {
|
if !validIds.contains(id) {
|
||||||
removedIds.append(id)
|
removedIds.append(id)
|
||||||
|
|
||||||
|
let itemSelectionLayer = self.visibleItemSelectionLayers[id]
|
||||||
|
|
||||||
if !transition.animation.isImmediate {
|
if !transition.animation.isImmediate {
|
||||||
if let hintDisappearingGroupFrame = hintDisappearingGroupFrame, hintDisappearingGroupFrame.groupId == id.groupId {
|
if let hintDisappearingGroupFrame = hintDisappearingGroupFrame, hintDisappearingGroupFrame.groupId == id.groupId {
|
||||||
if let previousAbsolutePosition = previousAbsoluteItemPositions?[.item(id: id)] {
|
if let previousAbsolutePosition = previousAbsoluteItemPositions?[.item(id: id)] {
|
||||||
@ -3961,30 +4015,93 @@ public final class EmojiPagerContentComponent: Component {
|
|||||||
itemLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.16, completion: { [weak itemLayer] _ in
|
itemLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.16, completion: { [weak itemLayer] _ in
|
||||||
itemLayer?.removeFromSuperlayer()
|
itemLayer?.removeFromSuperlayer()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if let itemSelectionLayer = itemSelectionLayer {
|
||||||
|
itemSelectionLayer.opacity = 0.0
|
||||||
|
itemSelectionLayer.animateScale(from: 1.0, to: 0.01, duration: 0.16)
|
||||||
|
itemSelectionLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.16, completion: { [weak itemSelectionLayer] _ in
|
||||||
|
itemSelectionLayer?.removeFromSuperlayer()
|
||||||
|
})
|
||||||
|
|
||||||
|
let itemSelectionTintContainerLayer = itemSelectionLayer.tintContainerLayer
|
||||||
|
itemSelectionTintContainerLayer.opacity = 0.0
|
||||||
|
itemSelectionTintContainerLayer.animateScale(from: 1.0, to: 0.01, duration: 0.16)
|
||||||
|
itemSelectionTintContainerLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.16, completion: { [weak itemSelectionTintContainerLayer] _ in
|
||||||
|
itemSelectionTintContainerLayer?.removeFromSuperlayer()
|
||||||
|
})
|
||||||
|
}
|
||||||
} else if let position = updatedItemPositions?[.item(id: id)], transitionHintInstalledGroupId != id.groupId {
|
} else if let position = updatedItemPositions?[.item(id: id)], transitionHintInstalledGroupId != id.groupId {
|
||||||
transition.setPosition(layer: itemLayer, position: position, completion: { [weak itemLayer] _ in
|
transition.setPosition(layer: itemLayer, position: position, completion: { [weak itemLayer] _ in
|
||||||
itemLayer?.removeFromSuperlayer()
|
itemLayer?.removeFromSuperlayer()
|
||||||
})
|
})
|
||||||
|
if let itemSelectionLayer = itemSelectionLayer {
|
||||||
|
transition.setPosition(layer: itemSelectionLayer, position: position, completion: { [weak itemSelectionLayer] _ in
|
||||||
|
itemSelectionLayer?.removeFromSuperlayer()
|
||||||
|
})
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
itemLayer.opacity = 0.0
|
itemLayer.opacity = 0.0
|
||||||
itemLayer.animateScale(from: 1.0, to: 0.01, duration: 0.2)
|
itemLayer.animateScale(from: 1.0, to: 0.01, duration: 0.2)
|
||||||
itemLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, completion: { [weak itemLayer] _ in
|
itemLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, completion: { [weak itemLayer] _ in
|
||||||
itemLayer?.removeFromSuperlayer()
|
itemLayer?.removeFromSuperlayer()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if let itemSelectionLayer = itemSelectionLayer {
|
||||||
|
itemSelectionLayer.opacity = 0.0
|
||||||
|
itemSelectionLayer.animateScale(from: 1.0, to: 0.01, duration: 0.2)
|
||||||
|
itemSelectionLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, completion: { [weak itemSelectionLayer] _ in
|
||||||
|
itemSelectionLayer?.removeFromSuperlayer()
|
||||||
|
})
|
||||||
|
|
||||||
|
let itemSelectionTintContainerLayer = itemSelectionLayer.tintContainerLayer
|
||||||
|
itemSelectionTintContainerLayer.opacity = 0.0
|
||||||
|
itemSelectionTintContainerLayer.animateScale(from: 1.0, to: 0.01, duration: 0.2)
|
||||||
|
itemSelectionTintContainerLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, completion: { [weak itemSelectionTintContainerLayer] _ in
|
||||||
|
itemSelectionTintContainerLayer?.removeFromSuperlayer()
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
itemLayer.removeFromSuperlayer()
|
itemLayer.removeFromSuperlayer()
|
||||||
|
if let itemSelectionLayer = itemSelectionLayer {
|
||||||
|
itemSelectionLayer.removeFromSuperlayer()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for id in removedIds {
|
for id in removedIds {
|
||||||
self.visibleItemLayers.removeValue(forKey: id)
|
self.visibleItemLayers.removeValue(forKey: id)
|
||||||
|
self.visibleItemSelectionLayers.removeValue(forKey: id)
|
||||||
|
|
||||||
if let view = self.visibleItemPlaceholderViews.removeValue(forKey: id) {
|
if let view = self.visibleItemPlaceholderViews.removeValue(forKey: id) {
|
||||||
view.removeFromSuperview()
|
view.removeFromSuperview()
|
||||||
removedPlaceholerViews = true
|
removedPlaceholerViews = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
var removedItemSelectionLayerIds: [ItemLayer.Key] = []
|
||||||
|
for (id, itemSelectionLayer) in self.visibleItemSelectionLayers {
|
||||||
|
var fileId: MediaId?
|
||||||
|
switch id.itemId {
|
||||||
|
case let .animation(id):
|
||||||
|
switch id {
|
||||||
|
case let .file(fileIdValue):
|
||||||
|
fileId = fileIdValue
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if let fileId = fileId, component.selectedItems.contains(fileId) {
|
||||||
|
} else {
|
||||||
|
itemSelectionLayer.removeFromSuperlayer()
|
||||||
|
removedItemSelectionLayerIds.append(id)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for id in removedItemSelectionLayerIds {
|
||||||
|
self.visibleItemSelectionLayers.removeValue(forKey: id)
|
||||||
|
}
|
||||||
|
|
||||||
var removedGroupHeaderIds: [AnyHashable] = []
|
var removedGroupHeaderIds: [AnyHashable] = []
|
||||||
for (id, groupHeaderLayer) in self.visibleGroupHeaders {
|
for (id, groupHeaderLayer) in self.visibleGroupHeaders {
|
||||||
@ -4563,7 +4680,7 @@ public final class EmojiPagerContentComponent: Component {
|
|||||||
return hasPremium
|
return hasPremium
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func emojiInputData(context: AccountContext, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, isStandalone: Bool, isStatusSelection: Bool, isReactionSelection: Bool, topReactionItems: [AvailableReactions.Reaction], areUnicodeEmojiEnabled: Bool, areCustomEmojiEnabled: Bool, chatPeerId: EnginePeer.Id?) -> Signal<EmojiPagerContentComponent, NoError> {
|
public static func emojiInputData(context: AccountContext, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, isStandalone: Bool, isStatusSelection: Bool, isReactionSelection: Bool, isQuickReactionSelection: Bool = false, topReactionItems: [AvailableReactions.Reaction], areUnicodeEmojiEnabled: Bool, areCustomEmojiEnabled: Bool, chatPeerId: EnginePeer.Id?, selectedItems: Set<MediaId> = Set()) -> Signal<EmojiPagerContentComponent, NoError> {
|
||||||
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 })
|
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 })
|
||||||
let isPremiumDisabled = premiumConfiguration.isPremiumDisabled
|
let isPremiumDisabled = premiumConfiguration.isPremiumDisabled
|
||||||
|
|
||||||
@ -4577,6 +4694,7 @@ public final class EmojiPagerContentComponent: Component {
|
|||||||
orderedItemListCollectionIds.append(Namespaces.OrderedItemList.CloudFeaturedStatusEmoji)
|
orderedItemListCollectionIds.append(Namespaces.OrderedItemList.CloudFeaturedStatusEmoji)
|
||||||
orderedItemListCollectionIds.append(Namespaces.OrderedItemList.CloudRecentStatusEmoji)
|
orderedItemListCollectionIds.append(Namespaces.OrderedItemList.CloudRecentStatusEmoji)
|
||||||
} else if isReactionSelection {
|
} else if isReactionSelection {
|
||||||
|
orderedItemListCollectionIds.append(Namespaces.OrderedItemList.CloudTopReactions)
|
||||||
orderedItemListCollectionIds.append(Namespaces.OrderedItemList.CloudRecentReactions)
|
orderedItemListCollectionIds.append(Namespaces.OrderedItemList.CloudRecentReactions)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -4612,6 +4730,7 @@ public final class EmojiPagerContentComponent: Component {
|
|||||||
var recentEmoji: OrderedItemListView?
|
var recentEmoji: OrderedItemListView?
|
||||||
var featuredStatusEmoji: OrderedItemListView?
|
var featuredStatusEmoji: OrderedItemListView?
|
||||||
var recentStatusEmoji: OrderedItemListView?
|
var recentStatusEmoji: OrderedItemListView?
|
||||||
|
var topReactions: OrderedItemListView?
|
||||||
var recentReactions: OrderedItemListView?
|
var recentReactions: OrderedItemListView?
|
||||||
for orderedView in view.orderedItemListsViews {
|
for orderedView in view.orderedItemListsViews {
|
||||||
if orderedView.collectionId == Namespaces.OrderedItemList.LocalRecentEmoji {
|
if orderedView.collectionId == Namespaces.OrderedItemList.LocalRecentEmoji {
|
||||||
@ -4622,6 +4741,8 @@ public final class EmojiPagerContentComponent: Component {
|
|||||||
recentStatusEmoji = orderedView
|
recentStatusEmoji = orderedView
|
||||||
} else if orderedView.collectionId == Namespaces.OrderedItemList.CloudRecentReactions {
|
} else if orderedView.collectionId == Namespaces.OrderedItemList.CloudRecentReactions {
|
||||||
recentReactions = orderedView
|
recentReactions = orderedView
|
||||||
|
} else if orderedView.collectionId == Namespaces.OrderedItemList.CloudTopReactions {
|
||||||
|
topReactions = orderedView
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -4698,6 +4819,41 @@ public final class EmojiPagerContentComponent: Component {
|
|||||||
}
|
}
|
||||||
} else if isReactionSelection {
|
} else if isReactionSelection {
|
||||||
var existingIds = Set<MessageReaction.Reaction>()
|
var existingIds = Set<MessageReaction.Reaction>()
|
||||||
|
|
||||||
|
var topReactionItems = topReactionItems
|
||||||
|
if topReactionItems.isEmpty {
|
||||||
|
if let topReactions = topReactions {
|
||||||
|
for item in topReactions.items {
|
||||||
|
guard let topReaction = item.contents.get(RecentReactionItem.self) else {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
switch topReaction.content {
|
||||||
|
case let .builtin(value):
|
||||||
|
if let reaction = availableReactions?.reactions.first(where: { $0.value == .builtin(value) }) {
|
||||||
|
topReactionItems.append(reaction)
|
||||||
|
} else {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
case let .custom(file):
|
||||||
|
topReactionItems.append(AvailableReactions.Reaction(
|
||||||
|
isEnabled: true,
|
||||||
|
isPremium: false,
|
||||||
|
value: .custom(file.fileId.id),
|
||||||
|
title: "",
|
||||||
|
staticIcon: file,
|
||||||
|
appearAnimation: file,
|
||||||
|
selectAnimation: file,
|
||||||
|
activateAnimation: file,
|
||||||
|
effectAnimation: file,
|
||||||
|
aroundAnimation: file,
|
||||||
|
centerAnimation: file
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for reactionItem in topReactionItems {
|
for reactionItem in topReactionItems {
|
||||||
if existingIds.contains(reactionItem.value) {
|
if existingIds.contains(reactionItem.value) {
|
||||||
continue
|
continue
|
||||||
@ -4717,7 +4873,7 @@ public final class EmojiPagerContentComponent: Component {
|
|||||||
if let groupIndex = itemGroupIndexById[groupId] {
|
if let groupIndex = itemGroupIndexById[groupId] {
|
||||||
itemGroups[groupIndex].items.append(resultItem)
|
itemGroups[groupIndex].items.append(resultItem)
|
||||||
|
|
||||||
if itemGroups[groupIndex].items.count >= 16 {
|
if itemGroups[groupIndex].items.count >= 8 * 2 {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -4732,7 +4888,12 @@ public final class EmojiPagerContentComponent: Component {
|
|||||||
hasRecent = true
|
hasRecent = true
|
||||||
}
|
}
|
||||||
|
|
||||||
let maxRecentLineCount = 4
|
let maxRecentLineCount: Int
|
||||||
|
#if DEBUG
|
||||||
|
maxRecentLineCount = 4
|
||||||
|
#else
|
||||||
|
maxRecentLineCount = 10
|
||||||
|
#endif
|
||||||
|
|
||||||
//TODO:localize
|
//TODO:localize
|
||||||
let popularTitle = hasRecent ? "Recently Used" : "Popular"
|
let popularTitle = hasRecent ? "Recently Used" : "Popular"
|
||||||
@ -4761,7 +4922,7 @@ public final class EmojiPagerContentComponent: Component {
|
|||||||
itemGroups[groupIndex].items.append(resultItem)
|
itemGroups[groupIndex].items.append(resultItem)
|
||||||
} else {
|
} else {
|
||||||
itemGroupIndexById[groupId] = itemGroups.count
|
itemGroupIndexById[groupId] = itemGroups.count
|
||||||
itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: popularTitle, subtitle: nil, isPremiumLocked: false, isFeatured: false, isExpandable: false, isClearable: hasRecent, headerItem: nil, items: [resultItem]))
|
itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: popularTitle, subtitle: nil, isPremiumLocked: false, isFeatured: false, isExpandable: false, isClearable: hasRecent && !isQuickReactionSelection, headerItem: nil, items: [resultItem]))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -4815,7 +4976,7 @@ public final class EmojiPagerContentComponent: Component {
|
|||||||
popularInsertIndex += 1
|
popularInsertIndex += 1
|
||||||
} else {
|
} else {
|
||||||
itemGroupIndexById[groupId] = itemGroups.count
|
itemGroupIndexById[groupId] = itemGroups.count
|
||||||
itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: popularTitle, subtitle: nil, isPremiumLocked: false, isFeatured: false, isExpandable: false, isClearable: hasRecent, headerItem: nil, items: [resultItem]))
|
itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: popularTitle, subtitle: nil, isPremiumLocked: false, isFeatured: false, isExpandable: false, isClearable: hasRecent && !isQuickReactionSelection, headerItem: nil, items: [resultItem]))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -5048,7 +5209,8 @@ public final class EmojiPagerContentComponent: Component {
|
|||||||
},
|
},
|
||||||
itemLayoutType: .compact,
|
itemLayoutType: .compact,
|
||||||
warpContentsOnEdges: isReactionSelection || isStatusSelection,
|
warpContentsOnEdges: isReactionSelection || isStatusSelection,
|
||||||
enableLongPress: isReactionSelection
|
enableLongPress: isReactionSelection && !isQuickReactionSelection,
|
||||||
|
selectedItems: selectedItems
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return emojiItems
|
return emojiItems
|
||||||
|
@ -986,9 +986,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
|||||||
strongSelf.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: strongSelf.context.account.peerId)),
|
strongSelf.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: strongSelf.context.account.peerId)),
|
||||||
contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState: strongSelf.presentationInterfaceState, context: strongSelf.context, messages: updatedMessages, controllerInteraction: strongSelf.controllerInteraction, selectAll: selectAll, interfaceInteraction: strongSelf.interfaceInteraction, messageNode: node as? ChatMessageItemView),
|
contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState: strongSelf.presentationInterfaceState, context: strongSelf.context, messages: updatedMessages, controllerInteraction: strongSelf.controllerInteraction, selectAll: selectAll, interfaceInteraction: strongSelf.interfaceInteraction, messageNode: node as? ChatMessageItemView),
|
||||||
peerMessageAllowedReactions(context: strongSelf.context, message: topMessage),
|
peerMessageAllowedReactions(context: strongSelf.context, message: topMessage),
|
||||||
|
peerMessageSelectedReactionFiles(context: strongSelf.context, message: topMessage),
|
||||||
topMessageReactions(context: strongSelf.context, message: topMessage),
|
topMessageReactions(context: strongSelf.context, message: topMessage),
|
||||||
ApplicationSpecificNotice.getChatTextSelectionTips(accountManager: strongSelf.context.sharedContext.accountManager)
|
ApplicationSpecificNotice.getChatTextSelectionTips(accountManager: strongSelf.context.sharedContext.accountManager)
|
||||||
).start(next: { peer, actions, allowedReactions, topReactions, chatTextSelectionTips in
|
).start(next: { peer, actions, allowedReactions, selectedReactionFiles, topReactions, chatTextSelectionTips in
|
||||||
guard let strongSelf = self else {
|
guard let strongSelf = self else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -1107,7 +1108,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
|||||||
topReactionItems: reactionItems,
|
topReactionItems: reactionItems,
|
||||||
areUnicodeEmojiEnabled: false,
|
areUnicodeEmojiEnabled: false,
|
||||||
areCustomEmojiEnabled: true,
|
areCustomEmojiEnabled: true,
|
||||||
chatPeerId: strongSelf.chatLocation.peerId
|
chatPeerId: strongSelf.chatLocation.peerId,
|
||||||
|
selectedItems: selectedReactionFiles
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1714,7 +1716,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
|||||||
} else {
|
} else {
|
||||||
strongSelf.chatDisplayNode.messageTransitionNode.dismissMessageReactionContexts(itemNode: itemNode)
|
strongSelf.chatDisplayNode.messageTransitionNode.dismissMessageReactionContexts(itemNode: itemNode)
|
||||||
|
|
||||||
if let removedReaction = removedReaction, let targetView = itemNode.targetReactionView(value: removedReaction), shouldDisplayInlineDateReactions(message: message, isPremium: strongSelf.presentationInterfaceState.isPremium) {
|
if let removedReaction = removedReaction, let targetView = itemNode.targetReactionView(value: removedReaction), shouldDisplayInlineDateReactions(message: message, isPremium: strongSelf.presentationInterfaceState.isPremium, forceInline: false) {
|
||||||
var hideRemovedReaction: Bool = false
|
var hideRemovedReaction: Bool = false
|
||||||
if let reactions = mergedMessageReactions(attributes: message.attributes) {
|
if let reactions = mergedMessageReactions(attributes: message.attributes) {
|
||||||
for reaction in reactions.reactions {
|
for reaction in reactions.reactions {
|
||||||
@ -16973,6 +16975,29 @@ func peerMessageAllowedReactions(context: AccountContext, message: Message) -> S
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func peerMessageSelectedReactionFiles(context: AccountContext, message: Message) -> Signal<Set<MediaId>, NoError> {
|
||||||
|
return context.engine.stickers.availableReactions()
|
||||||
|
|> take(1)
|
||||||
|
|> map { availableReactions -> Set<MediaId> in
|
||||||
|
var result = Set<MediaId>()
|
||||||
|
|
||||||
|
if let effectiveReactions = message.effectiveReactions {
|
||||||
|
for reaction in effectiveReactions {
|
||||||
|
switch reaction.value {
|
||||||
|
case .builtin:
|
||||||
|
if let availableReaction = availableReactions?.reactions.first(where: { $0.value == reaction.value }) {
|
||||||
|
result.insert(availableReaction.selectAnimation.fileId)
|
||||||
|
}
|
||||||
|
case let .custom(fileId):
|
||||||
|
result.insert(MediaId(namespace: Namespaces.Media.CloudFile, id: fileId))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
final class ChatControllerNavigationData: CustomViewControllerNavigationData {
|
final class ChatControllerNavigationData: CustomViewControllerNavigationData {
|
||||||
let peerId: PeerId
|
let peerId: PeerId
|
||||||
|
|
||||||
|
@ -499,7 +499,8 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
|
|||||||
},
|
},
|
||||||
itemLayoutType: .detailed,
|
itemLayoutType: .detailed,
|
||||||
warpContentsOnEdges: false,
|
warpContentsOnEdges: false,
|
||||||
enableLongPress: false
|
enableLongPress: false,
|
||||||
|
selectedItems: Set()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1061,7 +1061,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
|
|||||||
impressionCount: viewCount,
|
impressionCount: viewCount,
|
||||||
dateText: dateText,
|
dateText: dateText,
|
||||||
type: statusType,
|
type: statusType,
|
||||||
layoutInput: .standalone(reactionSettings: shouldDisplayInlineDateReactions(message: item.message, isPremium: item.associatedData.isPremium) ? ChatMessageDateAndStatusNode.StandaloneReactionSettings() : nil),
|
layoutInput: .standalone(reactionSettings: shouldDisplayInlineDateReactions(message: item.message, isPremium: item.associatedData.isPremium, forceInline: item.associatedData.forceInlineReactions) ? ChatMessageDateAndStatusNode.StandaloneReactionSettings() : nil),
|
||||||
constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude),
|
constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude),
|
||||||
availableReactions: item.associatedData.availableReactions,
|
availableReactions: item.associatedData.availableReactions,
|
||||||
reactions: dateReactionsAndPeers.reactions,
|
reactions: dateReactionsAndPeers.reactions,
|
||||||
@ -1216,7 +1216,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let reactions: ReactionsMessageAttribute
|
let reactions: ReactionsMessageAttribute
|
||||||
if shouldDisplayInlineDateReactions(message: item.message, isPremium: item.associatedData.isPremium) {
|
if shouldDisplayInlineDateReactions(message: item.message, isPremium: item.associatedData.isPremium, forceInline: item.associatedData.forceInlineReactions) {
|
||||||
reactions = ReactionsMessageAttribute(canViewList: false, reactions: [], recentPeers: [])
|
reactions = ReactionsMessageAttribute(canViewList: false, reactions: [], recentPeers: [])
|
||||||
} else {
|
} else {
|
||||||
reactions = mergedMessageReactions(attributes: item.message.attributes) ?? ReactionsMessageAttribute(canViewList: false, reactions: [], recentPeers: [])
|
reactions = mergedMessageReactions(attributes: item.message.attributes) ?? ReactionsMessageAttribute(canViewList: false, reactions: [], recentPeers: [])
|
||||||
|
@ -678,7 +678,7 @@ final class ChatMessageAttachedContentNode: ASDisplayNode {
|
|||||||
impressionCount: viewCount,
|
impressionCount: viewCount,
|
||||||
dateText: dateText,
|
dateText: dateText,
|
||||||
type: textStatusType,
|
type: textStatusType,
|
||||||
layoutInput: .trailingContent(contentWidth: trailingContentWidth, reactionSettings: shouldDisplayInlineDateReactions(message: message, isPremium: associatedData.isPremium) ? ChatMessageDateAndStatusNode.TrailingReactionSettings(displayInline: true, preferAdditionalInset: false) : nil),
|
layoutInput: .trailingContent(contentWidth: trailingContentWidth, reactionSettings: shouldDisplayInlineDateReactions(message: message, isPremium: associatedData.isPremium, forceInline: associatedData.forceInlineReactions) ? ChatMessageDateAndStatusNode.TrailingReactionSettings(displayInline: true, preferAdditionalInset: false) : nil),
|
||||||
constrainedSize: textConstrainedSize,
|
constrainedSize: textConstrainedSize,
|
||||||
availableReactions: associatedData.availableReactions,
|
availableReactions: associatedData.availableReactions,
|
||||||
reactions: dateReactionsAndPeers.reactions,
|
reactions: dateReactionsAndPeers.reactions,
|
||||||
|
@ -200,7 +200,8 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([
|
|||||||
|
|
||||||
let firstMessage = item.content.firstMessage
|
let firstMessage = item.content.firstMessage
|
||||||
|
|
||||||
let reactionsAreInline = shouldDisplayInlineDateReactions(message: firstMessage, isPremium: item.associatedData.isPremium)
|
let reactionsAreInline: Bool
|
||||||
|
reactionsAreInline = shouldDisplayInlineDateReactions(message: firstMessage, isPremium: item.associatedData.isPremium, forceInline: item.associatedData.forceInlineReactions)
|
||||||
if reactionsAreInline {
|
if reactionsAreInline {
|
||||||
needReactions = false
|
needReactions = false
|
||||||
}
|
}
|
||||||
@ -1681,7 +1682,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
|
|||||||
impressionCount: viewCount,
|
impressionCount: viewCount,
|
||||||
dateText: dateText,
|
dateText: dateText,
|
||||||
type: statusType,
|
type: statusType,
|
||||||
layoutInput: .standalone(reactionSettings: shouldDisplayInlineDateReactions(message: item.message, isPremium: item.associatedData.isPremium) ? ChatMessageDateAndStatusNode.StandaloneReactionSettings() : nil),
|
layoutInput: .standalone(reactionSettings: shouldDisplayInlineDateReactions(message: item.message, isPremium: item.associatedData.isPremium, forceInline: item.associatedData.forceInlineReactions) ? ChatMessageDateAndStatusNode.StandaloneReactionSettings() : nil),
|
||||||
constrainedSize: CGSize(width: 200.0, height: CGFloat.greatestFiniteMagnitude),
|
constrainedSize: CGSize(width: 200.0, height: CGFloat.greatestFiniteMagnitude),
|
||||||
availableReactions: item.associatedData.availableReactions,
|
availableReactions: item.associatedData.availableReactions,
|
||||||
reactions: dateReactionsAndPeers.reactions,
|
reactions: dateReactionsAndPeers.reactions,
|
||||||
|
@ -219,7 +219,7 @@ class ChatMessageContactBubbleContentNode: ChatMessageBubbleContentNode {
|
|||||||
impressionCount: viewCount,
|
impressionCount: viewCount,
|
||||||
dateText: dateText,
|
dateText: dateText,
|
||||||
type: statusType,
|
type: statusType,
|
||||||
layoutInput: .trailingContent(contentWidth: 1000.0, reactionSettings: shouldDisplayInlineDateReactions(message: item.message, isPremium: item.associatedData.isPremium) ? ChatMessageDateAndStatusNode.TrailingReactionSettings(displayInline: true, preferAdditionalInset: false) : nil),
|
layoutInput: .trailingContent(contentWidth: 1000.0, reactionSettings: shouldDisplayInlineDateReactions(message: item.message, isPremium: item.associatedData.isPremium, forceInline: item.associatedData.forceInlineReactions) ? ChatMessageDateAndStatusNode.TrailingReactionSettings(displayInline: true, preferAdditionalInset: false) : nil),
|
||||||
constrainedSize: CGSize(width: constrainedSize.width - sideInsets, height: .greatestFiniteMagnitude),
|
constrainedSize: CGSize(width: constrainedSize.width - sideInsets, height: .greatestFiniteMagnitude),
|
||||||
availableReactions: item.associatedData.availableReactions,
|
availableReactions: item.associatedData.availableReactions,
|
||||||
reactions: dateReactionsAndPeers.reactions,
|
reactions: dateReactionsAndPeers.reactions,
|
||||||
|
@ -1354,7 +1354,11 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func shouldDisplayInlineDateReactions(message: Message, isPremium: Bool) -> Bool {
|
func shouldDisplayInlineDateReactions(message: Message, isPremium: Bool, forceInline: Bool) -> Bool {
|
||||||
|
if forceInline {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
if message.id.peerId.namespace == Namespaces.Peer.CloudUser || message.id.peerId.namespace == Namespaces.Peer.SecretChat {
|
if message.id.peerId.namespace == Namespaces.Peer.CloudUser || message.id.peerId.namespace == Namespaces.Peer.SecretChat {
|
||||||
if let chatPeer = message.peers[message.id.peerId] as? TelegramUser, chatPeer.isPremium {
|
if let chatPeer = message.peers[message.id.peerId] as? TelegramUser, chatPeer.isPremium {
|
||||||
return false
|
return false
|
||||||
|
@ -543,7 +543,7 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureRecognizerD
|
|||||||
}
|
}
|
||||||
|
|
||||||
let reactions: ReactionsMessageAttribute
|
let reactions: ReactionsMessageAttribute
|
||||||
if shouldDisplayInlineDateReactions(message: item.message, isPremium: item.associatedData.isPremium) {
|
if shouldDisplayInlineDateReactions(message: item.message, isPremium: item.associatedData.isPremium, forceInline: item.associatedData.forceInlineReactions) {
|
||||||
reactions = ReactionsMessageAttribute(canViewList: false, reactions: [], recentPeers: [])
|
reactions = ReactionsMessageAttribute(canViewList: false, reactions: [], recentPeers: [])
|
||||||
} else {
|
} else {
|
||||||
reactions = mergedMessageReactions(attributes: item.message.attributes) ?? ReactionsMessageAttribute(canViewList: false, reactions: [], recentPeers: [])
|
reactions = mergedMessageReactions(attributes: item.message.attributes) ?? ReactionsMessageAttribute(canViewList: false, reactions: [], recentPeers: [])
|
||||||
|
@ -747,7 +747,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
|||||||
|
|
||||||
let dateText = stringForMessageTimestampStatus(accountPeerId: arguments.context.account.peerId, message: arguments.message, dateTimeFormat: arguments.presentationData.dateTimeFormat, nameDisplayOrder: arguments.presentationData.nameDisplayOrder, strings: arguments.presentationData.strings)
|
let dateText = stringForMessageTimestampStatus(accountPeerId: arguments.context.account.peerId, message: arguments.message, dateTimeFormat: arguments.presentationData.dateTimeFormat, nameDisplayOrder: arguments.presentationData.nameDisplayOrder, strings: arguments.presentationData.strings)
|
||||||
|
|
||||||
let displayReactionsInline = shouldDisplayInlineDateReactions(message: arguments.message, isPremium: arguments.associatedData.isPremium)
|
let displayReactionsInline = shouldDisplayInlineDateReactions(message: arguments.message, isPremium: arguments.associatedData.isPremium, forceInline: arguments.associatedData.forceInlineReactions)
|
||||||
var reactionSettings: ChatMessageDateAndStatusNode.TrailingReactionSettings?
|
var reactionSettings: ChatMessageDateAndStatusNode.TrailingReactionSettings?
|
||||||
|
|
||||||
if displayReactionsInline || arguments.displayReactions {
|
if displayReactionsInline || arguments.displayReactions {
|
||||||
|
@ -325,7 +325,7 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode {
|
|||||||
impressionCount: viewCount,
|
impressionCount: viewCount,
|
||||||
dateText: dateText,
|
dateText: dateText,
|
||||||
type: statusType,
|
type: statusType,
|
||||||
layoutInput: .standalone(reactionSettings: shouldDisplayInlineDateReactions(message: item.message, isPremium: item.associatedData.isPremium) ? ChatMessageDateAndStatusNode.StandaloneReactionSettings() : nil),
|
layoutInput: .standalone(reactionSettings: shouldDisplayInlineDateReactions(message: item.message, isPremium: item.associatedData.isPremium, forceInline: item.associatedData.forceInlineReactions) ? ChatMessageDateAndStatusNode.StandaloneReactionSettings() : nil),
|
||||||
constrainedSize: CGSize(width: max(1.0, maxDateAndStatusWidth), height: CGFloat.greatestFiniteMagnitude),
|
constrainedSize: CGSize(width: max(1.0, maxDateAndStatusWidth), height: CGFloat.greatestFiniteMagnitude),
|
||||||
availableReactions: item.associatedData.availableReactions,
|
availableReactions: item.associatedData.availableReactions,
|
||||||
reactions: dateReactionsAndPeers.reactions,
|
reactions: dateReactionsAndPeers.reactions,
|
||||||
|
@ -512,7 +512,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio
|
|||||||
impressionCount: dateAndStatus.viewCount,
|
impressionCount: dateAndStatus.viewCount,
|
||||||
dateText: dateAndStatus.dateText,
|
dateText: dateAndStatus.dateText,
|
||||||
type: dateAndStatus.type,
|
type: dateAndStatus.type,
|
||||||
layoutInput: .standalone(reactionSettings: shouldDisplayInlineDateReactions(message: message, isPremium: associatedData.isPremium) ? ChatMessageDateAndStatusNode.StandaloneReactionSettings() : nil),
|
layoutInput: .standalone(reactionSettings: shouldDisplayInlineDateReactions(message: message, isPremium: associatedData.isPremium, forceInline: associatedData.forceInlineReactions) ? ChatMessageDateAndStatusNode.StandaloneReactionSettings() : nil),
|
||||||
constrainedSize: CGSize(width: nativeSize.width - 30.0, height: CGFloat.greatestFiniteMagnitude),
|
constrainedSize: CGSize(width: nativeSize.width - 30.0, height: CGFloat.greatestFiniteMagnitude),
|
||||||
availableReactions: associatedData.availableReactions,
|
availableReactions: associatedData.availableReactions,
|
||||||
reactions: dateAndStatus.dateReactions,
|
reactions: dateAndStatus.dateReactions,
|
||||||
|
@ -252,7 +252,7 @@ class ChatMessageMapBubbleContentNode: ChatMessageBubbleContentNode {
|
|||||||
impressionCount: viewCount,
|
impressionCount: viewCount,
|
||||||
dateText: dateText,
|
dateText: dateText,
|
||||||
type: statusType,
|
type: statusType,
|
||||||
layoutInput: .standalone(reactionSettings: shouldDisplayInlineDateReactions(message: item.message, isPremium: item.associatedData.isPremium) ? ChatMessageDateAndStatusNode.StandaloneReactionSettings() : nil),
|
layoutInput: .standalone(reactionSettings: shouldDisplayInlineDateReactions(message: item.message, isPremium: item.associatedData.isPremium, forceInline: item.associatedData.forceInlineReactions) ? ChatMessageDateAndStatusNode.StandaloneReactionSettings() : nil),
|
||||||
constrainedSize: CGSize(width: constrainedSize.width, height: CGFloat.greatestFiniteMagnitude),
|
constrainedSize: CGSize(width: constrainedSize.width, height: CGFloat.greatestFiniteMagnitude),
|
||||||
availableReactions: item.associatedData.availableReactions,
|
availableReactions: item.associatedData.availableReactions,
|
||||||
reactions: dateReactionsAndPeers.reactions,
|
reactions: dateReactionsAndPeers.reactions,
|
||||||
|
@ -1073,7 +1073,7 @@ class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
|
|||||||
impressionCount: viewCount,
|
impressionCount: viewCount,
|
||||||
dateText: dateText,
|
dateText: dateText,
|
||||||
type: statusType,
|
type: statusType,
|
||||||
layoutInput: .trailingContent(contentWidth: 1000.0, reactionSettings: shouldDisplayInlineDateReactions(message: item.message, isPremium: item.associatedData.isPremium) ? ChatMessageDateAndStatusNode.TrailingReactionSettings(displayInline: true, preferAdditionalInset: false) : nil),
|
layoutInput: .trailingContent(contentWidth: 1000.0, reactionSettings: shouldDisplayInlineDateReactions(message: item.message, isPremium: item.associatedData.isPremium, forceInline: item.associatedData.forceInlineReactions) ? ChatMessageDateAndStatusNode.TrailingReactionSettings(displayInline: true, preferAdditionalInset: false) : nil),
|
||||||
constrainedSize: textConstrainedSize,
|
constrainedSize: textConstrainedSize,
|
||||||
availableReactions: item.associatedData.availableReactions,
|
availableReactions: item.associatedData.availableReactions,
|
||||||
reactions: dateReactionsAndPeers.reactions,
|
reactions: dateReactionsAndPeers.reactions,
|
||||||
|
@ -120,7 +120,7 @@ class ChatMessageRestrictedBubbleContentNode: ChatMessageBubbleContentNode {
|
|||||||
impressionCount: viewCount,
|
impressionCount: viewCount,
|
||||||
dateText: dateText,
|
dateText: dateText,
|
||||||
type: statusType,
|
type: statusType,
|
||||||
layoutInput: .trailingContent(contentWidth: textLayout.trailingLineWidth, reactionSettings: ChatMessageDateAndStatusNode.TrailingReactionSettings(displayInline: shouldDisplayInlineDateReactions(message: message, isPremium: item.associatedData.isPremium), preferAdditionalInset: false)),
|
layoutInput: .trailingContent(contentWidth: textLayout.trailingLineWidth, reactionSettings: ChatMessageDateAndStatusNode.TrailingReactionSettings(displayInline: shouldDisplayInlineDateReactions(message: message, isPremium: item.associatedData.isPremium, forceInline: item.associatedData.forceInlineReactions), preferAdditionalInset: false)),
|
||||||
constrainedSize: textConstrainedSize,
|
constrainedSize: textConstrainedSize,
|
||||||
availableReactions: item.associatedData.availableReactions,
|
availableReactions: item.associatedData.availableReactions,
|
||||||
reactions: dateReactionsAndPeers.reactions,
|
reactions: dateReactionsAndPeers.reactions,
|
||||||
|
@ -531,7 +531,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView {
|
|||||||
impressionCount: viewCount,
|
impressionCount: viewCount,
|
||||||
dateText: dateText,
|
dateText: dateText,
|
||||||
type: statusType,
|
type: statusType,
|
||||||
layoutInput: .standalone(reactionSettings: shouldDisplayInlineDateReactions(message: item.message, isPremium: item.associatedData.isPremium) ? ChatMessageDateAndStatusNode.StandaloneReactionSettings() : nil),
|
layoutInput: .standalone(reactionSettings: shouldDisplayInlineDateReactions(message: item.message, isPremium: item.associatedData.isPremium, forceInline: item.associatedData.forceInlineReactions) ? ChatMessageDateAndStatusNode.StandaloneReactionSettings() : nil),
|
||||||
constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude),
|
constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude),
|
||||||
availableReactions: item.associatedData.availableReactions,
|
availableReactions: item.associatedData.availableReactions,
|
||||||
reactions: dateReactionsAndPeers.reactions,
|
reactions: dateReactionsAndPeers.reactions,
|
||||||
@ -683,7 +683,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let reactions: ReactionsMessageAttribute
|
let reactions: ReactionsMessageAttribute
|
||||||
if shouldDisplayInlineDateReactions(message: item.message, isPremium: item.associatedData.isPremium) {
|
if shouldDisplayInlineDateReactions(message: item.message, isPremium: item.associatedData.isPremium, forceInline: item.associatedData.forceInlineReactions) {
|
||||||
reactions = ReactionsMessageAttribute(canViewList: false, reactions: [], recentPeers: [])
|
reactions = ReactionsMessageAttribute(canViewList: false, reactions: [], recentPeers: [])
|
||||||
} else {
|
} else {
|
||||||
reactions = mergedMessageReactions(attributes: item.message.attributes) ?? ReactionsMessageAttribute(canViewList: false, reactions: [], recentPeers: [])
|
reactions = mergedMessageReactions(attributes: item.message.attributes) ?? ReactionsMessageAttribute(canViewList: false, reactions: [], recentPeers: [])
|
||||||
|
@ -363,7 +363,7 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let dateLayoutInput: ChatMessageDateAndStatusNode.LayoutInput
|
let dateLayoutInput: ChatMessageDateAndStatusNode.LayoutInput
|
||||||
dateLayoutInput = .trailingContent(contentWidth: trailingWidthToMeasure, reactionSettings: ChatMessageDateAndStatusNode.TrailingReactionSettings(displayInline: shouldDisplayInlineDateReactions(message: item.message, isPremium: item.associatedData.isPremium), preferAdditionalInset: false))
|
dateLayoutInput = .trailingContent(contentWidth: trailingWidthToMeasure, reactionSettings: ChatMessageDateAndStatusNode.TrailingReactionSettings(displayInline: shouldDisplayInlineDateReactions(message: item.message, isPremium: item.associatedData.isPremium, forceInline: item.associatedData.forceInlineReactions), preferAdditionalInset: false))
|
||||||
|
|
||||||
statusSuggestedWidthAndContinue = statusLayout(ChatMessageDateAndStatusNode.Arguments(
|
statusSuggestedWidthAndContinue = statusLayout(ChatMessageDateAndStatusNode.Arguments(
|
||||||
context: item.context,
|
context: item.context,
|
||||||
|
@ -679,7 +679,7 @@ final class ChatTitleView: UIView, NavigationBarTitleView {
|
|||||||
case .scam:
|
case .scam:
|
||||||
titleCredibilityContent = .scam(color: self.theme.chat.message.incoming.scamColor)
|
titleCredibilityContent = .scam(color: self.theme.chat.message.incoming.scamColor)
|
||||||
case let .emojiStatus(emojiStatus):
|
case let .emojiStatus(emojiStatus):
|
||||||
titleCredibilityContent = .emojiStatus(status: emojiStatus, size: CGSize(width: 32.0, height: 32.0), placeholderColor: self.theme.list.mediaPlaceholderColor)
|
titleCredibilityContent = .animation(content: .customEmoji(fileId: emojiStatus.fileId), size: CGSize(width: 32.0, height: 32.0), placeholderColor: self.theme.list.mediaPlaceholderColor)
|
||||||
}
|
}
|
||||||
|
|
||||||
let titleCredibilitySize = self.titleCredibilityIconView.update(
|
let titleCredibilitySize = self.titleCredibilityIconView.update(
|
||||||
|
@ -2354,8 +2354,8 @@ final class PeerInfoHeaderNode: ASDisplayNode {
|
|||||||
emojiExpandedStatusContent = .scam(color: presentationData.theme.chat.message.incoming.scamColor)
|
emojiExpandedStatusContent = .scam(color: presentationData.theme.chat.message.incoming.scamColor)
|
||||||
case let .emojiStatus(emojiStatus):
|
case let .emojiStatus(emojiStatus):
|
||||||
currentEmojiStatus = emojiStatus
|
currentEmojiStatus = emojiStatus
|
||||||
emojiRegularStatusContent = .emojiStatus(status: emojiStatus, size: CGSize(width: 32.0, height: 32.0), placeholderColor: presentationData.theme.list.mediaPlaceholderColor)
|
emojiRegularStatusContent = .animation(content: .customEmoji(fileId: emojiStatus.fileId), size: CGSize(width: 32.0, height: 32.0), placeholderColor: presentationData.theme.list.mediaPlaceholderColor)
|
||||||
emojiExpandedStatusContent = .emojiStatus(status: emojiStatus, size: CGSize(width: 32.0, height: 32.0), placeholderColor: UIColor(rgb: 0xffffff, alpha: 0.15))
|
emojiExpandedStatusContent = .animation(content: .customEmoji(fileId: emojiStatus.fileId), size: CGSize(width: 32.0, height: 32.0), placeholderColor: UIColor(rgb: 0xffffff, alpha: 0.15))
|
||||||
}
|
}
|
||||||
|
|
||||||
let animateStatusIcon = !self.titleCredibilityIconView.bounds.isEmpty
|
let animateStatusIcon = !self.titleCredibilityIconView.bounds.isEmpty
|
||||||
|
@ -3097,6 +3097,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
|
|||||||
|
|
||||||
strongSelf.controller?.present(EmojiStatusSelectionController(
|
strongSelf.controller?.present(EmojiStatusSelectionController(
|
||||||
context: strongSelf.context,
|
context: strongSelf.context,
|
||||||
|
mode: .statusSelection,
|
||||||
sourceView: sourceView,
|
sourceView: sourceView,
|
||||||
emojiContent: EmojiPagerContentComponent.emojiInputData(
|
emojiContent: EmojiPagerContentComponent.emojiInputData(
|
||||||
context: strongSelf.context,
|
context: strongSelf.context,
|
||||||
|
@ -1351,7 +1351,7 @@ public final class SharedAccountContextImpl: SharedAccountContext {
|
|||||||
chatLocation = .peer(id: messages.first!.id.peerId)
|
chatLocation = .peer(id: messages.first!.id.peerId)
|
||||||
}
|
}
|
||||||
|
|
||||||
return ChatMessageItem(presentationData: ChatPresentationData(theme: ChatPresentationThemeData(theme: theme, wallpaper: wallpaper), fontSize: fontSize, strings: strings, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameOrder, disableAnimations: false, largeEmoji: false, chatBubbleCorners: chatBubbleCorners, animatedEmojiScale: 1.0, isPreview: true), context: context, chatLocation: chatLocation, associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .contact, automaticDownloadNetworkType: .cellular, isRecentActions: false, subject: nil, contactsPeerIds: Set(), animatedEmojiStickers: [:], forcedResourceStatus: forcedResourceStatus, availableReactions: availableReactions, defaultReaction: nil, isPremium: false), controllerInteraction: controllerInteraction, content: content, disableDate: true, additionalContent: nil)
|
return ChatMessageItem(presentationData: ChatPresentationData(theme: ChatPresentationThemeData(theme: theme, wallpaper: wallpaper), fontSize: fontSize, strings: strings, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameOrder, disableAnimations: false, largeEmoji: false, chatBubbleCorners: chatBubbleCorners, animatedEmojiScale: 1.0, isPreview: true), context: context, chatLocation: chatLocation, associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .contact, automaticDownloadNetworkType: .cellular, isRecentActions: false, subject: nil, contactsPeerIds: Set(), animatedEmojiStickers: [:], forcedResourceStatus: forcedResourceStatus, availableReactions: availableReactions, defaultReaction: nil, isPremium: false, forceInlineReactions: true), controllerInteraction: controllerInteraction, content: content, disableDate: true, additionalContent: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func makeChatMessageDateHeaderItem(context: AccountContext, timestamp: Int32, theme: PresentationTheme, strings: PresentationStrings, wallpaper: TelegramWallpaper, fontSize: PresentationFontSize, chatBubbleCorners: PresentationChatBubbleCorners, dateTimeFormat: PresentationDateTimeFormat, nameOrder: PresentationPersonNameOrder) -> ListViewItemHeader {
|
public func makeChatMessageDateHeaderItem(context: AccountContext, timestamp: Int32, theme: PresentationTheme, strings: PresentationStrings, wallpaper: TelegramWallpaper, fontSize: PresentationFontSize, chatBubbleCorners: PresentationChatBubbleCorners, dateTimeFormat: PresentationDateTimeFormat, nameOrder: PresentationPersonNameOrder) -> ListViewItemHeader {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user