Swiftgram/submodules/JoinLinkPreviewUI/Sources/JoinLinkPreviewPeerContentNode.swift
2024-06-12 23:04:04 +04:00

443 lines
19 KiB
Swift

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, 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<Empty>?
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)
if case let .request(isGroup, _, _, about, _, _, _, _) = content {
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<Empty>
var avatarIconTransition = ComponentTransition(transition)
if let current = self.avatarIcon {
avatarIcon = current
} else {
avatarIconTransition = avatarIconTransition.withAnimation(.none)
avatarIcon = ComponentView<Empty>()
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
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 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
}
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) {
}
}