[WIP] Topics

This commit is contained in:
Ali 2022-10-20 20:57:11 +04:00
parent 9a013c85c9
commit 99396ec7d4
17 changed files with 191 additions and 44 deletions

View File

@ -509,12 +509,7 @@ func chatForumTopicMenuItems(context: AccountContext, peerId: PeerId, threadId:
var items: [ContextMenuItem] = []
var canManage: Bool = false
if channel.hasPermission(.pinMessages) {
canManage = true
}
if canManage {
//TODO:localize
items.append(.action(ContextMenuActionItem(text: isPinned ? "Unpin" : "Pin", icon: { theme in generateTintedImage(image: UIImage(bundleImageName: isPinned ? "Chat/Context Menu/Unpin": "Chat/Context Menu/Pin"), color: theme.contextMenu.primaryColor) }, action: { _, f in
f(.default)
@ -714,7 +709,15 @@ func chatForumTopicMenuItems(context: AccountContext, peerId: PeerId, threadId:
}
})))
if canManage || threadData.isOwnedByMe {
var canManage = false
if channel.flags.contains(.isCreator) {
canManage = true
} else if channel.adminRights != nil {
canManage = true
} else if threadData.isOwnedByMe {
canManage = true
}
if canManage {
//TODO:localize
items.append(.action(ContextMenuActionItem(text: threadData.isClosed ? "Restart" : "Close", icon: { theme in generateTintedImage(image: UIImage(bundleImageName: threadData.isClosed ? "Chat/Context Menu/Play": "Chat/Context Menu/Pause"), color: theme.contextMenu.primaryColor) }, action: { _, f in
f(.default)

View File

@ -1590,10 +1590,14 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
let controller = ForumCreateTopicScreen(context: context, peerId: peerId, mode: .create)
controller.navigationPresentation = .modal
controller.completion = { title, fileId in
controller.completion = { [weak controller] title, fileId in
controller?.isInProgress = true
let _ = (context.engine.peers.createForumChannelTopic(id: peerId, title: title, iconColor: ForumCreateTopicScreen.iconColors.randomElement()!, iconFileId: fileId)
|> deliverOnMainQueue).start(next: { topicId in
let _ = context.sharedContext.navigateToForumThread(context: context, peerId: peerId, threadId: topicId, messageId: nil, navigationController: navigationController, activateInput: .text).start()
}, error: { _ in
controller?.isInProgress = false
})
}
strongSelf.push(controller)
@ -2631,12 +2635,17 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
let controller = ForumCreateTopicScreen(context: context, peerId: peerId, mode: .create)
controller.navigationPresentation = .modal
controller.completion = { title, fileId in
controller.completion = { [weak controller] title, fileId in
controller?.isInProgress = true
let _ = (context.engine.peers.createForumChannelTopic(id: peerId, title: title, iconColor: ForumCreateTopicScreen.iconColors.randomElement()!, iconFileId: fileId)
|> deliverOnMainQueue).start(next: { topicId in
if let navigationController = (sourceController.navigationController as? NavigationController) {
let _ = context.sharedContext.navigateToForumThread(context: context, peerId: peerId, threadId: topicId, messageId: nil, navigationController: navigationController, activateInput: .text).start()
}
}, error: { _ in
controller?.isInProgress = false
})
}
sourceController.push(controller)
@ -3709,6 +3718,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
var items: [ActionSheetItem] = []
//TODO:localize
items.append(ActionSheetTextItem(title: "This will delete the topic with all its messages", parseMarkdown: true))
items.append(ActionSheetButtonItem(title: "Delete", color: .destructive, action: { [weak self, weak actionSheet] in
actionSheet?.dismissAnimated()
self?.commitDeletePeerThread(peerId: peerId, threadId: threadId)

View File

@ -330,10 +330,10 @@ private func groupReferenceRevealOptions(strings: PresentationStrings, theme: Pr
return options
}
private func forumRevealOptions(strings: PresentationStrings, theme: PresentationTheme, isMuted: Bool?, isClosed: Bool, isPinned: Bool, isEditing: Bool, canManage: Bool) -> [ItemListRevealOption] {
private func forumRevealOptions(strings: PresentationStrings, theme: PresentationTheme, isMuted: Bool?, isClosed: Bool, isPinned: Bool, isEditing: Bool, canPin: Bool, canManage: Bool) -> [ItemListRevealOption] {
var options: [ItemListRevealOption] = []
if !isEditing {
if canManage {
if canPin {
if isPinned {
options.append(ItemListRevealOption(key: RevealOptionKey.unpin.rawValue, title: strings.DialogList_Unpin, icon: unpinIcon, color: theme.list.itemDisclosureActions.constructive.fillColor, textColor: theme.list.itemDisclosureActions.constructive.foregroundColor))
} else {
@ -1852,16 +1852,18 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
if case .forum = item.chatListLocation {
if case let .chat(itemPeer) = contentPeer, case let .channel(channel) = itemPeer.peer {
var canManage = false
if channel.hasPermission(.pinMessages) {
if channel.flags.contains(.isCreator) {
canManage = true
} else if channel.adminRights != nil {
canManage = true
} else if let threadInfo = threadInfo, threadInfo.isOwner {
canManage = true
} else if let threadInfo {
canManage = threadInfo.isOwner
}
var isClosed = false
if let threadInfo {
isClosed = threadInfo.isClosed
}
peerRevealOptions = forumRevealOptions(strings: item.presentationData.strings, theme: item.presentationData.theme, isMuted: (currentMutedIconImage != nil), isClosed: isClosed, isPinned: isPinned, isEditing: item.editing, canManage: canManage)
peerRevealOptions = forumRevealOptions(strings: item.presentationData.strings, theme: item.presentationData.theme, isMuted: (currentMutedIconImage != nil), isClosed: isClosed, isPinned: isPinned, isEditing: item.editing, canPin: channel.hasPermission(.pinMessages), canManage: canManage)
peerLeftRevealOptions = []
} else {
peerRevealOptions = []

View File

@ -270,7 +270,12 @@ public func chatListItemStrings(strings: PresentationStrings, nameDisplayOrder:
}
}
default:
hideAuthor = true
switch action.action {
case .topicCreated, .topicEdited:
hideAuthor = false
default:
hideAuthor = true
}
if let (text, textSpoilers, customEmojiRangesValue) = plainServiceMessageString(strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat, message: message, accountPeerId: accountPeerId, forChatList: true) {
messageText = text
spoilers = textSpoilers

View File

@ -226,7 +226,7 @@ func _internal_createForumChannelTopic(account: Account, peerId: PeerId, title:
}
if let topicId = topicId {
return resolveForumThreads(postbox: account.postbox, network: account.network, ids: [])
return resolveForumThreads(postbox: account.postbox, network: account.network, ids: [MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: Int32(clamping: topicId))])
|> castError(CreateForumChannelTopicError.self)
|> map { _ -> Int64 in
return topicId

View File

@ -31,6 +31,7 @@ swift_library(
"//submodules/TelegramUI/Components/EmojiTextAttachmentView:EmojiTextAttachmentView",
"//submodules/Components/PagerComponent:PagerComponent",
"//submodules/PremiumUI",
"//submodules/ProgressNavigationButtonNode",
],
visibility = [
"//visibility:public",

View File

@ -15,6 +15,7 @@ import MultilineTextComponent
import EmojiStatusComponent
import Postbox
import PremiumUI
import ProgressNavigationButtonNode
private final class TitleFieldComponent: Component {
typealias EnvironmentType = Empty
@ -162,6 +163,8 @@ private final class TitleFieldComponent: Component {
placeholderComponentView.frame = CGRect(origin: CGPoint(x: 62.0, y: floorToScreenPixels((availableSize.height - placeholderSize.height) / 2.0) + 1.0 - UIScreenPixel), size: placeholderSize)
}
self.placeholderView.view?.isHidden = !component.text.isEmpty
let iconSize = self.iconView.update(
transition: .easeInOut(duration: 0.2),
component: AnyComponent(EmojiStatusComponent(
@ -782,10 +785,32 @@ public class ForumCreateTopicScreen: ViewControllerComponentContainer {
case edit(topic: EngineMessageHistoryThread.Info)
}
private let context: AccountContext
private let mode: Mode
private var doneBarItem: UIBarButtonItem?
private var state: (String, Int64?) = ("", nil)
public var completion: (String, Int64?) -> Void = { _, _ in }
public var isInProgress: Bool = false {
didSet {
if self.isInProgress != oldValue {
if self.isInProgress {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
self.navigationItem.rightBarButtonItem = UIBarButtonItem(customDisplayNode: ProgressNavigationButtonNode(color: presentationData.theme.rootController.navigationBar.accentTextColor))
} else {
//TODO:localize
self.navigationItem.rightBarButtonItem = self.doneBarItem
}
}
}
}
public init(context: AccountContext, peerId: EnginePeer.Id, mode: ForumCreateTopicScreen.Mode) {
self.context = context
self.mode = mode
var titleUpdatedImpl: ((String) -> Void)?
var iconUpdatedImpl: ((Int64?) -> Void)?
var openPremiumImpl: (() -> Void)?
@ -802,12 +827,14 @@ public class ForumCreateTopicScreen: ViewControllerComponentContainer {
let title: String
let doneTitle: String
switch mode {
case .create:
title = "New Topic"
doneTitle = "Create"
case .edit:
title = "Edit Topic"
doneTitle = "Done"
case .create:
title = "New Topic"
doneTitle = "Create"
case let .edit(topic):
title = "Edit Topic"
doneTitle = "Done"
self.state = (topic.title, topic.icon)
}
self.title = title
@ -815,20 +842,21 @@ public class ForumCreateTopicScreen: ViewControllerComponentContainer {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(self.cancelPressed))
self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: doneTitle, style: .done, target: self, action: #selector(self.createPressed))
self.navigationItem.rightBarButtonItem?.isEnabled = false
self.doneBarItem = UIBarButtonItem(title: doneTitle, style: .done, target: self, action: #selector(self.createPressed))
self.navigationItem.rightBarButtonItem = self.doneBarItem
self.doneBarItem?.isEnabled = false
if case .edit = mode {
self.navigationItem.rightBarButtonItem?.isEnabled = true
self.doneBarItem?.isEnabled = true
}
titleUpdatedImpl = { [weak self] title in
guard let strongSelf = self else {
guard let self else {
return
}
strongSelf.navigationItem.rightBarButtonItem?.isEnabled = !title.isEmpty
self.doneBarItem?.isEnabled = !title.isEmpty
strongSelf.state = (title, strongSelf.state.1)
self.state = (title, self.state.1)
}
iconUpdatedImpl = { [weak self] fileId in

View File

@ -3038,7 +3038,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}
case let .replyThread(replyThreadMessage):
let peerId = replyThreadMessage.messageId.peerId
strongSelf.navigateToMessage(from: nil, to: .index(MessageIndex(id: MessageId(peerId: peerId, namespace: 0, id: 0), timestamp: timestamp - Int32(NSTimeZone.local.secondsFromGMT()))), scrollPosition: .bottom(0.0), rememberInStack: false, animated: true, completion: nil)
strongSelf.navigateToMessage(from: nil, to: .index(MessageIndex(id: MessageId(peerId: peerId, namespace: 0, id: 0), timestamp: timestamp - Int32(NSTimeZone.local.secondsFromGMT()))), scrollPosition: .bottom(0.0), rememberInStack: false, forceInCurrentChat: true, animated: true, completion: nil)
case .feed:
//TODO:implement
break

View File

@ -262,7 +262,9 @@ func canReplyInChat(_ chatPresentationInterfaceState: ChatPresentationInterfaceS
if let threadData = chatPresentationInterfaceState.threadData {
if threadData.isClosed {
var canManage = false
if channel.hasPermission(.pinMessages) {
if channel.flags.contains(.isCreator) {
canManage = true
} else if channel.adminRights != nil {
canManage = true
} else if threadData.isOwn {
canManage = true

View File

@ -162,7 +162,9 @@ func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState
if let threadData = chatPresentationInterfaceState.threadData {
if threadData.isClosed {
var canManage = false
if channel.hasPermission(.pinMessages) {
if channel.flags.contains(.isCreator) {
canManage = true
} else if channel.adminRights != nil {
canManage = true
} else if threadData.isOwn {
canManage = true

View File

@ -61,7 +61,9 @@ func titlePanelForChatPresentationInterfaceState(_ chatPresentationInterfaceStat
if let threadData = chatPresentationInterfaceState.threadData {
if threadData.isClosed {
var canManage = false
if channel.hasPermission(.pinMessages) {
if channel.flags.contains(.isCreator) {
canManage = true
} else if channel.adminRights != nil {
canManage = true
} else if threadData.isOwn {
canManage = true

View File

@ -102,7 +102,9 @@ private func peerButtons(_ state: ChatPresentationInterfaceState) -> [ChatReport
if let threadData = state.threadData {
if threadData.isClosed {
var canManage = false
if channel.hasPermission(.pinMessages) {
if channel.flags.contains(.isCreator) {
canManage = true
} else if channel.adminRights != nil {
canManage = true
} else if threadData.isOwn {
canManage = true
@ -597,7 +599,7 @@ final class ChatReportPeerTitlePanelNode: ChatTitleAccessoryPanelNode {
if let renderedPeer = interfaceState.renderedPeer {
chatPeer = renderedPeer.peers[renderedPeer.peerId]
}
if let chatPeer = chatPeer, let invitedBy = interfaceState.contactStatus?.invitedBy {
if let chatPeer = chatPeer, (updatedButtons.contains(.block) || updatedButtons.contains(.reportSpam) || updatedButtons.contains(.reportUserSpam)), let invitedBy = interfaceState.contactStatus?.invitedBy {
var inviteInfoTransition = transition
let inviteInfoNode: ChatInfoTitlePanelInviteInfoNode
if let current = self.inviteInfoNode {

View File

@ -1189,7 +1189,7 @@ func peerInfoHeaderButtons(peer: Peer?, cachedData: CachedPeerData?, isOpenedFro
return result
}
func peerInfoCanEdit(peer: Peer?, cachedData: CachedPeerData?, isContact: Bool?) -> Bool {
func peerInfoCanEdit(peer: Peer?, threadData: MessageHistoryThreadData?, cachedData: CachedPeerData?, isContact: Bool?) -> Bool {
if let user = peer as? TelegramUser {
if user.isDeleted {
return false
@ -1199,14 +1199,26 @@ func peerInfoCanEdit(peer: Peer?, cachedData: CachedPeerData?, isContact: Bool?)
}
return true
} else if let peer = peer as? TelegramChannel {
if peer.flags.contains(.isCreator) {
return true
} else if peer.hasPermission(.changeInfo) {
return true
} else if let _ = peer.adminRights {
return true
if peer.flags.contains(.isForum) {
if peer.flags.contains(.isCreator) {
return true
} else if let threadData = threadData, threadData.isOwnedByMe {
return true
} else if let _ = peer.adminRights {
return true
} else {
return false
}
} else {
if peer.flags.contains(.isCreator) {
return true
} else if peer.hasPermission(.changeInfo) {
return true
} else if let _ = peer.adminRights {
return true
}
return false
}
return false
} else if let peer = peer as? TelegramGroup {
if case .creator = peer.role {
return true

View File

@ -8183,7 +8183,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
leftNavigationButtons.append(PeerInfoHeaderNavigationButtonSpec(key: .qrCode, isForExpandedView: false))
rightNavigationButtons.append(PeerInfoHeaderNavigationButtonSpec(key: .edit, isForExpandedView: false))
rightNavigationButtons.append(PeerInfoHeaderNavigationButtonSpec(key: .search, isForExpandedView: true))
} else if peerInfoCanEdit(peer: self.data?.peer, cachedData: self.data?.cachedData, isContact: self.data?.isContact) {
} else if peerInfoCanEdit(peer: self.data?.peer, threadData: self.data?.threadData, cachedData: self.data?.cachedData, isContact: self.data?.isContact) {
rightNavigationButtons.append(PeerInfoHeaderNavigationButtonSpec(key: .edit, isForExpandedView: false))
}
if self.state.selectedMessageIds == nil {
@ -9602,6 +9602,31 @@ func presentAddMembersImpl(context: AccountContext, updatedPresentationData: (in
contactsController?.dismiss()
}, completed: {
contactsController?.dismiss()
let mappedPeerIds: [EnginePeer.Id] = peers.compactMap { peer -> EnginePeer.Id? in
switch peer {
case let .peer(id):
return id
default:
return nil
}
}
if !mappedPeerIds.isEmpty {
let _ = (context.engine.data.get(EngineDataMap(mappedPeerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init(id:))))
|> deliverOnMainQueue).start(next: { maybePeers in
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let peers = maybePeers.compactMap { $0.value }
//TODO:localize
let text: String
if peers.count == 1 {
text = "**\(peers[0].displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder))** added to the group."
} else {
text = "**\(peers.count)** members added to the group."
}
parentController?.present(UndoOverlayController(presentationData: presentationData, content: .peers(context: context, peers: peers, title: nil, text: text, customUndoText: nil), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current)
})
}
}))
}))
contactsController.dismissed = {

View File

@ -27,6 +27,7 @@ swift_library(
"//submodules/AvatarNode:AvatarNode",
"//submodules/AccountContext:AccountContext",
"//submodules/ComponentFlow:ComponentFlow",
"//submodules/AnimatedAvatarSetNode:AnimatedAvatarSetNode",
],
visibility = [
"//visibility:public",

View File

@ -42,6 +42,7 @@ public enum UndoOverlayContent {
case image(image: UIImage, text: String)
case notificationSoundAdded(title: String, text: String, action: (() -> Void)?)
case universal(animation: String, scale: CGFloat, colors: [String: UIColor], title: String?, text: String, customUndoText: String?)
case peers(context: AccountContext, peers: [EnginePeer], title: String?, text: String, customUndoText: String?)
}
public enum UndoOverlayAction {

View File

@ -17,6 +17,7 @@ import AnimationUI
import StickerResources
import AvatarNode
import AccountContext
import AnimatedAvatarSetNode
final class UndoOverlayControllerNode: ViewControllerTracingNode {
private let elevatedLayout: Bool
@ -25,6 +26,8 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
private let timerTextNode: ImmediateTextNode
private let avatarNode: AvatarNode?
private let iconNode: ASImageNode?
private var multiAvatarsNode: AnimatedAvatarSetNode?
private var multiAvatarsSize: CGSize?
private var iconImageSize: CGSize?
private let iconCheckNode: RadialStatusNode?
private let animationNode: AnimationNode?
@ -872,6 +875,44 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
self.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(14.0), textColor: .white)
displayUndo = true
self.originalRemainingSeconds = 5
case let .peers(context, peers, title, text, customUndoText):
self.avatarNode = nil
let multiAvatarsNode = AnimatedAvatarSetNode()
self.multiAvatarsNode = multiAvatarsNode
let avatarsContext = AnimatedAvatarSetContext()
self.multiAvatarsSize = multiAvatarsNode.update(context: context, content: avatarsContext.update(peers: peers, animated: false), itemSize: CGSize(width: 28.0, height: 28.0), animated: false, synchronousLoad: false)
self.iconNode = nil
self.iconCheckNode = nil
self.animationNode = nil
self.animatedStickerNode = nil
if let title = title {
self.titleNode.attributedText = NSAttributedString(string: title, font: Font.semibold(14.0), textColor: .white)
} else {
self.titleNode.attributedText = nil
}
let body = MarkdownAttributeSet(font: Font.regular(14.0), textColor: .white)
let bold = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: .white)
let link = MarkdownAttributeSet(font: Font.regular(14.0), textColor: undoTextColor)
let attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: body, bold: bold, link: link, linkAttribute: { contents in
return ("URL", contents)
}), textAlignment: .natural)
self.textNode.attributedText = attributedText
if text.contains("](") {
isUserInteractionEnabled = true
}
self.originalRemainingSeconds = isUserInteractionEnabled ? 5 : 3
self.textNode.maximumNumberOfLines = 5
if let customUndoText = customUndoText {
undoText = customUndoText
displayUndo = true
} else {
displayUndo = false
}
}
self.remainingSeconds = self.originalRemainingSeconds
@ -900,7 +941,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
switch content {
case .removedChat:
self.panelWrapperNode.addSubnode(self.timerTextNode)
case .archivedChat, .hidArchive, .revealedArchive, .autoDelete, .succeed, .emoji, .swipeToReply, .actionSucceeded, .stickersModified, .chatAddedToFolder, .chatRemovedFromFolder, .messagesUnpinned, .setProximityAlert, .invitedToVoiceChat, .linkCopied, .banned, .importedMessage, .audioRate, .forward, .gigagroupConversion, .linkRevoked, .voiceChatRecording, .voiceChatFlag, .voiceChatCanSpeak, .copy, .mediaSaved, .paymentSent, .image, .inviteRequestSent, .notificationSoundAdded, .universal:
case .archivedChat, .hidArchive, .revealedArchive, .autoDelete, .succeed, .emoji, .swipeToReply, .actionSucceeded, .stickersModified, .chatAddedToFolder, .chatRemovedFromFolder, .messagesUnpinned, .setProximityAlert, .invitedToVoiceChat, .linkCopied, .banned, .importedMessage, .audioRate, .forward, .gigagroupConversion, .linkRevoked, .voiceChatRecording, .voiceChatFlag, .voiceChatCanSpeak, .copy, .mediaSaved, .paymentSent, .image, .inviteRequestSent, .notificationSoundAdded, .universal, .peers:
if self.textNode.tapAttributeAction != nil || displayUndo {
self.isUserInteractionEnabled = true
} else {
@ -927,6 +968,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
self.animationNode?.isUserInteractionEnabled = false
self.iconCheckNode?.isUserInteractionEnabled = false
self.avatarNode?.isUserInteractionEnabled = false
self.multiAvatarsNode?.isUserInteractionEnabled = false
self.slotMachineNode?.isUserInteractionEnabled = false
self.animatedStickerNode?.isUserInteractionEnabled = false
@ -938,6 +980,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
self.animatedStickerNode.flatMap(self.panelWrapperNode.addSubnode)
self.slotMachineNode.flatMap(self.panelWrapperNode.addSubnode)
self.avatarNode.flatMap(self.panelWrapperNode.addSubnode)
self.multiAvatarsNode.flatMap(self.panelWrapperNode.addSubnode)
self.panelWrapperNode.addSubnode(self.buttonNode)
self.panelWrapperNode.addSubnode(self.titleNode)
self.panelWrapperNode.addSubnode(self.textNode)
@ -1088,7 +1131,10 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
if iconSize.width > leftInset {
leftInset = iconSize.width - 8.0
}
} else if let multiAvatarsSize = self.multiAvatarsSize {
leftInset = 13.0 + multiAvatarsSize.width + 20.0
}
let rightInset: CGFloat = 16.0
var contentHeight: CGFloat = 20.0
@ -1228,6 +1274,11 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
let avatarSize: CGFloat = 30.0
transition.updateFrame(node: avatarNode, frame: CGRect(origin: CGPoint(x: floor((leftInset - avatarSize) / 2.0), y: floor((contentHeight - avatarSize) / 2.0)), size: CGSize(width: avatarSize, height: avatarSize)))
}
if let multiAvatarsNode = self.multiAvatarsNode, let multiAvatarsSize = self.multiAvatarsSize {
let avatarsFrame = CGRect(origin: CGPoint(x: 13.0, y: floor((contentHeight - multiAvatarsSize.height) / 2.0) + verticalOffset), size: multiAvatarsSize)
transition.updateFrame(node: multiAvatarsNode, frame: avatarsFrame)
}
}
func animateIn(asReplacement: Bool) {