import Foundation import UIKit import AsyncDisplayKit import Display import TelegramCore import TelegramPresentationData import AvatarNode import AccountContext import SelectablePeerNode import ShareController import SolidRoundedButtonNode import ActivityIndicator import ComponentFlow import EmojiStatusComponent private let avatarFont = avatarPlaceholderFont(size: 42.0) private final class MoreNode: ASDisplayNode { private let avatarNode = AvatarNode(font: Font.regular(24.0)) init(count: Int) { super.init() self.addSubnode(self.avatarNode) self.avatarNode.setCustomLetters(["+\(count)"]) } func updateLayout(size: CGSize) { self.avatarNode.frame = CGRect(origin: CGPoint(x: floor((size.width - 60.0) / 2.0), y: 4.0), size: CGSize(width: 60.0, height: 60.0)) } } final class JoinLinkPreviewPeerContentNode: ASDisplayNode, ShareContentContainerNode { enum Content { case invite(isGroup: Bool, image: TelegramMediaImageRepresentation?, title: String, about: String?, memberCount: Int32, members: [EnginePeer]) case request(isGroup: Bool, image: TelegramMediaImageRepresentation?, title: String, about: String?, memberCount: Int32, isVerified: Bool, isFake: Bool, isScam: Bool) var isGroup: Bool { switch self { case let .invite(isGroup, _, _, _, _, _), let .request(isGroup, _, _, _, _, _, _, _): return isGroup } } var image: TelegramMediaImageRepresentation? { switch self { case let .invite(_, image, _, _, _, _), let .request(_, image, _, _, _, _, _, _): return image } } var title: String { switch self { case let .invite(_, _, title, _, _, _), let .request(_, _, title, _, _, _, _, _): return title } } var memberCount: Int32 { switch self { case let .invite(_, _, _, _, memberCount, _), let .request(_, _, _, _, memberCount, _, _, _): return memberCount } } var isVerified: Bool { switch self { case .invite: return false case let .request(_, _, _, _, _, isVerified, _, _): return isVerified } } var isFake: Bool { switch self { case .invite: return false case let .request(_, _, _, _, _, _, isFake, _): return isFake } } var isScam: Bool { switch self { case .invite: return false case let .request(_, _, _, _, _, _, _, isScam): return isScam } } } private var contentDidBeginDragging: (() -> Void)? private var contentOffsetUpdated: ((CGFloat, ContainedViewLayoutTransition) -> Void)? private let avatarNode: AvatarNode private let titleNode: ImmediateTextNode private var avatarIcon: ComponentView? private let countNode: ASTextNode private let aboutNode: ASTextNode private let descriptionNode: ASTextNode private let peersScrollNode: ASScrollNode private let peerNodes: [SelectablePeerNode] private let moreNode: MoreNode? private let actionButtonNode: SolidRoundedButtonNode private let context: AccountContext private let content: Content private let theme: PresentationTheme private let strings: PresentationStrings var join: (() -> Void)? init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, content: JoinLinkPreviewPeerContentNode.Content) { self.context = context self.content = content self.theme = theme self.strings = strings self.avatarNode = AvatarNode(font: avatarFont) self.titleNode = ImmediateTextNode() self.titleNode.maximumNumberOfLines = 2 self.titleNode.textAlignment = .center self.countNode = ASTextNode() self.aboutNode = ASTextNode() self.aboutNode.maximumNumberOfLines = 8 self.aboutNode.textAlignment = .center self.descriptionNode = ASTextNode() self.descriptionNode.maximumNumberOfLines = 3 self.descriptionNode.textAlignment = .center self.peersScrollNode = ASScrollNode() self.peersScrollNode.view.showsHorizontalScrollIndicator = false self.actionButtonNode = SolidRoundedButtonNode(theme: SolidRoundedButtonTheme(theme: theme), height: 52.0, cornerRadius: 11.0, gloss: false) let itemTheme = SelectablePeerNodeTheme(textColor: theme.actionSheet.primaryTextColor, secretTextColor: .green, selectedTextColor: theme.actionSheet.controlAccentColor, checkBackgroundColor: theme.actionSheet.opaqueItemBackgroundColor, checkFillColor: theme.actionSheet.controlAccentColor, checkColor: theme.actionSheet.opaqueItemBackgroundColor, avatarPlaceholderColor: theme.list.mediaPlaceholderColor) if case let .invite(isGroup, _, _, _, memberCount, members) = content { self.peerNodes = members.compactMap { peer in guard peer.id != context.account.peerId else { return nil } let node = SelectablePeerNode() node.setup(context: context, theme: theme, strings: strings, peer: EngineRenderedPeer(peer: peer), requiresPremiumForMessaging: false, synchronousLoad: false) node.theme = itemTheme return node } if members.count < Int(memberCount) { self.moreNode = MoreNode(count: Int(memberCount) - members.count) } else { self.moreNode = nil } self.actionButtonNode.title = isGroup ? strings.Invitation_JoinGroup : strings.Channel_JoinChannel } else { self.peerNodes = [] self.moreNode = nil self.actionButtonNode.title = content.isGroup ? strings.MemberRequests_RequestToJoinGroup : strings.MemberRequests_RequestToJoinChannel } super.init() let peer = TelegramGroup(id: EnginePeer.Id(0), title: content.title, photo: content.image.flatMap { [$0] } ?? [], participantCount: Int(content.memberCount), role: .member, membership: .Left, flags: [], defaultBannedRights: nil, migrationReference: nil, creationDate: 0, version: 0) self.addSubnode(self.avatarNode) self.avatarNode.setPeer(context: context, theme: theme, peer: EnginePeer(peer), emptyColor: theme.list.mediaPlaceholderColor) self.addSubnode(self.titleNode) self.titleNode.attributedText = NSAttributedString(string: content.title, font: Font.semibold(24.0), textColor: theme.actionSheet.primaryTextColor, paragraphAlignment: .center) self.addSubnode(self.countNode) let membersString: String if content.isGroup { if case let .invite(_, _, _, _, memberCount, members) = content, !members.isEmpty { membersString = strings.Invitation_Members(memberCount) } else { membersString = strings.Conversation_StatusMembers(content.memberCount) } } else { membersString = strings.Conversation_StatusSubscribers(content.memberCount) } self.countNode.attributedText = NSAttributedString(string: membersString, font: Font.regular(15.0), textColor: theme.actionSheet.secondaryTextColor, paragraphAlignment: .center) if !self.peerNodes.isEmpty { for peerNode in peerNodes { self.peersScrollNode.addSubnode(peerNode) } self.addSubnode(self.peersScrollNode) } self.moreNode.flatMap(self.peersScrollNode.addSubnode) switch content { case let .invite(_, _, _, about, _, _): if let about = about, !about.isEmpty { self.aboutNode.attributedText = NSAttributedString(string: about, font: Font.regular(17.0), textColor: theme.actionSheet.primaryTextColor, paragraphAlignment: .center) self.addSubnode(self.aboutNode) } case let .request(isGroup, _, _, about, _, _, _, _): if let about = about, !about.isEmpty { self.aboutNode.attributedText = NSAttributedString(string: about, font: Font.regular(17.0), textColor: theme.actionSheet.primaryTextColor, paragraphAlignment: .center) self.addSubnode(self.aboutNode) } self.descriptionNode.attributedText = NSAttributedString(string: isGroup ? strings.MemberRequests_RequestToJoinDescriptionGroup : strings.MemberRequests_RequestToJoinDescriptionChannel, font: Font.regular(15.0), textColor: theme.actionSheet.secondaryTextColor, paragraphAlignment: .center) self.addSubnode(self.descriptionNode) } self.actionButtonNode.pressed = { [weak self] in self?.join?() self?.actionButtonNode.transitionToProgress() } self.addSubnode(self.actionButtonNode) } func activate() { } func deactivate() { } func setEnsurePeerVisibleOnLayout(_ peerId: EnginePeer.Id?) { } func setDidBeginDragging(_ f: (() -> Void)?) { self.contentDidBeginDragging = f } func setContentOffsetUpdated(_ f: ((CGFloat, ContainedViewLayoutTransition) -> Void)?) { self.contentOffsetUpdated = f } func updateTheme(_ theme: PresentationTheme) { } func updateLayout(size: CGSize, isLandscape: Bool, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) { let showPeers = !self.peerNodes.isEmpty && !isLandscape var nodeHeight: CGFloat = (!showPeers ? 236.0 : 320.0) let paddedSize = CGSize(width: size.width - 60.0, height: size.height) self.peersScrollNode.isHidden = !showPeers var aboutSize: CGSize? var descriptionSize: CGSize? if self.aboutNode.supernode != nil { if isLandscape { self.aboutNode.maximumNumberOfLines = 3 } else { self.aboutNode.maximumNumberOfLines = 8 } let measuredSize = self.aboutNode.measure(paddedSize) nodeHeight += measuredSize.height + 20.0 aboutSize = measuredSize } if isLandscape { self.descriptionNode.removeFromSupernode() } else if self.descriptionNode.supernode == nil { self.addSubnode(self.descriptionNode) } if self.descriptionNode.supernode != nil { let measuredSize = self.descriptionNode.measure(paddedSize) nodeHeight += measuredSize.height + 20.0 + 10.0 descriptionSize = measuredSize } let constrainSize = CGSize(width: size.width - 32.0, height: size.height) var statusIcon: EmojiStatusComponent.Content? var constrainedTextSize = constrainSize if self.content.isFake { statusIcon = .text(color: self.theme.chat.message.incoming.scamColor, string: self.strings.Message_FakeAccount.uppercased()) constrainedTextSize.width -= 32.0 } else if self.content.isScam { statusIcon = .text(color: self.theme.chat.message.incoming.scamColor, string: self.strings.Message_ScamAccount.uppercased()) constrainedTextSize.width -= 32.0 } else if self.content.isVerified { statusIcon = .verified(fillColor: self.theme.list.itemCheckColors.fillColor, foregroundColor: self.theme.list.itemCheckColors.foregroundColor, sizeType: .compact) constrainedTextSize.width -= 24.0 } let titleInfo = self.titleNode.updateLayoutFullInfo(constrainedTextSize) let titleSize = titleInfo.size nodeHeight += titleSize.height let verticalOrigin = size.height - nodeHeight let avatarSize: CGFloat = 100.0 transition.updateFrame(node: self.avatarNode, frame: CGRect(origin: CGPoint(x: floor((size.width - avatarSize) / 2.0), y: verticalOrigin + 32.0), size: CGSize(width: avatarSize, height: avatarSize))) let titleFrame = CGRect(origin: CGPoint(x: floor((size.width - titleSize.width) / 2.0), y: verticalOrigin + 27.0 + avatarSize + 15.0), size: titleSize) transition.updateFrame(node: self.titleNode, frame: titleFrame) if let statusIcon, let lastLine = titleInfo.linesRects().last { let animationCache = self.context.animationCache let animationRenderer = self.context.animationRenderer let avatarIcon: ComponentView var avatarIconTransition = ComponentTransition(transition) if let current = self.avatarIcon { avatarIcon = current } else { avatarIconTransition = avatarIconTransition.withAnimation(.none) avatarIcon = ComponentView() self.avatarIcon = avatarIcon } let avatarIconComponent = EmojiStatusComponent( context: self.context, animationCache: animationCache, animationRenderer: animationRenderer, content: statusIcon, isVisibleForAnimations: true, action: nil, emojiFileUpdated: nil ) let iconSize = avatarIcon.update( transition: avatarIconTransition, component: AnyComponent(avatarIconComponent), environment: {}, containerSize: CGSize(width: 20.0, height: 20.0) ) if let avatarIconView = avatarIcon.view { if avatarIconView.superview == nil { avatarIconView.isUserInteractionEnabled = false self.view.addSubview(avatarIconView) } avatarIconTransition.setFrame(view: avatarIconView, frame: CGRect(origin: CGPoint(x: titleFrame.minX + floor((titleSize.width - lastLine.width) * 0.5) + lastLine.width + 5.0, y: 8.0 + titleFrame.minY + floorToScreenPixels(lastLine.midY - iconSize.height / 2.0) - lastLine.height), size: iconSize)) } } else if let avatarIcon = self.avatarIcon { self.avatarIcon = nil avatarIcon.view?.removeFromSuperview() } let countSize = self.countNode.measure(constrainSize) transition.updateFrame(node: self.countNode, frame: CGRect(origin: CGPoint(x: floor((size.width - countSize.width) / 2.0), y: verticalOrigin + 27.0 + avatarSize + 15.0 + titleSize.height + 3.0), size: countSize)) var verticalOffset = verticalOrigin + 27.0 + avatarSize + 15.0 + titleSize.height + 3.0 + countSize.height + 18.0 let peerSize = CGSize(width: 85.0, height: 95.0) let peerInset: CGFloat = 10.0 var peerOffset = peerInset for node in self.peerNodes { node.frame = CGRect(origin: CGPoint(x: peerOffset, y: 0.0), size: peerSize) peerOffset += peerSize.width } if let moreNode = self.moreNode { moreNode.updateLayout(size: peerSize) moreNode.frame = CGRect(origin: CGPoint(x: peerOffset, y: 0.0), size: peerSize) peerOffset += peerSize.width } self.peersScrollNode.view.contentSize = CGSize(width: CGFloat(self.peerNodes.count) * peerSize.width + (self.moreNode != nil ? peerSize.width : 0.0) + peerInset * 2.0, height: peerSize.height) transition.updateFrame(node: self.peersScrollNode, frame: CGRect(origin: CGPoint(x: 0.0, y: verticalOrigin + 27.0 + avatarSize + 15.0 + titleSize.height + 3.0 + countSize.height + 12.0), size: CGSize(width: size.width, height: peerSize.height))) if showPeers { verticalOffset += 100.0 } if let aboutSize = aboutSize { transition.updateFrame(node: self.aboutNode, frame: CGRect(origin: CGPoint(x: floor((size.width - aboutSize.width) / 2.0), y: verticalOffset), size: aboutSize)) verticalOffset += aboutSize.height + 20.0 } let buttonInset: CGFloat = 16.0 let actionButtonHeight = self.actionButtonNode.updateLayout(width: size.width - buttonInset * 2.0, transition: transition) transition.updateFrame(node: self.actionButtonNode, frame: CGRect(x: buttonInset, y: verticalOffset, width: size.width, height: actionButtonHeight)) verticalOffset += actionButtonHeight + 20.0 if let descriptionSize = descriptionSize { transition.updateFrame(node: self.descriptionNode, frame: CGRect(origin: CGPoint(x: floor((size.width - descriptionSize.width) / 2.0), y: verticalOffset), size: descriptionSize)) } self.contentOffsetUpdated?(-size.height + nodeHeight, transition) } func updateSelectedPeers(animated: Bool) { } } public enum ShareLoadingState { case preparing case progress(Float) case done } public final class JoinLinkPreviewLoadingContainerNode: ASDisplayNode, ShareContentContainerNode { private var contentDidBeginDragging: (() -> Void)? private var contentOffsetUpdated: ((CGFloat, ContainedViewLayoutTransition) -> Void)? private var theme: PresentationTheme private let activityIndicator: ActivityIndicator public init(theme: PresentationTheme) { self.theme = theme self.activityIndicator = ActivityIndicator(type: .custom(theme.actionSheet.controlAccentColor, 22.0, 2.0, false)) super.init() self.addSubnode(self.activityIndicator) } public func activate() { } public func deactivate() { } public func setEnsurePeerVisibleOnLayout(_ peerId: EnginePeer.Id?) { } public func setDidBeginDragging(_ f: (() -> Void)?) { self.contentDidBeginDragging = f } public func setContentOffsetUpdated(_ f: ((CGFloat, ContainedViewLayoutTransition) -> Void)?) { self.contentOffsetUpdated = f } public func updateTheme(_ theme: PresentationTheme) { self.theme = theme self.activityIndicator.type = .custom(theme.actionSheet.controlAccentColor, 22.0, 2.0, false) } public func updateLayout(size: CGSize, isLandscape: Bool, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) { let nodeHeight: CGFloat = 125.0 let indicatorSize = self.activityIndicator.calculateSizeThatFits(size) let indicatorFrame = CGRect(origin: CGPoint(x: floor((size.width - indicatorSize.width) / 2.0), y: size.height - nodeHeight + floor((nodeHeight - indicatorSize.height) / 2.0)), size: indicatorSize) transition.updateFrame(node: self.activityIndicator, frame: indicatorFrame) self.contentOffsetUpdated?(-size.height + nodeHeight, transition) } public func updateSelectedPeers(animated: Bool) { } }