mirror of
https://github.com/ichmagmaus111/ghostgram.git
synced 2026-07-25 17:00:50 +02:00
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:
@@ -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",
|
||||
],
|
||||
)
|
||||
+64
@@ -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
|
||||
}
|
||||
+3
-7
@@ -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)
|
||||
|
||||
|
||||
+2
-44
@@ -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
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+37
-45
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
-2
@@ -901,8 +901,6 @@ public final class ChatInlineSearchResultsListComponent: Component {
|
||||
},
|
||||
openStarsTopup: { _ in
|
||||
},
|
||||
dismissNotice: { _ in
|
||||
},
|
||||
editPeer: { _ in
|
||||
},
|
||||
openWebApp: { _ in
|
||||
|
||||
+1
@@ -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",
|
||||
|
||||
+124
@@ -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",
|
||||
|
||||
+160
-2
@@ -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
|
||||
|
||||
+80
-4
@@ -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)
|
||||
|
||||
+5
-2
@@ -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 {
|
||||
|
||||
+1
-1
@@ -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 ?? ""
|
||||
|
||||
+1
@@ -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,
|
||||
|
||||
+14
-61
@@ -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)
|
||||
|
||||
+2
-39
@@ -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
|
||||
|
||||
+2
-2
@@ -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)
|
||||
|
||||
+1
-1
@@ -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)
|
||||
|
||||
|
||||
+1
-1
@@ -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)
|
||||
|
||||
|
||||
+2
-2
@@ -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)
|
||||
}
|
||||
|
||||
+7
-7
@@ -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)
|
||||
|
||||
+9
-9
@@ -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 = [
|
||||
|
||||
+172
-412
@@ -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
|
||||
}
|
||||
|
||||
+8
-1
@@ -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)
|
||||
|
||||
+128
-2
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
+6
-5
@@ -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",
|
||||
|
||||
+114
-4
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
+1
@@ -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,
|
||||
|
||||
+54
-8
@@ -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)
|
||||
|
||||
+8
-1
@@ -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)
|
||||
|
||||
+1
-1
@@ -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)
|
||||
|
||||
|
||||
+3
-3
@@ -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
|
||||
|
||||
+2
-2
@@ -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)
|
||||
}
|
||||
|
||||
-1
@@ -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
|
||||
|
||||
+4
-2
@@ -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",
|
||||
],
|
||||
)
|
||||
+299
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+9
-9
@@ -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()
|
||||
})
|
||||
}
|
||||
|
||||
+6
-6
@@ -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: {},
|
||||
|
||||
+4
-5
@@ -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
|
||||
|
||||
-2
@@ -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,
|
||||
|
||||
+18
-17
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -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",
|
||||
|
||||
+57
-135
@@ -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",
|
||||
|
||||
+86
-376
@@ -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",
|
||||
|
||||
+42
-18
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
+10
-1
@@ -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) {
|
||||
|
||||
+2
-2
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user