mirror of
https://github.com/ichmagmaus111/ghostgram.git
synced 2026-11-03 22:27:13 +01:00
Ghostgram for iOS, rebuilt from scratch on Telegram iOS 13.0. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
254 lines
15 KiB
Swift
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)
|
|
}
|
|
}
|
|
}
|