chore: migrate to new version + fixed several critical bugs

- Migrated project to latest Telegram iOS base (v12.3.2+)
- Fixed circular dependency between GhostModeManager and MiscSettingsManager
- Fixed multiple Bazel build configuration errors (select() default conditions)
- Fixed duplicate type definitions in PeerInfoScreen
- Fixed swiftmodule directory resolution in build scripts
- Added Ghostgram Settings tab in main Settings menu with all 5 features
- Cleared sensitive credentials from config.json (template-only now)
- Excluded bazel-cache from version control
This commit is contained in:
ichmagmaus 812
2026-02-23 23:04:32 +01:00
parent 703e291bcb
commit db53826061
1017 changed files with 62337 additions and 40559 deletions
@@ -0,0 +1,27 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "ChatAgeRestrictionAlertController",
module_name = "ChatAgeRestrictionAlertController",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
"//submodules/Display:Display",
"//submodules/Postbox:Postbox",
"//submodules/TelegramCore:TelegramCore",
"//submodules/AccountContext:AccountContext",
"//submodules/TelegramPresentationData:TelegramPresentationData",
"//submodules/ComponentFlow",
"//submodules/TelegramUI/Components/AlertComponent",
"//submodules/TelegramUI/Components/AlertComponent/AlertCheckComponent",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,64 @@
import Foundation
import UIKit
import SwiftSignalKit
import AsyncDisplayKit
import Display
import Postbox
import TelegramCore
import TelegramPresentationData
import AccountContext
import ComponentFlow
import AlertComponent
import AlertCheckComponent
public func chatAgeRestrictionAlertController(
context: AccountContext,
updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?,
parentController: ViewController,
completion: @escaping (Bool) -> Void
) -> ViewController {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let strings = presentationData.strings
let checkState = AlertCheckComponent.ExternalState()
var content: [AnyComponentWithIdentity<AlertComponentEnvironment>] = []
content.append(AnyComponentWithIdentity(
id: "title",
component: AnyComponent(
AlertTitleComponent(title: strings.SensitiveContent_Title)
)
))
content.append(AnyComponentWithIdentity(
id: "text",
component: AnyComponent(
AlertTextComponent(content: .plain(strings.SensitiveContent_Text))
)
))
content.append(AnyComponentWithIdentity(
id: "check",
component: AnyComponent(
AlertCheckComponent(title: strings.SensitiveContent_ShowAlways, initialValue: false, externalState: checkState)
)
))
var effectiveUpdatedPresentationData: (PresentationData, Signal<PresentationData, NoError>)
if let updatedPresentationData {
effectiveUpdatedPresentationData = updatedPresentationData
} else {
effectiveUpdatedPresentationData = (presentationData, context.sharedContext.presentationData)
}
let alertController = AlertScreen(
configuration: AlertScreen.Configuration(actionAlignment: .vertical),
content: content,
actions: [
.init(title: strings.SensitiveContent_ViewAnyway, type: .default, action: {
completion(checkState.value)
}),
.init(title: strings.Common_Cancel)
],
updatedPresentationData: effectiveUpdatedPresentationData
)
return alertController
}
@@ -75,12 +75,8 @@ public final class ChatAvatarNavigationNode: ASDisplayNode {
strongSelf.contextAction?(strongSelf.containerNode, gesture)
}
self.containerNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 37.0, height: 37.0)).offsetBy(dx: 10.0, dy: 1.0)
self.avatarNode.frame = self.containerNode.bounds
#if DEBUG
//self.hasUnseenStories = true
#endif
self.containerNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 44.0, height: 44.0))
self.avatarNode.frame = self.containerNode.bounds.insetBy(dx: 3.0, dy: 3.0)
}
deinit {
@@ -265,7 +261,7 @@ public final class ChatAvatarNavigationNode: ASDisplayNode {
}
override public func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize {
return CGSize(width: 37.0, height: 37.0)
return CGSize(width: 44.0, height: 44.0)
}
public func onLayout() {
@@ -118,7 +118,7 @@ public final class ChatBotInfoItemNode: ListViewItemNode {
self.textNode = TextNode()
self.titleNode = TextNode()
super.init(layerBacked: false, dynamicBounce: true, rotated: true)
super.init(layerBacked: false, rotated: true)
self.transform = CATransform3DMakeRotation(CGFloat.pi, 0.0, 0.0, 1.0)
@@ -177,51 +177,8 @@ public final class ChatChannelSubscriberInputPanelNode: ChatInputPanelNode {
private var layoutData: (CGFloat, CGFloat, CGFloat, CGFloat, UIEdgeInsets, CGFloat, CGFloat, Bool, LayoutMetrics)?
public override init() {
/*self.button = HighlightableButton()
self.buttonBackgroundView = GlassBackgroundView()
self.buttonBackgroundView.isUserInteractionEnabled = false
self.buttonTitle = ImmediateTextNode()
self.buttonTitle.isUserInteractionEnabled = false
self.buttonTintTitle = ImmediateTextNode()
self.buttonBackgroundView.contentView.addSubview(self.buttonTitle.view)
self.buttonBackgroundView.maskContentView.addSubview(self.buttonTintTitle.view)
self.buttonBackgroundView.contentView.addSubview(self.button)
self.helpButton = HighlightableButton()
self.helpButtonBackgroundView = GlassBackgroundView()
self.helpButtonBackgroundView.isUserInteractionEnabled = false
self.helpButtonIconView = GlassBackgroundView.ContentImageView()
self.helpButtonBackgroundView.contentView.addSubview(self.helpButtonIconView)
self.helpButtonBackgroundView.contentView.addSubview(self.helpButton)
self.helpButtonBackgroundView.isHidden = true
self.giftButton = HighlightableButton()
self.giftButtonBackgroundView = GlassBackgroundView()
self.giftButtonBackgroundView.isUserInteractionEnabled = false
self.giftButtonIconView = GlassBackgroundView.ContentImageView()
self.giftButtonBackgroundView.contentView.addSubview(self.giftButtonIconView)
self.giftButtonBackgroundView.contentView.addSubview(self.giftButton)
self.giftButtonBackgroundView.isHidden = true
self.suggestedPostButton = HighlightableButton()
self.suggestedPostButtonBackgroundView = GlassBackgroundView()
self.suggestedPostButtonBackgroundView.isUserInteractionEnabled = false
self.suggestedPostButtonIconView = GlassBackgroundView.ContentImageView()
self.suggestedPostButtonBackgroundView.contentView.addSubview(self.suggestedPostButtonIconView)
self.suggestedPostButtonBackgroundView.contentView.addSubview(self.suggestedPostButton)
self.suggestedPostButtonBackgroundView.isHidden = true*/
super.init()
/*self.view.addSubview(self.buttonBackgroundView)
self.view.addSubview(self.helpButtonBackgroundView)
self.view.addSubview(self.giftButtonBackgroundView)
self.view.addSubview(self.suggestedPostButtonBackgroundView)
self.button.addTarget(self, action: #selector(self.buttonPressed), for: .touchUpInside)
self.helpButton.addTarget(self, action: #selector(self.helpPressed), for: .touchUpInside)
self.giftButton.addTarget(self, action: #selector(self.giftPressed), for: .touchUpInside)
self.suggestedPostButton.addTarget(self, action: #selector(self.suggestedPostPressed), for: .touchUpInside)*/
self.view.addSubview(self.panelContainer)
}
@@ -495,7 +452,8 @@ public final class ChatChannelSubscriberInputPanelNode: ChatInputPanelNode {
self?.buttonPressed()
}
)],
background: centerAction.isAccent ? .activeTint : .panel
background: centerAction.isAccent ? .activeTint : .panel,
keepWide: true
)
}
@@ -56,53 +56,53 @@ public enum ChatHistoryEntry: Identifiable, Comparable {
case UnreadEntry(MessageIndex, ChatPresentationData)
case ReplyCountEntry(MessageIndex, Bool, Int, ChatPresentationData)
case ChatInfoEntry(ChatInfoData, ChatPresentationData)
case SearchEntry(PresentationTheme, PresentationStrings)
public var stableId: UInt64 {
switch self {
case let .MessageEntry(message, _, _, _, _, attributes):
let type: UInt64
switch attributes.contentTypeHint {
case .generic:
type = 2
case .largeEmoji:
type = 3
case .animatedEmoji:
type = 4
}
return UInt64(message.stableId) | ((type << 40))
case let .MessageGroupEntry(groupInfo, _, _):
return UInt64(bitPattern: groupInfo) | ((UInt64(2) << 40))
case .UnreadEntry:
return UInt64(4) << 40
case .ReplyCountEntry:
return UInt64(5) << 40
case .ChatInfoEntry:
return UInt64(6) << 40
case .SearchEntry:
case let .MessageEntry(message, _, _, _, _, attributes):
let type: UInt64
switch attributes.contentTypeHint {
case .generic:
type = 2
case .largeEmoji:
type = 3
case .animatedEmoji:
type = 4
}
return UInt64(message.stableId) | ((type << 40))
case let .MessageGroupEntry(groupInfo, _, _):
return UInt64(bitPattern: groupInfo) | ((UInt64(2) << 40))
case .UnreadEntry:
return UInt64(4) << 40
case .ReplyCountEntry:
return UInt64(5) << 40
case let .ChatInfoEntry(infoData, _):
switch infoData {
case .newThreadInfo:
return UInt64(7) << 40
default:
return UInt64(6) << 40
}
}
}
public var index: MessageIndex {
switch self {
case let .MessageEntry(message, _, _, _, _, _):
return message.index
case let .MessageGroupEntry(_, messages, _):
return messages[messages.count - 1].0.index
case let .UnreadEntry(index, _):
return index
case let .ReplyCountEntry(index, _, _, _):
return index
case let .ChatInfoEntry(infoData, _):
switch infoData {
case .newThreadInfo:
return MessageIndex.absoluteUpperBound()
default:
return MessageIndex.absoluteLowerBound()
}
case .SearchEntry:
case let .MessageEntry(message, _, _, _, _, _):
return message.index
case let .MessageGroupEntry(_, messages, _):
return messages[messages.count - 1].0.index
case let .UnreadEntry(index, _):
return index
case let .ReplyCountEntry(index, _, _, _):
return index
case let .ChatInfoEntry(infoData, _):
switch infoData {
case .newThreadInfo:
return MessageIndex.absoluteUpperBound()
default:
return MessageIndex.absoluteLowerBound()
}
}
}
@@ -123,8 +123,6 @@ public enum ChatHistoryEntry: Identifiable, Comparable {
default:
return MessageIndex.absoluteLowerBound()
}
case .SearchEntry:
return MessageIndex.absoluteLowerBound()
}
}
@@ -297,12 +295,6 @@ public enum ChatHistoryEntry: Identifiable, Comparable {
} else {
return false
}
case let .SearchEntry(lhsTheme, lhsStrings):
if case let .SearchEntry(rhsTheme, rhsStrings) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings {
return true
} else {
return false
}
}
}
@@ -158,7 +158,7 @@ public final class ChatHistorySearchContainerNode: SearchDisplayControllerConten
self.themeAndStringsPromise = Promise((self.presentationData.theme, self.presentationData.strings, self.presentationData.dateTimeFormat, self.presentationData.listsFontSize))
self.dimNode = ASDisplayNode()
self.dimNode.backgroundColor = UIColor.black.withAlphaComponent(0.5)
self.dimNode.backgroundColor = .clear
self.listNode = ListView()
self.listNode.accessibilityPageScrolledString = { row, count in
return presentationData.strings.VoiceOver_ScrollStatus(row, count).string
@@ -901,8 +901,6 @@ public final class ChatInlineSearchResultsListComponent: Component {
},
openStarsTopup: { _ in
},
dismissNotice: { _ in
},
editPeer: { _ in
},
openWebApp: { _ in
@@ -440,6 +440,7 @@ private final class ChatMessageActionButtonNode: ASDisplayNode {
if node.iconNode == nil {
let iconNode = ASImageNode()
iconNode.contentMode = .center
iconNode.isUserInteractionEnabled = false
node.iconNode = iconNode
node.addSubnode(iconNode)
}
@@ -32,6 +32,7 @@ swift_library(
"//submodules/WallpaperBackgroundNode",
"//submodules/LocalMediaResources",
"//submodules/AppBundle",
"//submodules/TelegramStringFormatting",
"//submodules/ChatPresentationInterfaceState",
"//submodules/TelegramUI/Components/TextNodeWithEntities",
"//submodules/TelegramUI/Components/ChatControllerInteraction",
@@ -47,6 +47,7 @@ import ManagedDiceAnimationNode
import MessageHaptics
import ChatMessageTransitionNode
import ChatMessageSuggestedPostInfoNode
import TelegramStringFormatting
private let nameFont = Font.medium(14.0)
private let inlineBotPrefixFont = Font.regular(14.0)
@@ -99,6 +100,11 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
private var swipeToReplyNode: ChatMessageSwipeToReplyNode?
private var swipeToReplyFeedback: HapticFeedback?
private let labelNode: TextNodeWithEntities
private var labelBackgroundNode: WallpaperBubbleBackgroundNode?
private let labelBackgroundMaskNode: ASImageNode
private var cachedMaskLabelBackgroundImage: (CGPoint, UIImage, [CGRect])?
private var selectionNode: ChatMessageSelectionNode?
private var deliveryFailedNode: ChatMessageDeliveryFailedNode?
private var shareButtonNode: ChatMessageShareButton?
@@ -161,6 +167,12 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
self.textNode.textNode.displaysAsynchronously = false
self.textNode.textNode.isUserInteractionEnabled = false
self.labelNode = TextNodeWithEntities()
self.labelNode.textNode.isUserInteractionEnabled = false
self.labelNode.textNode.displaysAsynchronously = false
self.labelBackgroundMaskNode = ASImageNode()
super.init(rotated: rotated)
self.containerNode.shouldBegin = { [weak self] location in
@@ -469,9 +481,24 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
}
} else if let telegramDice = self.telegramDice, let diceNode = self.animationNode as? ManagedDiceAnimationNode {
if let value = telegramDice.value {
let wasRolling = diceNode.isRolling
diceNode.setState(value == 0 ? .rolling : .value(value, true))
if wasRolling && !diceNode.isRolling {
Queue.mainQueue().after(3.0, {
self.labelNode.textNode.alpha = 1.0
self.labelBackgroundNode?.alpha = 1.0
self.labelNode.textNode.layer.animateScale(from: 0.01, to: 1.0, duration: 0.25)
self.labelBackgroundNode?.layer.animateScale(from: 0.01, to: 1.0, duration: 0.25)
self.labelNode.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
self.labelBackgroundNode?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
})
}
} else {
diceNode.setState(.rolling)
self.labelNode.textNode.alpha = 0.0
self.labelBackgroundNode?.alpha = 0.0
}
} else if self.telegramFile == nil && self.telegramDice == nil {
let (emoji, fitz) = item.message.text.basicEmoji
@@ -816,6 +843,9 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
let actionButtonsLayout = ChatMessageActionButtonsNode.asyncLayout(self.actionButtonsNode)
let reactionButtonsLayout = ChatMessageReactionButtonsNode.asyncLayout(self.reactionButtonsNode)
let makeLabelLayout = TextNodeWithEntities.asyncLayout(self.labelNode)
let cachedMaskLabelBackgroundImage = self.cachedMaskLabelBackgroundImage
let makeForwardInfoLayout = ChatMessageForwardInfoNode.asyncLayout(self.forwardInfoNode)
let viaBotLayout = TextNode.asyncLayout(self.viaBotNode)
@@ -850,6 +880,44 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
let avatarInset: CGFloat
var hasAvatar = false
let labelAttributedText = universalServiceMessageString(presentationData: (item.presentationData.theme.theme, item.presentationData.theme.wallpaper), strings: item.presentationData.strings, nameDisplayOrder: item.presentationData.nameDisplayOrder, dateTimeFormat: item.presentationData.dateTimeFormat, message: EngineMessage(item.message), accountPeerId: item.context.account.peerId, forChatList: false, forForumOverview: false, forAdditionalServiceMessage: true)
let (labelLayout, labelApply) = makeLabelLayout(TextNodeLayoutArguments(attributedString: labelAttributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - 32.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
var labelRects = labelLayout.linesRects()
if labelRects.count > 1 {
let sortedIndices = (0 ..< labelRects.count).sorted(by: { labelRects[$0].width > labelRects[$1].width })
for i in 0 ..< sortedIndices.count {
let index = sortedIndices[i]
for j in -1 ... 1 {
if j != 0 && index + j >= 0 && index + j < sortedIndices.count {
if abs(labelRects[index + j].width - labelRects[index].width) < 40.0 {
labelRects[index + j].size.width = max(labelRects[index + j].width, labelRects[index].width)
labelRects[index].size.width = labelRects[index + j].size.width
}
}
}
}
}
for i in 0 ..< labelRects.count {
labelRects[i] = labelRects[i].insetBy(dx: -7.0, dy: floor((labelRects[i].height - 22.0) / 2.0))
labelRects[i].size.height = 22.0
labelRects[i].origin.x = floor((labelLayout.size.width - labelRects[i].width) / 2.0)
}
let backgroundMaskImage: (CGPoint, UIImage)?
var backgroundMaskUpdated = false
if labelLayout.size.height > 0.0 {
if let (currentOffset, currentImage, currentRects) = cachedMaskLabelBackgroundImage, currentRects == labelRects {
backgroundMaskImage = (currentOffset, currentImage)
} else {
backgroundMaskImage = LinkHighlightingNode.generateImage(color: .black, inset: 0.0, innerRadius: 11.0, outerRadius: 11.0, rects: labelRects, useModernPathCalculation: false)
backgroundMaskUpdated = true
}
} else {
backgroundMaskImage = nil
}
switch item.chatLocation {
case let .peer(peerId):
if peerId != item.context.account.peerId {
@@ -1061,6 +1129,7 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
var viewCount: Int? = nil
var dateReplies = 0
var starsCount: Int64?
var tonAmount: Int64?
var dateReactionsAndPeers = mergedMessageReactionsAndPeers(accountPeerId: item.context.account.peerId, accountPeer: item.associatedData.accountPeer, message: item.message)
if item.message.isRestricted(platform: "ios", contentSettings: item.context.currentContentSettings.with { $0 }) {
dateReactionsAndPeers = ([], [])
@@ -1079,6 +1148,10 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
}
}
if let stakeTonAmount = telegramDice?.tonAmount {
tonAmount = stakeTonAmount
}
let dateText = stringForMessageTimestampStatus(accountPeerId: item.context.account.peerId, message: item.message, dateTimeFormat: item.presentationData.dateTimeFormat, nameDisplayOrder: item.presentationData.nameDisplayOrder, strings: item.presentationData.strings, format: .regular, associatedData: item.associatedData)
var isReplyThread = false
@@ -1107,6 +1180,7 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
messageEffect: messageEffect,
replyCount: dateReplies,
starsCount: starsCount,
tonAmount: tonAmount,
isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread,
hasAutoremove: item.message.isSelfExpiring,
canViewReactionList: canViewMessageReactionList(message: item.message),
@@ -1211,6 +1285,7 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
quote: replyQuote,
todoItemId: replyTodoItemId,
story: replyStory,
isSummarized: false,
parentMessage: item.message,
constrainedSize: CGSize(width: availableContentWidth, height: CGFloat.greatestFiniteMagnitude),
animationCache: item.controllerInteraction.presentationContext.animationCache,
@@ -1501,6 +1576,10 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
} else {
updatedImageFrame = imageFrame.offsetBy(dx: 0.0, dy: floor((contentHeight - imageSize.height) / 2.0))
contextContentFrame = updatedImageFrame
if let telegramDice, let _ = telegramDice.tonAmount {
updatedImageFrame = updatedImageFrame.offsetBy(dx: 0.0, dy: -30.0)
}
}
var updatedContentFrame = updatedImageFrame
if isEmoji && emojiString == nil {
@@ -1508,6 +1587,51 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
contextContentFrame = updatedContentFrame
}
let labelFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((params.width - labelLayout.size.width) / 2.0), y: updatedImageFrame.maxY + 6.0), size: labelLayout.size)
strongSelf.labelNode.textNode.frame = labelFrame
if strongSelf.labelNode.textNode.supernode == nil, labelLayout.size.height > 0.0 {
strongSelf.addSubnode(strongSelf.labelNode.textNode)
}
let _ = labelApply(TextNodeWithEntities.Arguments(
context: item.context,
cache: item.controllerInteraction.presentationContext.animationCache,
renderer: item.controllerInteraction.presentationContext.animationRenderer,
placeholderColor: item.presentationData.theme.theme.chat.message.freeform.withWallpaper.reactionInactiveBackground,
attemptSynchronous: synchronousLoads
))
let baseBackgroundFrame = labelFrame.offsetBy(dx: 0.0, dy: -11.0)
if let (offset, image) = backgroundMaskImage {
if strongSelf.labelBackgroundNode == nil {
if let backgroundNode = item.controllerInteraction.presentationContext.backgroundNode?.makeBubbleBackground(for: .free) {
backgroundNode.alpha = strongSelf.labelNode.textNode.alpha
strongSelf.labelBackgroundNode = backgroundNode
strongSelf.insertSubnode(backgroundNode, at: 0)
}
}
if backgroundMaskUpdated, let backgroundNode = strongSelf.labelBackgroundNode {
if labelRects.count == 1 {
backgroundNode.clipsToBounds = true
backgroundNode.cornerRadius = labelRects[0].height / 2.0
backgroundNode.view.mask = nil
} else {
backgroundNode.clipsToBounds = false
backgroundNode.cornerRadius = 0.0
backgroundNode.view.mask = strongSelf.labelBackgroundMaskNode.view
}
}
if let backgroundNode = strongSelf.labelBackgroundNode {
backgroundNode.layer.frame = CGRect(origin: CGPoint(x: baseBackgroundFrame.minX + offset.x, y: baseBackgroundFrame.minY + offset.y), size: image.size)
}
strongSelf.labelBackgroundMaskNode.image = image
strongSelf.labelBackgroundMaskNode.frame = CGRect(origin: CGPoint(), size: image.size)
strongSelf.cachedMaskLabelBackgroundImage = (offset, image, labelRects)
}
if let (_, textApply) = textLayoutAndApply {
let placeholderColor = bubbleVariableColor(variableColor: item.presentationData.theme.theme.chat.message.stickerPlaceholderColor, wallpaper: item.presentationData.theme.wallpaper)
let _ = textApply(TextNodeWithEntities.Arguments(context: item.context, cache: item.controllerInteraction.presentationContext.animationCache, renderer: item.controllerInteraction.presentationContext.animationRenderer, placeholderColor: placeholderColor, attemptSynchronous: synchronousLoads))
@@ -94,6 +94,7 @@ swift_library(
"//submodules/TelegramStringFormatting",
"//submodules/AvatarNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageSuggestedPostInfoNode",
"//submodules/TelegramUI/Components/PremiumAlertController",
],
visibility = [
"//visibility:public",
@@ -83,6 +83,7 @@ import TelegramAnimatedStickerNode
import LottieMetal
import AvatarNode
import ChatMessageSuggestedPostInfoNode
import PremiumAlertController
private struct BubbleItemAttributes {
var index: Int?
@@ -689,6 +690,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
private var unlockButtonNode: ChatMessageUnlockMediaNode?
private var mediaInfoNode: ChatMessageStarsMediaInfoNode?
private var summarizeButtonNode: ChatMessageShareButton?
private var shareButtonNode: ChatMessageShareButton?
private let messageAccessibilityArea: AccessibilityAreaNode
@@ -1222,6 +1224,10 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
}
}
if let summarizeButtonNode = strongSelf.summarizeButtonNode, summarizeButtonNode.frame.contains(point) {
return .fail
}
if let shareButtonNode = strongSelf.shareButtonNode, shareButtonNode.frame.contains(point) {
return .fail
}
@@ -1724,6 +1730,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
let isFailed = item.content.firstMessage.effectivelyFailed(timestamp: item.context.account.network.getApproximateRemoteTimestamp())
var needsShareButton = false
var needsSummarizeButton = false
if incoming, case let .customChatContents(contents) = item.associatedData.subject, case .hashTagSearch = contents.kind {
needsShareButton = true
@@ -1751,6 +1758,10 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
needsShareButton = true
}
if let _ = item.message.attributes.first(where: { $0 is SummarizationMessageAttribute }) {
needsSummarizeButton = true
}
if let peer = item.message.peers[item.message.id.peerId] {
if let channel = peer as? TelegramChannel {
if case .broadcast = channel.info {
@@ -1789,6 +1800,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
loop: for media in item.message.media {
if media is TelegramMediaAction {
needsShareButton = false
needsSummarizeButton = false
break loop
} else if let media = media as? TelegramMediaFile, media.isInstantVideo {
mayHaveSeparateCommentsButton = true
@@ -1801,12 +1813,14 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
if mayHaveSeparateCommentsButton && hasCommentButton(item: item) {
} else {
needsShareButton = false
needsSummarizeButton = false
}
}
}
if isPreview {
needsShareButton = false
needsSummarizeButton = false
}
let isAd = item.content.firstMessage.adAttribute != nil
if isAd {
@@ -1815,13 +1829,14 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
for attribute in item.content.firstMessage.attributes {
if let attribute = attribute as? RestrictedContentMessageAttribute, attribute.platformText(platform: "ios", contentSettings: item.context.currentContentSettings.with { $0 }) != nil {
needsShareButton = false
needsSummarizeButton = false
}
}
if let subject = item.associatedData.subject, case .messageOptions = subject {
needsShareButton = false
}
var tmpWidth: CGFloat
if allowFullWidth {
tmpWidth = baseWidth
@@ -2312,6 +2327,17 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
}
}
let translateToLanguage = item.associatedData.translateToLanguage
var isSummarized = false
if item.controllerInteraction.summarizedMessageIds.contains(item.message.id) {
for attribute in item.message.attributes {
if let attribute = attribute as? SummarizationMessageAttribute, attribute.summaryForLang(translateToLanguage) != nil {
initialDisplayHeader = true
isSummarized = true
}
}
}
var displayHeader = false
if initialDisplayHeader {
if authorNameString != nil {
@@ -2339,6 +2365,9 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
displayHeader = true
}
}
if isSummarized {
displayHeader = true
}
}
let firstNodeTopPosition: ChatMessageBubbleRelativePosition
@@ -2717,7 +2746,11 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
hasReply = false
}
if !isInstantVideo, hasReply, (replyMessage != nil || replyForward != nil || replyStory != nil) {
if isSummarized {
hasReply = true
}
if !isInstantVideo, hasReply, (replyMessage != nil || replyForward != nil || replyStory != nil || isSummarized) {
if headerSize.height.isZero {
headerSize.height += 11.0
} else {
@@ -2733,6 +2766,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
quote: replyQuote,
todoItemId: replyTodoItemId,
story: replyStory,
isSummarized: isSummarized,
parentMessage: item.message,
constrainedSize: CGSize(width: maximumNodeWidth - layoutConstants.text.bubbleInsets.left - layoutConstants.text.bubbleInsets.right - 6.0, height: CGFloat.greatestFiniteMagnitude),
animationCache: item.controllerInteraction.presentationContext.animationCache,
@@ -3491,6 +3525,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
unlockButtonSizeAndApply: unlockButtonSizeApply,
mediaInfoOrigin: mediaInfoOrigin?.offsetBy(dx: 0.0, dy: layoutInsets.top),
mediaInfoSizeAndApply: mediaInfoSizeApply,
needsSummarizeButton: needsSummarizeButton,
needsShareButton: needsShareButton,
shareButtonOffset: shareButtonOffset,
avatarOffset: avatarOffset,
@@ -3556,6 +3591,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
unlockButtonSizeAndApply: (CGSize, (Bool) -> ChatMessageUnlockMediaNode?),
mediaInfoOrigin: CGPoint?,
mediaInfoSizeAndApply: (CGSize, (Bool) -> ChatMessageStarsMediaInfoNode?),
needsSummarizeButton: Bool,
needsShareButton: Bool,
shareButtonOffset: CGPoint?,
avatarOffset: CGFloat?,
@@ -4787,6 +4823,20 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
})
}
if needsSummarizeButton {
if strongSelf.summarizeButtonNode == nil {
let summarizeButtonNode = ChatMessageShareButton()
strongSelf.summarizeButtonNode = summarizeButtonNode
strongSelf.insertSubnode(summarizeButtonNode, belowSubnode: strongSelf.messageAccessibilityArea)
summarizeButtonNode.pressed = { [weak strongSelf] in
strongSelf?.toggleSummarization()
}
}
} else if let summarizeButtonNode = strongSelf.summarizeButtonNode {
strongSelf.summarizeButtonNode = nil
summarizeButtonNode.removeFromSupernode()
}
if needsShareButton {
if strongSelf.shareButtonNode == nil {
let shareButtonNode = ChatMessageShareButton()
@@ -4960,6 +5010,30 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
}
strongSelf.messageAccessibilityArea.frame = backgroundFrame
}
if let summarizeButtonNode = strongSelf.summarizeButtonNode {
let buttonSize = summarizeButtonNode.update(presentationData: item.presentationData, controllerInteraction: item.controllerInteraction, chatLocation: item.chatLocation, subject: item.associatedData.subject, message: item.message, account: item.context.account, disableComments: disablesComments, isSummarize: true)
var buttonFrame = CGRect(origin: CGPoint(x: !incoming ? backgroundFrame.minX - buttonSize.width - 8.0 : backgroundFrame.maxX + 8.0, y: backgroundFrame.minY + 1.0), size: buttonSize)
if let shareButtonOffset = shareButtonOffset {
if incoming {
buttonFrame.origin.x = shareButtonOffset.x
}
buttonFrame.origin.y = buttonFrame.origin.y + shareButtonOffset.y - (buttonSize.height - 30.0)
} else if !disablesComments {
buttonFrame.origin.y = buttonFrame.origin.y - (buttonSize.height - 30.0)
}
if isSidePanelOpen {
buttonFrame.origin.x -= buttonFrame.width * 0.5
buttonFrame.origin.y += buttonFrame.height * 0.5
}
animation.animator.updatePosition(layer: summarizeButtonNode.layer, position: buttonFrame.center, completion: nil)
animation.animator.updateBounds(layer: summarizeButtonNode.layer, bounds: CGRect(origin: CGPoint(), size: buttonFrame.size), completion: nil)
animation.animator.updateAlpha(layer: summarizeButtonNode.layer, alpha: (isCurrentlyPlayingMedia || isSidePanelOpen) ? 0.0 : 1.0, completion: nil)
animation.animator.updateScale(layer: summarizeButtonNode.layer, scale: (isCurrentlyPlayingMedia || isSidePanelOpen) ? 0.001 : 1.0, completion: nil)
}
if let shareButtonNode = strongSelf.shareButtonNode {
let buttonSize = shareButtonNode.update(presentationData: item.presentationData, controllerInteraction: item.controllerInteraction, chatLocation: item.chatLocation, subject: item.associatedData.subject, message: item.message, account: item.context.account, disableComments: disablesComments)
@@ -4994,6 +5068,30 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
strongSelf.backgroundFrameTransition = nil
}*/
strongSelf.messageAccessibilityArea.frame = backgroundFrame
if let summarizeButtonNode = strongSelf.summarizeButtonNode {
let buttonSize = summarizeButtonNode.update(presentationData: item.presentationData, controllerInteraction: item.controllerInteraction, chatLocation: item.chatLocation, subject: item.associatedData.subject, message: item.message, account: item.context.account, disableComments: disablesComments, isSummarize: true)
var buttonFrame = CGRect(origin: CGPoint(x: !incoming ? backgroundFrame.minX - buttonSize.width - 8.0 : backgroundFrame.maxX + 8.0, y: backgroundFrame.minY + 1.0), size: buttonSize)
if let shareButtonOffset = shareButtonOffset {
if incoming {
buttonFrame.origin.x = shareButtonOffset.x
}
buttonFrame.origin.y = buttonFrame.origin.y + shareButtonOffset.y - (buttonSize.height - 30.0)
} else if !disablesComments {
buttonFrame.origin.y = buttonFrame.origin.y - (buttonSize.height - 30.0)
}
if isSidePanelOpen {
buttonFrame.origin.x -= buttonFrame.width * 0.5
buttonFrame.origin.y += buttonFrame.height * 0.5
}
animation.animator.updatePosition(layer: summarizeButtonNode.layer, position: buttonFrame.center, completion: nil)
animation.animator.updateBounds(layer: summarizeButtonNode.layer, bounds: CGRect(origin: CGPoint(), size: buttonFrame.size), completion: nil)
animation.animator.updateAlpha(layer: summarizeButtonNode.layer, alpha: (isCurrentlyPlayingMedia || isSidePanelOpen) ? 0.0 : 1.0, completion: nil)
animation.animator.updateScale(layer: summarizeButtonNode.layer, scale: (isCurrentlyPlayingMedia || isSidePanelOpen) ? 0.001 : 1.0, completion: nil)
}
if let shareButtonNode = strongSelf.shareButtonNode {
let buttonSize = shareButtonNode.update(presentationData: item.presentationData, controllerInteraction: item.controllerInteraction, chatLocation: item.chatLocation, subject: item.associatedData.subject, message: item.message, account: item.context.account, disableComments: disablesComments)
@@ -5275,6 +5373,15 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
}
} else if let replyInfoNode = self.replyInfoNode, self.item?.controllerInteraction.tapMessage == nil, replyInfoNode.frame.contains(location) {
if let item = self.item {
if item.controllerInteraction.summarizedMessageIds.contains(item.message.id) {
return .action(InternalBubbleTapAction.Action({ [weak self] in
guard let self else {
return
}
self.toggleSummarization()
}))
}
for attribute in item.message.attributes {
if let attribute = attribute as? ReplyMessageAttribute {
if let threadId = item.message.threadId, Int32(clamping: threadId) == attribute.messageId.id, let quotedReply = item.message.attributes.first(where: { $0 is QuotedReplyMessageAttribute }) as? QuotedReplyMessageAttribute {
@@ -5824,6 +5931,10 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
return boostButtonNode.view
}
if let summarizeButtonNode = self.summarizeButtonNode, summarizeButtonNode.frame.contains(point) {
return summarizeButtonNode.view.hitTest(self.view.convert(point, to: summarizeButtonNode.view), with: event)
}
if let shareButtonNode = self.shareButtonNode, shareButtonNode.frame.contains(point) {
return shareButtonNode.view.hitTest(self.view.convert(point, to: shareButtonNode.view), with: event)
}
@@ -6516,6 +6627,14 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
container.updateAbsoluteRect(containerFrame, within: containerSize)
}
if let summarizeButtonNode = self.summarizeButtonNode {
var summarizeButtonNodeFrame = summarizeButtonNode.frame
summarizeButtonNodeFrame.origin.x += rect.minX
summarizeButtonNodeFrame.origin.y += rect.minY
summarizeButtonNode.updateAbsoluteRect(summarizeButtonNodeFrame, within: containerSize)
}
if let shareButtonNode = self.shareButtonNode {
var shareButtonNodeFrame = shareButtonNode.frame
shareButtonNodeFrame.origin.x += rect.minX
@@ -6854,6 +6973,45 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
self.updateVisibility(isScroll: false)
}
private func toggleSummarization() {
guard let item = self.item else {
return
}
if item.controllerInteraction.summarizedMessageIds.contains(item.message.id) {
item.controllerInteraction.summarizedMessageIds.remove(item.message.id)
let _ = item.controllerInteraction.requestMessageUpdate(item.message.id, false)
} else {
item.controllerInteraction.summarizedMessageIds.insert(item.message.id)
let _ = item.controllerInteraction.requestMessageUpdate(item.message.id, false)
let translateToLanguage = item.associatedData.translateToLanguage
var requestSummary = true
for attribute in item.message.attributes {
if let attribute = attribute as? SummarizationMessageAttribute, attribute.summaryForLang(translateToLanguage) != nil {
requestSummary = false
break
}
}
if requestSummary {
let _ = (item.context.engine.messages.summarizeMessage(messageId: item.message.id, translateToLang: translateToLanguage)
|> deliverOnMainQueue).start(error: { error in
if case .limitExceededPremium = error, let parentController = item.controllerInteraction.navigationController()?.topViewController as? ViewController {
item.controllerInteraction.summarizedMessageIds.remove(item.message.id)
let _ = item.controllerInteraction.requestMessageUpdate(item.message.id, false)
let controller = premiumAlertController(
context: item.context,
parentController: parentController,
title: item.presentationData.strings.Conversation_Summary_Limit_Title,
text: item.presentationData.strings.Conversation_Summary_Limit_Text
)
parentController.present(controller, in: .window(.root))
}
})
}
}
}
private func updateVisibility(isScroll: Bool) {
guard let item = self.item else {
return
@@ -12,6 +12,7 @@ import ReactionButtonListComponent
import ReactionImageComponent
import AnimationCache
import MultiAnimationRenderer
import TelegramStringFormatting
private func maybeAddRotationAnimation(_ layer: CALayer, duration: Double) {
if let _ = layer.animation(forKey: "clockFrameAnimation") {
@@ -197,8 +198,10 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode {
var messageEffect: AvailableMessageEffects.MessageEffect?
var replyCount: Int
var starsCount: Int64?
var tonAmount: Int64?
var isPinned: Bool
var hasAutoremove: Bool
var isDeleted: Bool
var canViewReactionList: Bool
var animationCache: AnimationCache
var animationRenderer: MultiAnimationRenderer
@@ -222,8 +225,10 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode {
messageEffect: AvailableMessageEffects.MessageEffect?,
replyCount: Int,
starsCount: Int64?,
tonAmount: Int64? = nil,
isPinned: Bool,
hasAutoremove: Bool,
isDeleted: Bool = false,
canViewReactionList: Bool,
animationCache: AnimationCache,
animationRenderer: MultiAnimationRenderer
@@ -246,8 +251,10 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode {
self.messageEffect = messageEffect
self.replyCount = replyCount
self.starsCount = starsCount
self.tonAmount = tonAmount
self.isPinned = isPinned
self.hasAutoremove = hasAutoremove
self.isDeleted = isDeleted
self.canViewReactionList = canViewReactionList
self.animationCache = animationCache
self.animationRenderer = animationRenderer
@@ -270,6 +277,7 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode {
private var replyCountNode: TextNode?
private var starsIcon: ASImageNode?
private var starsCountNode: TextNode?
private var deletedIcon: ASImageNode?
private var type: ChatMessageDateAndStatusType?
private var theme: ChatPresentationThemeData?
@@ -417,8 +425,10 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode {
} else if arguments.isPinned {
repliesImage = graphics.incomingDateAndStatusPinnedIcon
}
if (arguments.starsCount ?? 0) != 0 {
if (arguments.starsCount ?? 0) != 0 {
starsImage = graphics.incomingDateAndStatusStarsIcon
} else if (arguments.tonAmount ?? 0) != 0 {
starsImage = graphics.incomingDateAndStatusTonIcon
}
case let .BubbleOutgoing(status):
dateColor = arguments.presentationData.theme.theme.chat.message.outgoing.secondaryTextColor
@@ -438,6 +448,8 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode {
}
if (arguments.starsCount ?? 0) != 0 {
starsImage = graphics.outgoingDateAndStatusStarsIcon
} else if (arguments.tonAmount ?? 0) != 0 {
starsImage = graphics.outgoingDateAndStatusTonIcon
}
case .ImageIncoming:
dateColor = arguments.presentationData.theme.theme.chat.message.mediaDateAndStatusTextColor
@@ -457,6 +469,8 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode {
}
if (arguments.starsCount ?? 0) != 0 {
starsImage = graphics.mediaStarsIcon
} else if (arguments.tonAmount ?? 0) != 0 {
starsImage = graphics.mediaTonIcon
}
case let .ImageOutgoing(status):
dateColor = arguments.presentationData.theme.theme.chat.message.mediaDateAndStatusTextColor
@@ -477,6 +491,8 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode {
}
if (arguments.starsCount ?? 0) != 0 {
starsImage = graphics.mediaStarsIcon
} else if (arguments.tonAmount ?? 0) != 0 {
starsImage = graphics.mediaTonIcon
}
case .FreeIncoming:
let serviceColor = serviceMessageColorComponents(theme: arguments.presentationData.theme.theme, wallpaper: arguments.presentationData.theme.wallpaper)
@@ -498,6 +514,8 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode {
}
if (arguments.starsCount ?? 0) != 0 {
starsImage = graphics.freeStarsIcon
} else if (arguments.tonAmount ?? 0) != 0 {
starsImage = graphics.freeTonIcon
}
case let .FreeOutgoing(status):
let serviceColor = serviceMessageColorComponents(theme: arguments.presentationData.theme.theme, wallpaper: arguments.presentationData.theme.wallpaper)
@@ -519,6 +537,8 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode {
}
if (arguments.starsCount ?? 0) != 0 {
starsImage = graphics.freeStarsIcon
} else if (arguments.tonAmount ?? 0) != 0 {
starsImage = graphics.freeTonIcon
}
}
@@ -586,6 +606,36 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode {
currentStarsIcon = nil
}
// ANTIDELETE: Deleted message icon
var currentDeletedIcon = self?.deletedIcon
var deletedIconSize = CGSize()
if arguments.isDeleted {
if currentDeletedIcon == nil {
let iconNode = ASImageNode()
iconNode.isLayerBacked = true
iconNode.displayWithoutProcessing = true
iconNode.displaysAsynchronously = false
currentDeletedIcon = iconNode
}
// Use trash icon from bundle or create simple one
let deletedImage = UIImage(bundleImageName: "Chat/Message/DeletedIcon") ?? generateTintedImage(image: UIImage(systemName: "trash"), color: dateColor)
deletedIconSize = deletedImage?.size ?? CGSize(width: 10, height: 10)
// Scale down the image
if let img = deletedImage {
let scaledSize = CGSize(width: 10, height: 10)
UIGraphicsBeginImageContextWithOptions(scaledSize, false, 0.0)
img.draw(in: CGRect(origin: .zero, size: scaledSize))
let scaledImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
currentDeletedIcon?.image = scaledImage
deletedIconSize = scaledSize
} else {
currentDeletedIcon?.image = deletedImage
}
} else {
currentDeletedIcon = nil
}
if let outgoingStatus = outgoingStatus {
switch outgoingStatus {
case .Sending:
@@ -717,7 +767,7 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode {
let layoutAndApply = makeReplyCountLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: countString, font: dateFont, textColor: dateColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 100.0, height: 100.0)))
reactionInset += 14.0 + layoutAndApply.0.size.width + 4.0
if arguments.starsCount != nil {
if arguments.starsCount != nil || arguments.tonAmount != nil {
reactionInset += 3.0
}
replyCountLayoutAndApply = layoutAndApply
@@ -735,6 +785,11 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode {
countString = "\(starsCount)"
}
let layoutAndApply = makeStarsCountLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: countString, font: dateFont, textColor: dateColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 100.0, height: 100.0)))
reactionInset += 14.0 + layoutAndApply.0.size.width + 4.0
starsCountLayoutAndApply = layoutAndApply
} else if let tonAmount = arguments.tonAmount, tonAmount > 0 {
let countString = formatTonAmountText(tonAmount, dateTimeFormat: arguments.presentationData.dateTimeFormat)
let layoutAndApply = makeStarsCountLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: countString, font: dateFont, textColor: dateColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 100.0, height: 100.0)))
reactionInset += 14.0 + layoutAndApply.0.size.width + 4.0
starsCountLayoutAndApply = layoutAndApply
@@ -744,9 +799,15 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode {
reactionInset += 13.0
}
// ANTIDELETE: Add deleted icon space
var deletedIconWidth: CGFloat = 0.0
if arguments.isDeleted {
deletedIconWidth = deletedIconSize.width + 3.0
}
leftInset += reactionInset
let layoutSize = CGSize(width: leftInset + impressionWidth + date.size.width + statusWidth + backgroundInsets.left + backgroundInsets.right, height: date.size.height + backgroundInsets.top + backgroundInsets.bottom)
let layoutSize = CGSize(width: leftInset + deletedIconWidth + impressionWidth + date.size.width + statusWidth + backgroundInsets.left + backgroundInsets.right, height: date.size.height + backgroundInsets.top + backgroundInsets.bottom)
let verticalReactionsInset: CGFloat
let verticalInset: CGFloat
@@ -1089,7 +1150,22 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode {
strongSelf.impressionIcon = nil
}
animation.animator.updateFrame(layer: strongSelf.dateNode.layer, frame: CGRect(origin: CGPoint(x: leftOffset + leftInset + backgroundInsets.left + impressionWidth, y: backgroundInsets.top + 1.0 + offset + verticalInset), size: date.size), completion: nil)
// ANTIDELETE: Position deleted icon
if let currentDeletedIcon = currentDeletedIcon {
let deletedIconFrame = CGRect(origin: CGPoint(x: leftOffset + leftInset + backgroundInsets.left + impressionWidth, y: backgroundInsets.top + 1.0 + offset + verticalInset + floor((date.size.height - deletedIconSize.height) / 2.0)), size: deletedIconSize)
if currentDeletedIcon.supernode == nil {
strongSelf.deletedIcon = currentDeletedIcon
strongSelf.addSubnode(currentDeletedIcon)
currentDeletedIcon.frame = deletedIconFrame
} else {
animation.animator.updateFrame(layer: currentDeletedIcon.layer, frame: deletedIconFrame, completion: nil)
}
} else if let deletedIcon = strongSelf.deletedIcon {
deletedIcon.removeFromSupernode()
strongSelf.deletedIcon = nil
}
animation.animator.updateFrame(layer: strongSelf.dateNode.layer, frame: CGRect(origin: CGPoint(x: leftOffset + leftInset + backgroundInsets.left + impressionWidth + deletedIconWidth, y: backgroundInsets.top + 1.0 + offset + verticalInset), size: date.size), completion: nil)
if let clockFrameNode = clockFrameNode {
let clockPosition = CGPoint(x: leftOffset + backgroundInsets.left + clockPosition.x + reactionInset, y: backgroundInsets.top + clockPosition.y + verticalInset)
@@ -455,7 +455,9 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
switch action.action {
case let .giftPremium(_, _, daysValue, _, _, giftText, giftEntities):
months = max(3, Int32(round(Float(daysValue) / 30.0)))
if months == 12 {
if daysValue < 30 {
title = item.presentationData.strings.Notification_PremiumGift_DaysTitle(daysValue)
} else if months == 12 {
title = item.presentationData.strings.Notification_PremiumGift_YearsTitle(1)
} else {
title = item.presentationData.strings.Notification_PremiumGift_MonthsTitle(months)
@@ -513,7 +515,8 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
title = item.presentationData.strings.Notification_StarsGiveaway_Title
let starsString = item.presentationData.strings.Notification_StarsGiveaway_Subtitle_Stars(Int32(clamping: count)).replacingOccurrences(of: " ", with: "\u{00A0}")
text = item.presentationData.strings.Notification_StarsGiveaway_Subtitle(peerName, starsString).string
case let .giftCode(_, fromGiveaway, unclaimed, channelId, monthsValue, _, _, _, _, giftText, giftEntities):
case let .giftCode(_, fromGiveaway, unclaimed, channelId, daysValue, _, _, _, _, giftText, giftEntities):
let monthsValue = max(3, Int32(round(Float(daysValue) / 30.0)))
if channelId == nil {
months = monthsValue
if months == 12 {
@@ -131,7 +131,7 @@ public class ChatMessageGiftOfferBubbleContentNode: ChatMessageBubbleContentNode
case .stars:
priceString = item.presentationData.strings.Notification_StarGiftOffer_Offer_Stars(Int32(clamping: amount.amount.value))
case .ton:
priceString = "\(amount.amount) TON"
priceString = formatTonAmountText(amount.amount.value, dateTimeFormat: item.presentationData.dateTimeFormat) + " TON"
}
let peerName = item.message.peers[item.message.id.peerId].flatMap { EnginePeer($0) }?.compactDisplayTitle ?? ""
@@ -530,6 +530,7 @@ public class ChatMessageInstantVideoItemNode: ChatMessageItemView, ASGestureReco
quote: replyQuote,
todoItemId: replyTodoItemId,
story: replyStory,
isSummarized: false,
parentMessage: item.message,
constrainedSize: CGSize(width: max(0, availableWidth), height: CGFloat.greatestFiniteMagnitude),
animationCache: item.controllerInteraction.presentationContext.animationCache,
@@ -349,7 +349,7 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode {
}
private func transcribe() {
guard let arguments = self.arguments, let context = self.context, let message = self.message else {
guard let _ = self.arguments, let context = self.context, let message = self.message else {
return
}
@@ -358,43 +358,7 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode {
}
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: arguments.context.currentAppConfiguration.with { $0 })
let transcriptionText = self.forcedAudioTranscriptionText ?? transcribedText(message: message)
if transcriptionText == nil && !arguments.associatedData.alwaysDisplayTranscribeButton.providedByGroupBoost {
if premiumConfiguration.audioTransciptionTrialCount > 0 {
if !arguments.associatedData.isPremium {
if self.presentAudioTranscriptionTooltip(finished: false) {
return
}
}
} else {
guard arguments.associatedData.isPremium else {
if self.hapticFeedback == nil {
self.hapticFeedback = HapticFeedback()
}
self.hapticFeedback?.impact(.medium)
let tipController = UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_voiceToText", scale: 0.065, colors: [:], title: nil, text: presentationData.strings.Message_AudioTranscription_SubscribeToPremium, customUndoText: presentationData.strings.Message_AudioTranscription_SubscribeToPremiumAction, timeout: nil), elevatedLayout: false, position: .top, animateInAsReplacement: false, action: { action in
if case .undo = action {
var replaceImpl: ((ViewController) -> Void)?
let controller = context.sharedContext.makePremiumDemoController(context: context, subject: .voiceToText, forceDark: false, action: {
let controller = context.sharedContext.makePremiumIntroController(context: context, source: .settings, forceDark: false, dismissed: nil)
replaceImpl?(controller)
}, dismissed: nil)
replaceImpl = { [weak controller] c in
controller?.replace(with: c)
}
arguments.controllerInteraction.navigationController()?.pushViewController(controller, animated: true)
let _ = ApplicationSpecificNotice.incrementAudioTranscriptionSuggestion(accountManager: context.sharedContext.accountManager).startStandalone()
}
return false })
arguments.controllerInteraction.presentControllerInCurrent(tipController, nil)
return
}
}
}
// GHOSTGRAM: Premium check removed - local transcription is free!
var shouldBeginTranscription = false
var shouldExpandNow = false
@@ -420,7 +384,8 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode {
self.audioTranscriptionState = .inProgress
self.requestUpdateLayout(true)
if context.sharedContext.immediateExperimentalUISettings.localTranscription {
// GHOSTGRAM: Always use local transcription (free, private, on-device!)
if true {
let appLocale = presentationData.strings.baseLanguageCode
let signal: Signal<LocallyTranscribedAudio?, NoError> = context.engine.data.get(TelegramEngine.EngineData.Item.Messages.Message(id: message.id))
@@ -640,8 +605,6 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode {
var audioWaveform: AudioWaveform?
var isVoice = false
var audioDuration: Int32 = 0
var isConsumed: Bool?
var consumableContentIcon: UIImage?
for attribute in arguments.message.attributes {
if let attribute = attribute as? ConsumableContentMessageAttribute {
@@ -652,7 +615,6 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode {
consumableContentIcon = PresentationResourcesChat.chatBubbleConsumableContentOutgoingIcon(arguments.presentationData.theme.theme)
}
}
isConsumed = attribute.consumed
break
}
}
@@ -771,24 +733,8 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode {
if Namespaces.Message.allNonRegular.contains(arguments.message.id.namespace) {
displayTranscribe = false
} else if arguments.message.id.peerId.namespace != Namespaces.Peer.SecretChat && !isViewOnceMessage && !arguments.presentationData.isPreview {
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: arguments.context.currentAppConfiguration.with { $0 })
if arguments.associatedData.isPremium {
displayTranscribe = true
} else if premiumConfiguration.audioTransciptionTrialCount > 0 {
if arguments.incoming {
if audioDuration < premiumConfiguration.audioTransciptionTrialMaxDuration {
displayTranscribe = true
}
}
} else if arguments.associatedData.alwaysDisplayTranscribeButton.canBeDisplayed {
if audioDuration >= 60 {
displayTranscribe = true
} else if arguments.incoming && isConsumed == false && arguments.associatedData.alwaysDisplayTranscribeButton.displayForNotConsumed {
displayTranscribe = true
}
} else if arguments.associatedData.alwaysDisplayTranscribeButton.providedByGroupBoost {
displayTranscribe = true
}
// GHOSTGRAM: Always show transcribe button for voice messages
displayTranscribe = true
}
let transcribedText = forcedAudioTranscriptionText ?? transcribedText(message: arguments.message)
@@ -1564,8 +1510,15 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode {
if isTranslating, !rects.isEmpty {
if self.shimmeringNodes.isEmpty {
let color: UIColor
let isIncoming = arguments.message.effectivelyIncoming(arguments.context.account.peerId)
if arguments.presentationData.theme.theme.overallDarkAppearance {
color = isIncoming ? arguments.presentationData.theme.theme.chat.message.incoming.primaryTextColor.withAlphaComponent(0.1) : arguments.presentationData.theme.theme.chat.message.outgoing.primaryTextColor.withAlphaComponent(0.1)
} else {
color = isIncoming ? arguments.presentationData.theme.theme.chat.message.incoming.accentTextColor.withAlphaComponent(0.1) : arguments.presentationData.theme.theme.chat.message.outgoing.secondaryTextColor.withAlphaComponent(0.1)
}
for rects in rects {
let shimmeringNode = ShimmeringLinkNode(color: arguments.message.effectivelyIncoming(arguments.context.account.peerId) ? arguments.presentationData.theme.theme.chat.message.incoming.secondaryTextColor.withAlphaComponent(0.1) : arguments.presentationData.theme.theme.chat.message.outgoing.secondaryTextColor.withAlphaComponent(0.1))
let shimmeringNode = ShimmeringLinkNode(color: color)
shimmeringNode.updateRects(rects)
shimmeringNode.frame = self.bounds
shimmeringNode.updateLayout(self.bounds.size)
@@ -404,6 +404,7 @@ public class ChatMessageInteractiveInstantVideoNode: ASDisplayNode {
quote: replyQuote,
todoItemId: replyTodoItemId,
story: replyStory,
isSummarized: false,
parentMessage: item.message,
constrainedSize: CGSize(width: availableWidth, height: CGFloat.greatestFiniteMagnitude),
animationCache: item.controllerInteraction.presentationContext.animationCache,
@@ -1830,45 +1831,7 @@ public class ChatMessageInteractiveInstantVideoNode: ASDisplayNode {
return
}
let presentationData = item.context.sharedContext.currentPresentationData.with { $0 }
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: item.context.currentAppConfiguration.with { $0 })
let transcriptionText = transcribedText(message: item.message)
if transcriptionText == nil && !item.associatedData.alwaysDisplayTranscribeButton.providedByGroupBoost {
if premiumConfiguration.audioTransciptionTrialCount > 0 {
if !item.associatedData.isPremium {
if self.presentAudioTranscriptionTooltip(finished: false) {
return
}
}
} else {
guard item.associatedData.isPremium else {
if self.hapticFeedback == nil {
self.hapticFeedback = HapticFeedback()
}
self.hapticFeedback?.impact(.medium)
let tipController = UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_voiceToText", scale: 0.065, colors: [:], title: nil, text: presentationData.strings.Message_AudioTranscription_SubscribeToPremium, customUndoText: presentationData.strings.Message_AudioTranscription_SubscribeToPremiumAction, timeout: nil), elevatedLayout: false, position: .top, animateInAsReplacement: false, action: { action in
if case .undo = action {
let context = item.context
var replaceImpl: ((ViewController) -> Void)?
let controller = context.sharedContext.makePremiumDemoController(context: context, subject: .voiceToText, forceDark: false, action: {
let controller = context.sharedContext.makePremiumIntroController(context: context, source: .settings, forceDark: false, dismissed: nil)
replaceImpl?(controller)
}, dismissed: nil)
replaceImpl = { [weak controller] c in
controller?.replace(with: c)
}
item.controllerInteraction.navigationController()?.pushViewController(controller, animated: true)
let _ = ApplicationSpecificNotice.incrementAudioTranscriptionSuggestion(accountManager: item.context.sharedContext.accountManager).startStandalone()
}
return false })
item.controllerInteraction.presentControllerInCurrent(tipController, nil)
return
}
}
}
// GHOSTGRAM: Premium check removed - local transcription is free!
var shouldBeginTranscription = false
var shouldExpandNow = false
@@ -645,7 +645,7 @@ public final class ChatMessageDateHeaderNodeImpl: ListViewItemHeaderNode, ChatMe
let isRotated = controllerInteraction?.chatIsRotated ?? true
super.init(layerBacked: false, dynamicBounce: true, isRotated: isRotated, seeThrough: false)
super.init(layerBacked: false, isRotated: isRotated, seeThrough: false)
if isRotated {
self.transform = CATransform3DMakeRotation(CGFloat.pi, 0.0, 0.0, 1.0)
@@ -1012,7 +1012,7 @@ public final class ChatMessageAvatarHeaderNodeImpl: ListViewItemHeaderNode, Chat
let isRotated = controllerInteraction?.chatIsRotated ?? true
super.init(layerBacked: false, dynamicBounce: true, isRotated: isRotated, seeThrough: false)
super.init(layerBacked: false, isRotated: isRotated, seeThrough: false)
if isRotated {
self.transform = CATransform3DMakeRotation(CGFloat.pi, 0.0, 0.0, 1.0)
@@ -81,7 +81,7 @@ public class ChatReplyCountItemNode: ListViewItemNode {
self.backgroundColorNode = ASDisplayNode()
super.init(layerBacked: false, dynamicBounce: true, rotated: true)
super.init(layerBacked: false, rotated: true)
self.addSubnode(self.labelNode)
@@ -94,7 +94,7 @@ public class ChatUnreadItemNode: ListViewItemNode {
self.activateArea = AccessibilityAreaNode()
self.activateArea.accessibilityTraits = .staticText
super.init(layerBacked: false, dynamicBounce: true, rotated: true)
super.init(layerBacked: false, rotated: true)
self.addSubnode(self.backgroundNode)
@@ -112,7 +112,7 @@ public final class ChatMessageAccessibilityData {
if let chatPeer = message.peers[item.message.id.peerId] {
let authorName = message.author.flatMap(EnginePeer.init)?.displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder)
let (_, _, messageText, _, _) = chatListItemStrings(strings: item.presentationData.strings, nameDisplayOrder: item.presentationData.nameDisplayOrder, dateTimeFormat: item.presentationData.dateTimeFormat, contentSettings: item.context.currentContentSettings.with { $0 }, messages: [EngineMessage(message)], chatPeer: EngineRenderedPeer(peer: EnginePeer(chatPeer)), accountPeerId: item.context.account.peerId)
let (_, _, messageText, _, _, _) = chatListItemStrings(strings: item.presentationData.strings, nameDisplayOrder: item.presentationData.nameDisplayOrder, dateTimeFormat: item.presentationData.dateTimeFormat, contentSettings: item.context.currentContentSettings.with { $0 }, messages: [EngineMessage(message)], chatPeer: EngineRenderedPeer(peer: EnginePeer(chatPeer)), accountPeerId: item.context.account.peerId)
var text = messageText
@@ -664,7 +664,7 @@ open class ChatMessageItemView: ListViewItemNode, ChatMessageItemNodeProtocol {
public var effectAnimationNodes: [ChatMessageTransitionNode.DecorationItemNode] = []
public required init(rotated: Bool) {
super.init(layerBacked: false, dynamicBounce: true, rotated: rotated)
super.init(layerBacked: false, rotated: rotated)
if rotated {
self.transform = CATransform3DMakeRotation(CGFloat.pi, 0.0, 0.0, 1.0)
}
@@ -105,7 +105,7 @@ final class ChatCallNotificationItemNode: NotificationItemNode {
override public func updateLayout(width: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat {
self.validLayout = width
let panelHeight: CGFloat = 66.0
let panelHeight: CGFloat = 64.0
guard let item = self.item else {
return panelHeight
@@ -113,19 +113,19 @@ final class ChatCallNotificationItemNode: NotificationItemNode {
let presentationData = item.context.sharedContext.currentPresentationData.with { $0 }
let leftInset: CGFloat = 14.0
let rightInset: CGFloat = 14.0
let avatarSize: CGFloat = 38.0
let leftInset: CGFloat = 12.0
let rightInset: CGFloat = 12.0
let avatarSize: CGFloat = 40.0
let avatarTextSpacing: CGFloat = 10.0
let buttonSpacing: CGFloat = 14.0
let titleTextSpacing: CGFloat = 0.0
let titleTextSpacing: CGFloat = 1.0
let maxTextWidth: CGFloat = width - leftInset - avatarTextSpacing - rightInset - avatarSize * 2.0 - buttonSpacing - avatarTextSpacing
let titleSize = self.title.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: item.peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), font: Font.semibold(16.0), textColor: presentationData.theme.list.itemPrimaryTextColor))
text: .plain(NSAttributedString(string: item.peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), font: Font.semibold(15.0), textColor: presentationData.theme.list.itemPrimaryTextColor))
)),
environment: {},
containerSize: CGSize(width: maxTextWidth, height: 100.0)
@@ -134,7 +134,7 @@ final class ChatCallNotificationItemNode: NotificationItemNode {
let textSize = self.text.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: item.isVideo ? presentationData.strings.Notification_VideoCallIncoming : presentationData.strings.Notification_CallIncoming, font: Font.regular(13.0), textColor: presentationData.theme.list.itemPrimaryTextColor))
text: .plain(NSAttributedString(string: item.isVideo ? presentationData.strings.Notification_VideoCallIncoming : presentationData.strings.Notification_CallIncoming, font: Font.regular(15.0), textColor: presentationData.theme.list.itemPrimaryTextColor))
)),
environment: {},
containerSize: CGSize(width: maxTextWidth, height: 100.0)
@@ -381,11 +381,11 @@ final class ChatMessageNotificationItemNode: NotificationItemNode {
var applyImage: (() -> Void)?
if let imageDimensions = imageDimensions {
let boundingSize = CGSize(width: 55.0, height: 55.0)
var radius: CGFloat = 6.0
var radius: CGFloat = 20.0
if isRound {
radius = floor(boundingSize.width / 2.0)
}
applyImage = imageNodeLayout(TransformImageArguments(corners: ImageCorners(radius: radius), imageSize: imageDimensions.aspectFilled(boundingSize), boundingSize: boundingSize, intrinsicInsets: UIEdgeInsets()))
applyImage = imageNodeLayout(TransformImageArguments(corners: ImageCorners(radius: radius, curve: isRound ? .circular : .continuous), imageSize: imageDimensions.aspectFilled(boundingSize), boundingSize: boundingSize, intrinsicInsets: UIEdgeInsets()))
}
var updateImageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>?
@@ -424,16 +424,16 @@ final class ChatMessageNotificationItemNode: NotificationItemNode {
let compact = self.compact ?? false
let panelHeight: CGFloat = compact ? 64.0 : 74.0
let imageSize: CGSize = compact ? CGSize(width: 44.0, height: 44.0) : CGSize(width: 54.0, height: 54.0)
let imageSpacing: CGFloat = compact ? 19.0 : 23.0
let imageSize: CGSize = compact ? CGSize(width: 40.0, height: 40.0) : CGSize(width: 54.0, height: 54.0)
let imageSpacing: CGFloat = compact ? 22.0 : 23.0
let leftInset: CGFloat = imageSize.width + imageSpacing
var rightInset: CGFloat = 8.0
var rightInset: CGFloat = 10.0
if !self.imageNode.isHidden {
rightInset += imageSize.width + 8.0
rightInset += imageSize.width + 10.0
}
transition.updateFrame(node: self.avatarNode, frame: CGRect(origin: CGPoint(x: 10.0, y: (panelHeight - imageSize.height) / 2.0), size: imageSize))
transition.updateFrame(node: self.avatarNode, frame: CGRect(origin: CGPoint(x: 12.0, y: (panelHeight - imageSize.height) / 2.0), size: imageSize))
var titleInset: CGFloat = 0.0
if let image = self.titleIconNode.image {
@@ -465,7 +465,7 @@ final class ChatMessageNotificationItemNode: NotificationItemNode {
let textSpacing: CGFloat = 1.0
let titleFrame = CGRect(origin: CGPoint(x: leftInset + titleInset, y: 1.0 + floor((panelHeight - textLayout.size.height - titleLayout.size.height - textSpacing) / 2.0)), size: titleLayout.size)
let titleFrame = CGRect(origin: CGPoint(x: leftInset + titleInset, y: floor((panelHeight - textLayout.size.height - titleLayout.size.height - textSpacing) / 2.0)), size: titleLayout.size)
transition.updateFrame(node: self.titleNode, frame: titleFrame)
if let image = self.titleIconNode.image {
@@ -475,7 +475,7 @@ final class ChatMessageNotificationItemNode: NotificationItemNode {
let textFrame = CGRect(origin: CGPoint(x: leftInset, y: titleFrame.maxY + textSpacing), size: textLayout.size)
transition.updateFrame(node: self.textNode.textNode, frame: textFrame)
transition.updateFrame(node: self.imageNode, frame: CGRect(origin: CGPoint(x: width - 10.0 - imageSize.width, y: (panelHeight - imageSize.height) / 2.0), size: imageSize))
transition.updateFrame(node: self.imageNode, frame: CGRect(origin: CGPoint(x: width - 12.0 - imageSize.width, y: (panelHeight - imageSize.height) / 2.0), size: imageSize))
if !textLayout.spoilers.isEmpty, let item = self.item {
let presentationData = item.context.sharedContext.currentPresentationData.with({ $0 })
@@ -25,6 +25,8 @@ swift_library(
"//submodules/CheckNode",
"//submodules/TelegramUIPreferences",
"//submodules/TelegramUI/Components/Stars/StarsBalanceOverlayComponent",
"//submodules/TelegramUI/Components/AlertComponent",
"//submodules/TelegramUI/Components/AlertComponent/AlertCheckComponent",
"//submodules/Markdown",
],
visibility = [
@@ -16,309 +16,10 @@ import CheckNode
import Markdown
import TextFormat
import StarsBalanceOverlayComponent
import AlertComponent
import AlertCheckComponent
private let textFont = Font.regular(13.0)
private let boldTextFont = Font.semibold(13.0)
private func formattedText(_ text: String, fontSize: CGFloat, color: UIColor, linkColor: UIColor, textAlignment: NSTextAlignment = .natural) -> NSAttributedString {
return parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: Font.regular(fontSize), textColor: color), bold: MarkdownAttributeSet(font: Font.semibold(fontSize), textColor: color), link: MarkdownAttributeSet(font: Font.regular(fontSize), textColor: linkColor), linkAttribute: { _ in return (TelegramTextAttributes.URL, "") }), textAlignment: textAlignment)
}
private final class ChatMessagePaymentAlertContentNode: AlertContentNode, ASGestureRecognizerDelegate {
private let strings: PresentationStrings
private let title: String
private let text: String
private let optionText: String?
private let alignment: TextAlertContentActionLayout
private let titleNode: ImmediateTextNode
private let textNode: ImmediateTextNode
private let checkNode: InteractiveCheckNode
private let checkLabelNode: ImmediateTextNode
private let actionNodesSeparator: ASDisplayNode
private let actionNodes: [TextAlertContentActionNode]
private let actionVerticalSeparators: [ASDisplayNode]
private var validLayout: CGSize?
override var dismissOnOutsideTap: Bool {
return self.isUserInteractionEnabled
}
var dontAskAgain: Bool = false {
didSet {
self.checkNode.setSelected(self.dontAskAgain, animated: true)
}
}
var openTerms: () -> Void = {}
init(theme: AlertControllerTheme, ptheme: PresentationTheme, strings: PresentationStrings, title: String, text: String, optionText: String?, actions: [TextAlertAction], alignment: TextAlertContentActionLayout) {
self.strings = strings
self.title = title
self.text = text
self.optionText = optionText
self.alignment = alignment
self.titleNode = ImmediateTextNode()
self.titleNode.displaysAsynchronously = false
self.titleNode.maximumNumberOfLines = 1
self.titleNode.textAlignment = .center
self.textNode = ImmediateTextNode()
self.textNode.maximumNumberOfLines = 0
self.textNode.displaysAsynchronously = false
self.textNode.lineSpacing = 0.1
self.textNode.textAlignment = .center
self.checkNode = InteractiveCheckNode(theme: CheckNodeTheme(backgroundColor: theme.accentColor, strokeColor: theme.contrastColor, borderColor: theme.controlBorderColor, overlayBorder: false, hasInset: false, hasShadow: false))
self.checkLabelNode = ImmediateTextNode()
self.checkLabelNode.maximumNumberOfLines = 4
self.actionNodesSeparator = ASDisplayNode()
self.actionNodesSeparator.isLayerBacked = true
self.actionNodes = actions.map { action -> TextAlertContentActionNode in
return TextAlertContentActionNode(theme: theme, action: action)
}
var actionVerticalSeparators: [ASDisplayNode] = []
if actions.count > 1 {
for _ in 0 ..< actions.count - 1 {
let separatorNode = ASDisplayNode()
separatorNode.isLayerBacked = true
actionVerticalSeparators.append(separatorNode)
}
}
self.actionVerticalSeparators = actionVerticalSeparators
super.init()
self.addSubnode(self.titleNode)
self.addSubnode(self.textNode)
if let _ = optionText {
self.addSubnode(self.checkNode)
self.addSubnode(self.checkLabelNode)
}
self.addSubnode(self.actionNodesSeparator)
for actionNode in self.actionNodes {
self.addSubnode(actionNode)
}
for separatorNode in self.actionVerticalSeparators {
self.addSubnode(separatorNode)
}
self.checkNode.valueChanged = { [weak self] value in
if let strongSelf = self {
strongSelf.dontAskAgain = !strongSelf.dontAskAgain
}
}
self.checkLabelNode.highlightAttributeAction = { attributes in
if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] {
return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)
} else {
return nil
}
}
self.checkLabelNode.tapAttributeAction = { [weak self] attributes, _ in
if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] {
self?.openTerms()
}
}
self.updateTheme(theme)
}
override func didLoad() {
super.didLoad()
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(self.acceptTap(_:)))
tapGesture.delegate = self.wrappedGestureRecognizerDelegate
self.view.addGestureRecognizer(tapGesture)
}
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
let location = gestureRecognizer.location(in: self.checkLabelNode.view)
if self.checkLabelNode.bounds.contains(location) {
return true
}
return false
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if !self.bounds.contains(point) {
return nil
}
if let (_, attributes) = self.checkLabelNode.attributesAtPoint(self.view.convert(point, to: self.checkLabelNode.view)) {
if attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] == nil {
return self.view
}
}
return super.hitTest(point, with: event)
}
@objc private func acceptTap(_ gestureRecognizer: UITapGestureRecognizer) {
self.dontAskAgain = !self.dontAskAgain
}
override func updateTheme(_ theme: AlertControllerTheme) {
self.titleNode.attributedText = NSAttributedString(string: self.title, font: Font.semibold(17.0), textColor: theme.primaryColor, paragraphAlignment: .center)
self.textNode.attributedText = formattedText(self.text, fontSize: 13.0, color: theme.primaryColor, linkColor: theme.accentColor, textAlignment: .center)
self.checkLabelNode.attributedText = parseMarkdownIntoAttributedString(
self.optionText ?? "",
attributes: MarkdownAttributes(
body: MarkdownAttributeSet(font: textFont, textColor: theme.primaryColor),
bold: MarkdownAttributeSet(font: boldTextFont, textColor: theme.primaryColor),
link: MarkdownAttributeSet(font: textFont, textColor: theme.primaryColor),
linkAttribute: { _ in
return nil
}
)
)
self.actionNodesSeparator.backgroundColor = theme.separatorColor
for actionNode in self.actionNodes {
actionNode.updateTheme(theme)
}
for separatorNode in self.actionVerticalSeparators {
separatorNode.backgroundColor = theme.separatorColor
}
if let size = self.validLayout {
_ = self.updateLayout(size: size, transition: .immediate)
}
}
override func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize {
var size = size
size.width = min(size.width, 270.0)
self.validLayout = size
var origin: CGPoint = CGPoint(x: 0.0, y: 17.0)
let titleSize = self.titleNode.updateLayout(CGSize(width: size.width - 32.0, height: size.height))
transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - titleSize.width) / 2.0), y: origin.y), size: titleSize))
origin.y += titleSize.height + 4.0
var entriesHeight: CGFloat = 0.0
let textSize = self.textNode.updateLayout(CGSize(width: size.width - 32.0, height: size.height))
transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - textSize.width) / 2.0), y: origin.y), size: textSize))
origin.y += textSize.height
if self.checkLabelNode.supernode != nil {
origin.y += 21.0
entriesHeight += 21.0
let checkSize = CGSize(width: 22.0, height: 22.0)
let condensedSize = CGSize(width: size.width - 76.0, height: size.height)
let spacing: CGFloat = 12.0
let acceptTermsSize = self.checkLabelNode.updateLayout(condensedSize)
let acceptTermsTotalWidth = checkSize.width + spacing + acceptTermsSize.width
let acceptTermsOriginX = floorToScreenPixels((size.width - acceptTermsTotalWidth) / 2.0)
transition.updateFrame(node: self.checkNode, frame: CGRect(origin: CGPoint(x: acceptTermsOriginX, y: origin.y - 3.0), size: checkSize))
transition.updateFrame(node: self.checkLabelNode, frame: CGRect(origin: CGPoint(x: acceptTermsOriginX + checkSize.width + spacing, y: origin.y), size: acceptTermsSize))
origin.y += acceptTermsSize.height
entriesHeight += acceptTermsSize.height
origin.y += 21.0
}
let actionButtonHeight: CGFloat = 44.0
var minActionsWidth: CGFloat = 0.0
let maxActionWidth: CGFloat = floor(size.width / CGFloat(self.actionNodes.count))
let actionTitleInsets: CGFloat = 8.0
var effectiveActionLayout = self.alignment
for actionNode in self.actionNodes {
let actionTitleSize = actionNode.titleNode.updateLayout(CGSize(width: maxActionWidth, height: actionButtonHeight))
if case .horizontal = effectiveActionLayout, actionTitleSize.height > actionButtonHeight * 0.6667 {
effectiveActionLayout = .vertical
}
switch effectiveActionLayout {
case .horizontal:
minActionsWidth += actionTitleSize.width + actionTitleInsets
case .vertical:
minActionsWidth = max(minActionsWidth, actionTitleSize.width + actionTitleInsets)
}
}
let insets = UIEdgeInsets(top: 18.0, left: 18.0, bottom: 18.0, right: 18.0)
let contentWidth = max(size.width, minActionsWidth)
var actionsHeight: CGFloat = 0.0
switch effectiveActionLayout {
case .horizontal:
actionsHeight = actionButtonHeight
case .vertical:
actionsHeight = actionButtonHeight * CGFloat(self.actionNodes.count)
}
let resultSize = CGSize(width: contentWidth, height: titleSize.height + textSize.height + entriesHeight + actionsHeight + 3.0 + insets.top + insets.bottom)
transition.updateFrame(node: self.actionNodesSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel)))
var actionOffset: CGFloat = 0.0
let actionWidth: CGFloat = floor(resultSize.width / CGFloat(self.actionNodes.count))
var separatorIndex = -1
var nodeIndex = 0
for actionNode in self.actionNodes {
if separatorIndex >= 0 {
let separatorNode = self.actionVerticalSeparators[separatorIndex]
switch effectiveActionLayout {
case .horizontal:
transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: actionOffset - UIScreenPixel, y: resultSize.height - actionsHeight), size: CGSize(width: UIScreenPixel, height: actionsHeight - UIScreenPixel)))
case .vertical:
transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel)))
}
}
separatorIndex += 1
let currentActionWidth: CGFloat
switch effectiveActionLayout {
case .horizontal:
if nodeIndex == self.actionNodes.count - 1 {
currentActionWidth = resultSize.width - actionOffset
} else {
currentActionWidth = actionWidth
}
case .vertical:
currentActionWidth = resultSize.width
}
let actionNodeFrame: CGRect
switch effectiveActionLayout {
case .horizontal:
actionNodeFrame = CGRect(origin: CGPoint(x: actionOffset, y: resultSize.height - actionsHeight), size: CGSize(width: currentActionWidth, height: actionButtonHeight))
actionOffset += currentActionWidth
case .vertical:
actionNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset), size: CGSize(width: currentActionWidth, height: actionButtonHeight))
actionOffset += actionButtonHeight
}
transition.updateFrame(node: actionNode, frame: actionNodeFrame)
nodeIndex += 1
}
return resultSize
}
}
public class ChatMessagePaymentAlertController: AlertController {
public class ChatMessagePaymentAlertController: AlertScreen {
private let context: AccountContext?
private let presentationData: PresentationData
private weak var parentNavigationController: NavigationController?
@@ -327,29 +28,26 @@ public class ChatMessagePaymentAlertController: AlertController {
private let animateBalanceOverlay: Bool
private var didUpdateCurrency = false
public var currency: CurrencyAmount.Currency {
didSet {
self.didUpdateCurrency = true
if let layout = self.validLayout {
self.containerLayoutUpdated(layout, transition: .animated(duration: 0.25, curve: .easeInOut))
}
}
}
private var initialCurrency: CurrencyAmount.Currency?
public var currency: CurrencyAmount.Currency?
private var currencyDisposable: Disposable?
private let balance = ComponentView<Empty>()
private var didAppear = false
private var validLayout: ContainerViewLayout?
public init(
context: AccountContext?,
presentationData: PresentationData,
contentNode: AlertContentNode,
updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil,
configuration: Configuration = AlertScreen.Configuration(),
contentSignal: Signal<[AnyComponentWithIdentity<AlertComponentEnvironment>], NoError>,
actionsSignal: Signal<[AlertScreen.Action], NoError>,
navigationController: NavigationController?,
chatPeerId: EnginePeer.Id,
showBalance: Bool = true,
currency: CurrencyAmount.Currency = .stars,
currencySignal: Signal<CurrencyAmount.Currency, NoError> = .single(.stars),
animateBalanceOverlay: Bool = true
) {
self.context = context
@@ -357,31 +55,84 @@ public class ChatMessagePaymentAlertController: AlertController {
self.parentNavigationController = navigationController
self.chatPeerId = chatPeerId
self.showBalance = showBalance
self.currency = currency
self.animateBalanceOverlay = animateBalanceOverlay
super.init(theme: AlertControllerTheme(presentationData: presentationData), contentNode: contentNode)
var effectiveUpdatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)
if let updatedPresentationData {
effectiveUpdatedPresentationData = updatedPresentationData
} else {
effectiveUpdatedPresentationData = (initial: presentationData, signal: .single(presentationData))
}
super.init(
configuration: configuration,
contentSignal: contentSignal,
actionsSignal: actionsSignal,
updatedPresentationData: effectiveUpdatedPresentationData
)
self.willDismiss = { [weak self] in
self.currencyDisposable = (currencySignal
|> distinctUntilChanged
|> deliverOnMainQueue).start(next: { [weak self] currency in
guard let self else {
return
}
self.animateOut()
}
if self.currency == nil {
self.initialCurrency = currency
}
self.currency = currency
if let layout = self.validLayout {
self.containerLayoutUpdated(layout, transition: .animated(duration: 0.25, curve: .easeInOut))
}
})
}
public convenience init(
context: AccountContext?,
presentationData: PresentationData,
updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil,
configuration: Configuration = AlertScreen.Configuration(),
content: [AnyComponentWithIdentity<AlertComponentEnvironment>],
actions: [AlertScreen.Action],
navigationController: NavigationController?,
chatPeerId: EnginePeer.Id,
showBalance: Bool = true,
currency: CurrencyAmount.Currency = .stars,
animateBalanceOverlay: Bool = true
) {
self.init(
context: context,
presentationData: presentationData,
updatedPresentationData: updatedPresentationData,
configuration: configuration,
contentSignal: .single(content),
actionsSignal: .single(actions),
navigationController: navigationController,
chatPeerId: chatPeerId,
showBalance: showBalance,
currencySignal: .single(currency),
animateBalanceOverlay: animateBalanceOverlay
)
}
required public init(coder aDecoder: NSCoder) {
preconditionFailure()
}
override public func dismiss(completion: (() -> Void)? = nil) {
super.dismiss(completion: completion)
self.animateOut()
}
private func animateOut() {
if !self.animateBalanceOverlay {
if self.currency == .ton && self.didUpdateCurrency {
if case .ton = self.currency, let initialCurrency, initialCurrency != self.currency {
self.currency = .stars
if let layout = self.validLayout {
self.containerLayoutUpdated(layout, transition: .animated(duration: 0.25, curve: .easeInOut))
}
}
Queue.mainQueue().after(0.39, {
})
} else {
if let view = self.balance.view {
view.layer.animateScale(from: 1.0, to: 0.8, duration: 0.4, removeOnCompletion: false)
@@ -389,18 +140,10 @@ public class ChatMessagePaymentAlertController: AlertController {
}
}
}
public override func dismissAnimated() {
super.dismissAnimated()
self.animateOut()
}
public override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
self.validLayout = layout
if !self.didAppear {
self.didAppear = true
if !layout.metrics.isTablet && layout.size.width > layout.size.height {
@@ -410,7 +153,7 @@ public class ChatMessagePaymentAlertController: AlertController {
}
}
if let context = self.context, let _ = self.parentNavigationController, self.showBalance {
if let context = self.context, let _ = self.parentNavigationController, self.showBalance, let currency = self.currency {
let insets = layout.insets(options: .statusBar)
var balanceTransition = ComponentTransition(transition)
if self.balance.view == nil {
@@ -424,12 +167,12 @@ public class ChatMessagePaymentAlertController: AlertController {
context: context,
peerId: self.chatPeerId.namespace == Namespaces.Peer.CloudChannel ? self.chatPeerId : context.account.peerId,
theme: self.presentationData.theme,
currency: self.currency,
currency: currency,
action: { [weak self] in
guard let self, let starsContext = context.starsContext, let navigationController = self.parentNavigationController else {
guard let self, let starsContext = context.starsContext, let navigationController = self.parentNavigationController, let currency = self.currency else {
return
}
switch self.currency {
switch currency {
case .stars:
let _ = (context.engine.payments.starsTopUpOptions()
|> take(1)
@@ -452,7 +195,7 @@ public class ChatMessagePaymentAlertController: AlertController {
}
context.sharedContext.applicationBindings.openUrl(fragmentUrl)
}
self.dismissAnimated()
self.dismiss(completion: nil)
}
)
),
@@ -486,57 +229,66 @@ public func chatMessagePaymentAlertController(
hasCheck: Bool = true,
navigationController: NavigationController?,
completion: @escaping (Bool) -> Void
) -> AlertController {
let theme = defaultDarkColorPresentationTheme
let presentationData = updatedPresentationData?.initial ?? presentationData
) -> ViewController {
let strings = presentationData.strings
var completionImpl: (() -> Void)?
var dismissImpl: (() -> Void)?
let title = presentationData.strings.Chat_PaidMessage_Confirm_Title
let actionTitle = presentationData.strings.Chat_PaidMessage_Confirm_PayForMessage(count)
let messagesString = presentationData.strings.Chat_PaidMessage_Confirm_Text_Messages(count)
let actions: [TextAlertAction] = [TextAlertAction(type: .defaultAction, title: actionTitle, action: {
completionImpl?()
dismissImpl?()
}), TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {
dismissImpl?()
})]
let messagesString = strings.Chat_PaidMessage_Confirm_Text_Messages(count)
let text: String
if peers.count == 1, let peer = peers.first {
let amountString = presentationData.strings.Chat_PaidMessage_Confirm_Text_Stars(Int32(clamping: amount.value))
let totalString = presentationData.strings.Chat_PaidMessage_Confirm_Text_Stars(Int32(clamping: amount.value * Int64(count)))
let amountString = strings.Chat_PaidMessage_Confirm_Text_Stars(Int32(clamping: amount.value))
let totalString = strings.Chat_PaidMessage_Confirm_Text_Stars(Int32(clamping: amount.value * Int64(count)))
if case let .channel(channel) = peer.chatOrMonoforumMainPeer, case .broadcast = channel.info {
text = presentationData.strings.Chat_PaidMessage_Confirm_SingleComment_Text(EnginePeer(channel).compactDisplayTitle, amountString, totalString, messagesString).string
text = strings.Chat_PaidMessage_Confirm_SingleComment_Text(EnginePeer(channel).compactDisplayTitle, amountString, totalString, messagesString).string
} else {
text = presentationData.strings.Chat_PaidMessage_Confirm_Single_Text(peer.chatOrMonoforumMainPeer?.compactDisplayTitle ?? " ", amountString, totalString, messagesString).string
text = strings.Chat_PaidMessage_Confirm_Single_Text(peer.chatOrMonoforumMainPeer?.compactDisplayTitle ?? " ", amountString, totalString, messagesString).string
}
} else {
let amount = totalAmount ?? amount
let usersString = presentationData.strings.Chat_PaidMessage_Confirm_Text_Users(Int32(peers.count))
let totalString = presentationData.strings.Chat_PaidMessage_Confirm_Text_Stars(Int32(clamping: amount.value * Int64(count)))
text = presentationData.strings.Chat_PaidMessage_Confirm_Multiple_Text(usersString, totalString, messagesString).string
let usersString = strings.Chat_PaidMessage_Confirm_Text_Users(Int32(peers.count))
let totalString = strings.Chat_PaidMessage_Confirm_Text_Stars(Int32(clamping: amount.value * Int64(count)))
text = strings.Chat_PaidMessage_Confirm_Multiple_Text(usersString, totalString, messagesString).string
}
let checkState = AlertCheckComponent.ExternalState()
var content: [AnyComponentWithIdentity<AlertComponentEnvironment>] = []
content.append(AnyComponentWithIdentity(
id: "title",
component: AnyComponent(
AlertTitleComponent(title: strings.Chat_PaidMessage_Confirm_Title)
)
))
content.append(AnyComponentWithIdentity(
id: "text",
component: AnyComponent(
AlertTextComponent(content: .plain(text))
)
))
if hasCheck {
content.append(AnyComponentWithIdentity(
id: "check",
component: AnyComponent(
AlertCheckComponent(title: strings.Chat_PaidMessage_Confirm_DontAskAgain, initialValue: false, externalState: checkState)
)
))
}
let optionText = hasCheck ? presentationData.strings.Chat_PaidMessage_Confirm_DontAskAgain : nil
let contentNode = ChatMessagePaymentAlertContentNode(theme: AlertControllerTheme(presentationData: presentationData), ptheme: theme, strings: strings, title: title, text: text, optionText: optionText, actions: actions, alignment: .vertical)
completionImpl = { [weak contentNode] in
guard let contentNode else {
return
}
completion(contentNode.dontAskAgain)
}
let controller = ChatMessagePaymentAlertController(context: context, presentationData: presentationData, contentNode: contentNode, navigationController: navigationController, chatPeerId: context?.account.peerId ?? peers[0].peerId)
dismissImpl = { [weak controller] in
controller?.dismissAnimated()
}
return controller
let alertController = ChatMessagePaymentAlertController(
context: context,
presentationData: presentationData,
updatedPresentationData: updatedPresentationData,
configuration: AlertScreen.Configuration(actionAlignment: .vertical, allowInputInset: true),
content: content,
actions: [
.init(title: strings.Chat_PaidMessage_Confirm_PayForMessage(count), type: .default, action: {
completion(checkState.value)
}),
.init(title: strings.Common_Cancel)
],
navigationController: navigationController,
chatPeerId: context?.account.peerId ?? peers[0].peerId
)
return alertController
}
public func chatMessageRemovePaymentAlertController(
@@ -548,47 +300,55 @@ public func chatMessageRemovePaymentAlertController(
amount: StarsAmount?,
navigationController: NavigationController?,
completion: @escaping (Bool) -> Void
) -> AlertController {
let theme = defaultDarkColorPresentationTheme
let presentationData = updatedPresentationData?.initial ?? presentationData
) -> ViewController {
let strings = presentationData.strings
var completionImpl: (() -> Void)?
var dismissImpl: (() -> Void)?
let actions: [TextAlertAction] = [
TextAlertAction(type: .genericAction, title: strings.Common_Cancel, action: {
dismissImpl?()
}),
TextAlertAction(type: .defaultAction, title: strings.Chat_PaidMessage_RemoveFee_Yes, action: {
completionImpl?()
dismissImpl?()
})
]
let title = strings.Chat_PaidMessage_RemoveFee_Title
let text: String
if let context, chatPeer.id != context.account.peerId {
if case .user = chatPeer {
text = strings.Chat_PaidMessage_RemoveFee_Text(peer.compactDisplayTitle).string
} else if let context, chatPeer.id != context.account.peerId {
text = strings.Channel_RemoveFeeAlert_Text(peer.compactDisplayTitle).string
} else {
text = strings.Chat_PaidMessage_RemoveFee_Text(peer.compactDisplayTitle).string
}
let checkState = AlertCheckComponent.ExternalState()
let optionText = amount.flatMap { strings.Chat_PaidMessage_RemoveFee_Refund(strings.Chat_PaidMessage_RemoveFee_Refund_Stars(Int32(clamping: $0.value))).string }
let contentNode = ChatMessagePaymentAlertContentNode(theme: AlertControllerTheme(presentationData: presentationData), ptheme: theme, strings: strings, title: title, text: text, optionText: optionText, actions: actions, alignment: .horizontal)
completionImpl = { [weak contentNode] in
guard let contentNode else {
return
}
completion(contentNode.dontAskAgain)
var content: [AnyComponentWithIdentity<AlertComponentEnvironment>] = []
content.append(AnyComponentWithIdentity(
id: "title",
component: AnyComponent(
AlertTitleComponent(title: strings.Chat_PaidMessage_RemoveFee_Title)
)
))
content.append(AnyComponentWithIdentity(
id: "text",
component: AnyComponent(
AlertTextComponent(content: .plain(text))
)
))
if let amount {
content.append(AnyComponentWithIdentity(
id: "check",
component: AnyComponent(
AlertCheckComponent(title: strings.Chat_PaidMessage_RemoveFee_Refund(strings.Chat_PaidMessage_RemoveFee_Refund_Stars(Int32(clamping: amount.value))).string, initialValue: false, externalState: checkState)
)
))
}
let controller = ChatMessagePaymentAlertController(context: context, presentationData: presentationData, contentNode: contentNode, navigationController: navigationController, chatPeerId: chatPeer.id)
dismissImpl = { [weak controller] in
controller?.dismissAnimated()
}
return controller
let alertController = ChatMessagePaymentAlertController(
context: context,
presentationData: presentationData,
updatedPresentationData: updatedPresentationData,
content: content,
actions: [
.init(title: strings.Common_Cancel),
.init(title: strings.Chat_PaidMessage_RemoveFee_Yes, type: .default, action: {
completion(checkState.value)
})
],
navigationController: navigationController,
chatPeerId: chatPeer.id
)
return alertController
}
@@ -1678,8 +1678,15 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
if isTranslating, !rects.isEmpty {
if self.shimmeringNodes.isEmpty {
let color: UIColor
let isIncoming = item.message.effectivelyIncoming(item.context.account.peerId)
if item.presentationData.theme.theme.overallDarkAppearance {
color = isIncoming ? item.presentationData.theme.theme.chat.message.incoming.primaryTextColor.withAlphaComponent(0.1) : item.presentationData.theme.theme.chat.message.outgoing.primaryTextColor.withAlphaComponent(0.1)
} else {
color = isIncoming ? item.presentationData.theme.theme.chat.message.incoming.accentTextColor.withAlphaComponent(0.1) : item.presentationData.theme.theme.chat.message.outgoing.secondaryTextColor.withAlphaComponent(0.1)
}
for rects in rects {
let shimmeringNode = ShimmeringLinkNode(color: item.message.effectivelyIncoming(item.context.account.peerId) ? item.presentationData.theme.theme.chat.message.incoming.secondaryTextColor.withAlphaComponent(0.1) : item.presentationData.theme.theme.chat.message.outgoing.secondaryTextColor.withAlphaComponent(0.1))
let shimmeringNode = ShimmeringLinkNode(color: color)
shimmeringNode.updateRects(rects)
shimmeringNode.frame = self.bounds
shimmeringNode.updateLayout(self.bounds.size)
@@ -85,6 +85,7 @@ public class ChatMessageReplyInfoNode: ASDisplayNode {
public let quote: (quote: EngineMessageReplyQuote, isQuote: Bool)?
public let todoItemId: Int32?
public let story: StoryId?
public let isSummarized: Bool
public let parentMessage: Message
public let constrainedSize: CGSize
public let animationCache: AnimationCache?
@@ -101,6 +102,7 @@ public class ChatMessageReplyInfoNode: ASDisplayNode {
quote: (quote: EngineMessageReplyQuote, isQuote: Bool)?,
todoItemId: Int32?,
story: StoryId?,
isSummarized: Bool,
parentMessage: Message,
constrainedSize: CGSize,
animationCache: AnimationCache?,
@@ -116,6 +118,7 @@ public class ChatMessageReplyInfoNode: ASDisplayNode {
self.quote = quote
self.todoItemId = todoItemId
self.story = story
self.isSummarized = isSummarized
self.parentMessage = parentMessage
self.constrainedSize = constrainedSize
self.animationCache = animationCache
@@ -133,6 +136,7 @@ public class ChatMessageReplyInfoNode: ASDisplayNode {
}
private let backgroundView: MessageInlineBlockBackgroundView
private var starsView: StarsView?
private var quoteIconView: UIImageView?
private let contentNode: ASDisplayNode
private var titleNode: TextNode?
@@ -206,7 +210,6 @@ public class ChatMessageReplyInfoNode: ASDisplayNode {
var secondaryColor: UIColor?
var tertiaryColor: UIColor?
var authorNameColor: UIColor?
var dashSecondaryColor: UIColor?
var dashTertiaryColor: UIColor?
@@ -239,6 +242,10 @@ public class ChatMessageReplyInfoNode: ASDisplayNode {
break
}
if arguments.isSummarized {
authorNameColor = nil
}
switch arguments.type {
case let .bubble(incoming):
titleColor = incoming ? (authorNameColor ?? arguments.presentationData.theme.theme.chat.message.incoming.accentTextColor) : arguments.presentationData.theme.theme.chat.message.outgoing.accentTextColor
@@ -437,7 +444,7 @@ public class ChatMessageReplyInfoNode: ASDisplayNode {
}
var textLeftInset: CGFloat = 0.0
let messageText: NSAttributedString
var messageText: NSAttributedString
var todoItemCompleted: Bool?
if let todoItemId = arguments.todoItemId, let todo = arguments.message?.media.first(where: { $0 is TelegramMediaTodo }) as? TelegramMediaTodo, let todoItem = todo.items.first(where: { $0.id == todoItemId }) {
messageText = stringWithAppliedEntities(todoItem.text, entities: todoItem.entities, baseColor: textColor, linkColor: textColor, baseFont: textFont, linkFont: textFont, boldFont: textFont, italicFont: textFont, boldItalicFont: textFont, fixedFont: textFont, blockQuoteFont: textFont, underlineLinks: false, message: nil)
@@ -607,6 +614,11 @@ public class ChatMessageReplyInfoNode: ASDisplayNode {
}
adjustedConstrainedTextSize.width -= textLeftInset
if arguments.isSummarized {
titleString = NSAttributedString(string: arguments.presentationData.strings.Conversation_Summary_Title, font: titleFont, textColor: titleColor)
messageText = NSAttributedString(string: arguments.presentationData.strings.Conversation_Summary_Text, font: textFont, textColor: titleColor)
}
let (titleLayout, titleApply) = titleNodeLayout(TextNodeLayoutArguments(attributedString: titleString, backgroundColor: nil, maximumNumberOfLines: maxTitleNumberOfLines, truncationType: .end, constrainedSize: CGSize(width: contrainedTextSize.width - additionalTitleWidth, height: contrainedTextSize.height), alignment: .natural, cutout: nil, insets: textInsets))
if isExpiredStory || isStory {
contrainedTextSize.width -= 26.0
@@ -687,6 +699,11 @@ public class ChatMessageReplyInfoNode: ASDisplayNode {
node = ChatMessageReplyInfoNode()
}
var animation = animation
if node.titleNode == nil {
animation = .None
}
node.previousMediaReference = updatedMediaReference
//node.textNode?.textNode.displaysAsynchronously = !arguments.presentationData.isPreview
@@ -925,6 +942,22 @@ public class ChatMessageReplyInfoNode: ASDisplayNode {
giftEmojiLayer.removeFromSuperlayer()
}
if arguments.isSummarized {
let starsView: StarsView
if let current = node.starsView {
starsView = current
} else {
starsView = StarsView()
node.starsView = starsView
node.contentNode.view.insertSubview(starsView, at: 1)
}
starsView.frame = CGRect(origin: CGPoint(), size: backgroundFrame.size)
starsView.update(size: backgroundFrame.size, color: mainColor)
} else if let starsView = node.starsView {
node.starsView = nil
starsView.removeFromSuperview()
}
node.contentNode.frame = CGRect(origin: CGPoint(), size: size)
return node
@@ -1065,3 +1098,96 @@ public class ChatMessageReplyInfoNode: ASDisplayNode {
return nil
}
}
private final class StarsView: UIView {
private let staticEmitterLayer = CAEmitterLayer()
private var currentColor: UIColor?
override init(frame: CGRect) {
super.init(frame: frame)
self.clipsToBounds = true
self.layer.addSublayer(self.staticEmitterLayer)
}
required init(coder: NSCoder) {
preconditionFailure()
}
private func setupEmitter(size: CGSize) {
guard let currentColor = self.currentColor else {
return
}
let color = currentColor
self.staticEmitterLayer.emitterShape = .rectangle
self.staticEmitterLayer.emitterSize = size
self.staticEmitterLayer.emitterMode = .surface
self.layer.addSublayer(self.staticEmitterLayer)
let staticEmitter = CAEmitterCell()
staticEmitter.name = "emitter"
staticEmitter.contents = UIImage(bundleImageName: "Premium/Stars/Particle")?.cgImage
staticEmitter.birthRate = 20.0
staticEmitter.lifetime = 3.2
staticEmitter.velocity = 18.0
staticEmitter.velocityRange = 3
staticEmitter.scale = 0.1
staticEmitter.scaleRange = 0.08
staticEmitter.emissionRange = .pi * 2.0
staticEmitter.setValue(3.0, forKey: "mass")
staticEmitter.setValue(2.0, forKey: "massRange")
let staticColors: [Any] = [
color.withAlphaComponent(0.0).cgColor,
color.cgColor,
color.withAlphaComponent(0.0).cgColor,
color.withAlphaComponent(0.0).cgColor,
color.cgColor,
color.withAlphaComponent(0.0).cgColor
]
let staticColorBehavior = CAEmitterCell.createEmitterBehavior(type: "colorOverLife")
staticColorBehavior.setValue(staticColors, forKey: "colors")
staticEmitter.setValue([staticColorBehavior], forKey: "emitterBehaviors")
let attractor = CAEmitterCell.createEmitterBehavior(type: "simpleAttractor")
attractor.setValue("attractor", forKey: "name")
attractor.setValue(20, forKey: "falloff")
attractor.setValue(35, forKey: "radius")
self.staticEmitterLayer.setValue([attractor], forKey: "emitterBehaviors")
self.staticEmitterLayer.setValue(4.0, forKeyPath: "emitterBehaviors.attractor.stiffness")
self.staticEmitterLayer.setValue(false, forKeyPath: "emitterBehaviors.attractor.enabled")
self.staticEmitterLayer.emitterCells = [staticEmitter]
}
func update(size: CGSize, color: UIColor) {
if self.staticEmitterLayer.emitterCells == nil {
self.currentColor = color
self.setupEmitter(size: size)
} else if self.currentColor != color {
self.currentColor = color
let staticColors: [Any] = [
UIColor.white.withAlphaComponent(0.0).cgColor,
UIColor.white.withAlphaComponent(0.35).cgColor,
color.cgColor,
color.cgColor,
color.withAlphaComponent(0.0).cgColor
]
let staticColorBehavior = CAEmitterCell.createEmitterBehavior(type: "colorOverLife")
staticColorBehavior.setValue(staticColors, forKey: "colors")
for cell in self.staticEmitterLayer.emitterCells ?? [] {
cell.setValue([staticColorBehavior], forKey: "emitterBehaviors")
}
}
let emitterPosition = CGPoint(x: size.width * 0.5, y: size.height * 0.5)
self.staticEmitterLayer.frame = CGRect(origin: .zero, size: size)
self.staticEmitterLayer.emitterPosition = emitterPosition
self.staticEmitterLayer.setValue(emitterPosition, forKeyPath: "emitterBehaviors.attractor.position")
}
}
@@ -121,7 +121,6 @@ private final class GlassButtonView: UIView {
override init(frame: CGRect) {
self.backgroundView = GlassBackgroundView()
self.backgroundView.isUserInteractionEnabled = false
self.iconView = GlassBackgroundView.ContentImageView()
self.backgroundView.contentView.addSubview(self.iconView)
@@ -460,11 +459,13 @@ public final class ChatMessageSelectionInputPanelNode: ChatInputPanelNode {
override public func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, additionalSideInsets: UIEdgeInsets, maxHeight: CGFloat, maxOverlayHeight: CGFloat, isSecondary: Bool, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics, isMediaInputExpanded: Bool) -> CGFloat {
self.validLayout = (width, leftInset, rightInset, bottomInset, additionalSideInsets, maxHeight, maxOverlayHeight, metrics, isSecondary, isMediaInputExpanded)
var leftInset = leftInset
leftInset += 16.0
var leftInset = leftInset + 8.0
var rightInset = rightInset + 8.0
var rightInset = rightInset
rightInset += 16.0
if bottomInset <= 32.0 {
leftInset += 18.0
rightInset += 18.0
}
let panelHeight = defaultHeight(metrics: metrics)
@@ -20,6 +20,7 @@ swift_library(
"//submodules/WallpaperBackgroundNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageItemCommon",
"//submodules/ContextUI",
"//submodules/Components/HierarchyTrackingLayer",
],
visibility = [
"//visibility:public",
@@ -10,6 +10,7 @@ import Postbox
import WallpaperBackgroundNode
import ChatMessageItemCommon
import ContextUI
import HierarchyTrackingLayer
public class ChatMessageShareButton: ASDisplayNode {
private let referenceNode: ContextReferenceContentNode
@@ -21,15 +22,18 @@ public class ChatMessageShareButton: ASDisplayNode {
private let topButton: HighlightTrackingButtonNode
private let topIconNode: ASImageNode
private var topIconOffset = CGPoint()
private var bottomButton: HighlightTrackingButtonNode?
private var bottomIconNode: ASImageNode?
private var starsView: StarsView?
private var separatorNode: ASDisplayNode?
private var theme: PresentationTheme?
private var isReplies: Bool = false
private var hasMore: Bool = false
private var isExpand: Bool = false
private var textNode: ImmediateTextNode?
@@ -103,7 +107,7 @@ public class ChatMessageShareButton: ASDisplayNode {
self.morePressed?()
}
public func update(presentationData: ChatPresentationData, controllerInteraction: ChatControllerInteraction, chatLocation: ChatLocation, subject: ChatControllerSubject?, message: Message, account: Account, disableComments: Bool = false) -> CGSize {
public func update(presentationData: ChatPresentationData, controllerInteraction: ChatControllerInteraction, chatLocation: ChatLocation, subject: ChatControllerSubject?, message: Message, account: Account, disableComments: Bool = false, isSummarize: Bool = false) -> CGSize {
var isReplies = false
var isNavigate = false
var replyCount = 0
@@ -134,15 +138,27 @@ public class ChatMessageShareButton: ASDisplayNode {
hasMore = true
}
if self.theme !== presentationData.theme.theme || self.isReplies != isReplies || self.hasMore != hasMore {
var isExpand = false
if controllerInteraction.summarizedMessageIds.contains(message.id) {
isExpand = true
}
if self.theme !== presentationData.theme.theme || self.isReplies != isReplies || self.hasMore != hasMore || self.isExpand != isExpand {
self.theme = presentationData.theme.theme
self.isReplies = isReplies
self.hasMore = hasMore
self.isExpand = isExpand
var updatedIconImage: UIImage?
var updatedBottomIconImage: UIImage?
var updatedIconOffset = CGPoint()
if let _ = message.adAttribute {
if isSummarize {
if isExpand {
updatedIconImage = PresentationResourcesChat.chatFreeExpandButtonIcon(presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper)
} else {
updatedIconImage = PresentationResourcesChat.chatFreeCollapseButtonIcon(presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper)
}
} else if let _ = message.adAttribute {
updatedIconImage = PresentationResourcesChat.chatFreeCloseButtonIcon(presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper)
updatedIconOffset = CGPoint(x: UIScreenPixel, y: UIScreenPixel)
@@ -167,6 +183,17 @@ public class ChatMessageShareButton: ASDisplayNode {
updatedIconImage = PresentationResourcesChat.chatFreeShareButtonIcon(presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper)
}
if isSummarize {
if self.topIconNode.image != nil, let snapshotView = self.topIconNode.view.snapshotContentTree() {
self.view.addSubview(snapshotView)
snapshotView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.25, removeOnCompletion: false, completion: { _ in
snapshotView.removeFromSuperview()
})
self.topIconNode.layer.animateScale(from: 0.01, to: 1.0, duration: 0.25)
}
}
self.topIconNode.image = updatedIconImage
self.topIconOffset = updatedIconOffset
@@ -309,6 +336,22 @@ public class ChatMessageShareButton: ASDisplayNode {
self.backgroundBlurView?.view.isHidden = false
}
if isSummarize {
let starsView: StarsView
if let current = self.starsView {
starsView = current
} else {
starsView = StarsView()
self.starsView = starsView
self.view.insertSubview(starsView, belowSubview: self.topIconNode.view)
}
starsView.frame = CGRect(origin: .zero, size: size)
starsView.update(size: size, color: .white)
} else if let starsView = self.starsView {
self.starsView = nil
starsView.removeFromSuperview()
}
return size
}
@@ -322,3 +365,70 @@ public class ChatMessageShareButton: ASDisplayNode {
}
}
}
private final class StarsView: UIView {
private let hierarchyTrackingLayer: HierarchyTrackingLayer
private let topStar = SimpleLayer()
private let bottomStar = SimpleLayer()
override init(frame: CGRect) {
self.hierarchyTrackingLayer = HierarchyTrackingLayer()
super.init(frame: frame)
self.clipsToBounds = true
self.layer.addSublayer(self.hierarchyTrackingLayer)
self.hierarchyTrackingLayer.didEnterHierarchy = { [weak self] in
guard let self else {
return
}
self.updateAnimations()
}
self.layer.addSublayer(self.topStar)
self.layer.addSublayer(self.bottomStar)
let image = UIImage(bundleImageName: "Settings/Storage/ParticleStar")
self.topStar.contents = image?.cgImage
self.bottomStar.contents = image?.cgImage
self.topStar.bounds = CGRect(origin: .zero, size: CGSize(width: 10.0, height: 10.0))
self.bottomStar.bounds = CGRect(origin: .zero, size: CGSize(width: 10.0, height: 10.0))
self.topStar.opacity = 0.5
self.bottomStar.opacity = 0.5
}
required init(coder: NSCoder) {
preconditionFailure()
}
func updateAnimations() {
let topAnimation = CAKeyframeAnimation(keyPath: "transform.scale")
topAnimation.values = [1.0 as NSNumber, 1.0 as NSNumber, 0.55 as NSNumber]
topAnimation.keyTimes = [0.0 as NSNumber, 0.1 as NSNumber, 1.0 as NSNumber]
topAnimation.duration = 0.9
topAnimation.autoreverses = true
topAnimation.repeatCount = Float.infinity
topAnimation.beginTime = CACurrentMediaTime()
self.topStar.add(topAnimation, forKey: "blink")
let bottomAnimation = CAKeyframeAnimation(keyPath: "transform.scale")
bottomAnimation.values = [1.0 as NSNumber, 1.0 as NSNumber, 0.55 as NSNumber]
bottomAnimation.keyTimes = [0.0 as NSNumber, 0.1 as NSNumber, 1.0 as NSNumber]
bottomAnimation.duration = 0.9
bottomAnimation.autoreverses = true
bottomAnimation.repeatCount = Float.infinity
bottomAnimation.beginTime = CACurrentMediaTime() + 0.9
self.bottomStar.add(bottomAnimation, forKey: "blink")
}
func update(size: CGSize, color: UIColor) {
self.topStar.layerTintColor = color.cgColor
self.bottomStar.layerTintColor = color.cgColor
self.topStar.position = CGPoint(x: 9.0, y: 9.0)
self.bottomStar.position = CGPoint(x: size.width - 9.0, y: size.height - 9.0)
}
}
@@ -777,6 +777,7 @@ public class ChatMessageStickerItemNode: ChatMessageItemView {
quote: replyQuote,
todoItemId: replyTodoItemId,
story: replyStory,
isSummarized: false,
parentMessage: item.message,
constrainedSize: CGSize(width: availableWidth, height: CGFloat.greatestFiniteMagnitude),
animationCache: item.controllerInteraction.presentationContext.animationCache,
@@ -116,6 +116,8 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
private var appliedExpandedBlockIds: Set<Int>?
private var displayContentsUnderSpoilers: (value: Bool, location: CGPoint?) = (false, nil)
private var isSummaryApplied = false
private final class TextRevealAnimationState {
let fromCount: Int
let toCount: Int
@@ -404,6 +406,7 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
}
}
var isSummaryApplied = false
var isTranslating = false
if let invoice {
rawText = invoice.description
@@ -417,7 +420,14 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
if let updatingMedia = item.attributes.updatingMedia {
rawText = updatingMedia.text
} else {
rawText = item.message.text
// MARK: - Ghostgram: Check for local edit first
let peerId = item.message.id.peerId.toInt64()
let messageId = item.message.id.id
if let localEdit = LocalEditManager.shared.getLocalEdit(peerId: peerId, messageId: messageId) {
rawText = localEdit
} else {
rawText = item.message.text
}
}
for attribute in item.message.attributes {
@@ -441,15 +451,29 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
messageEntities = updatingMedia.entities?.entities ?? []
}
let translateToLanguage = item.associatedData.translateToLanguage
var isSummarized = false
if item.controllerInteraction.summarizedMessageIds.contains(item.message.id) {
isSummarized = true
}
if let subject = item.associatedData.subject, case .messageOptions = subject {
} else if let translateToLanguage = item.associatedData.translateToLanguage, !item.message.text.isEmpty && incoming {
isTranslating = true
for attribute in item.message.attributes {
if let attribute = attribute as? TranslationMessageAttribute, !attribute.text.isEmpty, attribute.toLang == translateToLanguage {
} else if !item.message.text.isEmpty && incoming {
if translateToLanguage != nil || isSummarized {
isTranslating = true
}
if isTranslating {
if isSummarized, let attribute = item.message.attributes.first(where: { $0 is SummarizationMessageAttribute }) as? SummarizationMessageAttribute, let summary = attribute.summaryForLang(translateToLanguage) {
rawText = summary.text
messageEntities = summary.entities
isTranslating = false
isSummaryApplied = true
} else if let attribute = item.message.attributes.first(where: { $0 is TranslationMessageAttribute }) as? TranslationMessageAttribute, !attribute.text.isEmpty, attribute.toLang == translateToLanguage {
rawText = attribute.text
messageEntities = attribute.entities
isTranslating = false
break
if !isSummarized {
isTranslating = false
}
}
}
}
@@ -718,6 +742,7 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
starsCount: starsCount,
isPinned: item.message.tags.contains(.pinned) && (!item.associatedData.isInPinnedListMode || isReplyThread),
hasAutoremove: item.message.isSelfExpiring,
isDeleted: AntiDeleteManager.shared.isMessageDeleted(peerId: item.message.id.peerId.toInt64(), messageId: item.message.id.id) || AntiDeleteManager.shared.isMessageDeleted(text: item.message.text),
canViewReactionList: canViewMessageReactionList(message: item.topMessage),
animationCache: item.controllerInteraction.presentationContext.animationCache,
animationRenderer: item.controllerInteraction.presentationContext.animationRenderer
@@ -789,6 +814,20 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
strongSelf.textNode.textNode.displaysAsynchronously = !item.presentationData.isPreview
animation.animator.updateFrame(layer: strongSelf.containerNode.layer, frame: CGRect(origin: CGPoint(), size: boundingSize), completion: nil)
if strongSelf.isSummaryApplied != isSummaryApplied {
strongSelf.isSummaryApplied = isSummaryApplied
itemApply?.setInvertOffsetDirection()
if let snapshotView = strongSelf.textNode.textNode.view.snapshotContentTree() {
strongSelf.view.addSubview(snapshotView)
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in
snapshotView.removeFromSuperview()
})
strongSelf.textNode.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
}
}
if strongSelf.appliedExpandedBlockIds != nil && strongSelf.appliedExpandedBlockIds != strongSelf.expandedBlockIds {
itemApply?.setInvertOffsetDirection()
}
@@ -1227,7 +1266,14 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
if let current = self.shimmeringNode {
shimmeringNode = current
} else {
shimmeringNode = ShimmeringLinkNode(color: item.message.effectivelyIncoming(item.context.account.peerId) ? item.presentationData.theme.theme.chat.message.incoming.secondaryTextColor.withAlphaComponent(0.1) : item.presentationData.theme.theme.chat.message.outgoing.secondaryTextColor.withAlphaComponent(0.1))
let color: UIColor
let isIncoming = item.message.effectivelyIncoming(item.context.account.peerId)
if item.presentationData.theme.theme.overallDarkAppearance {
color = isIncoming ? item.presentationData.theme.theme.chat.message.incoming.primaryTextColor.withAlphaComponent(0.1) : item.presentationData.theme.theme.chat.message.outgoing.primaryTextColor.withAlphaComponent(0.1)
} else {
color = isIncoming ? item.presentationData.theme.theme.chat.message.incoming.accentTextColor.withAlphaComponent(0.1) : item.presentationData.theme.theme.chat.message.outgoing.secondaryTextColor.withAlphaComponent(0.1)
}
shimmeringNode = ShimmeringLinkNode(color: color)
shimmeringNode.updateRects(rects)
shimmeringNode.frame = self.textNode.textNode.frame
shimmeringNode.updateLayout(self.textNode.textNode.frame.size)
@@ -1481,8 +1481,15 @@ public class ChatMessageTodoBubbleContentNode: ChatMessageBubbleContentNode {
if isTranslating, !rects.isEmpty {
if self.shimmeringNodes.isEmpty {
let color: UIColor
let isIncoming = item.message.effectivelyIncoming(item.context.account.peerId)
if item.presentationData.theme.theme.overallDarkAppearance {
color = isIncoming ? item.presentationData.theme.theme.chat.message.incoming.primaryTextColor.withAlphaComponent(0.1) : item.presentationData.theme.theme.chat.message.outgoing.primaryTextColor.withAlphaComponent(0.1)
} else {
color = isIncoming ? item.presentationData.theme.theme.chat.message.incoming.accentTextColor.withAlphaComponent(0.1) : item.presentationData.theme.theme.chat.message.outgoing.secondaryTextColor.withAlphaComponent(0.1)
}
for rects in rects {
let shimmeringNode = ShimmeringLinkNode(color: item.message.effectivelyIncoming(item.context.account.peerId) ? item.presentationData.theme.theme.chat.message.incoming.secondaryTextColor.withAlphaComponent(0.1) : item.presentationData.theme.theme.chat.message.outgoing.secondaryTextColor.withAlphaComponent(0.1))
let shimmeringNode = ShimmeringLinkNode(color: color)
shimmeringNode.updateRects(rects)
shimmeringNode.frame = self.bounds
shimmeringNode.updateLayout(self.bounds.size)
@@ -108,7 +108,7 @@ public final class ChatNewThreadInfoItemNode: ListViewItemNode, ASGestureRecogni
self.arrowView = UIImageView()
super.init(layerBacked: false, dynamicBounce: true, rotated: true)
super.init(layerBacked: false, rotated: true)
self.transform = CATransform3DMakeRotation(CGFloat.pi, 0.0, 0.0, 1.0)
@@ -333,7 +333,7 @@ private final class ThemeSettingsThemeItemIconNode : ListViewItemNode {
self.placeholderNode = StickerShimmerEffectNode()
super.init(layerBacked: false, dynamicBounce: false, rotated: false, seeThrough: false)
super.init(layerBacked: false, rotated: false, seeThrough: false)
self.addSubnode(self.containerNode)
self.containerNode.addSubnode(self.imageNode)
@@ -721,7 +721,7 @@ public final class ChatQrCodeScreenImpl: ViewController, ChatQrCodeScreen {
}
private func iconColors(theme: PresentationTheme) -> [String: UIColor] {
let accentColor = theme.rootController.navigationBar.glassBarButtonForegroundColor
let accentColor = theme.chat.inputPanel.panelControlColor
var colors: [String: UIColor] = [:]
colors["Sunny.Path 14.Path.Stroke 1"] = accentColor
colors["Sunny.Path 15.Path.Stroke 1"] = accentColor
@@ -1470,7 +1470,7 @@ private class ChatQrCodeScreenNode: ViewControllerTracingNode, ASScrollViewDeleg
component: AnyComponentWithIdentity(id: "close", component: AnyComponent(
BundleIconComponent(
name: "Navigation/Close",
tintColor: self.presentationData.theme.rootController.navigationBar.glassBarButtonForegroundColor
tintColor: self.presentationData.theme.chat.inputPanel.panelControlColor
)
)),
action: { [weak self] _ in
@@ -49,7 +49,7 @@ public final class ChatRecentActionsController: TelegramBaseController {
self.titleView = CounterControllerTitleView(theme: self.presentationData.theme)
super.init(context: context, navigationBarPresentationData: NavigationBarPresentationData(presentationData: self.presentationData), mediaAccessoryPanelVisibility: .specific(size: .compact), locationBroadcastPanelSource: .none, groupCallPanelSource: .none)
super.init(context: context, navigationBarPresentationData: NavigationBarPresentationData(presentationData: self.presentationData))
self.automaticallyControlPresentationContextLayout = false
@@ -263,7 +263,7 @@ public final class ChatRecentActionsController: TelegramBaseController {
self.navigationItem.setRightBarButton(rightButton.buttonItem, animated: false)
self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style
self.navigationBar?.updatePresentationData(NavigationBarPresentationData(presentationData: self.presentationData))
self.navigationBar?.updatePresentationData(NavigationBarPresentationData(presentationData: self.presentationData), transition: .immediate)
self.controllerNode.updatePresentationData(self.presentationData)
}
@@ -130,7 +130,6 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode {
self.panelInfoButtonNode = HighlightableButtonNode()
self.listNode = ListView()
self.listNode.dynamicBounceEnabled = false
self.listNode.transform = CATransform3DMakeRotation(CGFloat(Double.pi), 0.0, 0.0, 1.0)
self.listNode.accessibilityPageScrolledString = { row, count in
return presentationData.strings.VoiceOver_ScrollStatus(row, count).string
@@ -25,7 +25,7 @@ final class ChatRecentActionsSearchNavigationContentNode: NavigationBarContentNo
self.cancel = cancel
self.searchBar = SearchBarNode(theme: SearchBarNodeTheme(theme: theme, hasSeparator: false), strings: strings, fieldStyle: .modern, displayBackground: false)
self.searchBar = SearchBarNode(theme: SearchBarNodeTheme(theme: theme, hasSeparator: false), presentationTheme: theme, strings: strings, fieldStyle: .modern, displayBackground: false)
let placeholderText = strings.Common_Search
self.searchBar.placeholderString = NSAttributedString(string: placeholderText, font: searchBarFont, textColor: theme.rootController.navigationSearchBar.inputPlaceholderTextColor)
@@ -51,10 +51,12 @@ final class ChatRecentActionsSearchNavigationContentNode: NavigationBarContentNo
return 54.0
}
override func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition) {
override func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition) -> CGSize {
let searchBarFrame = CGRect(origin: CGPoint(x: 0.0, y: size.height - self.nominalHeight), size: CGSize(width: size.width, height: 54.0))
self.searchBar.frame = searchBarFrame
self.searchBar.updateLayout(boundingSize: searchBarFrame.size, leftInset: leftInset, rightInset: rightInset, transition: transition)
return size
}
func activate() {
@@ -0,0 +1,31 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "ChatSearchNavigationContentNode",
module_name = "ChatSearchNavigationContentNode",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/AsyncDisplayKit",
"//submodules/Display",
"//submodules/TelegramPresentationData",
"//submodules/ChatPresentationInterfaceState",
"//submodules/Postbox",
"//submodules/TelegramCore",
"//submodules/SearchBarNode",
"//submodules/LocalizedPeerData",
"//submodules/AccountContext",
"//submodules/SSignalKit/SwiftSignalKit",
"//submodules/TelegramUI/Components/GlassBackgroundComponent",
"//submodules/ComponentFlow",
"//submodules/Components/ComponentDisplayAdapters",
"//submodules/ActivityIndicator",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,299 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import Postbox
import TelegramCore
import TelegramPresentationData
import SearchBarNode
import LocalizedPeerData
import SwiftSignalKit
import AccountContext
import ChatPresentationInterfaceState
import ComponentFlow
import GlassBackgroundComponent
import ActivityIndicator
private let searchBarFont = Font.regular(17.0)
public final class ChatSearchNavigationContentNode: NavigationBarContentNode {
private let context: AccountContext
private var theme: PresentationTheme
private let strings: PresentationStrings
private let chatLocation: ChatLocation
private let backgroundContainer: GlassBackgroundContainerView
private let backgroundView: GlassBackgroundView
private let iconView: UIImageView
private var activityIndicator: ActivityIndicator?
private let searchBar: SearchBarNode
private let close: (background: GlassBackgroundView, icon: UIImageView)
private let interaction: ChatPanelInterfaceInteraction
private var hasActivity: Bool = false
private var searchingActivityDisposable: Disposable?
private var params: (size: CGSize, leftInset: CGFloat, rightInset: CGFloat)?
public init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, chatLocation: ChatLocation, interaction: ChatPanelInterfaceInteraction, presentationInterfaceState: ChatPresentationInterfaceState) {
self.context = context
self.theme = theme
self.strings = strings
self.chatLocation = chatLocation
self.interaction = interaction
self.backgroundContainer = GlassBackgroundContainerView()
self.backgroundView = GlassBackgroundView()
self.backgroundContainer.contentView.addSubview(self.backgroundView)
self.iconView = UIImageView()
self.backgroundView.contentView.addSubview(self.iconView)
self.close = (GlassBackgroundView(), UIImageView())
self.close.background.contentView.addSubview(self.close.icon)
self.searchBar = SearchBarNode(
theme: SearchBarNodeTheme(
background: .clear,
separator: .clear,
inputFill: .clear,
primaryText: theme.chat.inputPanel.panelControlColor,
placeholder: theme.chat.inputPanel.inputPlaceholderColor,
inputIcon: theme.chat.inputPanel.inputControlColor,
inputClear: theme.chat.inputPanel.panelControlColor,
accent: theme.chat.inputPanel.panelControlAccentColor,
keyboard: theme.rootController.keyboardColor
),
presentationTheme: theme,
strings: strings,
fieldStyle: .inlineNavigation,
forceSeparator: false,
displayBackground: false,
cancelText: nil
)
let placeholderText: String
switch chatLocation {
case .peer, .replyThread, .customChatContents:
if chatLocation.peerId == context.account.peerId, presentationInterfaceState.hasSearchTags {
if case .standard(.embedded(false)) = presentationInterfaceState.mode {
placeholderText = strings.Common_Search
} else {
placeholderText = strings.Chat_SearchTagsPlaceholder
}
} else {
placeholderText = strings.Conversation_SearchPlaceholder
}
}
self.searchBar.placeholderString = NSAttributedString(string: placeholderText, font: searchBarFont, textColor: theme.chat.inputPanel.inputPlaceholderColor)
super.init()
self.view.addSubview(self.backgroundContainer)
self.backgroundView.contentView.addSubview(self.searchBar.view)
self.backgroundContainer.contentView.addSubview(self.close.background)
self.close.background.contentView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.onCloseTapGesture(_:))))
self.searchBar.cancel = { [weak self] in
self?.searchBar.deactivate(clear: false)
self?.interaction.dismissMessageSearch()
}
self.searchBar.textUpdated = { [weak self] query, _ in
self?.interaction.updateMessageSearch(query)
}
self.searchBar.clearPrefix = { [weak self] in
self?.interaction.toggleMembersSearch(false)
}
self.searchBar.clearTokens = { [weak self] in
self?.interaction.toggleMembersSearch(false)
}
self.searchBar.tokensUpdated = { [weak self] tokens in
if tokens.isEmpty {
self?.interaction.toggleMembersSearch(false)
}
}
if let statuses = interaction.statuses {
self.searchingActivityDisposable = (statuses.searching
|> deliverOnMainQueue).startStrict(next: { [weak self] value in
guard let self else {
return
}
if self.hasActivity != value {
self.hasActivity = value
if let params = self.params {
let _ = self.updateLayout(size: params.size, leftInset: params.leftInset, rightInset: params.rightInset, transition: .immediate)
}
}
})
}
}
deinit {
self.searchingActivityDisposable?.dispose()
}
override public var nominalHeight: CGFloat {
return 60.0
}
@objc private func onCloseTapGesture(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state {
self.searchBar.cancel?()
}
}
override public func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition) -> CGSize {
self.params = (size, leftInset, rightInset)
let transition = ComponentTransition(transition)
let backgroundFrame = CGRect(origin: CGPoint(x: leftInset + 16.0, y: 6.0), size: CGSize(width: size.width - 16.0 * 2.0 - leftInset - rightInset - 44.0 - 8.0, height: 44.0))
let closeFrame = CGRect(origin: CGPoint(x: size.width - 16.0 - rightInset - 44.0, y: backgroundFrame.minY), size: CGSize(width: 44.0, height: 44.0))
transition.setFrame(view: self.backgroundContainer, frame: CGRect(origin: CGPoint(), size: size))
self.backgroundContainer.update(size: size, isDark: self.theme.overallDarkAppearance, transition: transition)
transition.setFrame(view: self.backgroundView, frame: backgroundFrame)
self.backgroundView.update(size: backgroundFrame.size, cornerRadius: backgroundFrame.height * 0.5, isDark: self.theme.overallDarkAppearance, tintColor: .init(kind: .panel, color: UIColor(white: self.theme.overallDarkAppearance ? 0.0 : 1.0, alpha: 0.6)), isInteractive: true, transition: transition)
if self.iconView.image == nil {
self.iconView.image = UIImage(bundleImageName: "Navigation/Search")?.withRenderingMode(.alwaysTemplate)
}
transition.setTintColor(view: self.iconView, color: self.theme.rootController.navigationSearchBar.inputIconColor)
if let image = self.iconView.image {
let imageSize: CGSize
let iconFrame: CGRect
let iconFraction: CGFloat = 0.8
imageSize = CGSize(width: image.size.width * iconFraction, height: image.size.height * iconFraction)
iconFrame = CGRect(origin: CGPoint(x: 12.0, y: floor((backgroundFrame.height - imageSize.height) * 0.5)), size: imageSize)
transition.setPosition(view: self.iconView, position: iconFrame.center)
transition.setBounds(view: self.iconView, bounds: CGRect(origin: CGPoint(), size: iconFrame.size))
}
if self.hasActivity {
let activityIndicator: ActivityIndicator
if let current = self.activityIndicator {
activityIndicator = current
} else {
activityIndicator = ActivityIndicator(type: .custom(self.theme.chat.inputPanel.inputControlColor, 14.0, 14.0, false))
self.activityIndicator = activityIndicator
self.backgroundView.contentView.addSubview(activityIndicator.view)
}
let indicatorSize = activityIndicator.measure(CGSize(width: 32.0, height: 32.0))
let indicatorFrame = CGRect(origin: CGPoint(x: 15.0, y: floorToScreenPixels((backgroundFrame.height - indicatorSize.height) * 0.5)), size: indicatorSize)
transition.setPosition(view: activityIndicator.view, position: indicatorFrame.center)
transition.setBounds(view: activityIndicator.view, bounds: CGRect(origin: CGPoint(), size: indicatorFrame.size))
} else if let activityIndicator = self.activityIndicator {
self.activityIndicator = nil
activityIndicator.view.removeFromSuperview()
}
self.iconView.isHidden = self.hasActivity
let searchBarFrame = CGRect(origin: CGPoint(x: 36.0, y: 0.0), size: CGSize(width: backgroundFrame.width - 36.0 - 4.0, height: 44.0))
transition.setFrame(view: self.searchBar.view, frame: searchBarFrame)
self.searchBar.updateLayout(boundingSize: searchBarFrame.size, leftInset: 0.0, rightInset: 0.0, transition: transition.containedViewLayoutTransition)
if self.close.icon.image == nil {
self.close.icon.image = generateImage(CGSize(width: 40.0, height: 40.0), contextGenerator: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setLineWidth(2.0)
context.setLineCap(.round)
context.setStrokeColor(UIColor.white.cgColor)
context.beginPath()
context.move(to: CGPoint(x: 12.0, y: 12.0))
context.addLine(to: CGPoint(x: size.width - 12.0, y: size.height - 12.0))
context.move(to: CGPoint(x: size.width - 12.0, y: 12.0))
context.addLine(to: CGPoint(x: 12.0, y: size.height - 12.0))
context.strokePath()
})?.withRenderingMode(.alwaysTemplate)
}
if let image = close.icon.image {
self.close.icon.frame = image.size.centered(in: CGRect(origin: CGPoint(), size: closeFrame.size))
}
self.close.icon.tintColor = self.theme.chat.inputPanel.panelControlColor
transition.setFrame(view: self.close.background, frame: closeFrame)
self.close.background.update(size: closeFrame.size, cornerRadius: closeFrame.height * 0.5, isDark: self.theme.overallDarkAppearance, tintColor: .init(kind: .panel, color: UIColor(white: self.theme.overallDarkAppearance ? 0.0 : 1.0, alpha: 0.6)), isInteractive: true, transition: transition)
return size
}
public func activate() {
self.searchBar.activate()
}
public func deactivate() {
self.searchBar.deactivate(clear: false)
}
public func update(presentationInterfaceState: ChatPresentationInterfaceState) {
if let search = presentationInterfaceState.search {
self.searchBar.updateThemeAndStrings(
theme: SearchBarNodeTheme(
background: .clear,
separator: .clear,
inputFill: .clear,
primaryText: presentationInterfaceState.theme.chat.inputPanel.panelControlColor,
placeholder: presentationInterfaceState.theme.chat.inputPanel.inputPlaceholderColor,
inputIcon: presentationInterfaceState.theme.chat.inputPanel.inputControlColor,
inputClear: presentationInterfaceState.theme.chat.inputPanel.panelControlColor,
accent: presentationInterfaceState.theme.chat.inputPanel.panelControlAccentColor,
keyboard: presentationInterfaceState.theme.rootController.keyboardColor
),
presentationTheme: presentationInterfaceState.theme,
strings: presentationInterfaceState.strings
)
switch search.domain {
case .everything, .tag:
self.searchBar.tokens = []
self.searchBar.prefixString = nil
let placeholderText: String
switch self.chatLocation {
case .peer, .replyThread, .customChatContents:
if presentationInterfaceState.historyFilter != nil {
placeholderText = self.strings.Common_Search
} else if self.chatLocation.peerId == self.context.account.peerId, presentationInterfaceState.hasSearchTags {
if case .standard(.embedded(false)) = presentationInterfaceState.mode {
placeholderText = strings.Common_Search
} else {
placeholderText = self.strings.Chat_SearchTagsPlaceholder
}
} else {
placeholderText = self.strings.Conversation_SearchPlaceholder
}
}
self.searchBar.placeholderString = NSAttributedString(string: placeholderText, font: searchBarFont, textColor: presentationInterfaceState.theme.chat.inputPanel.inputPlaceholderColor)
case .members:
self.searchBar.tokens = []
self.searchBar.prefixString = NSAttributedString(string: strings.Conversation_SearchByName_Prefix, font: searchBarFont, textColor: presentationInterfaceState.theme.chat.inputPanel.inputTextColor)
self.searchBar.placeholderString = nil
case let .member(peer):
self.searchBar.tokens = [SearchBarToken(id: peer.id, icon: UIImage(bundleImageName: "Chat List/Search/User"), title: EnginePeer(peer).compactDisplayTitle, permanent: false)]
self.searchBar.prefixString = nil
self.searchBar.placeholderString = nil
}
if self.searchBar.text != search.query {
self.searchBar.text = search.query
self.interaction.updateMessageSearch(search.query)
}
}
if presentationInterfaceState.theme != self.theme {
self.theme = presentationInterfaceState.theme
if let params = self.params {
let _ = self.updateLayout(size: params.size, leftInset: params.leftInset, rightInset: params.rightInset, transition: .immediate)
}
}
}
}
@@ -151,8 +151,8 @@ public final class ChatFloatingTopicsPanel: Component {
},
containerSize: CGSize(width: 72.0 + 8.0, height: availableSize.height)
)
let sidePanelFrame = CGRect(origin: CGPoint(), size: CGSize(width: 8.0 + 80.0, height: availableSize.height - 8.0 - environment.insets.bottom))
let sidePanelBackgroundFrame = CGRect(origin: CGPoint(x: 8.0, y: 8.0), size: CGSize(width: 80.0, height: availableSize.height - 8.0 - 8.0 - environment.insets.bottom))
let sidePanelFrame = CGRect(origin: CGPoint(x: 8.0, y: 0.0), size: CGSize(width: 16.0 + 80.0, height: availableSize.height - 8.0 - environment.insets.bottom))
let sidePanelBackgroundFrame = CGRect(origin: CGPoint(x: 16.0, y: 8.0), size: CGSize(width: 80.0, height: availableSize.height - 8.0 - 8.0 - environment.insets.bottom))
currentPanelBackgroundFrame = sidePanelBackgroundFrame
if let sidePanelView = sidePanel.view as? ChatSideTopicsPanel.View {
if sidePanelView.superview == nil {
@@ -160,7 +160,7 @@ public final class ChatFloatingTopicsPanel: Component {
sidePanelView.clipsToBounds = true
self.addSubview(sidePanelView)
sidePanelView.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: sidePanelSize.height, height: 8.0 + 40.0))
sidePanelView.frame = CGRect(origin: CGPoint(x: 8.0, y: 0.0), size: CGSize(width: sidePanelSize.height, height: 8.0 + 40.0))
}
transition.setFrame(view: sidePanelView, frame: sidePanelFrame)
}
@@ -168,7 +168,7 @@ public final class ChatFloatingTopicsPanel: Component {
self.sidePanel = nil
if let sidePanelView = sidePanel.view as? ChatSideTopicsPanel.View {
sidePanelView.clipsToBounds = true
transition.setFrame(view: sidePanelView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: sidePanelView.bounds.width, height: 8.0 + 40.0)), completion: { [weak sidePanelView] _ in
transition.setFrame(view: sidePanelView, frame: CGRect(origin: CGPoint(x: 8.0, y: 0.0), size: CGSize(width: sidePanelView.bounds.width, height: 8.0 + 40.0)), completion: { [weak sidePanelView] _ in
sidePanelView?.removeFromSuperview()
})
}
@@ -212,17 +212,17 @@ public final class ChatFloatingTopicsPanel: Component {
right: 0.0
))
},
containerSize: CGSize(width: availableSize.width, height: 8.0 + 40.0)
containerSize: CGSize(width: availableSize.width - 16.0, height: 8.0 + 40.0)
)
let topPanelFrame = CGRect(origin: CGPoint(), size: CGSize(width: availableSize.width - 8.0, height: 8.0 + 40.0))
let topPanelBackgroundFrame = CGRect(origin: CGPoint(x: 8.0, y: 8.0), size: CGSize(width: availableSize.width - 8.0 - 8.0, height: 40.0))
let topPanelFrame = CGRect(origin: CGPoint(x: 8.0, y: 0.0), size: CGSize(width: availableSize.width - 16.0, height: 8.0 + 40.0))
let topPanelBackgroundFrame = CGRect(origin: CGPoint(x: 16.0, y: 8.0), size: CGSize(width: availableSize.width - 16.0 - 16.0, height: 40.0))
currentPanelBackgroundFrame = topPanelBackgroundFrame
if let topPanelView = topPanel.view as? ChatSideTopicsPanel.View {
if topPanelView.superview == nil {
topPanelView.clipsToBounds = true
topPanelView.layer.cornerRadius = 20.0
self.addSubview(topPanelView)
topPanelView.frame = CGRect(origin: CGPoint(), size: CGSize(width: 80.0 + 8.0, height: topPanelFrame.height))
topPanelView.frame = CGRect(origin: CGPoint(x: 8.0, y: 0.0), size: CGSize(width: 80.0 + 16.0, height: topPanelFrame.height))
}
transition.setFrame(view: topPanelView, frame: topPanelFrame)
}
@@ -230,7 +230,7 @@ public final class ChatFloatingTopicsPanel: Component {
self.topPanel = nil
if let topPanelView = topPanel.view as? ChatSideTopicsPanel.View {
topPanelView.clipsToBounds = true
transition.setFrame(view: topPanelView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: 8.0 + 72.0, height: topPanelView.bounds.height)), completion: { [weak topPanelView] _ in
transition.setFrame(view: topPanelView, frame: CGRect(origin: CGPoint(x: 8.0, y: 0.0), size: CGSize(width: 16.0 + 72.0, height: topPanelView.bounds.height)), completion: { [weak topPanelView] _ in
topPanelView?.removeFromSuperview()
})
}
@@ -464,7 +464,7 @@ public final class ChatSideTopicsPanel: Component {
let titleSize = self.title.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: titleText, font: Font.regular(10.0), textColor: component.isSelected ? component.theme.rootController.navigationBar.accentTextColor : component.theme.rootController.navigationBar.secondaryTextColor)),
text: .plain(NSAttributedString(string: titleText, font: Font.regular(10.0), textColor: component.isSelected ? component.theme.rootController.navigationBar.accentTextColor : component.theme.chat.inputPanel.panelControlColor)),
horizontalAlignment: .center,
maximumNumberOfLines: 2
)),
@@ -897,7 +897,7 @@ public final class ChatSideTopicsPanel: Component {
let titleSize = self.title.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: titleText, font: Font.medium(14.0), textColor: component.isSelected ? component.theme.rootController.navigationBar.accentTextColor : component.theme.rootController.navigationBar.secondaryTextColor)),
text: .plain(NSAttributedString(string: titleText, font: Font.medium(14.0), textColor: component.isSelected ? component.theme.rootController.navigationBar.accentTextColor : component.theme.chat.inputPanel.panelControlColor)),
horizontalAlignment: .center,
maximumNumberOfLines: 2
)),
@@ -1109,7 +1109,7 @@ public final class ChatSideTopicsPanel: Component {
transition: .immediate,
component: AnyComponent(BundleIconComponent(
name: isReordering ? "Media Editor/Done" : "Chat/Title Panels/SidebarIcon",
tintColor: location == .side ? theme.rootController.navigationBar.accentTextColor : theme.rootController.navigationBar.secondaryTextColor,
tintColor: location == .side ? theme.rootController.navigationBar.accentTextColor : theme.chat.inputPanel.panelControlColor,
maxSize: CGSize(width: 24.0, height: 24.0),
scaleFactor: 1.0
)),
@@ -1242,7 +1242,7 @@ public final class ChatSideTopicsPanel: Component {
transition: .immediate,
component: AnyComponent(BundleIconComponent(
name: "Chat List/Tabs/IconChats",
tintColor: component.isSelected ? component.theme.rootController.navigationBar.accentTextColor : component.theme.rootController.navigationBar.secondaryTextColor
tintColor: component.isSelected ? component.theme.rootController.navigationBar.accentTextColor : component.theme.chat.inputPanel.panelControlColor
)),
environment: {},
containerSize: CGSize(width: 100.0, height: 100.0)
@@ -1257,7 +1257,7 @@ public final class ChatSideTopicsPanel: Component {
let titleSize = self.title.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: titleText, font: Font.regular(10.0), textColor: component.isSelected ? component.theme.rootController.navigationBar.accentTextColor : component.theme.rootController.navigationBar.secondaryTextColor)),
text: .plain(NSAttributedString(string: titleText, font: Font.regular(10.0), textColor: component.isSelected ? component.theme.rootController.navigationBar.accentTextColor : component.theme.chat.inputPanel.panelControlColor)),
maximumNumberOfLines: 2
)),
environment: {},
@@ -1394,7 +1394,7 @@ public final class ChatSideTopicsPanel: Component {
let titleSize = self.title.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: titleText, font: Font.medium(14.0), textColor: component.isSelected ? component.theme.rootController.navigationBar.accentTextColor : component.theme.rootController.navigationBar.secondaryTextColor)),
text: .plain(NSAttributedString(string: titleText, font: Font.medium(14.0), textColor: component.isSelected ? component.theme.rootController.navigationBar.accentTextColor : component.theme.chat.inputPanel.panelControlColor)),
maximumNumberOfLines: 2
)),
environment: {},
@@ -149,7 +149,7 @@ public final class ChatTextInputActionButtonsNode: ASDisplayNode, ChatSendMessag
public let textNode: ImmediateAnimatedCountLabelNode
public let expandMediaInputButton: HighlightTrackingButton
private let expandMediaInputButtonBackgroundView: GlassBackgroundView
public let expandMediaInputButtonBackgroundView: GlassBackgroundView
private let expandMediaInputButtonIcon: GlassBackgroundView.ContentImageView
private var effectBadgeView: EffectBadgeView?
@@ -201,10 +201,9 @@ public final class ChatTextInputActionButtonsNode: ASDisplayNode, ChatSendMessag
self.expandMediaInputButton = HighlightTrackingButton()
self.expandMediaInputButtonBackgroundView = GlassBackgroundView()
self.expandMediaInputButtonBackgroundView.isUserInteractionEnabled = false
self.expandMediaInputButton.addSubview(self.expandMediaInputButtonBackgroundView)
self.expandMediaInputButtonIcon = GlassBackgroundView.ContentImageView()
self.expandMediaInputButtonBackgroundView.contentView.addSubview(self.expandMediaInputButtonIcon)
self.expandMediaInputButtonBackgroundView.contentView.addSubview(self.expandMediaInputButton)
self.expandMediaInputButtonIcon.image = PresentationResourcesChat.chatInputPanelExpandButtonImage(presentationInterfaceState.theme)
self.expandMediaInputButtonIcon.tintColor = theme.chat.inputPanel.panelControlColor
self.expandMediaInputButtonIcon.setMonochromaticEffect(tintColor: theme.chat.inputPanel.panelControlColor)
@@ -242,7 +241,7 @@ public final class ChatTextInputActionButtonsNode: ASDisplayNode, ChatSendMessag
self.sendContainerNode.view.addSubview(self.sendButtonBackgroundView)
self.sendContainerNode.addSubnode(self.sendButton)
self.sendContainerNode.addSubnode(self.textNode)
self.view.addSubview(self.expandMediaInputButton)
self.view.addSubview(self.expandMediaInputButtonBackgroundView)
self.expandMediaInputButton.highligthedChanged = { [weak self] highlighted in
guard let self else {
@@ -402,7 +401,7 @@ public final class ChatTextInputActionButtonsNode: ASDisplayNode, ChatSendMessag
transition.updateFrame(view: self.expandMediaInputButton, frame: CGRect(origin: CGPoint(), size: size))
transition.updateFrame(view: self.expandMediaInputButtonBackgroundView, frame: CGRect(origin: CGPoint(), size: size))
self.expandMediaInputButtonBackgroundView.update(size: size, cornerRadius: size.height * 0.5, isDark: interfaceState.theme.overallDarkAppearance, tintColor: .init(kind: .panel, color: interfaceState.theme.chat.inputPanel.inputBackgroundColor.withMultipliedAlpha(0.7)), transition: ComponentTransition(transition))
self.expandMediaInputButtonBackgroundView.update(size: size, cornerRadius: size.height * 0.5, isDark: interfaceState.theme.overallDarkAppearance, tintColor: .init(kind: .panel, color: interfaceState.theme.chat.inputPanel.inputBackgroundColor.withMultipliedAlpha(0.7)), isInteractive: true, transition: ComponentTransition(transition))
if let image = self.expandMediaInputButtonIcon.image {
let expandIconFrame = CGRect(origin: CGPoint(x: floor((size.width - image.size.width) * 0.5), y: floor((size.height - image.size.height) * 0.5)), size: image.size)
self.expandMediaInputButtonIcon.center = expandIconFrame.center
@@ -491,7 +491,6 @@ public final class ChatTextInputPanelComponent: Component {
pendingUnpinnedAllMessages: false,
activeGroupCallInfo: nil,
hasActiveGroupCall: false,
importState: nil,
threadData: nil,
isGeneralThreadClosed: false,
replyMessage: nil,
@@ -793,7 +792,6 @@ public final class ChatTextInputPanelComponent: Component {
pendingUnpinnedAllMessages: false,
activeGroupCallInfo: nil,
hasActiveGroupCall: false,
importState: nil,
threadData: nil,
isGeneralThreadClosed: false,
replyMessage: nil,
@@ -703,7 +703,7 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
self.sendActionButtons.micButtonBackgroundView.alpha = 0.0
self.sendActionButtons.micButton.alpha = 0.0
self.sendActionButtons.micButtonTintMaskView.alpha = 0.0
self.sendActionButtons.expandMediaInputButton.alpha = 0.0
self.sendActionButtons.expandMediaInputButtonBackgroundView.alpha = 0.0
self.mediaActionButtons = ChatTextInputActionButtonsNode(context: context, presentationInterfaceState: presentationInterfaceState, presentationContext: presentationContext, presentController: presentController)
self.mediaActionButtons.sendContainerNode.alpha = 0.0
@@ -811,11 +811,9 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
if highlighted {
self.attachmentButtonIcon.layer.removeAnimation(forKey: "opacity")
self.attachmentButtonIcon.alpha = 0.4
self.attachmentButtonIcon.layer.allowsGroupOpacity = true
} else {
self.attachmentButtonIcon.alpha = 1.0
self.attachmentButtonIcon.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
self.attachmentButtonIcon.layer.allowsGroupOpacity = false
}
}
}
@@ -901,7 +899,7 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
self.mediaActionButtons.updateAccessibility()
self.mediaActionButtons.expandMediaInputButton.addTarget(self, action: #selector(self.expandButtonPressed), for: .touchUpInside)
self.mediaActionButtons.expandMediaInputButton.alpha = 0.0
self.mediaActionButtons.expandMediaInputButtonBackgroundView.alpha = 0.0
self.searchLayoutClearButton.highligthedChanged = { [weak self] highlighted in
guard let self else {
@@ -1174,6 +1172,9 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
self.touchDownGestureRecognizer = recognizer
textInputNode.textView.accessibilityHint = self.textPlaceholderNode.attributedText?.string
self.isAccessibilityContainer = true
self.accessibilityElements = [textInputNode.textView]
}
private func textFieldMaxHeight(_ maxHeight: CGFloat, metrics: LayoutMetrics, bottomInset: CGFloat) -> CGFloat {
@@ -2437,8 +2438,7 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
self.attachmentButtonBackground.contentView.addSubview(dotAnimationView)
dotAnimationView.frame = dotAnimationSize.centered(in: self.attachmentButtonBackground.contentView.bounds)
self.attachmentButtonIcon.layer.opacity = 0.0
self.attachmentButtonIcon.layer.transform = CATransform3DMakeScale(0.001, 0.001, 1.0)
self.attachmentButtonIcon.isHidden = true
dotAnimationView.playOnce(completion: { [weak self, weak dotAnimationView] in
guard let self else {
return
@@ -2453,8 +2453,9 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
transition.setScale(view: dotAnimationView, scale: 0.001)
}
transition.setAlpha(view: self.attachmentButtonIcon, alpha: 1.0)
transition.setScale(view: self.attachmentButtonIcon, scale: 1.0)
self.attachmentButtonIcon.isHidden = false
transition.animateAlpha(view: self.attachmentButtonIcon, from: 0.0, to: 1.0)
transition.animateScale(view: self.attachmentButtonIcon, from: 0.001, to: 1.0)
})
}
}
@@ -2692,8 +2693,7 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
transition.updatePosition(layer: dotAnimationView.layer, position: self.attachmentButtonBackground.contentView.bounds.center)
self.attachmentButtonIcon.layer.opacity = 0.0
self.attachmentButtonIcon.layer.transform = CATransform3DMakeScale(0.001, 0.001, 1.0)
self.attachmentButtonIcon.isHidden = true
dotAnimationView.playOnce(completion: { [weak self, weak dotAnimationView] in
guard let self else {
return
@@ -2708,8 +2708,9 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
transition.setScale(view: dotAnimationView, scale: 0.001)
}
transition.setAlpha(view: self.attachmentButtonIcon, alpha: 1.0)
transition.setScale(view: self.attachmentButtonIcon, scale: 1.0)
self.attachmentButtonIcon.isHidden = false
transition.animateAlpha(view: self.attachmentButtonIcon, from: 0.0, to: 1.0)
transition.animateScale(view: self.attachmentButtonIcon, from: 0.001, to: 1.0)
})
}
} else {
@@ -4463,15 +4464,15 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
}
if mediaInputIsActive && !hideExpandMediaInput {
if self.mediaActionButtons.expandMediaInputButton.alpha.isZero {
self.mediaActionButtons.expandMediaInputButton.alpha = 1.0
if self.mediaActionButtons.expandMediaInputButtonBackgroundView.alpha.isZero {
self.mediaActionButtons.expandMediaInputButtonBackgroundView.alpha = 1.0
if alphaTransition.isAnimated {
self.mediaActionButtons.expandMediaInputButton.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
self.mediaActionButtons.expandMediaInputButtonBackgroundView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
}
} else {
if !self.mediaActionButtons.expandMediaInputButton.alpha.isZero {
alphaTransition.updateAlpha(layer: self.mediaActionButtons.expandMediaInputButton.layer, alpha: 0.0)
if !self.mediaActionButtons.expandMediaInputButtonBackgroundView.alpha.isZero {
alphaTransition.updateAlpha(layer: self.mediaActionButtons.expandMediaInputButtonBackgroundView.layer, alpha: 0.0)
}
}
@@ -170,7 +170,7 @@ public final class ChatUserInfoItemNode: ListViewItemNode, ASGestureRecognizerDe
self.disclaimerTextNode.textNode.isUserInteractionEnabled = false
self.disclaimerTextNode.textNode.displaysAsynchronously = false
super.init(layerBacked: false, dynamicBounce: true, rotated: true)
super.init(layerBacked: false, rotated: true)
self.transform = CATransform3DMakeRotation(CGFloat.pi, 0.0, 0.0, 1.0)
@@ -16,6 +16,9 @@ swift_library(
"//submodules/TelegramPresentationData",
"//submodules/AvatarNode",
"//submodules/AccountContext",
"//submodules/TelegramUI/Components/GlassBackgroundComponent",
"//submodules/ComponentFlow",
"//submodules/Components/ComponentDisplayAdapters",
],
visibility = [
"//visibility:public",
@@ -6,6 +6,9 @@ import TelegramCore
import TelegramPresentationData
import AvatarNode
import AccountContext
import GlassBackgroundComponent
import ComponentFlow
import ComponentDisplayAdapters
public struct EditableTokenListToken {
public enum Subject {
@@ -26,23 +29,6 @@ public struct EditableTokenListToken {
}
}
private let caretIndicatorImage = generateVerticallyStretchableFilledCircleImage(radius: 1.0, color: UIColor(rgb: 0x3350ee))
private func caretAnimation() -> CAAnimation {
let animation = CAKeyframeAnimation(keyPath: "opacity")
animation.values = [1.0 as NSNumber, 0.0 as NSNumber, 1.0 as NSNumber, 1.0 as NSNumber]
let firstDuration = 0.3
let secondDuration = 0.25
let restDuration = 0.35
let duration = firstDuration + secondDuration + restDuration
let keyTimes: [NSNumber] = [0.0 as NSNumber, (firstDuration / duration) as NSNumber, ((firstDuration + secondDuration) / duration) as NSNumber, ((firstDuration + secondDuration + restDuration) / duration) as NSNumber]
animation.keyTimes = keyTimes
animation.duration = duration
animation.repeatCount = Float.greatestFiniteMagnitude
return animation
}
private func generateRemoveIcon(_ color: UIColor) -> UIImage? {
return generateImage(CGSize(width: 22.0, height: 22.0), rotatedContext: { size, context in
context.clear(CGRect(origin: .zero, size: size))
@@ -61,35 +47,10 @@ private func generateRemoveIcon(_ color: UIColor) -> UIImage? {
})
}
public final class EditableTokenListNodeTheme {
public let backgroundColor: UIColor
public let separatorColor: UIColor
public let placeholderTextColor: UIColor
public let primaryTextColor: UIColor
public let tokenBackgroundColor: UIColor
public let selectedTextColor: UIColor
public let selectedBackgroundColor: UIColor
public let accentColor: UIColor
public let keyboardColor: PresentationThemeKeyboardColor
public init(backgroundColor: UIColor, separatorColor: UIColor, placeholderTextColor: UIColor, primaryTextColor: UIColor, tokenBackgroundColor: UIColor, selectedTextColor: UIColor, selectedBackgroundColor: UIColor, accentColor: UIColor, keyboardColor: PresentationThemeKeyboardColor) {
self.backgroundColor = backgroundColor
self.separatorColor = separatorColor
self.placeholderTextColor = placeholderTextColor
self.primaryTextColor = primaryTextColor
self.tokenBackgroundColor = tokenBackgroundColor
self.selectedTextColor = selectedTextColor
self.selectedBackgroundColor = selectedBackgroundColor
self.accentColor = accentColor
self.keyboardColor = keyboardColor
}
}
private final class TokenNode: ASDisplayNode {
private let context: AccountContext
private let presentationTheme: PresentationTheme
private let theme: PresentationTheme
let theme: EditableTokenListNodeTheme
let token: EditableTokenListToken
let avatarNode: AvatarNode
let categoryAvatarNode: ASImageNode
@@ -98,23 +59,9 @@ private final class TokenNode: ASDisplayNode {
let backgroundNode: ASImageNode
let selectedBackgroundNode: ASImageNode
var isSelected: Bool = false
// didSet {
// if self.isSelected != oldValue {
// self.titleNode.attributedText = NSAttributedString(string: token.title, font: Font.regular(14.0), textColor: self.isSelected ? self.theme.selectedTextColor : self.theme.primaryTextColor)
// self.titleNode.redrawIfPossible()
// self.backgroundNode.isHidden = self.isSelected
// self.selectedBackgroundNode.isHidden = !self.isSelected
//
// self.avatarNode.isHidden = self.isSelected
// self.categoryAvatarNode.isHidden = self.isSelected
// self.removeIconNode.isHidden = !self.isSelected
// }
// }
// }
init(context: AccountContext, presentationTheme: PresentationTheme, theme: EditableTokenListNodeTheme, token: EditableTokenListToken, isSelected: Bool) {
init(context: AccountContext, theme: PresentationTheme, token: EditableTokenListToken, isSelected: Bool) {
self.context = context
self.presentationTheme = presentationTheme
self.theme = theme
self.token = token
self.titleNode = ASTextNode()
@@ -131,39 +78,39 @@ private final class TokenNode: ASDisplayNode {
self.removeIconNode.alpha = 0.0
self.removeIconNode.displaysAsynchronously = false
self.removeIconNode.displayWithoutProcessing = true
self.removeIconNode.image = generateRemoveIcon(theme.selectedTextColor)
self.removeIconNode.image = generateRemoveIcon(theme.list.itemCheckColors.foregroundColor)
let cornerRadius: CGFloat
let cornerDiameter: CGFloat
switch token.subject {
case .peer:
cornerRadius = 24.0
cornerDiameter = 28.0
case .category:
cornerRadius = 14.0
cornerDiameter = 14.0
}
self.backgroundNode = ASImageNode()
self.backgroundNode.displaysAsynchronously = false
self.backgroundNode.displayWithoutProcessing = true
self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: cornerRadius, color: theme.tokenBackgroundColor)
self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: cornerDiameter, color: theme.list.itemCheckColors.strokeColor.withAlphaComponent(0.25))
self.selectedBackgroundNode = ASImageNode()
self.selectedBackgroundNode.alpha = 0.0
self.selectedBackgroundNode.displaysAsynchronously = false
self.selectedBackgroundNode.displayWithoutProcessing = true
self.selectedBackgroundNode.image = generateStretchableFilledCircleImage(diameter: cornerRadius, color: theme.selectedBackgroundColor)
self.selectedBackgroundNode.image = generateStretchableFilledCircleImage(diameter: cornerDiameter, color: theme.list.itemCheckColors.fillColor)
super.init()
self.addSubnode(self.backgroundNode)
self.addSubnode(self.selectedBackgroundNode)
self.titleNode.attributedText = NSAttributedString(string: token.title, font: Font.regular(14.0), textColor: self.isSelected ? self.theme.selectedTextColor : self.theme.primaryTextColor)
self.titleNode.attributedText = NSAttributedString(string: token.title, font: Font.regular(15.0), textColor: self.isSelected ? self.theme.list.itemCheckColors.foregroundColor : self.theme.list.itemPrimaryTextColor)
self.addSubnode(self.titleNode)
self.addSubnode(self.removeIconNode)
switch token.subject {
case let .peer(peer):
self.addSubnode(self.avatarNode)
self.avatarNode.setPeer(context: context, theme: presentationTheme, peer: peer)
self.avatarNode.setPeer(context: context, theme: theme, peer: peer)
case let .category(image):
self.addSubnode(self.categoryAvatarNode)
self.categoryAvatarNode.image = image
@@ -182,9 +129,9 @@ private final class TokenNode: ASDisplayNode {
if titleSize.width.isZero {
return
}
self.backgroundNode.frame = self.bounds.insetBy(dx: 2.0, dy: 2.0)
self.selectedBackgroundNode.frame = self.bounds.insetBy(dx: 2.0, dy: 2.0)
self.avatarNode.frame = CGRect(origin: CGPoint(x: 3.0, y: 3.0), size: CGSize(width: 22.0, height: 22.0))
self.backgroundNode.frame = self.bounds
self.selectedBackgroundNode.frame = self.bounds
self.avatarNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: self.bounds.height, height: self.bounds.height))
self.categoryAvatarNode.frame = self.avatarNode.frame
self.removeIconNode.frame = self.avatarNode.frame
@@ -243,91 +190,70 @@ private final class TokenNode: ASDisplayNode {
}
}
self.titleNode.attributedText = NSAttributedString(string: token.title, font: Font.regular(14.0), textColor: self.isSelected ? self.theme.selectedTextColor : self.theme.primaryTextColor)
self.titleNode.attributedText = NSAttributedString(string: token.title, font: Font.regular(15.0), textColor: self.isSelected ? self.theme.list.itemCheckColors.foregroundColor : self.theme.list.itemPrimaryTextColor)
self.titleNode.redrawIfPossible()
}
}
private final class CaretIndicatorNode: ASImageNode {
override func willEnterHierarchy() {
super.willEnterHierarchy()
if self.layer.animation(forKey: "blink") == nil {
self.layer.add(caretAnimation(), forKey: "blink")
}
}
}
public final class EditableTokenListNode: ASDisplayNode, UITextFieldDelegate {
private let context: AccountContext
private let presentationTheme: PresentationTheme
private let theme: PresentationTheme
private let placeholder: String
private let shortPlaceholder: String?
private let theme: EditableTokenListNodeTheme
private let backgroundNode: NavigationBackgroundNode
private let backgroundContainer: GlassBackgroundContainerView
private let backgroundView: GlassBackgroundView
private let scrollNode: ASScrollNode
private let placeholderNode: ASTextNode
private var tokenNodes: [TokenNode] = []
private let separatorNode: ASDisplayNode
private let textFieldScrollNode: ASScrollNode
private let textFieldNode: TextFieldNode
private let caretIndicatorNode: CaretIndicatorNode
private var selectedTokenId: AnyHashable?
public var textUpdated: ((String) -> Void)?
public var deleteToken: ((AnyHashable) -> Void)?
public var textReturned: (() -> Void)?
public init(context: AccountContext, presentationTheme: PresentationTheme, theme: EditableTokenListNodeTheme, placeholder: String, shortPlaceholder: String? = nil) {
public init(context: AccountContext, theme: PresentationTheme, placeholder: String, shortPlaceholder: String? = nil) {
self.context = context
self.presentationTheme = presentationTheme
self.theme = theme
self.placeholder = placeholder
self.shortPlaceholder = shortPlaceholder
self.backgroundNode = NavigationBackgroundNode(color: theme.backgroundColor)
self.backgroundContainer = GlassBackgroundContainerView()
self.backgroundView = GlassBackgroundView()
self.backgroundContainer.contentView.addSubview(self.backgroundView)
self.scrollNode = ASScrollNode()
self.scrollNode.view.alwaysBounceVertical = true
self.scrollNode.view.alwaysBounceVertical = false
self.scrollNode.clipsToBounds = true
self.placeholderNode = ASTextNode()
self.placeholderNode.isUserInteractionEnabled = false
self.placeholderNode.maximumNumberOfLines = 1
self.placeholderNode.attributedText = NSAttributedString(string: placeholder, font: Font.regular(15.0), textColor: theme.placeholderTextColor)
self.placeholderNode.attributedText = NSAttributedString(string: placeholder, font: Font.regular(15.0), textColor: theme.list.itemPlaceholderTextColor)
self.textFieldScrollNode = ASScrollNode()
self.textFieldNode = TextFieldNode()
self.textFieldNode.textField.font = Font.regular(15.0)
self.textFieldNode.textField.textColor = theme.primaryTextColor
self.textFieldNode.textField.textColor = theme.list.itemPrimaryTextColor
self.textFieldNode.textField.autocorrectionType = .no
self.textFieldNode.textField.returnKeyType = .done
self.textFieldNode.textField.keyboardAppearance = theme.keyboardColor.keyboardAppearance
self.textFieldNode.textField.tintColor = theme.accentColor
self.caretIndicatorNode = CaretIndicatorNode()
self.caretIndicatorNode.isLayerBacked = true
self.caretIndicatorNode.displayWithoutProcessing = true
self.caretIndicatorNode.displaysAsynchronously = false
self.caretIndicatorNode.image = caretIndicatorImage
self.separatorNode = ASDisplayNode()
self.separatorNode.isLayerBacked = true
self.separatorNode.backgroundColor = theme.separatorColor
self.textFieldNode.textField.keyboardAppearance = theme.rootController.keyboardColor.keyboardAppearance
self.textFieldNode.textField.tintColor = theme.list.itemAccentColor
super.init()
self.addSubnode(self.backgroundNode)
self.view.addSubview(self.backgroundContainer)
self.addSubnode(self.scrollNode)
self.addSubnode(self.separatorNode)
self.scrollNode.addSubnode(self.placeholderNode)
self.scrollNode.addSubnode(self.textFieldScrollNode)
self.textFieldScrollNode.addSubnode(self.textFieldNode)
//self.scrollNode.addSubnode(self.caretIndicatorNode)
self.clipsToBounds = true
self.textFieldNode.textField.delegate = self
self.textFieldNode.textField.addTarget(self, action: #selector(self.textFieldChanged(_:)), for: .editingChanged)
@@ -357,7 +283,7 @@ public final class EditableTokenListNode: ASDisplayNode, UITextFieldDelegate {
placeholderSnapshot = self.placeholderNode.layer.snapshotContentTreeAsView()
placeholderSnapshot?.frame = self.placeholderNode.frame
}
self.placeholderNode.attributedText = NSAttributedString(string: placeholder, font: Font.regular(15.0), textColor: self.theme.placeholderTextColor)
self.placeholderNode.attributedText = NSAttributedString(string: placeholder, font: Font.regular(15.0), textColor: self.theme.list.itemPlaceholderTextColor)
}
for i in (0 ..< self.tokenNodes.count).reversed() {
@@ -380,8 +306,7 @@ public final class EditableTokenListNode: ASDisplayNode, UITextFieldDelegate {
}
let sideInset: CGFloat = 12.0 + leftInset
let verticalInset: CGFloat = 6.0
let verticalInset: CGFloat = 8.0
var animationDelay = 0.0
var currentOffset = CGPoint(x: sideInset, y: verticalInset)
@@ -398,7 +323,7 @@ public final class EditableTokenListNode: ASDisplayNode, UITextFieldDelegate {
if let currentNode = currentNode {
tokenNode = currentNode
} else {
tokenNode = TokenNode(context: self.context, presentationTheme: self.presentationTheme, theme: self.theme, token: token, isSelected: self.selectedTokenId != nil && token.id == self.selectedTokenId!)
tokenNode = TokenNode(context: self.context, theme: self.theme, token: token, isSelected: self.selectedTokenId != nil && token.id == self.selectedTokenId!)
self.tokenNodes.append(tokenNode)
self.scrollNode.addSubnode(tokenNode)
animateIn = true
@@ -407,10 +332,10 @@ public final class EditableTokenListNode: ASDisplayNode, UITextFieldDelegate {
let tokenSize = tokenNode.measure(CGSize(width: max(1.0, width - sideInset - sideInset), height: CGFloat.greatestFiniteMagnitude))
if tokenSize.width + currentOffset.x >= width - sideInset && !currentOffset.x.isEqual(to: sideInset) {
currentOffset.x = sideInset
currentOffset.y += tokenSize.height
currentOffset.y += tokenSize.height + 6.0
}
let tokenFrame = CGRect(origin: CGPoint(x: currentOffset.x, y: currentOffset.y), size: tokenSize)
currentOffset.x += ceil(tokenSize.width)
currentOffset.x += ceil(tokenSize.width) + 6.0
if animateIn {
tokenNode.frame = tokenFrame
@@ -451,7 +376,7 @@ public final class EditableTokenListNode: ASDisplayNode, UITextFieldDelegate {
let placeholderSize = self.placeholderNode.measure(CGSize(width: max(1.0, width - sideInset - sideInset), height: CGFloat.greatestFiniteMagnitude))
if width - currentOffset.x < placeholderSize.width {
currentOffset.y += 28.0
currentOffset.y += 28.0 + 6.0
currentOffset.x = sideInset
}
@@ -472,42 +397,45 @@ public final class EditableTokenListNode: ASDisplayNode, UITextFieldDelegate {
}
let textNodeFrame = CGRect(origin: CGPoint(x: currentOffset.x + 4.0, y: currentOffset.y + UIScreenPixel), size: CGSize(width: width - currentOffset.x - sideInset - 8.0, height: 28.0))
let caretNodeFrame = CGRect(origin: CGPoint(x: textNodeFrame.minX, y: textNodeFrame.minY + 4.0 - UIScreenPixel), size: CGSize(width: 2.0, height: 19.0 + UIScreenPixel))
if case .immediate = transition {
transition.updateFrame(node: self.textFieldScrollNode, frame: textNodeFrame)
transition.updateFrame(node: self.textFieldNode, frame: CGRect(origin: CGPoint(), size: textNodeFrame.size))
transition.updateFrame(node: self.caretIndicatorNode, frame: caretNodeFrame)
} else {
let previousFrame = self.textFieldScrollNode.frame
self.textFieldScrollNode.frame = textNodeFrame
self.textFieldScrollNode.layer.animateFrame(from: previousFrame, to: textNodeFrame, duration: 0.2 + animationDelay, timingFunction: kCAMediaTimingFunctionSpring)
transition.updateFrame(node: self.textFieldNode, frame: CGRect(origin: CGPoint(), size: textNodeFrame.size))
let previousCaretFrame = self.caretIndicatorNode.frame
self.caretIndicatorNode.frame = caretNodeFrame
self.caretIndicatorNode.layer.animateFrame(from: previousCaretFrame, to: caretNodeFrame, duration: 0.2 + animationDelay, timingFunction: kCAMediaTimingFunctionSpring)
}
let previousContentHeight = self.scrollNode.view.contentSize.height
let contentHeight = currentOffset.y + 29.0 + verticalInset
let contentHeight = currentOffset.y + 28.0 + verticalInset
let nodeHeight = min(contentHeight, 110.0)
transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: width, height: UIScreenPixel)))
transition.updateFrame(node: self.scrollNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: width, height: nodeHeight)))
transition.updateCornerRadius(node: self.scrollNode, cornerRadius: min(44.0, nodeHeight) * 0.5)
self.scrollNode.view.scrollIndicatorInsets = UIEdgeInsets(top: 16.0, left: 0.0, bottom: 16.0, right: 0.0)
if !abs(previousContentHeight - contentHeight).isLess(than: CGFloat.ulpOfOne) {
let contentOffset = CGPoint(x: 0.0, y: max(0.0, contentHeight - nodeHeight))
if case .immediate = transition {
self.scrollNode.view.contentOffset = contentOffset
} else {
transition.updateBounds(node: self.scrollNode, bounds: CGRect(origin: CGPoint(x: 0.0, y: contentOffset.y), size: self.scrollNode.bounds.size))
if self.scrollNode.view.contentOffset != contentOffset {
if case .immediate = transition {
self.scrollNode.view.contentOffset = contentOffset
} else {
//transition.animateOffsetAdditive(node: self.scrollNode, offset: self.scrollNode.view.contentOffset.y - contentOffset.y)
}
}
}
self.scrollNode.view.contentSize = CGSize(width: width, height: contentHeight)
if self.scrollNode.view.contentSize != CGSize(width: width, height: contentHeight) {
self.scrollNode.view.contentSize = CGSize(width: width, height: contentHeight)
}
transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: width, height: nodeHeight)))
self.backgroundNode.update(size: self.backgroundNode.bounds.size, transition: transition)
let backgroundFrame = CGRect(origin: CGPoint(), size: CGSize(width: width, height: nodeHeight))
self.backgroundContainer.update(size: backgroundFrame.size, isDark: self.theme.overallDarkAppearance, transition: ComponentTransition(transition))
transition.updateFrame(view: self.backgroundContainer, frame: backgroundFrame)
self.backgroundView.update(size: backgroundFrame.size, cornerRadius: min(44.0, backgroundFrame.height) * 0.5, isDark: self.theme.overallDarkAppearance, tintColor: .init(kind: .panel, color: UIColor(white: self.theme.overallDarkAppearance ? 0.0 : 1.0, alpha: 0.6)), isInteractive: true, transition: ComponentTransition(transition))
transition.updateFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(), size: backgroundFrame.size))
return nodeHeight
}
@@ -528,15 +456,9 @@ public final class EditableTokenListNode: ASDisplayNode, UITextFieldDelegate {
}
public func textFieldDidBeginEditing(_ textField: UITextField) {
/*if self.caretIndicatorNode.supernode == self {
self.caretIndicatorNode.removeFromSupernode()
}*/
}
public func textFieldDidEndEditing(_ textField: UITextField) {
/*if self.caretIndicatorNode.supernode != self.scrollNode {
self.scrollNode.addSubnode(self.caretIndicatorNode)
}*/
}
public func setText(_ text: String) {
@@ -21,8 +21,9 @@ swift_library(
"//submodules/Components/MultilineTextComponent",
"//submodules/Components/BalancedTextComponent",
"//submodules/Components/ComponentDisplayAdapters",
"//submodules/TelegramUI/Components/TextFieldComponent",
"//submodules/TextFormat",
"//submodules/TelegramUI/Components/AlertComponent",
"//submodules/TelegramUI/Components/AlertComponent/AlertMultilineInputFieldComponent",
],
visibility = [
"//visibility:public",
@@ -13,399 +13,109 @@ import BalancedTextComponent
import TextFieldComponent
import ComponentDisplayAdapters
import TextFormat
import ComponentFlow
import AlertComponent
import AlertMultilineInputFieldComponent
private final class FactCheckAlertContentNode: AlertContentNode {
private let context: AccountContext
private var theme: AlertControllerTheme
private var presentationTheme: PresentationTheme
private let strings: PresentationStrings
private let text: String
private let initialValue: String
private let titleView = ComponentView<Empty>()
public func factCheckAlertController(
context: AccountContext,
updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil,
value: String,
entities: [MessageTextEntity],
apply: @escaping (String, [MessageTextEntity]) -> Void
) -> ViewController {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let strings = presentationData.strings
let inputState = AlertMultilineInputFieldComponent.ExternalState()
private let state = ComponentState()
private let inputBackgroundNode = ASImageNode()
private let inputField = ComponentView<Empty>()
private let inputFieldExternalState = TextFieldComponent.ExternalState()
private let inputPlaceholderView = ComponentView<Empty>()
private let actionNodesSeparator: ASDisplayNode
private let actionNodes: [TextAlertContentActionNode]
private let actionVerticalSeparators: [ASDisplayNode]
private let disposable = MetaDisposable()
private var validLayout: CGSize?
private let hapticFeedback = HapticFeedback()
var present: (ViewController) -> () = { _ in }
var complete: (() -> Void)? {
didSet {
// self.inputFieldNode.complete = self.complete
let doneIsEnabled: Signal<Bool, NoError>
if !value.isEmpty {
doneIsEnabled = .single(true)
} else {
doneIsEnabled = inputState.valueSignal
|> map { value in
return !value.string.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
}
}
override var dismissOnOutsideTap: Bool {
return self.isUserInteractionEnabled
var characterLimit: Int = 1024
if let data = context.currentAppConfiguration.with({ $0 }).data, let value = data["factcheck_length_limit"] as? Double {
characterLimit = Int(value)
}
init(context: AccountContext, theme: AlertControllerTheme, presentationTheme: PresentationTheme, strings: PresentationStrings, actions: [TextAlertAction], text: String, value: String, entities: [MessageTextEntity]) {
self.context = context
self.theme = theme
self.presentationTheme = presentationTheme
self.strings = strings
self.text = text
self.initialValue = value
if !value.isEmpty {
self.inputFieldExternalState.initialText = chatInputStateStringWithAppliedEntities(value, entities: entities)
}
self.actionNodesSeparator = ASDisplayNode()
self.actionNodesSeparator.isLayerBacked = true
self.actionNodes = actions.map { action -> TextAlertContentActionNode in
return TextAlertContentActionNode(theme: theme, action: action)
}
var actionVerticalSeparators: [ASDisplayNode] = []
if actions.count > 1 {
for _ in 0 ..< actions.count - 1 {
let separatorNode = ASDisplayNode()
separatorNode.isLayerBacked = true
actionVerticalSeparators.append(separatorNode)
}
}
self.actionVerticalSeparators = actionVerticalSeparators
super.init()
self.inputBackgroundNode.displaysAsynchronously = false
self.inputBackgroundNode.image = generateStretchableFilledCircleImage(diameter: 16.0, color: presentationTheme.actionSheet.inputHollowBackgroundColor, strokeColor: presentationTheme.actionSheet.inputBorderColor, strokeWidth: UIScreenPixel)
self.addSubnode(self.actionNodesSeparator)
self.addSubnode(self.inputBackgroundNode)
for actionNode in self.actionNodes {
self.addSubnode(actionNode)
}
self.actionNodes.last?.actionEnabled = true
for separatorNode in self.actionVerticalSeparators {
self.addSubnode(separatorNode)
}
self.updateTheme(theme)
self.state._updated = { [weak self] transition, _ in
guard let self, let _ = self.validLayout else {
return
}
self.requestLayout?(transition.containedViewLayoutTransition)
}
}
let initialValue = chatInputStateStringWithAppliedEntities(value, entities: entities)
deinit {
self.disposable.dispose()
}
var textAndEntities: (String, [MessageTextEntity]) {
let text = self.inputFieldExternalState.text.string
let entities = generateChatInputTextEntities(self.inputFieldExternalState.text)
return (text, entities)
}
override func updateTheme(_ theme: AlertControllerTheme) {
self.theme = theme
self.actionNodesSeparator.backgroundColor = theme.separatorColor
for actionNode in self.actionNodes {
actionNode.updateTheme(theme)
}
for separatorNode in self.actionVerticalSeparators {
separatorNode.backgroundColor = theme.separatorColor
}
if let size = self.validLayout {
_ = self.updateLayout(size: size, transition: .immediate)
}
}
override func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize {
var size = size
size.width = min(size.width, 270.0)
let measureSize = CGSize(width: size.width - 16.0 * 2.0, height: CGFloat.greatestFiniteMagnitude)
let hadValidLayout = self.validLayout != nil
self.validLayout = size
var origin: CGPoint = CGPoint(x: 0.0, y: 16.0)
let spacing: CGFloat = 5.0
let titleSize = self.titleView.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: self.text, font: Font.semibold(17.0), textColor: self.theme.primaryColor)),
horizontalAlignment: .center,
maximumNumberOfLines: 0
)),
environment: {},
containerSize: CGSize(width: measureSize.width, height: 1000.0)
var presentImpl: ((ViewController) -> Void)?
var content: [AnyComponentWithIdentity<AlertComponentEnvironment>] = []
content.append(AnyComponentWithIdentity(
id: "title",
component: AnyComponent(
AlertTitleComponent(title: strings.FactCheck_Title)
)
let titleFrame = CGRect(origin: CGPoint(x: floor((size.width - titleSize.width) * 0.5), y: origin.y), size: titleSize)
if let titleComponentView = self.titleView.view {
if titleComponentView.superview == nil {
self.view.addSubview(titleComponentView)
}
titleComponentView.frame = titleFrame
}
origin.y += titleSize.height + 17.0
let actionButtonHeight: CGFloat = 44.0
var minActionsWidth: CGFloat = 0.0
let maxActionWidth: CGFloat = floor(size.width / CGFloat(self.actionNodes.count))
let actionTitleInsets: CGFloat = 8.0
var effectiveActionLayout = TextAlertContentActionLayout.horizontal
for actionNode in self.actionNodes {
let actionTitleSize = actionNode.titleNode.updateLayout(CGSize(width: maxActionWidth, height: actionButtonHeight))
if case .horizontal = effectiveActionLayout, actionTitleSize.height > actionButtonHeight * 0.6667 {
effectiveActionLayout = .vertical
}
switch effectiveActionLayout {
case .horizontal:
minActionsWidth += actionTitleSize.width + actionTitleInsets
case .vertical:
minActionsWidth = max(minActionsWidth, actionTitleSize.width + actionTitleInsets)
}
}
let insets = UIEdgeInsets(top: 18.0, left: 18.0, bottom: 9.0, right: 18.0)
var contentWidth = max(titleSize.width, minActionsWidth)
contentWidth = max(contentWidth, 234.0)
var actionsHeight: CGFloat = 0.0
switch effectiveActionLayout {
case .horizontal:
actionsHeight = actionButtonHeight
case .vertical:
actionsHeight = actionButtonHeight * CGFloat(self.actionNodes.count)
}
let resultWidth = contentWidth + insets.left + insets.right
let inputInset: CGFloat = 16.0
let inputWidth = resultWidth - inputInset * 2.0
var characterLimit: Int = 1024
if let data = self.context.currentAppConfiguration.with({ $0 }).data, let value = data["factcheck_length_limit"] as? Double {
characterLimit = Int(value)
}
let inputFieldSize = self.inputField.update(
transition: .immediate,
component: AnyComponent(TextFieldComponent(
context: self.context,
theme: self.presentationTheme,
strings: self.strings,
externalState: self.inputFieldExternalState,
fontSize: 14.0,
textColor: self.presentationTheme.actionSheet.inputTextColor,
accentColor: self.presentationTheme.actionSheet.controlAccentColor,
insets: UIEdgeInsets(top: 8.0, left: 2.0, bottom: 8.0, right: 2.0),
hideKeyboard: false,
customInputView: nil,
resetText: nil,
isOneLineWhenUnfocused: false,
))
content.append(AnyComponentWithIdentity(
id: "input",
component: AnyComponent(
AlertMultilineInputFieldComponent(
context: context,
initialValue: initialValue,
placeholder: strings.FactCheck_Placeholder,
characterLimit: characterLimit,
formatMenuAvailability: .available([.bold, .italic]),
emptyLineHandling: .oneConsecutive,
formatMenuAvailability: .available([.bold, .italic, .link]),
returnKeyType: .default,
lockedFormatAction: {
},
present: { [weak self] c in
self?.present(c)
},
paste: { _ in
},
returnKeyAction: nil,
backspaceKeyAction: nil
)),
environment: {},
containerSize: CGSize(width: inputWidth, height: 270.0)
)
self.inputField.parentState = self.state
let inputFieldFrame = CGRect(origin: CGPoint(x: inputInset, y: origin.y), size: inputFieldSize)
if let inputFieldView = self.inputField.view as? TextFieldComponent.View {
if inputFieldView.superview == nil {
self.view.addSubview(inputFieldView)
}
transition.updateFrame(view: inputFieldView, frame: inputFieldFrame)
transition.updateFrame(node: self.inputBackgroundNode, frame: inputFieldFrame)
if !hadValidLayout {
inputFieldView.activateInput()
}
}
let inputPlaceholderSize = self.inputPlaceholderView.update(
transition: .immediate,
component: AnyComponent(
MultilineTextComponent(text: .plain(NSAttributedString(
string: self.strings.FactCheck_Placeholder,
font: Font.regular(14.0),
textColor: self.presentationTheme.actionSheet.inputPlaceholderColor
)))
),
environment: {},
containerSize: CGSize(width: inputWidth, height: 240.0)
)
let inputPlaceholderFrame = CGRect(origin: CGPoint(x: inputInset + 10.0, y: floorToScreenPixels(inputFieldFrame.midY - inputPlaceholderSize.height / 2.0)), size: inputPlaceholderSize)
if let inputPlaceholderView = self.inputPlaceholderView.view {
if inputPlaceholderView.superview == nil {
inputPlaceholderView.isUserInteractionEnabled = false
self.view.addSubview(inputPlaceholderView)
}
inputPlaceholderView.frame = inputPlaceholderFrame
inputPlaceholderView.isHidden = self.inputFieldExternalState.hasText
}
if let lastActionNode = self.actionNodes.last {
if self.initialValue.isEmpty {
lastActionNode.actionEnabled = self.inputFieldExternalState.hasText
} else {
if self.inputFieldExternalState.hasText {
lastActionNode.action = TextAlertAction(
type: .defaultAction,
title: self.strings.Common_Done,
action: lastActionNode.action.action
)
} else {
lastActionNode.action = TextAlertAction(
type: .defaultDestructiveAction,
title: self.strings.FactCheck_Remove,
action: lastActionNode.action.action
)
isInitiallyFocused: true,
externalState: inputState,
present: { c in
presentImpl?(c)
}
}
)
)
))
let doneIsRemove: Signal<Bool, NoError>
if !value.isEmpty {
doneIsRemove = inputState.valueSignal
|> map { value in
return value.string.isEmpty
}
let resultSize = CGSize(width: resultWidth, height: titleSize.height + spacing + inputFieldSize.height + 17.0 + actionsHeight + insets.top + insets.bottom)
transition.updateFrame(node: self.actionNodesSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel)))
var actionOffset: CGFloat = 0.0
let actionWidth: CGFloat = floor(resultSize.width / CGFloat(self.actionNodes.count))
var separatorIndex = -1
var nodeIndex = 0
for actionNode in self.actionNodes {
if separatorIndex >= 0 {
let separatorNode = self.actionVerticalSeparators[separatorIndex]
switch effectiveActionLayout {
case .horizontal:
transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: actionOffset - UIScreenPixel, y: resultSize.height - actionsHeight), size: CGSize(width: UIScreenPixel, height: actionsHeight - UIScreenPixel)))
case .vertical:
transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel)))
}
}
separatorIndex += 1
let currentActionWidth: CGFloat
switch effectiveActionLayout {
case .horizontal:
if nodeIndex == self.actionNodes.count - 1 {
currentActionWidth = resultSize.width - actionOffset
} else {
currentActionWidth = actionWidth
}
case .vertical:
currentActionWidth = resultSize.width
}
let actionNodeFrame: CGRect
switch effectiveActionLayout {
case .horizontal:
actionNodeFrame = CGRect(origin: CGPoint(x: actionOffset, y: resultSize.height - actionsHeight), size: CGSize(width: currentActionWidth, height: actionButtonHeight))
actionOffset += currentActionWidth
case .vertical:
actionNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset), size: CGSize(width: currentActionWidth, height: actionButtonHeight))
actionOffset += actionButtonHeight
}
transition.updateFrame(node: actionNode, frame: actionNodeFrame)
nodeIndex += 1
}
return resultSize
|> distinctUntilChanged
} else {
doneIsRemove = .single(false)
}
func deactivateInput() {
if let inputFieldView = self.inputField.view as? TextFieldComponent.View {
inputFieldView.deactivateInput()
}
let actionsSignal: Signal<[AlertScreen.Action], NoError> = doneIsRemove
|> map { doneIsRemove in
var actions: [AlertScreen.Action] = []
actions.append(.init(title: strings.Common_Cancel))
let doneTitle: String = doneIsRemove ? strings.FactCheck_Remove : strings.Common_Done
let doneType: AlertScreen.Action.ActionType = doneIsRemove ? .defaultDestructive : .default
actions.append(
.init(id: "done", title: doneTitle, type: doneType, action: {
let (text, entities) = inputState.textAndEntities
apply(text, entities)
}, isEnabled: doneIsEnabled)
)
return actions
}
func animateError() {
if let inputFieldView = self.inputField.view as? TextFieldComponent.View {
inputFieldView.layer.addShakeAnimation()
}
var effectiveUpdatedPresentationData: (PresentationData, Signal<PresentationData, NoError>)
if let updatedPresentationData {
effectiveUpdatedPresentationData = updatedPresentationData
} else {
effectiveUpdatedPresentationData = (presentationData, context.sharedContext.presentationData)
}
self.hapticFeedback.error()
let alertController = AlertScreen(
configuration: AlertScreen.Configuration(allowInputInset: true),
contentSignal: .single(content),
actionsSignal: actionsSignal,
updatedPresentationData: effectiveUpdatedPresentationData
)
presentImpl = { [weak alertController] c in
alertController?.present(c, in: .window(.root))
}
}
public func factCheckAlertController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, value: String, entities: [MessageTextEntity], apply: @escaping (String, [MessageTextEntity]) -> Void) -> AlertController {
let presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 }
var dismissImpl: ((Bool) -> Void)?
var applyImpl: (() -> Void)?
let actions: [TextAlertAction] = [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {
dismissImpl?(true)
}), TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Done, action: {
dismissImpl?(true)
applyImpl?()
})]
let contentNode = FactCheckAlertContentNode(context: context, theme: AlertControllerTheme(presentationData: presentationData), presentationTheme: presentationData.theme, strings: presentationData.strings, actions: actions, text: presentationData.strings.FactCheck_Title, value: value, entities: entities)
contentNode.complete = {
applyImpl?()
}
applyImpl = { [weak contentNode] in
guard let contentNode = contentNode else {
return
}
let (text, entities) = contentNode.textAndEntities
apply(text, entities)
}
let controller = AlertController(theme: AlertControllerTheme(presentationData: presentationData), contentNode: contentNode)
let presentationDataDisposable = (updatedPresentationData?.signal ?? context.sharedContext.presentationData).start(next: { [weak controller] presentationData in
controller?.theme = AlertControllerTheme(presentationData: presentationData)
})
controller.dismissed = { _ in
presentationDataDisposable.dispose()
}
dismissImpl = { [weak controller] animated in
contentNode.deactivateInput()
if animated {
controller?.dismissAnimated()
} else {
controller?.dismiss()
}
}
contentNode.present = { [weak controller] c in
controller?.present(c, in: .window(.root))
}
return controller
return alertController
}
@@ -29,6 +29,8 @@ swift_library(
"//submodules/TelegramUI/Components/AnimationCache",
"//submodules/TelegramUI/Components/MultiAnimationRenderer",
"//submodules/TelegramUI/Components/Chat/AccessoryPanelNode",
"//submodules/ComponentFlow",
"//submodules/TelegramUI/Components/AlertComponent",
],
visibility = [
"//visibility:public",
@@ -20,6 +20,8 @@ import AnimationCache
import MultiAnimationRenderer
import AccessoryPanelNode
import AppBundle
import ComponentFlow
import AlertComponent
func textStringForForwardedMessage(_ message: Message, strings: PresentationStrings) -> (text: String, entities: [MessageTextEntity], isMedia: Bool) {
for media in message.media {
@@ -378,26 +380,48 @@ public final class ForwardAccessoryPanelNode: AccessoryPanelNode {
string = self.strings.Conversation_ForwardOptions_Text(messages, peerDisplayTitle)
}
let font = Font.regular(floor(self.fontSize.baseDisplaySize * 15.0 / 17.0))
let boldFont = Font.semibold(floor(self.fontSize.baseDisplaySize * 15.0 / 17.0))
let body = MarkdownAttributeSet(font: font, textColor: self.theme.actionSheet.secondaryTextColor)
let bold = MarkdownAttributeSet(font: boldFont, textColor: self.theme.actionSheet.secondaryTextColor)
let font = Font.regular(15.0)
let boldFont = Font.semibold(15.0)
let body = MarkdownAttributeSet(font: font, textColor: self.theme.actionSheet.primaryTextColor)
let bold = MarkdownAttributeSet(font: boldFont, textColor: self.theme.actionSheet.primaryTextColor)
let text = addAttributesToStringWithRanges(string._tuple, body: body, argumentAttributes: [0: bold, 1: bold], textAlignment: .natural)
let title = NSAttributedString(string: self.strings.Conversation_ForwardOptions_Title(messageCount), font: Font.semibold(floor(self.fontSize.baseDisplaySize)), textColor: self.theme.actionSheet.primaryTextColor, paragraphAlignment: .center)
let text = addAttributesToStringWithRanges(string._tuple, body: body, argumentAttributes: [0: bold, 1: bold], textAlignment: .center)
var content: [AnyComponentWithIdentity<AlertComponentEnvironment>] = []
content.append(AnyComponentWithIdentity(
id: "title",
component: AnyComponent(
AlertTitleComponent(
title: strings.Conversation_ForwardOptions_Title(messageCount)
)
)
))
content.append(AnyComponentWithIdentity(
id: "text",
component: AnyComponent(
AlertTextComponent(content: .attributed(text))
)
))
let alertController = richTextAlertController(context: self.context, title: title, text: text, actions: [TextAlertAction(type: .genericAction, title: self.strings.Conversation_ForwardOptions_ShowOptions, action: { [weak self] in
if let strongSelf = self {
strongSelf.interfaceInteraction?.presentForwardOptions(strongSelf.view)
Queue.mainQueue().after(0.5) {
strongSelf.updateThemeAndStrings(theme: strongSelf.theme, strings: strongSelf.strings, forwardOptionsState: strongSelf.forwardOptionsState, force: true)
}
let _ = ApplicationSpecificNotice.incrementChatForwardOptionsTip(accountManager: strongSelf.context.sharedContext.accountManager, count: 3).start()
}
}), TextAlertAction(type: .destructiveAction, title: self.strings.Conversation_ForwardOptions_CancelForwarding, action: { [weak self] in
self?.dismiss?()
})], actionLayout: .vertical)
let alertController = AlertScreen(
context: context,
configuration: AlertScreen.Configuration(actionAlignment: .vertical),
content: content,
actions: [
.init(title: strings.Conversation_ForwardOptions_ShowOptions, action: { [weak self] in
guard let self else {
return
}
self.interfaceInteraction?.presentForwardOptions(self.view)
Queue.mainQueue().after(0.5) {
self.updateThemeAndStrings(theme: self.theme, strings: self.strings, forwardOptionsState: self.forwardOptionsState, force: true)
}
let _ = ApplicationSpecificNotice.incrementChatForwardOptionsTip(accountManager: self.context.sharedContext.accountManager, count: 3).start()
}),
.init(title: strings.Conversation_ForwardOptions_CancelForwarding, type: .destructive, action: { [weak self] in
self?.dismiss?()
})
]
)
self.interfaceInteraction?.presentController(alertController, nil)
}
@@ -100,7 +100,7 @@ public struct InteractiveEmojiConfiguration {
}
public static func with(appConfiguration: AppConfiguration) -> InteractiveEmojiConfiguration {
if let data = appConfiguration.data, let emojis = data["emojies_send_dice"] as? [String] {
if let data = appConfiguration.data, var emojis = data["emojies_send_dice"] as? [String] {
var successParameters: [String: InteractiveEmojiSuccessParameters] = [:]
if let success = data["emojies_send_dice_success"] as? [String: [String: Double]] {
for (key, dict) in success {
@@ -109,6 +109,11 @@ public struct InteractiveEmojiConfiguration {
}
}
}
#if DEBUG
if !emojis.contains("🎲") {
emojis.append("🎲")
}
#endif
return InteractiveEmojiConfiguration(emojis: emojis, successParameters: successParameters)
} else {
return .defaultValue
@@ -126,6 +131,10 @@ public final class ManagedDiceAnimationNode: ManagedAnimationNode {
private let configuration = Promise<InteractiveEmojiConfiguration?>()
private let emojis = Promise<[TelegramMediaFile]>()
public var isRolling: Bool {
return self.diceState == .rolling
}
public var success: (() -> Void)?
public init(context: AccountContext, emoji: String) {
@@ -93,7 +93,7 @@ public final class ShimmeringLinkNode: ASDisplayNode {
self.shimmerEffectNode.updateAbsoluteRect(CGRect(origin: .zero, size: size), within: size)
self.borderShimmerEffectNode.updateAbsoluteRect(CGRect(origin: .zero, size: size), within: size)
self.shimmerEffectNode.update(backgroundColor: .clear, foregroundColor: self.color.withAlphaComponent(min(1.0, self.color.alpha * 1.2)), horizontal: true, effectSize: nil, globalTimeOffset: false, duration: nil)
self.borderShimmerEffectNode.update(backgroundColor: .clear, foregroundColor: self.color.withAlphaComponent(min(1.0, self.color.alpha * 1.5)), horizontal: true, effectSize: nil, globalTimeOffset: false, duration: nil)
self.shimmerEffectNode.update(backgroundColor: .clear, foregroundColor: self.color.withMultipliedAlpha(1.75), horizontal: true, effectSize: nil, globalTimeOffset: false, duration: nil)
self.borderShimmerEffectNode.update(backgroundColor: .clear, foregroundColor: self.color.withMultipliedAlpha(2.0), horizontal: true, effectSize: nil, globalTimeOffset: false, duration: nil)
}
}