Swiftgram/TelegramUI/ChatListItem.swift
Ilya Laktyushin 47d146b229 Added online statuses in chat list and share menu
Added recent stickers clearing
Added sending logs via email
Added forward recipient change on forward acccessory panel tap
Tweaked undo panel design
Various UI fixes
2019-04-09 23:49:26 +04:00

1392 lines
77 KiB
Swift

import Foundation
import UIKit
import AsyncDisplayKit
import Postbox
import Display
import SwiftSignalKit
import TelegramCore
enum ChatListItemContent {
case peer(message: Message?, peer: RenderedPeer, combinedReadState: CombinedPeerReadState?, notificationSettings: PeerNotificationSettings?, presence: PeerPresence?, summaryInfo: ChatListMessageTagSummaryInfo, embeddedState: PeerChatListEmbeddedInterfaceState?, inputActivities: [(Peer, PeerInputActivity)]?, isAd: Bool, ignoreUnreadBadge: Bool)
//case groupReference(groupId: PeerGroupId, message: Message?, topPeers: [Peer], counters: GroupReferenceUnreadCounters)
var chatLocation: ChatLocation {
switch self {
case let .peer(_, peer, _, _, _, _, _, _, _, _):
return .peer(peer.peerId)
/*case let .groupReference(groupId, _, _, _):
return .group(groupId)*/
}
}
}
class ChatListItem: ListViewItem {
let presentationData: ChatListPresentationData
let account: Account
let peerGroupId: PeerGroupId?
let index: ChatListIndex
let content: ChatListItemContent
let editing: Bool
let hasActiveRevealControls: Bool
let selected: Bool
let enableContextActions: Bool
let interaction: ChatListNodeInteraction
let selectable: Bool = true
let header: ListViewItemHeader?
init(presentationData: ChatListPresentationData, account: Account, peerGroupId: PeerGroupId?, index: ChatListIndex, content: ChatListItemContent, editing: Bool, hasActiveRevealControls: Bool, selected: Bool, header: ListViewItemHeader?, enableContextActions: Bool, interaction: ChatListNodeInteraction) {
self.presentationData = presentationData
self.peerGroupId = peerGroupId
self.account = account
self.index = index
self.content = content
self.editing = editing
self.hasActiveRevealControls = hasActiveRevealControls
self.selected = selected
self.header = header
self.enableContextActions = enableContextActions
self.interaction = interaction
}
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 = ChatListItemNode()
let (first, last, firstWithHeader, nextIsPinned) = ChatListItem.mergeType(item: self, previousItem: previousItem, nextItem: nextItem)
node.insets = ChatListItemNode.insets(first: first, last: last, firstWithHeader: firstWithHeader)
let (nodeLayout, apply) = node.asyncLayout()(self, params, first, last, firstWithHeader, nextIsPinned)
node.insets = nodeLayout.insets
node.contentSize = nodeLayout.contentSize
Queue.mainQueue().async {
completion(node, {
return (nil, { _ in
node.setupItem(item: self, synchronousLoads: synchronousLoads)
apply(synchronousLoads, false)
node.updateIsHighlighted(transition: .immediate)
})
})
}
}
}
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 {
assert(node() is ChatListItemNode)
if let nodeValue = node() as? ChatListItemNode {
nodeValue.setupItem(item: self, synchronousLoads: false)
let layout = nodeValue.asyncLayout()
async {
let (first, last, firstWithHeader, nextIsPinned) = ChatListItem.mergeType(item: self, previousItem: previousItem, nextItem: nextItem)
var animated = true
if case .None = animation {
animated = false
}
let (nodeLayout, apply) = layout(self, params, first, last, firstWithHeader, nextIsPinned)
Queue.mainQueue().async {
completion(nodeLayout, { _ in
apply(false, animated)
})
}
}
}
}
}
func selected(listView: ListView) {
switch self.content {
case let .peer(message, peer, _, _, _, _, _, _, isAd, _):
if let message = message, let peer = peer.peer {
self.interaction.messageSelected(peer, message, isAd)
} else if let peer = peer.peer {
self.interaction.peerSelected(peer)
} else if let peer = peer.peers[peer.peerId] {
self.interaction.peerSelected(peer)
}
/*case let .groupReference(groupId, _, _, _):
self.interaction.groupSelected(groupId)*/
}
}
static func mergeType(item: ChatListItem, previousItem: ListViewItem?, nextItem: ListViewItem?) -> (first: Bool, last: Bool, firstWithHeader: Bool, nextIsPinned: Bool) {
var first = false
var last = false
var firstWithHeader = false
if let previousItem = previousItem {
if let header = item.header {
if let previousItem = previousItem as? ChatListItem {
firstWithHeader = header.id != previousItem.header?.id
} else {
firstWithHeader = true
}
}
} else {
first = true
firstWithHeader = item.header != nil
}
var nextIsPinned = false
if let nextItem = nextItem as? ChatListItem {
if nextItem.index.pinningIndex != nil {
nextIsPinned = true
}
} else {
last = true
}
return (first, last, firstWithHeader, nextIsPinned)
}
}
private let titleFont = Font.medium(16.0)
private let textFont = Font.regular(15.0)
private let dateFont = Font.regular(14.0)
private let badgeFont = Font.regular(14.0)
private let pinIcon = ItemListRevealOptionIcon.animation(animation: "anim_pin", keysToColor: nil)
private let unpinIcon = ItemListRevealOptionIcon.animation(animation: "anim_unpin", keysToColor: ["un Outlines.Group 1.Stroke 1"])
private let muteIcon = ItemListRevealOptionIcon.animation(animation: "anim_mute", keysToColor: ["un Outlines.Group 1.Stroke 1"])
private let unmuteIcon = ItemListRevealOptionIcon.animation(animation: "anim_unmute", keysToColor: nil)
private let deleteIcon = ItemListRevealOptionIcon.animation(animation: "anim_delete", keysToColor: nil)
private let groupIcon = ItemListRevealOptionIcon.animation(animation: "anim_group", keysToColor: nil)
private let ungroupIcon = ItemListRevealOptionIcon.animation(animation: "anim_ungroup", keysToColor: ["un Outlines.Group 1.Stroke 1"])
private let readIcon = ItemListRevealOptionIcon.animation(animation: "anim_read", keysToColor: nil)
private let unreadIcon = ItemListRevealOptionIcon.animation(animation: "anim_unread", keysToColor: ["Oval.Oval.Stroke 1"])
private enum RevealOptionKey: Int32 {
case pin
case unpin
case mute
case unmute
case delete
case group
case ungroup
case toggleMarkedUnread
}
private let itemHeight: CGFloat = 76.0
private func revealOptions(strings: PresentationStrings, theme: PresentationTheme, isPinned: Bool?, isMuted: Bool?, hasPeerGroupId: Bool?, canDelete: Bool, isEditing: Bool) -> [ItemListRevealOption] {
var options: [ItemListRevealOption] = []
if !isEditing {
if let isPinned = isPinned {
if isPinned {
options.append(ItemListRevealOption(key: RevealOptionKey.unpin.rawValue, title: strings.DialogList_Unpin, icon: unpinIcon, color: theme.list.itemDisclosureActions.neutral1.fillColor, textColor: theme.list.itemDisclosureActions.neutral1.foregroundColor))
} else {
options.append(ItemListRevealOption(key: RevealOptionKey.pin.rawValue, title: strings.DialogList_Pin, icon: pinIcon, color: theme.list.itemDisclosureActions.neutral1.fillColor, textColor: theme.list.itemDisclosureActions.neutral1.foregroundColor))
}
}
if let isMuted = isMuted {
if isMuted {
options.append(ItemListRevealOption(key: RevealOptionKey.unmute.rawValue, title: strings.ChatList_Unmute, icon: unmuteIcon, color: theme.list.itemDisclosureActions.neutral2.fillColor, textColor: theme.list.itemDisclosureActions.neutral2.foregroundColor))
} else {
options.append(ItemListRevealOption(key: RevealOptionKey.mute.rawValue, title: strings.ChatList_Mute, icon: muteIcon, color: theme.list.itemDisclosureActions.neutral2.fillColor, textColor: theme.list.itemDisclosureActions.neutral2.foregroundColor))
}
}
if let hasPeerGroupId = hasPeerGroupId {
if hasPeerGroupId {
options.append(ItemListRevealOption(key: RevealOptionKey.ungroup.rawValue, title: "Ungroup", icon: ungroupIcon, color: theme.list.itemAccentColor, textColor: theme.list.itemDisclosureActions.neutral2.foregroundColor))
} else {
options.append(ItemListRevealOption(key: RevealOptionKey.group.rawValue, title: "Group", icon: groupIcon, color: theme.list.itemAccentColor, textColor: theme.list.itemDisclosureActions.neutral2.foregroundColor))
}
}
}
if canDelete {
options.append(ItemListRevealOption(key: RevealOptionKey.delete.rawValue, title: strings.Common_Delete, icon: deleteIcon, color: theme.list.itemDisclosureActions.destructive.fillColor, textColor: theme.list.itemDisclosureActions.destructive.foregroundColor))
}
return options
}
private func leftRevealOptions(strings: PresentationStrings, theme: PresentationTheme, isUnread: Bool) -> [ItemListRevealOption] {
var options: [ItemListRevealOption] = []
if isUnread {
options.append(ItemListRevealOption(key: RevealOptionKey.toggleMarkedUnread.rawValue, title: strings.DialogList_Read, icon: readIcon, color: theme.list.itemDisclosureActions.inactive.fillColor, textColor: theme.list.itemDisclosureActions.neutral1.foregroundColor))
} else {
options.append(ItemListRevealOption(key: RevealOptionKey.toggleMarkedUnread.rawValue, title: strings.DialogList_Unread, icon: unreadIcon, color: theme.list.itemDisclosureActions.accent.fillColor, textColor: theme.list.itemDisclosureActions.accent.foregroundColor))
}
return options
}
private let separatorHeight = 1.0 / UIScreen.main.scale
private let avatarFont: UIFont = UIFont(name: ".SFCompactRounded-Semibold", size: 26.0)!
class ChatListItemNode: ItemListRevealOptionsItemNode {
var item: ChatListItem?
private let backgroundNode: ASDisplayNode
private let highlightedBackgroundNode: ASDisplayNode
let avatarNode: AvatarNode
var multipleAvatarsNode: MultipleAvatarsNode?
let titleNode: TextNode
let authorNode: TextNode
let textNode: TextNode
let inputActivitiesNode: ChatListInputActivitiesNode
let dateNode: TextNode
let statusNode: ASImageNode
let separatorNode: ASDisplayNode
let badgeNode: ChatListBadgeNode
let onlineNode: ChatListOnlineNode
let mentionBadgeNode: ASImageNode
var secretIconNode: ASImageNode?
var verificationIconNode: ASImageNode?
let mutedIconNode: ASImageNode
var selectableControlNode: ItemListSelectableControlNode?
var reorderControlNode: ItemListEditableReorderControlNode?
private var peerPresenceManager: PeerPresenceStatusManager?
var layoutParams: (ChatListItem, first: Bool, last: Bool, firstWithHeader: Bool, nextIsPinned: Bool, ListViewItemLayoutParams)?
private var isHighlighted: Bool = false
override var canBeSelected: Bool {
if self.selectableControlNode != nil {
return false
} else {
return super.canBeSelected
}
}
override var defaultAccessibilityLabel: String? {
get {
return self.accessibilityLabel
} set(value) {
}
}
override var accessibilityAttributedLabel: NSAttributedString? {
get {
return self.accessibilityLabel.flatMap(NSAttributedString.init(string:))
} set(value) {
}
}
override var accessibilityAttributedValue: NSAttributedString? {
get {
return self.accessibilityValue.flatMap(NSAttributedString.init(string:))
} set(value) {
}
}
override var accessibilityLabel: String? {
get {
guard let item = self.item else {
return nil
}
switch item.content {
/*case .groupReference:
return nil*/
case let .peer(peer):
guard let chatMainPeer = peer.peer.chatMainPeer else {
return nil
}
return chatMainPeer.displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder)
}
} set(value) {
}
}
override var accessibilityValue: String? {
get {
guard let item = self.item else {
return nil
}
switch item.content {
/*case .groupReference:
return nil*/
case let .peer(peer):
if let message = peer.message {
var result = ""
if message.flags.contains(.Incoming) {
result += "Message"
} else {
result += "Outgoing message"
}
let (_, initialHideAuthor, messageText) = chatListItemStrings(strings: item.presentationData.strings, nameDisplayOrder: item.presentationData.nameDisplayOrder, message: peer.message, chatPeer: peer.peer, accountPeerId: item.account.peerId)
if message.flags.contains(.Incoming), !initialHideAuthor, let author = message.author, author is TelegramUser {
result += "\nFrom: \(author.displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder))"
}
if !message.flags.contains(.Incoming), let combinedReadState = peer.combinedReadState, combinedReadState.isOutgoingMessageIndexRead(message.index) {
result += "\nRead"
}
result += "\n\(messageText)"
return result
} else {
return "Empty"
}
}
} set(value) {
}
}
required init() {
self.backgroundNode = ASDisplayNode()
self.backgroundNode.isLayerBacked = true
self.backgroundNode.displaysAsynchronously = false
self.avatarNode = AvatarNode(font: avatarFont)
self.highlightedBackgroundNode = ASDisplayNode()
self.highlightedBackgroundNode.isLayerBacked = true
self.titleNode = TextNode()
self.titleNode.isUserInteractionEnabled = false
self.titleNode.displaysAsynchronously = true
self.authorNode = TextNode()
self.authorNode.isUserInteractionEnabled = false
self.authorNode.displaysAsynchronously = true
self.textNode = TextNode()
self.textNode.isUserInteractionEnabled = false
self.textNode.displaysAsynchronously = true
self.inputActivitiesNode = ChatListInputActivitiesNode()
self.inputActivitiesNode.isUserInteractionEnabled = false
self.inputActivitiesNode.alpha = 0.0
self.dateNode = TextNode()
self.dateNode.isUserInteractionEnabled = false
self.dateNode.displaysAsynchronously = true
self.statusNode = ASImageNode()
self.statusNode.displaysAsynchronously = false
self.statusNode.displayWithoutProcessing = true
self.badgeNode = ChatListBadgeNode()
self.onlineNode = ChatListOnlineNode()
self.mentionBadgeNode = ASImageNode()
self.mentionBadgeNode.isLayerBacked = true
self.mentionBadgeNode.displaysAsynchronously = false
self.mentionBadgeNode.displayWithoutProcessing = true
self.mutedIconNode = ASImageNode()
self.mutedIconNode.isLayerBacked = true
self.mutedIconNode.displaysAsynchronously = false
self.mutedIconNode.displayWithoutProcessing = true
self.separatorNode = ASDisplayNode()
self.separatorNode.isLayerBacked = true
super.init(layerBacked: false, dynamicBounce: false, rotated: false, seeThrough: false)
self.isAccessibilityElement = true
self.addSubnode(self.backgroundNode)
self.addSubnode(self.separatorNode)
self.addSubnode(self.avatarNode)
self.addSubnode(self.onlineNode)
self.addSubnode(self.titleNode)
self.addSubnode(self.authorNode)
self.addSubnode(self.textNode)
self.addSubnode(self.dateNode)
self.addSubnode(self.statusNode)
self.addSubnode(self.badgeNode)
self.addSubnode(self.mentionBadgeNode)
self.addSubnode(self.mutedIconNode)
self.peerPresenceManager = PeerPresenceStatusManager(update: { [weak self] in
if let strongSelf = self, let layoutParams = strongSelf.layoutParams {
let (_, apply) = strongSelf.asyncLayout()(layoutParams.0, layoutParams.5, layoutParams.1, layoutParams.2, layoutParams.3, layoutParams.4)
let _ = apply(false, false)
}
})
}
func setupItem(item: ChatListItem, synchronousLoads: Bool) {
self.item = item
var peer: Peer?
switch item.content {
case let .peer(_, peerValue, _, _, _, _, _, _, _, _):
peer = peerValue.chatMainPeer
/*case .groupReference:
break*/
}
if let peer = peer {
var overrideImage: AvatarNodeImageOverride?
if peer.id == item.account.peerId {
overrideImage = .savedMessagesIcon
}
self.avatarNode.setPeer(account: item.account, theme: item.presentationData.theme, peer: peer, overrideImage: overrideImage, emptyColor: item.presentationData.theme.list.mediaPlaceholderColor, synchronousLoad: synchronousLoads)
}
}
override func layoutForParams(_ params: ListViewItemLayoutParams, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) {
let layout = self.asyncLayout()
let (first, last, firstWithHeader, nextIsPinned) = ChatListItem.mergeType(item: item as! ChatListItem, previousItem: previousItem, nextItem: nextItem)
let (nodeLayout, apply) = layout(item as! ChatListItem, params, first, last, firstWithHeader, nextIsPinned)
apply(false, false)
self.contentSize = nodeLayout.contentSize
self.insets = nodeLayout.insets
}
class func insets(first: Bool, last: Bool, firstWithHeader: Bool) -> UIEdgeInsets {
return UIEdgeInsets(top: firstWithHeader ? 29.0 : 0.0, left: 0.0, bottom: 0.0, right: 0.0)
}
override func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) {
super.setHighlighted(highlighted, at: point, animated: animated)
self.isHighlighted = highlighted
self.updateIsHighlighted(transition: (animated && !highlighted) ? .animated(duration: 0.3, curve: .easeInOut) : .immediate)
}
func updateIsHighlighted(transition: ContainedViewLayoutTransition) {
var reallyHighlighted = self.isHighlighted
if let item = self.item {
let itemChatLocation = item.content.chatLocation
if itemChatLocation == item.interaction.highlightedChatLocation?.location {
reallyHighlighted = true
}
}
if reallyHighlighted {
if self.highlightedBackgroundNode.supernode == nil {
self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: self.separatorNode)
self.highlightedBackgroundNode.alpha = 0.0
}
self.highlightedBackgroundNode.layer.removeAllAnimations()
transition.updateAlpha(layer: self.highlightedBackgroundNode.layer, alpha: 1.0)
if let item = self.item {
self.onlineNode.setImage(PresentationResourcesChatList.recentStatusOnlineIcon(item.presentationData.theme, state: .highlighted))
}
} else {
if self.highlightedBackgroundNode.supernode != nil {
transition.updateAlpha(layer: self.highlightedBackgroundNode.layer, alpha: 0.0, completion: { [weak self] completed in
if let strongSelf = self {
if completed {
strongSelf.highlightedBackgroundNode.removeFromSupernode()
}
}
})
}
if let item = self.item {
let onlineIcon: UIImage?
if item.index.pinningIndex != nil {
onlineIcon = PresentationResourcesChatList.recentStatusOnlineIcon(item.presentationData.theme, state: .pinned)
} else {
onlineIcon = PresentationResourcesChatList.recentStatusOnlineIcon(item.presentationData.theme, state: .regular)
}
self.onlineNode.setImage(onlineIcon)
}
}
}
override func tapped() {
guard let item = self.item, item.editing else {
return
}
item.interaction.togglePeerSelected(item.index.messageIndex.id.peerId)
}
func asyncLayout() -> (_ item: ChatListItem, _ params: ListViewItemLayoutParams, _ first: Bool, _ last: Bool, _ firstWithHeader: Bool, _ nextIsPinned: Bool) -> (ListViewItemNodeLayout, (Bool, Bool) -> Void) {
let dateLayout = TextNode.asyncLayout(self.dateNode)
let textLayout = TextNode.asyncLayout(self.textNode)
let titleLayout = TextNode.asyncLayout(self.titleNode)
let authorLayout = TextNode.asyncLayout(self.authorNode)
let inputActivitiesLayout = self.inputActivitiesNode.asyncLayout()
let badgeLayout = self.badgeNode.asyncLayout()
let onlineLayout = self.onlineNode.asyncLayout()
let selectableControlLayout = ItemListSelectableControlNode.asyncLayout(self.selectableControlNode)
let reorderControlLayout = ItemListEditableReorderControlNode.asyncLayout(self.reorderControlNode)
let currentItem = self.layoutParams?.0
let multipleAvatarsLayout = MultipleAvatarsNode.asyncLayout(self.multipleAvatarsNode)
return { item, params, first, last, firstWithHeader, nextIsPinned in
let account = item.account
var message: Message?
let itemPeer: RenderedPeer
let combinedReadState: CombinedPeerReadState?
let unreadCount: (count: Int32, unread: Bool, muted: Bool)
let notificationSettings: PeerNotificationSettings?
let peerPresence: PeerPresence?
let embeddedState: PeerChatListEmbeddedInterfaceState?
let summaryInfo: ChatListMessageTagSummaryInfo
let inputActivities: [(Peer, PeerInputActivity)]?
let isPeerGroup: Bool
let isAd: Bool
var multipleAvatarsApply: ((Bool) -> MultipleAvatarsNode)?
switch item.content {
case let .peer(messageValue, peerValue, combinedReadStateValue, notificationSettingsValue, peerPresenceValue, summaryInfoValue, embeddedStateValue, inputActivitiesValue, isAdValue, ignoreUnreadBadge):
message = messageValue
itemPeer = peerValue
combinedReadState = combinedReadStateValue
if let combinedReadState = combinedReadState, !isAdValue && !ignoreUnreadBadge {
unreadCount = (combinedReadState.count, combinedReadState.isUnread, notificationSettingsValue?.isRemovedFromTotalUnreadCount ?? false)
} else {
unreadCount = (0, false, false)
}
if isAdValue {
notificationSettings = nil
} else {
notificationSettings = notificationSettingsValue
}
peerPresence = peerPresenceValue
embeddedState = embeddedStateValue
summaryInfo = summaryInfoValue
inputActivities = inputActivitiesValue
isPeerGroup = false
isAd = isAdValue
/*case let .groupReference(_, messageValue, topPeersValue, counters):
if let messageValue = messageValue {
itemPeer = RenderedPeer(message: messageValue)
} else {
itemPeer = RenderedPeer(peerId: item.index.messageIndex.id.peerId, peers: SimpleDictionary())
}
message = messageValue
combinedReadState = nil
notificationSettings = nil
embeddedState = nil
summaryInfo = ChatListMessageTagSummaryInfo()
inputActivities = nil
isPeerGroup = true
multipleAvatarsApply = multipleAvatarsLayout(item.account, item.presentationData.theme, topPeersValue, CGSize(width: 60.0, height: 60.0))
if counters.unreadCount > 0 {
let count = counters.unreadCount + counters.unreadMutedCount
unreadCount = (count, count > 0, false)
} else if counters.unreadMutedCount > 0 {
unreadCount = (counters.unreadMutedCount, counters.unreadMutedCount > 0, true)
} else{
unreadCount = (0, false, false)
}
isAd = false*/
}
if let messageValue = message {
for media in messageValue.media {
if let media = media as? TelegramMediaAction, case .historyCleared = media.action {
message = nil
}
}
}
let theme = item.presentationData.theme.chatList
var updatedTheme: PresentationTheme?
if currentItem?.presentationData.theme !== item.presentationData.theme {
updatedTheme = item.presentationData.theme
}
var authorAttributedString: NSAttributedString?
var textAttributedString: NSAttributedString?
var dateAttributedString: NSAttributedString?
var titleAttributedString: NSAttributedString?
var badgeAttributedString: NSAttributedString?
var statusImage: UIImage?
var currentBadgeBackgroundImage: UIImage?
var currentMentionBadgeImage: UIImage?
var currentMutedIconImage: UIImage?
var currentVerificationIconImage: UIImage?
var currentSecretIconImage: UIImage?
var selectableControlSizeAndApply: (CGFloat, (CGSize, Bool) -> ItemListSelectableControlNode)?
var reorderControlSizeAndApply: (CGSize, (Bool) -> ItemListEditableReorderControlNode)?
let editingOffset: CGFloat
var reorderInset: CGFloat = 0.0
if item.editing {
let sizeAndApply = selectableControlLayout(item.presentationData.theme.list.itemCheckColors.strokeColor, item.presentationData.theme.list.itemCheckColors.fillColor, item.presentationData.theme.list.itemCheckColors.foregroundColor, item.selected, true)
if !isAd {
selectableControlSizeAndApply = sizeAndApply
}
editingOffset = sizeAndApply.0
if item.index.pinningIndex != nil && !isAd {
let sizeAndApply = reorderControlLayout(itemHeight, item.presentationData.theme)
reorderControlSizeAndApply = sizeAndApply
reorderInset = sizeAndApply.0.width
}
} else {
editingOffset = 0.0
}
let leftInset: CGFloat = params.leftInset + 78.0
let (peer, initialHideAuthor, messageText) = chatListItemStrings(strings: item.presentationData.strings, nameDisplayOrder: item.presentationData.nameDisplayOrder, message: message, chatPeer: itemPeer, accountPeerId: item.account.peerId)
var hideAuthor = initialHideAuthor
if isPeerGroup {
hideAuthor = false
}
let attributedText: NSAttributedString
var hasDraft = false
if let embeddedState = embeddedState as? ChatEmbeddedInterfaceState {
hasDraft = true
authorAttributedString = NSAttributedString(string: item.presentationData.strings.DialogList_Draft, font: textFont, textColor: theme.messageDraftTextColor)
attributedText = NSAttributedString(string: embeddedState.text.string, font: textFont, textColor: theme.messageTextColor)
} else if let message = message {
attributedText = NSAttributedString(string: messageText as String, font: textFont, textColor: theme.messageTextColor)
var peerText: String?
if let author = message.author as? TelegramUser, let peer = peer, !(peer is TelegramUser) {
if let peer = peer as? TelegramChannel, case .broadcast = peer.info {
} else {
peerText = author.id == account.peerId ? item.presentationData.strings.DialogList_You : author.displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder)
}
}/* else if case .groupReference = item.content {
if let messagePeer = itemPeer.chatMainPeer {
peerText = messagePeer.displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder)
}
}*/
if let peerText = peerText {
authorAttributedString = NSAttributedString(string: peerText, font: textFont, textColor: theme.authorNameColor)
}
} else {
attributedText = NSAttributedString(string: messageText as String, font: textFont, textColor: theme.messageTextColor)
}
switch item.content {
case .peer:
if peer?.id == item.account.peerId {
titleAttributedString = NSAttributedString(string: item.presentationData.strings.DialogList_SavedMessages, font: titleFont, textColor: theme.titleColor)
} else if let displayTitle = peer?.displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder) {
titleAttributedString = NSAttributedString(string: displayTitle, font: titleFont, textColor: item.index.messageIndex.id.peerId.namespace == Namespaces.Peer.SecretChat ? theme.secretTitleColor : theme.titleColor)
}
/*case .groupReference:
titleAttributedString = NSAttributedString(string: "Feed", font: titleFont, textColor: theme.titleColor)*/
}
textAttributedString = attributedText
var t = Int(item.index.messageIndex.timestamp)
var timeinfo = tm()
localtime_r(&t, &timeinfo)
let timestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970)
let dateText = stringForRelativeTimestamp(strings: item.presentationData.strings, relativeTimestamp: item.index.messageIndex.timestamp, relativeTo: timestamp, dateTimeFormat: item.presentationData.dateTimeFormat)
if isAd {
dateAttributedString = NSAttributedString(string: item.presentationData.strings.DialogList_AdLabel, font: dateFont, textColor: theme.dateTextColor)
} else {
dateAttributedString = NSAttributedString(string: dateText, font: dateFont, textColor: theme.dateTextColor)
}
if let message = message, message.author?.id == account.peerId && !hasDraft {
if message.flags.isSending && !message.isSentOrAcknowledged {
statusImage = PresentationResourcesChatList.pendingImage(item.presentationData.theme)
} else {
if let combinedReadState = combinedReadState, combinedReadState.isOutgoingMessageIndexRead(message.index) {
statusImage = PresentationResourcesChatList.doubleCheckImage(item.presentationData.theme)
} else {
statusImage = PresentationResourcesChatList.singleCheckImage(item.presentationData.theme)
}
}
}
if unreadCount.unread {
if let message = message, message.tags.contains(.unseenPersonalMessage), unreadCount.count == 1 {
} else {
let badgeTextColor: UIColor
if unreadCount.muted {
currentBadgeBackgroundImage = PresentationResourcesChatList.badgeBackgroundInactive(item.presentationData.theme)
badgeTextColor = theme.unreadBadgeInactiveTextColor
} else {
currentBadgeBackgroundImage = PresentationResourcesChatList.badgeBackgroundActive(item.presentationData.theme)
badgeTextColor = theme.unreadBadgeActiveTextColor
}
let unreadCountText: String
if unreadCount.count > 1000 {
unreadCountText = "\(unreadCount.count / 1000)K"
} else {
unreadCountText = "\(unreadCount.count)"
}
badgeAttributedString = NSAttributedString(string: unreadCount.count > 0 ? unreadCountText : " ", font: badgeFont, textColor: badgeTextColor)
}
}
let tagSummaryCount = summaryInfo.tagSummaryCount ?? 0
let actionsSummaryCount = summaryInfo.actionsSummaryCount ?? 0
let totalMentionCount = tagSummaryCount - actionsSummaryCount
if totalMentionCount > 0 {
currentMentionBadgeImage = PresentationResourcesChatList.badgeBackgroundMention(item.presentationData.theme)
} else if item.index.pinningIndex != nil && !isAd && currentBadgeBackgroundImage == nil {
currentMentionBadgeImage = PresentationResourcesChatList.badgeBackgroundPinned(item.presentationData.theme)
}
if let notificationSettings = notificationSettings as? TelegramPeerNotificationSettings {
if case let .muted(until) = notificationSettings.muteState, until >= Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) {
currentMutedIconImage = PresentationResourcesChatList.mutedIcon(item.presentationData.theme)
}
}
let statusWidth = statusImage?.size.width ?? 0.0
var titleIconsWidth: CGFloat = 0.0
if let currentMutedIconImage = currentMutedIconImage {
if titleIconsWidth.isZero {
titleIconsWidth += 4.0
}
titleIconsWidth += currentMutedIconImage.size.width
}
var isVerified = false
let isSecret = item.index.messageIndex.id.peerId.namespace == Namespaces.Peer.SecretChat
if case .peer = item.content {
if let peer = itemPeer.chatMainPeer {
if let peer = peer as? TelegramUser {
isVerified = peer.flags.contains(.isVerified)
} else if let peer = peer as? TelegramChannel {
isVerified = peer.flags.contains(.isVerified)
}
}
}
if isSecret {
currentSecretIconImage = PresentationResourcesChatList.secretIcon(item.presentationData.theme)
}
if isVerified {
currentVerificationIconImage = PresentationResourcesChatList.verifiedIcon(item.presentationData.theme)
}
if let currentSecretIconImage = currentSecretIconImage {
titleIconsWidth += currentSecretIconImage.size.width + 2.0
}
if let currentVerificationIconImage = currentVerificationIconImage {
if titleIconsWidth.isZero {
titleIconsWidth += 4.0
} else {
titleIconsWidth += 2.0
}
titleIconsWidth += currentVerificationIconImage.size.width
}
let rawContentRect = CGRect(origin: CGPoint(x: 2.0, y: 8.0), size: CGSize(width: params.width - leftInset - params.rightInset - 10.0 - 1.0 - editingOffset, height: itemHeight - 12.0 - 9.0))
let (dateLayout, dateApply) = dateLayout(TextNodeLayoutArguments(attributedString: dateAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: rawContentRect.width, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let (badgeLayout, badgeApply) = badgeLayout(CGSize(width: rawContentRect.width, height: CGFloat.greatestFiniteMagnitude), currentBadgeBackgroundImage, badgeAttributedString)
var badgeSize: CGFloat = 0.0
badgeSize += badgeLayout.width
if let currentMentionBadgeImage = currentMentionBadgeImage {
if !badgeSize.isZero {
badgeSize += currentMentionBadgeImage.size.width + 4.0
} else {
badgeSize += currentMentionBadgeImage.size.width + 5.0
}
}
badgeSize = max(badgeSize, reorderInset)
let (authorLayout, authorApply) = authorLayout(TextNodeLayoutArguments(attributedString: hideAuthor ? nil : authorAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: rawContentRect.width - badgeSize, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets(top: 2.0, left: 1.0, bottom: 2.0, right: 1.0)))
let (textLayout, textApply) = textLayout(TextNodeLayoutArguments(attributedString: textAttributedString, backgroundColor: nil, maximumNumberOfLines: authorAttributedString == nil ? 2 : 1, truncationType: .end, constrainedSize: CGSize(width: rawContentRect.width - badgeSize, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets(top: 2.0, left: 1.0, bottom: 2.0, right: 1.0)))
let titleRect = CGRect(origin: rawContentRect.origin, size: CGSize(width: rawContentRect.width - dateLayout.size.width - 10.0 - statusWidth - titleIconsWidth, height: rawContentRect.height))
let (titleLayout, titleApply) = titleLayout(TextNodeLayoutArguments(attributedString: titleAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: titleRect.width, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
var inputActivitiesSize: CGSize?
var inputActivitiesApply: (() -> Void)?
if let inputActivities = inputActivities, !inputActivities.isEmpty {
let (size, apply) = inputActivitiesLayout(CGSize(width: rawContentRect.width - badgeSize, height: 40.0), item.presentationData.strings, item.presentationData.theme.chatList.messageTextColor, item.index.messageIndex.id.peerId, inputActivities)
inputActivitiesSize = size
inputActivitiesApply = apply
}
var online = false
let peerRevealOptions: [ItemListRevealOption]
let peerLeftRevealOptions: [ItemListRevealOption]
switch item.content {
case let .peer(_, renderedPeer, _, _, presence, _ ,_ ,_, _, _):
if let peer = renderedPeer.peer, let presence = presence as? TelegramUserPresence, !isServicePeer(peer) && peer.id != item.account.peerId {
let relativeStatus = relativeUserPresenceStatus(presence, relativeTo: timestamp)
if case .online = relativeStatus {
online = true
}
}
var hasPeerGroupId: Bool?
if GlobalExperimentalSettings.enableFeed {
if let chatMainPeer = itemPeer.chatMainPeer as? TelegramChannel, case .broadcast = chatMainPeer.info {
hasPeerGroupId = item.peerGroupId != nil
}
}
var isPinned: Bool?
if item.peerGroupId == nil {
isPinned = item.index.pinningIndex != nil
}
if item.enableContextActions && !isAd {
peerRevealOptions = revealOptions(strings: item.presentationData.strings, theme: item.presentationData.theme, isPinned: isPinned, isMuted: item.account.peerId != item.index.messageIndex.id.peerId ? (currentMutedIconImage != nil) : nil, hasPeerGroupId: hasPeerGroupId, canDelete: true, isEditing: item.editing)
if itemPeer.peerId != item.account.peerId {
peerLeftRevealOptions = leftRevealOptions(strings: item.presentationData.strings, theme: item.presentationData.theme, isUnread: unreadCount.unread)
} else {
peerLeftRevealOptions = []
}
} else {
peerRevealOptions = []
peerLeftRevealOptions = []
}
/*case .groupReference:
let isPinned = item.index.pinningIndex != nil
if item.enableContextActions {
peerRevealOptions = revealOptions(strings: item.presentationData.strings, theme: item.presentationData.theme, isPinned: isPinned, isMuted: nil, hasPeerGroupId: nil, canDelete: false, isEditing: item.editing)
} else {
peerRevealOptions = []
}
peerLeftRevealOptions = []*/
}
let (onlineLayout, onlineApply) = onlineLayout(online)
var animateOnline = false
if let currentItem = currentItem, currentItem.content.chatLocation == item.content.chatLocation {
animateOnline = true
}
let insets = ChatListItemNode.insets(first: first, last: last, firstWithHeader: firstWithHeader)
let layout = ListViewItemNodeLayout(contentSize: CGSize(width: params.width, height: itemHeight), insets: insets)
return (layout, { [weak self] synchronousLoads, animated in
if let strongSelf = self {
strongSelf.layoutParams = (item, first, last, firstWithHeader, nextIsPinned, params)
if let _ = updatedTheme {
strongSelf.separatorNode.backgroundColor = item.presentationData.theme.chatList.itemSeparatorColor
strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.chatList.itemHighlightedBackgroundColor
}
let revealOffset = strongSelf.revealOffset
let transition: ContainedViewLayoutTransition
if animated {
transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring)
} else {
transition = .immediate
}
var crossfadeContent = false
if let selectableControlSizeAndApply = selectableControlSizeAndApply {
let selectableControlSize = CGSize(width: selectableControlSizeAndApply.0, height: layout.contentSize.height)
let selectableControlFrame = CGRect(origin: CGPoint(x: params.leftInset + revealOffset, y: 0.0), size: selectableControlSize)
if strongSelf.selectableControlNode == nil {
crossfadeContent = true
let selectableControlNode = selectableControlSizeAndApply.1(selectableControlSize, false)
strongSelf.selectableControlNode = selectableControlNode
strongSelf.addSubnode(selectableControlNode)
selectableControlNode.frame = selectableControlFrame
transition.animatePosition(node: selectableControlNode, from: CGPoint(x: -selectableControlFrame.size.width / 2.0, y: selectableControlFrame.midY))
selectableControlNode.alpha = 0.0
transition.updateAlpha(node: selectableControlNode, alpha: 1.0)
} else if let selectableControlNode = strongSelf.selectableControlNode {
transition.updateFrame(node: selectableControlNode, frame: selectableControlFrame)
let _ = selectableControlSizeAndApply.1(selectableControlSize, transition.isAnimated)
}
} else if let selectableControlNode = strongSelf.selectableControlNode {
crossfadeContent = true
var selectableControlFrame = selectableControlNode.frame
selectableControlFrame.origin.x = -selectableControlFrame.size.width
strongSelf.selectableControlNode = nil
transition.updateAlpha(node: selectableControlNode, alpha: 0.0)
transition.updateFrame(node: selectableControlNode, frame: selectableControlFrame, completion: { [weak selectableControlNode] _ in
selectableControlNode?.removeFromSupernode()
})
}
if let reorderControlSizeAndApply = reorderControlSizeAndApply {
let reorderControlFrame = CGRect(origin: CGPoint(x: params.width + revealOffset - params.rightInset - reorderControlSizeAndApply.0.width, y: 0.0), size: reorderControlSizeAndApply.0)
if strongSelf.reorderControlNode == nil {
let reorderControlNode = reorderControlSizeAndApply.1(false)
strongSelf.reorderControlNode = reorderControlNode
strongSelf.addSubnode(reorderControlNode)
reorderControlNode.frame = reorderControlFrame
reorderControlNode.alpha = 0.0
transition.updateAlpha(node: reorderControlNode, alpha: 1.0)
transition.updateAlpha(node: strongSelf.dateNode, alpha: 0.0)
transition.updateAlpha(node: strongSelf.badgeNode, alpha: 0.0)
transition.updateAlpha(node: strongSelf.mentionBadgeNode, alpha: 0.0)
transition.updateAlpha(node: strongSelf.statusNode, alpha: 0.0)
} else if let reorderControlNode = strongSelf.reorderControlNode {
let _ = reorderControlSizeAndApply.1(false)
transition.updateFrame(node: reorderControlNode, frame: reorderControlFrame)
}
} else if let reorderControlNode = strongSelf.reorderControlNode {
strongSelf.reorderControlNode = nil
transition.updateAlpha(node: reorderControlNode, alpha: 0.0, completion: { [weak reorderControlNode] _ in
reorderControlNode?.removeFromSupernode()
})
transition.updateAlpha(node: strongSelf.dateNode, alpha: 1.0)
transition.updateAlpha(node: strongSelf.badgeNode, alpha: 1.0)
transition.updateAlpha(node: strongSelf.mentionBadgeNode, alpha: 1.0)
transition.updateAlpha(node: strongSelf.statusNode, alpha: 1.0)
}
let avatarFrame = CGRect(origin: CGPoint(x: leftInset - 78.0 + editingOffset + 10.0 + revealOffset, y: 7.0), size: CGSize(width: 60.0, height: 60.0))
transition.updateFrame(node: strongSelf.avatarNode, frame: avatarFrame)
let onlineFrame = CGRect(origin: CGPoint(x: avatarFrame.maxX - onlineLayout.width - 2.0, y: avatarFrame.maxY - onlineLayout.height - 2.0), size: onlineLayout)
transition.updateFrame(node: strongSelf.onlineNode, frame: onlineFrame)
let onlineIcon: UIImage?
if strongSelf.isHighlighted {
onlineIcon = PresentationResourcesChatList.recentStatusOnlineIcon(item.presentationData.theme, state: .highlighted)
} else if item.index.pinningIndex != nil {
onlineIcon = PresentationResourcesChatList.recentStatusOnlineIcon(item.presentationData.theme, state: .pinned)
} else {
onlineIcon = PresentationResourcesChatList.recentStatusOnlineIcon(item.presentationData.theme, state: .regular)
}
strongSelf.onlineNode.setImage(onlineIcon)
if let multipleAvatarsApply = multipleAvatarsApply {
strongSelf.avatarNode.isHidden = true
let multipleAvatarsNode = multipleAvatarsApply(animated && strongSelf.multipleAvatarsNode != nil)
if strongSelf.multipleAvatarsNode != multipleAvatarsNode {
strongSelf.multipleAvatarsNode?.removeFromSupernode()
strongSelf.multipleAvatarsNode = multipleAvatarsNode
strongSelf.addSubnode(multipleAvatarsNode)
multipleAvatarsNode.frame = avatarFrame
} else {
transition.updateFrame(node: multipleAvatarsNode, frame: avatarFrame)
}
} else if let multipleAvatarsNode = strongSelf.multipleAvatarsNode {
multipleAvatarsNode.removeFromSupernode()
strongSelf.avatarNode.isHidden = false
}
let _ = dateApply()
let _ = textApply()
let _ = authorApply()
let _ = titleApply()
let _ = badgeApply()
let _ = onlineApply(animateOnline)
let contentRect = rawContentRect.offsetBy(dx: editingOffset + leftInset + revealOffset, dy: 0.0)
strongSelf.dateNode.frame = CGRect(origin: CGPoint(x: contentRect.origin.x + contentRect.size.width - dateLayout.size.width, y: contentRect.origin.y + 2.0), size: dateLayout.size)
if let statusImage = statusImage {
strongSelf.statusNode.image = statusImage
strongSelf.statusNode.isHidden = false
let statusSize = statusImage.size
strongSelf.statusNode.frame = CGRect(origin: CGPoint(x: contentRect.origin.x + contentRect.size.width - dateLayout.size.width - 2.0 - statusSize.width, y: contentRect.origin.y + 2.0 + floor((dateLayout.size.height - statusSize.height) / 2.0)), size: statusSize)
} else {
strongSelf.statusNode.image = nil
strongSelf.statusNode.isHidden = true
}
let badgeWidth = badgeLayout.width
if let _ = currentBadgeBackgroundImage {
let badgeFrame = CGRect(x: contentRect.maxX - badgeWidth, y: contentRect.maxY - badgeLayout.height - 2.0, width: badgeWidth, height: badgeLayout.height)
strongSelf.badgeNode.frame = badgeFrame
}
//: CGFloat
// if let currentBadgeBackgroundImage = currentBadgeBackgroundImage {
// strongSelf.badgeBackgroundNode.image = currentBadgeBackgroundImage
// strongSelf.badgeBackgroundNode.isHidden = false
//
// badgeBackgroundWidth = max(badgeLayout.size.width + 10.0, currentBadgeBackgroundImage.size.width)
// let badgeBackgroundFrame = CGRect(x: contentRect.maxX - badgeBackgroundWidth, y: contentRect.maxY - currentBadgeBackgroundImage.size.height - 2.0, width: badgeBackgroundWidth, height: currentBadgeBackgroundImage.size.height)
// let badgeTextFrame = CGRect(origin: CGPoint(x: badgeBackgroundFrame.midX - badgeLayout.size.width / 2.0, y: badgeBackgroundFrame.minY + 2.0), size: badgeLayout.size)
//
// strongSelf.badgeTextNode.frame = badgeTextFrame
// strongSelf.badgeBackgroundNode.frame = badgeBackgroundFrame
// } else {
// badgeBackgroundWidth = 0.0
// strongSelf.badgeBackgroundNode.image = nil
// strongSelf.badgeBackgroundNode.isHidden = true
// }
if let currentMentionBadgeImage = currentMentionBadgeImage {
strongSelf.mentionBadgeNode.image = currentMentionBadgeImage
strongSelf.mentionBadgeNode.isHidden = false
let mentionBadgeSize = currentMentionBadgeImage.size
let mentionBadgeOffset: CGFloat
if badgeWidth.isZero {
mentionBadgeOffset = contentRect.maxX - mentionBadgeSize.width
} else {
mentionBadgeOffset = contentRect.maxX - badgeWidth - 6.0 - mentionBadgeSize.width
}
let badgeBackgroundWidth = mentionBadgeSize.width
let badgeBackgroundFrame = CGRect(x: mentionBadgeOffset, y: contentRect.maxY - mentionBadgeSize.height - 2.0, width: badgeBackgroundWidth, height: mentionBadgeSize.height)
strongSelf.mentionBadgeNode.frame = badgeBackgroundFrame
} else {
strongSelf.mentionBadgeNode.image = nil
strongSelf.mentionBadgeNode.isHidden = true
}
var titleOffset: CGFloat = 0.0
if let currentSecretIconImage = currentSecretIconImage {
let iconNode: ASImageNode
if let current = strongSelf.secretIconNode {
iconNode = current
} else {
iconNode = ASImageNode()
iconNode.isLayerBacked = true
iconNode.displaysAsynchronously = false
iconNode.displayWithoutProcessing = true
strongSelf.addSubnode(iconNode)
strongSelf.secretIconNode = iconNode
}
iconNode.image = currentSecretIconImage
transition.updateFrame(node: iconNode, frame: CGRect(origin: CGPoint(x: contentRect.origin.x, y: contentRect.origin.y + 4.0), size: currentSecretIconImage.size))
titleOffset += currentSecretIconImage.size.width + 3.0
} else if let secretIconNode = strongSelf.secretIconNode {
strongSelf.secretIconNode = nil
secretIconNode.removeFromSupernode()
}
var nextTitleIconOrigin: CGFloat = contentRect.origin.x + titleLayout.size.width + 3.0 + titleOffset
if let currentVerificationIconImage = currentVerificationIconImage {
let iconNode: ASImageNode
if let current = strongSelf.verificationIconNode {
iconNode = current
} else {
iconNode = ASImageNode()
iconNode.isLayerBacked = true
iconNode.displaysAsynchronously = false
iconNode.displayWithoutProcessing = true
strongSelf.addSubnode(iconNode)
strongSelf.verificationIconNode = iconNode
}
iconNode.image = currentVerificationIconImage
transition.updateFrame(node: iconNode, frame: CGRect(origin: CGPoint(x: nextTitleIconOrigin, y: contentRect.origin.y + 3.0), size: currentVerificationIconImage.size))
nextTitleIconOrigin += currentVerificationIconImage.size.width + 5.0
} else if let verificationIconNode = strongSelf.verificationIconNode {
strongSelf.verificationIconNode = nil
verificationIconNode.removeFromSupernode()
}
if let currentMutedIconImage = currentMutedIconImage {
strongSelf.mutedIconNode.image = currentMutedIconImage
strongSelf.mutedIconNode.isHidden = false
transition.updateFrame(node: strongSelf.mutedIconNode, frame: CGRect(origin: CGPoint(x: nextTitleIconOrigin, y: contentRect.origin.y + 6.0), size: currentMutedIconImage.size))
nextTitleIconOrigin += currentMutedIconImage.size.width + 3.0
} else {
strongSelf.mutedIconNode.image = nil
strongSelf.mutedIconNode.isHidden = true
}
let contentDeltaX = contentRect.origin.x - (strongSelf.titleNode.frame.minX - titleOffset)
strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: contentRect.origin.x + titleOffset, y: contentRect.origin.y + UIScreenPixel), size: titleLayout.size)
let authorNodeFrame = CGRect(origin: CGPoint(x: contentRect.origin.x, y: contentRect.minY + titleLayout.size.height), size: authorLayout.size)
strongSelf.authorNode.frame = authorNodeFrame
let textNodeFrame = CGRect(origin: CGPoint(x: contentRect.origin.x, y: contentRect.minY + titleLayout.size.height - 1.0 + UIScreenPixel + (authorLayout.size.height.isZero ? 0.0 : (authorLayout.size.height - 3.0))), size: textLayout.size)
strongSelf.textNode.frame = textNodeFrame
var animateInputActivitiesFrame = false
if let inputActivities = inputActivities, !inputActivities.isEmpty {
if strongSelf.inputActivitiesNode.supernode == nil {
strongSelf.addSubnode(strongSelf.inputActivitiesNode)
} else {
animateInputActivitiesFrame = true
}
if strongSelf.inputActivitiesNode.alpha.isZero {
strongSelf.inputActivitiesNode.alpha = 1.0
strongSelf.textNode.alpha = 0.0
strongSelf.authorNode.alpha = 0.0
if animated {
strongSelf.inputActivitiesNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15)
strongSelf.textNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15)
strongSelf.authorNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15)
}
}
} else {
if !strongSelf.inputActivitiesNode.alpha.isZero {
strongSelf.inputActivitiesNode.alpha = 0.0
strongSelf.textNode.alpha = 1.0
strongSelf.authorNode.alpha = 1.0
if animated {
strongSelf.inputActivitiesNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, completion: { value in
if let strongSelf = self, value {
strongSelf.inputActivitiesNode.removeFromSupernode()
}
})
strongSelf.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15)
strongSelf.authorNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15)
} else {
strongSelf.inputActivitiesNode.removeFromSupernode()
}
}
}
if let inputActivitiesSize = inputActivitiesSize {
let inputActivitiesFrame = CGRect(origin: CGPoint(x: authorNodeFrame.minX + 1.0, y: authorNodeFrame.minY + UIScreenPixel), size: inputActivitiesSize)
if animateInputActivitiesFrame {
transition.updateFrame(node: strongSelf.inputActivitiesNode, frame: inputActivitiesFrame)
} else {
strongSelf.inputActivitiesNode.frame = inputActivitiesFrame
}
}
inputActivitiesApply?()
if !contentDeltaX.isZero {
let titlePosition = strongSelf.titleNode.position
transition.animatePosition(node: strongSelf.titleNode, from: CGPoint(x: titlePosition.x - contentDeltaX, y: titlePosition.y))
let textPosition = strongSelf.textNode.position
transition.animatePosition(node: strongSelf.textNode, from: CGPoint(x: textPosition.x - contentDeltaX, y: textPosition.y))
let authorPosition = strongSelf.authorNode.position
transition.animatePosition(node: strongSelf.authorNode, from: CGPoint(x: authorPosition.x - contentDeltaX, y: authorPosition.y))
}
if crossfadeContent {
strongSelf.authorNode.recursivelyEnsureDisplaySynchronously(true)
strongSelf.titleNode.recursivelyEnsureDisplaySynchronously(true)
strongSelf.textNode.recursivelyEnsureDisplaySynchronously(true)
}
let separatorInset: CGFloat
if (!nextIsPinned && item.index.pinningIndex != nil) || last {
separatorInset = 0.0
} else {
separatorInset = editingOffset + leftInset + rawContentRect.origin.x
}
transition.updateFrame(node: strongSelf.separatorNode, frame: CGRect(origin: CGPoint(x: separatorInset, y: itemHeight - separatorHeight), size: CGSize(width: params.width - separatorInset, height: separatorHeight)))
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(), size: layout.contentSize)
if item.selected {
strongSelf.backgroundNode.backgroundColor = theme.itemSelectedBackgroundColor
} else if item.index.pinningIndex != nil {
strongSelf.backgroundNode.backgroundColor = theme.pinnedItemBackgroundColor
} else {
strongSelf.backgroundNode.backgroundColor = theme.itemBackgroundColor
}
let topNegativeInset: CGFloat = 0.0
strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -separatorHeight - topNegativeInset), size: CGSize(width: layout.contentSize.width, height: layout.contentSize.height + separatorHeight + topNegativeInset))
if let peerPresence = peerPresence as? TelegramUserPresence {
strongSelf.peerPresenceManager?.reset(presence: peerPresence)
}
strongSelf.updateLayout(size: layout.contentSize, leftInset: params.leftInset, rightInset: params.rightInset)
if item.editing {
strongSelf.setRevealOptions((left: [], right: []))
} else {
strongSelf.setRevealOptions((left: peerLeftRevealOptions, right: peerRevealOptions))
}
strongSelf.setRevealOptionsOpened(item.hasActiveRevealControls, animated: true)
strongSelf.view.accessibilityLabel = strongSelf.accessibilityLabel
strongSelf.view.accessibilityValue = strongSelf.accessibilityValue
}
})
}
}
override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
}
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.clipsToBounds = true
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
}
override public func header() -> ListViewItemHeader? {
if let item = self.layoutParams?.0 {
return item.header
} else {
return nil
}
}
override func updateRevealOffset(offset: CGFloat, transition: ContainedViewLayoutTransition) {
super.updateRevealOffset(offset: offset, transition: transition)
if let _ = self.item, let params = self.layoutParams?.5 {
let editingOffset: CGFloat
if let selectableControlNode = self.selectableControlNode {
editingOffset = selectableControlNode.bounds.size.width
var selectableControlFrame = selectableControlNode.frame
selectableControlFrame.origin.x = params.leftInset + offset
transition.updateFrame(node: selectableControlNode, frame: selectableControlFrame)
} else {
editingOffset = 0.0
}
if let reorderControlNode = self.reorderControlNode {
var reorderControlFrame = reorderControlNode.frame
reorderControlFrame.origin.x = params.width - params.rightInset - reorderControlFrame.size.width + offset
transition.updateFrame(node: reorderControlNode, frame: reorderControlFrame)
}
let leftInset: CGFloat = params.leftInset + 78.0
let rawContentRect = CGRect(origin: CGPoint(x: 2.0, y: 8.0), size: CGSize(width: params.width - leftInset - params.rightInset - 10.0 - 1.0 - editingOffset, height: itemHeight - 12.0 - 9.0))
let contentRect = rawContentRect.offsetBy(dx: editingOffset + leftInset + offset, dy: 0.0)
var avatarFrame = self.avatarNode.frame
avatarFrame.origin.x = leftInset - 78.0 + editingOffset + 10.0 + offset
transition.updateFrame(node: self.avatarNode, frame: avatarFrame)
if let multipleAvatarsNode = self.multipleAvatarsNode {
transition.updateFrame(node: multipleAvatarsNode, frame: avatarFrame)
}
var onlineFrame = self.onlineNode.frame
onlineFrame.origin.x = avatarFrame.maxX - onlineFrame.width - 2.0
transition.updateFrame(node: self.onlineNode, frame: onlineFrame)
var titleOffset: CGFloat = 0.0
if let secretIconNode = self.secretIconNode, let image = secretIconNode.image {
transition.updateFrame(node: secretIconNode, frame: CGRect(origin: CGPoint(x: contentRect.minX, y: secretIconNode.frame.minY), size: image.size))
titleOffset += image.size.width + 3.0
}
let titleFrame = self.titleNode.frame
transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: contentRect.origin.x + titleOffset, y: titleFrame.origin.y), size: titleFrame.size))
let authorFrame = self.authorNode.frame
transition.updateFrame(node: self.authorNode, frame: CGRect(origin: CGPoint(x: contentRect.origin.x, y: authorFrame.origin.y), size: authorFrame.size))
transition.updateFrame(node: self.inputActivitiesNode, frame: CGRect(origin: CGPoint(x: contentRect.origin.x, y: self.inputActivitiesNode.frame.minY), size: self.inputActivitiesNode.bounds.size))
let textFrame = self.textNode.frame
transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: contentRect.origin.x, y: textFrame.origin.y), size: textFrame.size))
let dateFrame = self.dateNode.frame
transition.updateFrame(node: self.dateNode, frame: CGRect(origin: CGPoint(x: contentRect.origin.x + contentRect.size.width - dateFrame.size.width, y: dateFrame.minY), size: dateFrame.size))
let statusFrame = self.statusNode.frame
transition.updateFrame(node: self.statusNode, frame: CGRect(origin: CGPoint(x: contentRect.origin.x + contentRect.size.width - dateFrame.size.width - 2.0 - statusFrame.size.width, y: statusFrame.minY), size: statusFrame.size))
var nextTitleIconOrigin: CGFloat = contentRect.origin.x + titleFrame.size.width + 3.0 + titleOffset
if let verificationIconNode = self.verificationIconNode {
transition.updateFrame(node: verificationIconNode, frame: CGRect(origin: CGPoint(x: nextTitleIconOrigin, y: verificationIconNode.frame.origin.y), size: verificationIconNode.bounds.size))
nextTitleIconOrigin += verificationIconNode.bounds.size.width + 5.0
}
let mutedIconFrame = self.mutedIconNode.frame
transition.updateFrame(node: self.mutedIconNode, frame: CGRect(origin: CGPoint(x: nextTitleIconOrigin, y: contentRect.origin.y + 6.0), size: mutedIconFrame.size))
nextTitleIconOrigin += mutedIconFrame.size.width + 3.0
let badgeFrame = self.badgeNode.frame
let updatedBadgeFrame = CGRect(origin: CGPoint(x: contentRect.maxX - badgeFrame.size.width, y: contentRect.maxY - badgeFrame.size.height - 2.0), size: badgeFrame.size)
transition.updateFrame(node: self.badgeNode, frame: updatedBadgeFrame)
let mentionBadgeSize = self.mentionBadgeNode.bounds.size
if mentionBadgeSize != CGSize.zero {
let mentionBadgeOffset: CGFloat
if updatedBadgeFrame.size.width.isZero {
mentionBadgeOffset = contentRect.maxX - mentionBadgeSize.width
} else {
mentionBadgeOffset = contentRect.maxX - updatedBadgeFrame.size.width - 6.0 - mentionBadgeSize.width
}
let badgeBackgroundWidth = mentionBadgeSize.width
let badgeBackgroundFrame = CGRect(x: mentionBadgeOffset, y: self.mentionBadgeNode.frame.origin.y, width: badgeBackgroundWidth, height: mentionBadgeSize.height)
transition.updateFrame(node: self.mentionBadgeNode, frame: badgeBackgroundFrame)
}
// let badgeTextFrame = self.badgeTextNode.frame
// transition.updateFrame(node: self.badgeTextNode, frame: CGRect(origin: CGPoint(x: updatedBadgeFrame.midX - badgeTextFrame.size.width / 2.0, y: badgeTextFrame.minY), size: badgeTextFrame.size))
}
}
override func touchesToOtherItemsPrevented() {
super.touchesToOtherItemsPrevented()
if let item = self.item {
item.interaction.setPeerIdWithRevealedOptions(nil, nil)
}
}
override func revealOptionsInteractivelyOpened() {
if let item = self.item {
item.interaction.setPeerIdWithRevealedOptions(item.index.messageIndex.id.peerId, nil)
}
}
override func revealOptionsInteractivelyClosed() {
if let item = self.item {
item.interaction.setPeerIdWithRevealedOptions(nil, item.index.messageIndex.id.peerId)
}
}
override func revealOptionSelected(_ option: ItemListRevealOption, animated: Bool) {
var close = true
if let item = self.item {
switch option.key {
case RevealOptionKey.pin.rawValue:
let itemId: PinnedItemId
switch item.content {
case .peer:
itemId = .peer(item.index.messageIndex.id.peerId)
/*case let .groupReference(groupId, _, _, _):
itemId = .group(groupId)*/
}
item.interaction.setItemPinned(itemId, true)
case RevealOptionKey.unpin.rawValue:
let itemId: PinnedItemId
switch item.content {
case .peer:
itemId = .peer(item.index.messageIndex.id.peerId)
/*case let .groupReference(groupId, _, _, _):
itemId = .group(groupId)*/
}
item.interaction.setItemPinned(itemId, false)
case RevealOptionKey.mute.rawValue:
item.interaction.setPeerMuted(item.index.messageIndex.id.peerId, true)
close = false
case RevealOptionKey.unmute.rawValue:
item.interaction.setPeerMuted(item.index.messageIndex.id.peerId, false)
close = false
case RevealOptionKey.delete.rawValue:
item.interaction.deletePeer(item.index.messageIndex.id.peerId)
case RevealOptionKey.group.rawValue:
item.interaction.updatePeerGrouping(item.index.messageIndex.id.peerId, true)
case RevealOptionKey.ungroup.rawValue:
item.interaction.updatePeerGrouping(item.index.messageIndex.id.peerId, false)
case RevealOptionKey.toggleMarkedUnread.rawValue:
item.interaction.togglePeerMarkedUnread(item.index.messageIndex.id.peerId, animated)
close = false
default:
break
}
}
if close {
self.setRevealOptionsOpened(false, animated: true)
self.revealOptionsInteractivelyClosed()
}
}
override func isReorderable(at point: CGPoint) -> Bool {
if let reorderControlNode = self.reorderControlNode, reorderControlNode.frame.contains(point) {
return true
}
return false
}
func flashHighlight() {
if self.highlightedBackgroundNode.supernode == nil {
self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: self.separatorNode)
self.highlightedBackgroundNode.alpha = 0.0
}
self.highlightedBackgroundNode.layer.removeAllAnimations()
self.highlightedBackgroundNode.layer.animate(from: 1.0 as NSNumber, to: 0.0 as NSNumber, keyPath: "opacity", timingFunction: kCAMediaTimingFunctionEaseOut, duration: 0.3, delay: 0.7, completion: { [weak self] _ in
self?.updateIsHighlighted(transition: .immediate)
})
}
}