Files
ichmagmaus111andClaude Opus 5.5 0622057a4f Ghostgram: open-source release
Ghostgram for iOS, rebuilt from scratch on Telegram iOS 13.0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 17:57:09 +01:00

254 lines
15 KiB
Swift

import Foundation
import LottieSettings
import UIKit
import AsyncDisplayKit
import Display
import SwiftSignalKit
import TelegramCore
import AccountContext
import TelegramPresentationData
import TelegramUIPreferences
import TextFormat
import WallpaperBackgroundNode
import AvatarNode
import ChatMessageBubbleContentNode
import ChatMessageItemCommon
import ChatControllerInteraction
import Markdown
private func communityChangedPeer(message: EngineRawMessage) -> (EnginePeer.Id, TelegramCommunity)? {
for media in message.media {
guard let action = media as? TelegramMediaAction else {
continue
}
if case let .communityChanged(communityId) = action.action, let communityId, let community = message.peers[communityId] as? TelegramCommunity {
return (communityId, community)
}
}
return nil
}
public class ChatMessageCommunityChangedBubbleContentNode: ChatMessageBubbleContentNode {
private var mediaBackgroundContent: WallpaperBubbleBackgroundNode?
private let mediaBackgroundNode: NavigationBackgroundNode
private let avatarShadowNode: ASImageNode
private let avatarNode: AvatarNode
private let subtitleNode: TextNode
private let buttonNode: HighlightTrackingButtonNode
private let buttonTitleNode: TextNode
private var absoluteRect: (CGRect, CGSize)?
required public init(lottieSettings: LottieRenderingSettings) {
self.mediaBackgroundNode = NavigationBackgroundNode(color: .clear)
self.mediaBackgroundNode.clipsToBounds = true
self.mediaBackgroundNode.cornerRadius = 24.0
self.avatarShadowNode = ASImageNode()
self.avatarShadowNode.displaysAsynchronously = false
self.avatarShadowNode.displayWithoutProcessing = true
self.avatarShadowNode.isHidden = true
self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 28.0))
self.avatarNode.isUserInteractionEnabled = false
self.subtitleNode = TextNode()
self.subtitleNode.isUserInteractionEnabled = false
self.subtitleNode.displaysAsynchronously = false
self.buttonNode = HighlightTrackingButtonNode()
self.buttonNode.clipsToBounds = true
self.buttonNode.cornerRadius = 17.0
self.buttonTitleNode = TextNode()
self.buttonTitleNode.isUserInteractionEnabled = false
self.buttonTitleNode.displaysAsynchronously = false
super.init(lottieSettings: lottieSettings)
self.addSubnode(self.mediaBackgroundNode)
self.addSubnode(self.avatarShadowNode)
self.addSubnode(self.avatarNode)
self.addSubnode(self.subtitleNode)
self.addSubnode(self.buttonNode)
self.addSubnode(self.buttonTitleNode)
self.buttonNode.highligthedChanged = { [weak self] highlighted in
guard let strongSelf = self else {
return
}
if highlighted {
strongSelf.buttonNode.layer.removeAnimation(forKey: "opacity")
strongSelf.buttonNode.alpha = 0.4
strongSelf.buttonTitleNode.layer.removeAnimation(forKey: "opacity")
strongSelf.buttonTitleNode.alpha = 0.4
} else {
strongSelf.buttonNode.alpha = 1.0
strongSelf.buttonNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
strongSelf.buttonTitleNode.alpha = 1.0
strongSelf.buttonTitleNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
}
}
self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside)
}
required public init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc private func buttonPressed() {
guard let item = self.item, let (communityId, _) = communityChangedPeer(message: item.message) else {
return
}
let controller = item.context.sharedContext.makeCommunityViewScreen(context: item.context, communityId: communityId, mode: .sheet)
if let navigationController = item.controllerInteraction.navigationController() {
navigationController.pushViewController(controller)
} else {
item.controllerInteraction.presentControllerInCurrent(controller, nil)
}
}
override public func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize, _ avatarInset: CGFloat) -> (ChatMessageBubbleContentProperties, unboundSize: CGSize?, maxWidth: CGFloat, layout: (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))) {
let makeSubtitleLayout = TextNode.asyncLayout(self.subtitleNode)
let makeButtonTitleLayout = TextNode.asyncLayout(self.buttonTitleNode)
return { [weak self] item, _, _, _, _, _ in
let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: true, headerSpacing: 0.0, hidesBackground: .always, forceFullCorners: false, forceAlignment: .center)
return (contentProperties, nil, CGFloat.greatestFiniteMagnitude, { [weak self] constrainedSize, _ in
let width: CGFloat = 180.0
let imageSize = CGSize(width: 80.0, height: 80.0)
let primaryTextColor = serviceMessageColorComponents(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper).primaryText
let community = communityChangedPeer(message: item.message)?.1
let authorName = item.message.author.flatMap { peer -> String? in
let title = EnginePeer(peer).compactDisplayTitle
return title.isEmpty ? nil : title
} ?? ""
var isGroup = false
let messagePeer = item.message.peers[item.message.id.peerId]
let isBot = messagePeer is TelegramUser
if let channel = messagePeer as? TelegramChannel, case .group = channel.info {
isGroup = true
}
let text: String
if isBot {
text = item.presentationData.strings.Notification_CommunityAddedBot("**\(community?.title ?? "")**").string
} else if isGroup {
if item.message.author?.id == item.context.account.peerId {
text = item.presentationData.strings.Notification_CommunityAddedGroupYou("**\(community?.title ?? "")**").string
} else {
if item.message.author?.id.namespace != Namespaces.Peer.CloudUser {
text = item.presentationData.strings.Notification_CommunityAddedGroupUnknown("**\(community?.title ?? "")**").string
} else {
text = item.presentationData.strings.Notification_CommunityAddedGroup("**\(authorName)**", "**\(community?.title ?? "")**").string
}
}
} else {
text = item.presentationData.strings.Notification_CommunityAddedChannel("**\(community?.title ?? "")**").string
}
let attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(
body: MarkdownAttributeSet(font: Font.regular(13.0), textColor: primaryTextColor),
bold: MarkdownAttributeSet(font: Font.semibold(13.0), textColor: primaryTextColor),
link: MarkdownAttributeSet(font: Font.regular(13.0), textColor: primaryTextColor),
linkAttribute: { url in
return ("URL", url)
}
), textAlignment: .center)
let (subtitleLayout, subtitleApply) = makeSubtitleLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: width - 32.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
let (buttonTitleLayout, buttonTitleApply) = makeButtonTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.presentationData.strings.Community_CommunityAdded_View, font: Font.semibold(15.0), textColor: primaryTextColor, paragraphAlignment: .center), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: width - 32.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
let backgroundSize = CGSize(width: width, height: subtitleLayout.size.height + 165.0)
return (backgroundSize.width, { [weak self] boundingWidth in
return (backgroundSize, { [weak self] _, synchronousLoads, _ in
guard let strongSelf = self else {
return
}
strongSelf.item = item
let mediaBackgroundFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((boundingWidth - width) / 2.0), y: 0.0), size: backgroundSize)
strongSelf.mediaBackgroundNode.frame = mediaBackgroundFrame
strongSelf.mediaBackgroundNode.updateColor(color: selectDateFillStaticColor(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper), enableBlur: item.controllerInteraction.enableFullTranslucency && dateFillNeedsBlur(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper), transition: .immediate)
strongSelf.mediaBackgroundNode.update(size: mediaBackgroundFrame.size, transition: .immediate)
let buttonColor = item.presentationData.theme.theme.overallDarkAppearance ? UIColor(rgb: 0xffffff, alpha: 0.12) : UIColor(rgb: 0x000000, alpha: 0.12)
strongSelf.buttonNode.backgroundColor = buttonColor
let avatarFrame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - imageSize.width) / 2.0), y: 15.0), size: imageSize)
if let shadowImage = UIImage(bundleImageName: "Components/CommunityShadow"), community != nil {
strongSelf.avatarShadowNode.isHidden = false
strongSelf.avatarShadowNode.image = generateTintedImage(image: shadowImage, color: buttonColor.withMultipliedAlpha(4.0))
let aspectRatio = shadowImage.size.width / shadowImage.size.height
let shadowSize = CGSize(width: imageSize.width * aspectRatio, height: imageSize.width)
strongSelf.avatarShadowNode.frame = shadowSize.centered(around: avatarFrame.center).offsetBy(dx: -11.0, dy: 0.0)
} else {
strongSelf.avatarShadowNode.isHidden = true
}
strongSelf.avatarNode.frame = avatarFrame
if let community {
strongSelf.avatarNode.setPeer(context: item.context, theme: item.presentationData.theme.theme, peer: EnginePeer(community), clipStyle: .roundedRect, synchronousLoad: synchronousLoads, displayDimensions: imageSize)
}
let _ = subtitleApply()
let _ = buttonTitleApply()
let subtitleFrame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - subtitleLayout.size.width) / 2.0), y: mediaBackgroundFrame.minY + 108.0), size: subtitleLayout.size)
strongSelf.subtitleNode.frame = subtitleFrame
let buttonTitleFrame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - buttonTitleLayout.size.width) / 2.0), y: subtitleFrame.maxY + 18.0), size: buttonTitleLayout.size)
strongSelf.buttonTitleNode.frame = buttonTitleFrame
let buttonSize = CGSize(width: buttonTitleLayout.size.width + 38.0, height: 34.0)
strongSelf.buttonNode.frame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - buttonSize.width) / 2.0), y: subtitleFrame.maxY + 10.0), size: buttonSize)
if item.controllerInteraction.presentationContext.backgroundNode?.hasExtraBubbleBackground() == true {
if strongSelf.mediaBackgroundContent == nil, let backgroundContent = item.controllerInteraction.presentationContext.backgroundNode?.makeBubbleBackground(for: .free) {
strongSelf.mediaBackgroundNode.isHidden = true
backgroundContent.clipsToBounds = true
backgroundContent.allowsGroupOpacity = true
backgroundContent.cornerRadius = 24.0
strongSelf.mediaBackgroundContent = backgroundContent
strongSelf.insertSubnode(backgroundContent, at: 0)
}
strongSelf.mediaBackgroundContent?.frame = mediaBackgroundFrame
} else {
strongSelf.mediaBackgroundNode.isHidden = false
strongSelf.mediaBackgroundContent?.removeFromSupernode()
strongSelf.mediaBackgroundContent = nil
}
if let (rect, size) = strongSelf.absoluteRect {
strongSelf.updateAbsoluteRect(rect, within: size)
}
})
})
})
}
}
override public func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) {
self.absoluteRect = (rect, containerSize)
}
override public func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction {
if self.mediaBackgroundNode.frame.contains(point) {
return ChatMessageBubbleContentTapAction(content: .custom({ [weak self] in
self?.buttonPressed()
}))
} else {
return ChatMessageBubbleContentTapAction(content: .none)
}
}
}