Update Ghostgram features

This commit is contained in:
ichmagmaus 812
2026-03-07 18:19:16 +01:00
parent 32b0dddcd9
commit 13a43b9068
902 changed files with 148295 additions and 62348 deletions
@@ -83,6 +83,7 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
public let contextSourceNode: ContextExtractedContentContainingNode
private let containerNode: ContextControllerSourceNode
public let imageNode: TransformImageNode
public var sizeCoefficient: Float = 1.0
private var enableSynchronousImageApply: Bool = false
private var backgroundNode: WallpaperBubbleBackgroundNode?
public private(set) var placeholderNode: StickerShimmerEffectNode
@@ -117,7 +118,7 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
private let disposables = DisposableSet()
private var viaBotNode: TextNode?
private let dateAndStatusNode: ChatMessageDateAndStatusNode
public let dateAndStatusNode: ChatMessageDateAndStatusNode
private var threadInfoNode: ChatMessageThreadInfoNode?
private var replyInfoNode: ChatMessageReplyInfoNode?
private var replyBackgroundContent: WallpaperBubbleBackgroundNode?
@@ -1165,6 +1166,7 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
context: item.context,
presentationData: item.presentationData,
edited: edited,
isDeleted: item.message.ghostgramIsDeleted,
impressionCount: viewCount,
dateText: dateText,
type: statusType,
@@ -1410,11 +1412,11 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
ReplyMarkupMessageAttribute(
rows: [
ReplyMarkupRow(buttons: [
ReplyMarkupButton(title: item.presentationData.strings.Chat_PostApproval_Message_ActionReject, titleWhenForwarded: nil, action: .callback(requiresPassword: false, data: buttonDecline)),
ReplyMarkupButton(title: item.presentationData.strings.Chat_PostApproval_Message_ActionApprove, titleWhenForwarded: nil, action: .callback(requiresPassword: false, data: buttonApprove))
ReplyMarkupButton(title: item.presentationData.strings.Chat_PostApproval_Message_ActionReject, titleWhenForwarded: nil, action: .callback(requiresPassword: false, data: buttonDecline), style: nil),
ReplyMarkupButton(title: item.presentationData.strings.Chat_PostApproval_Message_ActionApprove, titleWhenForwarded: nil, action: .callback(requiresPassword: false, data: buttonApprove), style: nil)
]),
ReplyMarkupRow(buttons: [
ReplyMarkupButton(title: item.presentationData.strings.Chat_PostApproval_Message_ActionSuggestChanges, titleWhenForwarded: nil, action: .callback(requiresPassword: false, data: buttonSuggestChanges))
ReplyMarkupButton(title: item.presentationData.strings.Chat_PostApproval_Message_ActionSuggestChanges, titleWhenForwarded: nil, action: .callback(requiresPassword: false, data: buttonSuggestChanges), style: nil)
])
],
flags: [],
@@ -749,6 +749,7 @@ public final class ChatMessageAttachedContentNode: ASDisplayNode {
context: context,
presentationData: presentationData,
edited: edited && !isPreview,
isDeleted: message.ghostgramIsDeleted,
impressionCount: !isPreview ? viewCount : nil,
dateText: dateText,
type: statusType,
@@ -1,15 +1,25 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
sgdeps = [
"//Swiftgram/SGStrings:SGStrings",
"//Swiftgram/SGSimpleSettings:SGSimpleSettings",
"//submodules/TranslateUI:TranslateUI"
]
sgsrc = [
"//Swiftgram/SGDoubleTapMessageAction:SGDoubleTapMessageAction"
]
swift_library(
name = "ChatMessageBubbleItemNode",
module_name = "ChatMessageBubbleItemNode",
srcs = glob([
srcs = sgsrc + glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
deps = sgdeps + [
"//submodules/AsyncDisplayKit",
"//submodules/Display",
"//submodules/SSignalKit/SwiftSignalKit",
@@ -1,3 +1,6 @@
import SGStrings
import SGSimpleSettings
import TranslateUI
import Foundation
import UIKit
import AsyncDisplayKit
@@ -132,7 +135,7 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([
outer: for (message, itemAttributes) in item.content {
for attribute in message.attributes {
if let attribute = attribute as? RestrictedContentMessageAttribute, attribute.platformText(platform: "ios", contentSettings: item.context.currentContentSettings.with { $0 }) != nil {
if let attribute = attribute as? RestrictedContentMessageAttribute, attribute.platformText(platform: "ios", contentSettings: item.context.currentContentSettings.with { $0 }, chatId: message.author?.id.id._internalGetInt64Value()) != nil {
result.append((message, ChatMessageRestrictedBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default)))
needReactions = false
break outer
@@ -316,6 +319,35 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([
isMediaInverted = true
}
// MARK: Swiftgram
var message = message
if message.canRevealContent(contentSettings: item.context.currentContentSettings.with { $0 }) {
let originalTextLength = message.text.count
let noticeString = i18n("Message.HoldToShowOrReport", item.presentationData.strings.baseLanguageCode)
message = message.withUpdatedText(message.text + "\n" + noticeString)
let noticeStringLength = noticeString.count
let startIndex = originalTextLength + 1 // +1 for the newline character
// Calculate the end index, which is the start index plus the length of noticeString
let endIndex = startIndex + noticeStringLength
var newAttributes = message.attributes
newAttributes.append(
TextEntitiesMessageAttribute(
entities: [
MessageTextEntity(
range: startIndex..<endIndex,
// TODO(swiftgram): Add more instructions to collapsed block?
type: .BlockQuote(isCollapsed: false) //.Custom(type: ApplicationSpecificEntityType.Button)
)
]
)
)
message = message.withUpdatedAttributes(newAttributes)
}
if isMediaInverted {
result.insert((message, ChatMessageTextBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: isFile ? .condensed : .default)), at: addedPriceInfo ? 1 : 0)
} else {
@@ -482,7 +514,7 @@ private func mapVisibility(_ visibility: ListViewItemNodeVisibility, boundsSize:
}
private func isDeletedBubbleMessage(_ message: Message) -> Bool {
return AntiDeleteManager.shared.isMessageDeleted(peerId: message.id.peerId.toInt64(), messageId: message.id.id) || AntiDeleteManager.shared.isMessageDeleted(text: message.text)
return message.ghostgramIsDeleted
}
public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode {
@@ -1235,7 +1267,6 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
if let shareButtonNode = strongSelf.shareButtonNode, shareButtonNode.frame.contains(point) {
return .fail
}
if let actionButtonsNode = strongSelf.actionButtonsNode {
if let _ = actionButtonsNode.hitTest(strongSelf.view.convert(point, to: actionButtonsNode.view), with: nil) {
return .fail
@@ -1734,6 +1765,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
let isFailed = item.content.firstMessage.effectivelyFailed(timestamp: item.context.account.network.getApproximateRemoteTimestamp())
var needsShareButton = false
var mayHaveSeparateCommentsButton = false // MARK: Swiftgram
var needsSummarizeButton = false
if incoming, case let .customChatContents(contents) = item.associatedData.subject, case .hashTagSearch = contents.kind {
@@ -1789,7 +1821,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
needsShareButton = true
}
}
var mayHaveSeparateCommentsButton = false
// var mayHaveSeparateCommentsButton = false // MARK: Swiftgram
if !needsShareButton {
loop: for media in item.message.media {
if media is TelegramMediaGame || media is TelegramMediaInvoice {
@@ -1831,7 +1863,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
needsShareButton = true
}
for attribute in item.content.firstMessage.attributes {
if let attribute = attribute as? RestrictedContentMessageAttribute, attribute.platformText(platform: "ios", contentSettings: item.context.currentContentSettings.with { $0 }) != nil {
if let attribute = attribute as? RestrictedContentMessageAttribute, attribute.platformText(platform: "ios", contentSettings: item.context.currentContentSettings.with { $0 }, chatId: item.content.firstMessage.author?.id.id._internalGetInt64Value()) != nil {
needsShareButton = false
needsSummarizeButton = false
}
@@ -1862,11 +1894,21 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
}
tmpWidth -= deliveryFailedInset
// MARK: Swifgram
let renderWideChannelPosts: Bool
if let channel = item.message.peers[item.message.id.peerId] as? TelegramChannel, case .broadcast = channel.info, SGSimpleSettings.shared.wideChannelPosts, !(mayHaveSeparateCommentsButton && hasCommentButton(item: item)) {
renderWideChannelPosts = true
tmpWidth = baseWidth
needsShareButton = false
} else {
renderWideChannelPosts = false
}
let (contentNodeMessagesAndClasses, needSeparateContainers, needReactions) = contentNodeMessagesAndClassesForItem(item)
var maximumContentWidth = floor(tmpWidth - layoutConstants.bubble.edgeInset * 3.0 - layoutConstants.bubble.contentInsets.left - layoutConstants.bubble.contentInsets.right - avatarInset)
if (needsShareButton && !isSidePanelOpen) {
if needsShareButton && !isSidePanelOpen {
maximumContentWidth -= 10.0
}
@@ -2386,13 +2428,29 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
var mosaicStatusSizeAndApply: (CGSize, (ListViewItemUpdateAnimation) -> ChatMessageDateAndStatusNode)?
if let mosaicRange = mosaicRange {
let maxSize = layoutConstants.image.maxDimensions.fittedToWidthOrSmaller(maximumContentWidth - layoutConstants.image.bubbleInsets.left - layoutConstants.image.bubbleInsets.right)
let (innerFramesAndPositions, innerSize) = chatMessageBubbleMosaicLayout(maxSize: maxSize, itemSizes: contentPropertiesAndLayouts[mosaicRange].map { item in
// MARK: Swiftgram
var maxDimensions = layoutConstants.image.maxDimensions
if renderWideChannelPosts {
maxDimensions.width = maximumContentWidth
}
var maxSize = maxDimensions.fittedToWidthOrSmaller(maximumContentWidth - layoutConstants.image.bubbleInsets.left - layoutConstants.image.bubbleInsets.right)
var (innerFramesAndPositions, innerSize) = chatMessageBubbleMosaicLayout(maxSize: maxSize, itemSizes: contentPropertiesAndLayouts[mosaicRange].map { item in
guard let size = item.0, size.width > 0.0, size.height > 0 else {
return CGSize(width: 256.0, height: 256.0)
}
return size
})
}/*, TODO(swiftgram): fillWidth: SGSimpleSettings.shared.wideChannelPosts */)
// MARK: Swiftgram
if innerSize.height > maxSize.height, maxDimensions.width != layoutConstants.image.maxDimensions.width {
maxDimensions.width = max(round(maxDimensions.width * maxSize.height / innerSize.height), layoutConstants.image.maxDimensions.width)
maxSize = maxDimensions.fittedToWidthOrSmaller(maximumContentWidth - layoutConstants.image.bubbleInsets.left - layoutConstants.image.bubbleInsets.right)
(innerFramesAndPositions, innerSize) = chatMessageBubbleMosaicLayout(maxSize: maxSize, itemSizes: contentPropertiesAndLayouts[mosaicRange].map { item in
guard let size = item.0, size.width > 0.0, size.height > 0 else {
return CGSize(width: 256.0, height: 256.0)
}
return size
}/*, TODO(swiftgram): fillWidth: SGSimpleSettings.shared.wideChannelPosts */)
}
let framesAndPositions = innerFramesAndPositions.map { ($0.0.offsetBy(dx: layoutConstants.image.bubbleInsets.left, dy: layoutConstants.image.bubbleInsets.top), $0.1) }
@@ -2474,6 +2532,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
context: item.context,
presentationData: item.presentationData,
edited: edited && !item.presentationData.isPreview,
isDeleted: message.ghostgramIsDeleted,
impressionCount: !item.presentationData.isPreview ? viewCount : nil,
dateText: dateText,
type: statusType,
@@ -2852,7 +2911,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
ReplyMarkupMessageAttribute(
rows: [
ReplyMarkupRow(
buttons: [ReplyMarkupButton(title: item.presentationData.strings.Channel_AdminLog_ShowMoreMessages(Int32(messages.count - 1)), titleWhenForwarded: nil, action: .callback(requiresPassword: false, data: MemoryBuffer(data: Data())))]
buttons: [ReplyMarkupButton(title: item.presentationData.strings.Channel_AdminLog_ShowMoreMessages(Int32(messages.count - 1)), titleWhenForwarded: nil, action: .callback(requiresPassword: false, data: MemoryBuffer(data: Data())), style: nil)]
)
],
flags: [],
@@ -2891,8 +2950,8 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
ReplyMarkupMessageAttribute(
rows: [
ReplyMarkupRow(buttons: [
ReplyMarkupButton(title: item.presentationData.strings.Chat_GiftPurchaseOffer_Reject, titleWhenForwarded: nil, action: .callback(requiresPassword: false, data: buttonDecline)),
ReplyMarkupButton(title: item.presentationData.strings.Chat_GiftPurchaseOffer_Accept, titleWhenForwarded: nil, action: .callback(requiresPassword: false, data: buttonApprove))
ReplyMarkupButton(title: item.presentationData.strings.Chat_GiftPurchaseOffer_Reject, titleWhenForwarded: nil, action: .callback(requiresPassword: false, data: buttonDecline), style: nil),
ReplyMarkupButton(title: item.presentationData.strings.Chat_GiftPurchaseOffer_Accept, titleWhenForwarded: nil, action: .callback(requiresPassword: false, data: buttonApprove), style: nil)
])
],
flags: [],
@@ -2940,11 +2999,11 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
ReplyMarkupMessageAttribute(
rows: [
ReplyMarkupRow(buttons: [
ReplyMarkupButton(title: item.presentationData.strings.Chat_PostApproval_Message_ActionReject, titleWhenForwarded: nil, action: .callback(requiresPassword: false, data: buttonDecline)),
ReplyMarkupButton(title: item.presentationData.strings.Chat_PostApproval_Message_ActionApprove, titleWhenForwarded: nil, action: .callback(requiresPassword: false, data: buttonApprove))
ReplyMarkupButton(title: item.presentationData.strings.Chat_PostApproval_Message_ActionReject, titleWhenForwarded: nil, action: .callback(requiresPassword: false, data: buttonDecline), style: nil),
ReplyMarkupButton(title: item.presentationData.strings.Chat_PostApproval_Message_ActionApprove, titleWhenForwarded: nil, action: .callback(requiresPassword: false, data: buttonApprove), style: nil)
]),
ReplyMarkupRow(buttons: [
ReplyMarkupButton(title: item.presentationData.strings.Chat_PostApproval_Message_ActionSuggestChanges, titleWhenForwarded: nil, action: .callback(requiresPassword: false, data: buttonSuggestChanges))
ReplyMarkupButton(title: item.presentationData.strings.Chat_PostApproval_Message_ActionSuggestChanges, titleWhenForwarded: nil, action: .callback(requiresPassword: false, data: buttonSuggestChanges), style: nil)
])
],
flags: [],
@@ -2978,7 +3037,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
ReplyMarkupMessageAttribute(
rows: [
ReplyMarkupRow(buttons: [
ReplyMarkupButton(title: item.presentationData.strings.Chat_MessageContinueLastThread, titleWhenForwarded: nil, action: .callback(requiresPassword: false, data: button))
ReplyMarkupButton(title: item.presentationData.strings.Chat_MessageContinueLastThread, titleWhenForwarded: nil, action: .callback(requiresPassword: false, data: button), style: nil)
])
],
flags: [],
@@ -4371,7 +4430,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
strongSelf.replyInfoNode = nil
}
}
let deletedMessageAlpha = CGFloat(AntiDeleteManager.shared.deletedMessageDisplayAlpha)
var deletedMessageStableIds = Set<UInt32>()
for (message, _) in item.content {
@@ -4524,7 +4583,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
}
contentContainer?.update(size: relativeFrame.size, contentOrigin: contentOrigin, selectionInsets: selectionInsets, index: index, presentationData: item.presentationData, graphics: graphics, backgroundType: backgroundType, presentationContext: item.controllerInteraction.presentationContext, mediaBox: item.context.account.postbox.mediaBox, messageSelection: itemSelection)
if let contentContainer = contentContainer {
let containerAlpha: CGFloat = deletedMessageStableIds.contains(stableId) ? deletedMessageAlpha : 1.0
if case .System = animation {
@@ -4536,7 +4595,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
index += 1
}
let mainContainerAlpha: CGFloat
if contentContainerNodeFrames.isEmpty, !deletedMessageStableIds.isEmpty {
mainContainerAlpha = deletedMessageAlpha
@@ -5153,7 +5212,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
animation.animator.updateAlpha(layer: shareButtonNode.layer, alpha: (isCurrentlyPlayingMedia || isSidePanelOpen) ? 0.0 : 1.0, completion: nil)
animation.animator.updateScale(layer: shareButtonNode.layer, scale: (isCurrentlyPlayingMedia || isSidePanelOpen) ? 0.001 : 1.0, completion: nil)
}
if case .System = animation, strongSelf.mainContextSourceNode.isExtractedToContextPreview {
legacyTransition.updateFrame(node: strongSelf.backgroundNode, frame: backgroundFrame)
if let backgroundHighlightNode = strongSelf.backgroundHighlightNode {
@@ -5291,18 +5350,32 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
case let .optionalAction(f):
f()
case let .openContextMenu(openContextMenu):
switch (sgDoubleTapMessageAction(incoming: openContextMenu.tapMessage.effectivelyIncoming(item.context.account.peerId), message: openContextMenu.tapMessage)) {
case SGSimpleSettings.MessageDoubleTapAction.none.rawValue:
break
case SGSimpleSettings.MessageDoubleTapAction.edit.rawValue:
item.controllerInteraction.sgStartMessageEdit(openContextMenu.tapMessage)
default:
if canAddMessageReactions(message: openContextMenu.tapMessage) {
item.controllerInteraction.updateMessageReaction(openContextMenu.tapMessage, .default, false, nil)
} else {
item.controllerInteraction.openMessageContextMenu(openContextMenu.tapMessage, openContextMenu.selectAll, self, openContextMenu.subFrame, nil, nil)
}
}
}
} else if case .tap = gesture {
item.controllerInteraction.clickThroughMessage(self.view, location)
} else if case .doubleTap = gesture {
switch (sgDoubleTapMessageAction(incoming: item.message.effectivelyIncoming(item.context.account.peerId), message: item.message)) {
case SGSimpleSettings.MessageDoubleTapAction.none.rawValue:
break
case SGSimpleSettings.MessageDoubleTapAction.edit.rawValue:
item.controllerInteraction.sgStartMessageEdit(item.message)
default:
if canAddMessageReactions(message: item.message) {
item.controllerInteraction.updateMessageReaction(item.message, .default, false, nil)
}
}
}
}
default:
@@ -5971,7 +6044,6 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
if let shareButtonNode = self.shareButtonNode, shareButtonNode.frame.contains(point) {
return shareButtonNode.view.hitTest(self.view.convert(point, to: shareButtonNode.view), with: event)
}
if let selectionNode = self.selectionNode {
if let result = self.traceSelectionNodes(parent: self, point: point.offsetBy(dx: -42.0, dy: 0.0)) {
return result.view
@@ -298,6 +298,7 @@ public class ChatMessageContactBubbleContentNode: ChatMessageBubbleContentNode {
context: item.context,
presentationData: item.presentationData,
edited: edited,
isDeleted: item.topMessage.ghostgramIsDeleted,
impressionCount: viewCount,
dateText: dateText,
type: statusType,
@@ -1,5 +1,9 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
sgdeps = [
"//Swiftgram/SGSimpleSettings:SGSimpleSettings"
]
swift_library(
name = "ChatMessageDateAndStatusNode",
module_name = "ChatMessageDateAndStatusNode",
@@ -9,7 +13,7 @@ swift_library(
copts = [
"-warnings-as-errors",
],
deps = [
deps = sgdeps + [
"//submodules/AsyncDisplayKit",
"//submodules/Postbox",
"//submodules/TelegramCore",
@@ -1,3 +1,4 @@
import SGSimpleSettings
import Foundation
import UIKit
import AsyncDisplayKit
@@ -30,6 +31,10 @@ private func maybeAddRotationAnimation(_ layer: CALayer, duration: Double) {
layer.add(basicAnimation, forKey: "clockFrameAnimation")
}
private func generateDeletedStatusIcon(color: UIColor) -> UIImage? {
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: color)?.precomposed()
}
public enum ChatMessageDateAndStatusOutgoingType: Equatable {
case Sent(read: Bool)
case Sending
@@ -183,6 +188,7 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode {
var context: AccountContext
var presentationData: ChatPresentationData
var edited: Bool
var isDeleted: Bool
var impressionCount: Int?
var dateText: String
var type: ChatMessageDateAndStatusType
@@ -201,7 +207,6 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode {
var tonAmount: Int64?
var isPinned: Bool
var hasAutoremove: Bool
var isDeleted: Bool
var canViewReactionList: Bool
var animationCache: AnimationCache
var animationRenderer: MultiAnimationRenderer
@@ -210,6 +215,7 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode {
context: AccountContext,
presentationData: ChatPresentationData,
edited: Bool,
isDeleted: Bool,
impressionCount: Int?,
dateText: String,
type: ChatMessageDateAndStatusType,
@@ -228,7 +234,6 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode {
tonAmount: Int64? = nil,
isPinned: Bool,
hasAutoremove: Bool,
isDeleted: Bool = false,
canViewReactionList: Bool,
animationCache: AnimationCache,
animationRenderer: MultiAnimationRenderer
@@ -236,6 +241,7 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode {
self.context = context
self.presentationData = presentationData
self.edited = edited
self.isDeleted = isDeleted
self.impressionCount = impressionCount == 0 ? nil : impressionCount
self.dateText = dateText
self.type = type
@@ -254,7 +260,6 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode {
self.tonAmount = tonAmount
self.isPinned = isPinned
self.hasAutoremove = hasAutoremove
self.isDeleted = isDeleted
self.canViewReactionList = canViewReactionList
self.animationCache = animationCache
self.animationRenderer = animationRenderer
@@ -268,6 +273,7 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode {
private var clockFrameNode: ASImageNode?
private var clockMinNode: ASImageNode?
private let dateNode: TextNode
private var deletedIcon: ASImageNode?
private var impressionIcon: ASImageNode?
private var reactionNodes: [MessageReaction.Reaction: StatusReactionNode] = [:]
private let reactionButtonsContainer = ReactionButtonsAsyncLayoutContainer()
@@ -277,7 +283,6 @@ 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?
@@ -330,6 +335,7 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode {
var clockMinNode = self.clockMinNode
var currentBackgroundNode = self.backgroundNode
var currentDeletedIcon = self.deletedIcon
var currentImpressionIcon = self.impressionIcon
var currentRepliesIcon = self.repliesIcon
var currentStarsIcon = self.starsIcon
@@ -353,6 +359,7 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode {
let loadedCheckPartialImage: UIImage?
let clockFrameImage: UIImage?
let clockMinImage: UIImage?
let deletedImage: UIImage?
var impressionImage: UIImage?
var repliesImage: UIImage?
var starsImage: UIImage?
@@ -541,6 +548,8 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode {
starsImage = graphics.freeTonIcon
}
}
deletedImage = arguments.isDeleted ? generateDeletedStatusIcon(color: dateColor) : nil
var updatedDateText = arguments.dateText
if arguments.edited {
@@ -561,6 +570,23 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode {
var checkReadFrame: CGRect?
var clockPosition = CGPoint()
var deletedSize = CGSize()
var deletedWidth: CGFloat = 0.0
if deletedImage != nil {
if currentDeletedIcon == nil {
let iconNode = ASImageNode()
iconNode.isLayerBacked = true
iconNode.displayWithoutProcessing = true
iconNode.displaysAsynchronously = false
iconNode.contentMode = .scaleAspectFit
currentDeletedIcon = iconNode
}
deletedSize = CGSize(width: 11.0, height: 11.0)
deletedWidth = deletedSize.width + 4.0
} else {
currentDeletedIcon = nil
}
var impressionSize = CGSize()
var impressionWidth: CGFloat = 0.0
@@ -606,36 +632,6 @@ 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:
@@ -670,7 +666,7 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode {
clockMinNode?.displayWithoutProcessing = true
clockMinNode?.frame = CGRect(origin: CGPoint(), size: clockMinImage?.size ?? CGSize())
}
clockPosition = CGPoint(x: leftInset + date.size.width + 8.5, y: 7.5 + offset)
clockPosition = CGPoint(x: leftInset + deletedWidth + impressionWidth + date.size.width + 8.5, y: 7.5 + offset)
case let .Sent(read):
let hideStatus: Bool
switch arguments.type {
@@ -710,9 +706,9 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode {
let checkSize = loadedCheckFullImage!.size
if read {
checkReadFrame = CGRect(origin: CGPoint(x: leftInset + impressionWidth + date.size.width + 5.0 + statusWidth - checkSize.width, y: 3.0 + offset), size: checkSize)
checkReadFrame = CGRect(origin: CGPoint(x: leftInset + deletedWidth + impressionWidth + date.size.width + 5.0 + statusWidth - checkSize.width, y: 3.0 + offset), size: checkSize)
}
checkSentFrame = CGRect(origin: CGPoint(x: leftInset + impressionWidth + date.size.width + 5.0 + statusWidth - checkSize.width - checkOffset, y: 3.0 + offset), size: checkSize)
checkSentFrame = CGRect(origin: CGPoint(x: leftInset + deletedWidth + impressionWidth + date.size.width + 5.0 + statusWidth - checkSize.width - checkOffset, y: 3.0 + offset), size: checkSize)
}
case .Failed:
statusWidth = 0.0
@@ -799,15 +795,9 @@ 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 + deletedIconWidth + impressionWidth + date.size.width + statusWidth + backgroundInsets.left + backgroundInsets.right, height: date.size.height + backgroundInsets.top + backgroundInsets.bottom)
let layoutSize = CGSize(width: leftInset + deletedWidth + impressionWidth + date.size.width + statusWidth + backgroundInsets.left + backgroundInsets.right, height: date.size.height + backgroundInsets.top + backgroundInsets.bottom)
let verticalReactionsInset: CGFloat
let verticalInset: CGFloat
@@ -1132,8 +1122,26 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode {
let _ = dateApply()
if let currentDeletedIcon = currentDeletedIcon {
let deletedIconFrame = CGRect(origin: CGPoint(x: leftOffset + leftInset + backgroundInsets.left, y: backgroundInsets.top + 1.0 + offset + verticalInset + floor((date.size.height - deletedSize.height) / 2.0)), size: deletedSize)
currentDeletedIcon.displaysAsynchronously = false
if currentDeletedIcon.image !== deletedImage {
currentDeletedIcon.image = deletedImage
}
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
}
if let currentImpressionIcon = currentImpressionIcon {
let impressionIconFrame = CGRect(origin: CGPoint(x: leftOffset + leftInset + backgroundInsets.left, y: backgroundInsets.top + 1.0 + offset + verticalInset + floor((date.size.height - impressionSize.height) / 2.0)), size: impressionSize)
let impressionIconFrame = CGRect(origin: CGPoint(x: leftOffset + leftInset + backgroundInsets.left + deletedWidth, y: backgroundInsets.top + 1.0 + offset + verticalInset + floor((date.size.height - impressionSize.height) / 2.0)), size: impressionSize)
currentImpressionIcon.displaysAsynchronously = false
if currentImpressionIcon.image !== impressionImage {
currentImpressionIcon.image = impressionImage
@@ -1150,22 +1158,7 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode {
strongSelf.impressionIcon = 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)
animation.animator.updateFrame(layer: strongSelf.dateNode.layer, frame: CGRect(origin: CGPoint(x: leftOffset + leftInset + backgroundInsets.left + deletedWidth + impressionWidth, 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)
@@ -1527,5 +1520,7 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode {
}
public func shouldDisplayInlineDateReactions(message: Message, isPremium: Bool, forceInline: Bool) -> Bool {
return false
// MARK: Swiftgram
// With 10.13 it now hides reactions in favor of message effect badge
return SGSimpleSettings.shared.hideReactions
}
@@ -437,6 +437,7 @@ public class ChatMessageFactCheckBubbleContentNode: ChatMessageBubbleContentNode
context: item.context,
presentationData: item.presentationData,
edited: edited && !item.presentationData.isPreview,
isDeleted: message.ghostgramIsDeleted,
impressionCount: !item.presentationData.isPreview ? viewCount : nil,
dateText: dateText,
type: statusType,
@@ -282,7 +282,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
switch action.action {
case let .starGift(gift, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _):
releasedBy = gift.releasedBy
case let .starGiftUnique(gift, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _):
case let .starGiftUnique(gift, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _):
releasedBy = gift.releasedBy
default:
break
@@ -378,6 +378,17 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
}
override public func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize, _ avatarInset: CGFloat) -> (ChatMessageBubbleContentProperties, unboundSize: CGSize?, maxWidth: CGFloat, layout: (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))) {
return { item, _, _, _, _, _ in
let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: true, headerSpacing: 0.0, hidesBackground: .always, forceFullCorners: false, forceAlignment: .center)
return (contentProperties, nil, 220.0, { _, _ in
return (220.0, { _ in
return (CGSize(width: 220.0, height: 120.0), { [weak self] _, _, _ in
self?.item = item
})
})
})
}
#if false
let makeLabelLayout = TextNode.asyncLayout(self.labelNode)
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
let makeSubtitleLayout = TextNodeWithEntities.asyncLayout(self.subtitleNode)
@@ -402,10 +413,10 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
let cachedTonImage = self.cachedTonImage
return { item, layoutConstants, _, _, _, _ in
let asyncLayout: (ChatMessageBubbleContentItem, ChatMessageItemLayoutConstants, ChatMessageBubblePreparePosition, Bool?, CGSize, CGFloat) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))) = { (item: ChatMessageBubbleContentItem, layoutConstants: ChatMessageItemLayoutConstants, preparePosition: ChatMessageBubblePreparePosition, messageSelection: Bool?, constrainedSize: CGSize, avatarInset: CGFloat) in
let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: true, headerSpacing: 0.0, hidesBackground: .always, forceFullCorners: false, forceAlignment: .center)
return (contentProperties, nil, CGFloat.greatestFiniteMagnitude, { constrainedSize, position in
return (contentProperties, nil, CGFloat.greatestFiniteMagnitude, { (constrainedSize: CGSize, position: ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void)) in
var giftSize = CGSize(width: 220.0, height: 240.0)
let incoming: Bool
@@ -1471,6 +1482,8 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
})
})
}
return asyncLayout
#endif
}
override public func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) {
@@ -565,6 +565,7 @@ public class ChatMessageGiveawayBubbleContentNode: ChatMessageBubbleContentNode,
context: item.context,
presentationData: item.presentationData,
edited: edited,
isDeleted: item.topMessage.ghostgramIsDeleted,
impressionCount: !item.presentationData.isPreview ? viewCount : nil,
dateText: dateText,
type: statusType,
@@ -1,5 +1,10 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
sgdeps = [
"//Swiftgram/SGSimpleSettings:SGSimpleSettings"
]
swift_library(
name = "ChatMessageInteractiveFileNode",
module_name = "ChatMessageInteractiveFileNode",
@@ -9,7 +14,7 @@ swift_library(
copts = [
"-warnings-as-errors",
],
deps = [
deps = sgdeps + [
"//submodules/AsyncDisplayKit",
"//submodules/Postbox",
"//submodules/SSignalKit/SwiftSignalKit",
@@ -1,3 +1,4 @@
import SGSimpleSettings
import Foundation
import UIKit
import AsyncDisplayKit
@@ -349,16 +350,53 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode {
}
private func transcribe() {
guard let _ = self.arguments, let context = self.context, let message = self.message else {
guard let arguments = self.arguments, let context = self.context, let message = self.message else {
return
}
if !context.isPremium, case .inProgress = self.audioTranscriptionState {
if /*!context.isPremium,*/ case .inProgress = self.audioTranscriptionState {
return
}
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
// GHOSTGRAM: Premium check removed - local transcription is free!
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: arguments.context.currentAppConfiguration.with { $0 })
let transcriptionText = self.forcedAudioTranscriptionText ?? transcribedText(message: message)
// MARK: Swiftgram
if transcriptionText == nil && false {
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
}
}
}
var shouldBeginTranscription = false
var shouldExpandNow = false
@@ -384,8 +422,7 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode {
self.audioTranscriptionState = .inProgress
self.requestUpdateLayout(true)
// GHOSTGRAM: Always use local transcription (free, private, on-device!)
if true {
if context.sharedContext.immediateExperimentalUISettings.localTranscription || !arguments.associatedData.isPremium || SGSimpleSettings.shared.transcriptionBackend == SGSimpleSettings.TranscriptionBackend.apple.rawValue {
let appLocale = presentationData.strings.baseLanguageCode
let signal: Signal<LocallyTranscribedAudio?, NoError> = context.engine.data.get(TelegramEngine.EngineData.Item.Messages.Message(id: message.id))
@@ -417,7 +454,8 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode {
guard let result = result else {
return .single(nil)
}
return transcribeAudio(path: result, appLocale: appLocale)
return transcribeAudio(path: result, appLocale: arguments.controllerInteraction.sgGetChatPredictedLang() ?? appLocale)
}
self.transcribeDisposable = (signal
@@ -605,6 +643,8 @@ 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 {
@@ -615,6 +655,7 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode {
consumableContentIcon = PresentationResourcesChat.chatBubbleConsumableContentOutgoingIcon(arguments.presentationData.theme.theme)
}
}
isConsumed = attribute.consumed
break
}
}
@@ -733,8 +774,25 @@ 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 {
// GHOSTGRAM: Always show transcribe button for voice messages
displayTranscribe = true
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: arguments.context.currentAppConfiguration.with { $0 })
// MARK: Swiftgram
if arguments.associatedData.isPremium || true {
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
}
}
let transcribedText = forcedAudioTranscriptionText ?? transcribedText(message: arguments.message)
@@ -749,7 +807,7 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode {
}
let currentTime = Int32(Date().timeIntervalSince1970)
if transcribedText == nil, let cooldownUntilTime = arguments.associatedData.audioTranscriptionTrial.cooldownUntilTime, cooldownUntilTime > currentTime {
if transcribedText == nil, let cooldownUntilTime = arguments.associatedData.audioTranscriptionTrial.cooldownUntilTime, cooldownUntilTime > currentTime, { return false }() /* MARK: Swiftgram */ {
updatedAudioTranscriptionState = .locked
}
@@ -899,6 +957,7 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode {
context: arguments.context,
presentationData: arguments.presentationData,
edited: edited && !arguments.presentationData.isPreview,
isDeleted: arguments.topMessage.ghostgramIsDeleted,
impressionCount: !arguments.presentationData.isPreview ? viewCount : nil,
dateText: dateText,
type: statusType,
@@ -2173,4 +2232,3 @@ public final class FileMessageSelectionNode: ASDisplayNode {
self.checkNode.frame = CGRect(origin: checkOrigin, size: checkSize)
}
}
@@ -577,6 +577,7 @@ public class ChatMessageInteractiveInstantVideoNode: ASDisplayNode {
context: item.context,
presentationData: item.presentationData,
edited: edited && !sentViaBot && !item.presentationData.isPreview,
isDeleted: item.topMessage.ghostgramIsDeleted,
impressionCount: !item.presentationData.isPreview ? viewCount : nil,
dateText: dateText,
type: statusType,
@@ -1,5 +1,9 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
sgdeps = [
"//Swiftgram/SGSimpleSettings:SGSimpleSettings"
]
swift_library(
name = "ChatMessageInteractiveMediaNode",
module_name = "ChatMessageInteractiveMediaNode",
@@ -9,7 +13,7 @@ swift_library(
copts = [
"-warnings-as-errors",
],
deps = [
deps = sgdeps + [
"//submodules/AsyncDisplayKit",
"//submodules/Postbox",
"//submodules/SSignalKit/SwiftSignalKit",
@@ -1,3 +1,4 @@
import SGSimpleSettings
import Foundation
import UIKit
import AsyncDisplayKit
@@ -947,6 +948,8 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr
var isSticker = false
var maxDimensions = layoutConstants.image.maxDimensions
var maxHeight = layoutConstants.image.maxDimensions.height
// MARK: Swiftgram
var imageOriginalMaxDimensions: CGSize?
var isStory = false
var isGift = false
@@ -969,6 +972,19 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr
}
} else if let image = media as? TelegramMediaImage, let dimensions = largestImageRepresentation(image.representations)?.dimensions {
unboundSize = CGSize(width: max(10.0, floor(dimensions.cgSize.width * 0.5)), height: max(10.0, floor(dimensions.cgSize.height * 0.5)))
// MARK: Swiftgram
if let channel = message.peers[message.id.peerId] as? TelegramChannel, case .broadcast = channel.info, SGSimpleSettings.shared.wideChannelPosts {
imageOriginalMaxDimensions = maxDimensions
switch sizeCalculation {
case let .constrained(constrainedSize):
maxDimensions.width = constrainedSize.width
case .unconstrained:
maxDimensions.width = unboundSize.width
}
if message.text.isEmpty {
maxDimensions.width = max(layoutConstants.image.maxDimensions.width, unboundSize.aspectFitted(CGSize(width: maxDimensions.width, height: layoutConstants.image.minDimensions.height)).width)
}
}
} else if let file = media as? TelegramMediaFile, var dimensions = file.dimensions {
if let thumbnail = file.previewRepresentations.first {
let dimensionsVertical = dimensions.width < dimensions.height
@@ -1112,6 +1128,7 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr
context: context,
presentationData: presentationData,
edited: dateAndStatus.edited && !presentationData.isPreview,
isDeleted: message.ghostgramIsDeleted,
impressionCount: !presentationData.isPreview ? dateAndStatus.viewCount : nil,
dateText: dateAndStatus.dateText,
type: dateAndStatus.type,
@@ -1196,6 +1213,9 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr
}
boundingSize = CGSize(width: boundingWidth, height: filledSize.height).cropped(CGSize(width: CGFloat.greatestFiniteMagnitude, height: maxHeight))
if let imageOriginalMaxDimensions = imageOriginalMaxDimensions {
boundingSize.height = min(boundingSize.height, nativeSize.aspectFitted(imageOriginalMaxDimensions).height)
}
boundingSize.height = max(boundingSize.height, layoutConstants.image.minDimensions.height)
boundingSize.width = max(boundingSize.width, layoutConstants.image.minDimensions.width)
switch contentMode {
@@ -2992,6 +3012,7 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr
icon = .eye
}
}
if displaySpoiler, let context = self.context {
let extendedMediaOverlayNode: ExtendedMediaOverlayNode
@@ -3272,6 +3293,10 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr
}
public func scrubberTransition() -> GalleryItemScrubberTransition? {
if "".isEmpty {
return nil
}
final class TimestampContainerTransitionView: UIView {
let containerView: UIView
let containerMaskView: UIImageView
@@ -1,5 +1,10 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
sgdeps = [
"//Swiftgram/SGSimpleSettings:SGSimpleSettings",
"//submodules/TranslateUI:TranslateUI"
]
swift_library(
name = "ChatMessageItemImpl",
module_name = "ChatMessageItemImpl",
@@ -9,7 +14,7 @@ swift_library(
copts = [
"-warnings-as-errors",
],
deps = [
deps = sgdeps + [
"//submodules/Postbox",
"//submodules/AsyncDisplayKit",
"//submodules/Display",
@@ -1,3 +1,5 @@
import SGSimpleSettings
import TranslateUI
import Foundation
import UIKit
import Postbox
@@ -327,14 +329,14 @@ public final class ChatMessageItemImpl: ChatMessageItem, CustomStringConvertible
}
displayAuthorInfo = incoming && peerId.isGroupOrChannel && effectiveAuthor != nil
if let channel = content.firstMessage.peers[content.firstMessage.id.peerId] as? TelegramChannel, channel.isForumOrMonoForum {
if let chatPeer = content.firstMessage.peers[content.firstMessage.id.peerId], chatPeer.isForumOrMonoForum {
if case .replyThread = chatLocation {
if channel.isMonoForum && chatLocation.threadId != context.account.peerId.toInt64() {
if chatPeer.isMonoForum && chatLocation.threadId != context.account.peerId.toInt64() {
displayAuthorInfo = false
}
} else {
if channel.isMonoForum {
if let linkedMonoforumId = channel.linkedMonoforumId, let mainChannel = content.firstMessage.peers[linkedMonoforumId] as? TelegramChannel, mainChannel.hasPermission(.manageDirect) {
if chatPeer.isMonoForum {
if let chatPeer = chatPeer as? TelegramChannel, let linkedMonoforumId = chatPeer.linkedMonoforumId, let mainChannel = content.firstMessage.peers[linkedMonoforumId] as? TelegramChannel, mainChannel.hasPermission(.manageDirect) {
headerSeparableThreadId = content.firstMessage.threadId
if let threadId = content.firstMessage.threadId, let peer = content.firstMessage.peers[EnginePeer.Id(threadId)] {
@@ -533,6 +535,17 @@ public final class ChatMessageItemImpl: ChatMessageItem, CustomStringConvertible
let configure = {
let node = (viewClassName as! ChatMessageItemView.Type).init(rotated: self.controllerInteraction.chatIsRotated)
if let node = node as? ChatMessageStickerItemNode {
node.sizeCoefficient = Float(SGSimpleSettings.shared.stickerSize) / 100.0
if !SGSimpleSettings.shared.stickerTimestamp {
node.dateAndStatusNode.isHidden = true
}
} else if let node = node as? ChatMessageAnimatedStickerItemNode {
node.sizeCoefficient = Float(SGSimpleSettings.shared.stickerSize) / 100.0
if !SGSimpleSettings.shared.stickerTimestamp {
node.dateAndStatusNode.isHidden = true
}
}
node.setupItem(self, synchronousLoad: synchronousLoads)
let nodeLayout = node.asyncLayout()
@@ -1,3 +1,4 @@
import SGSimpleSettings
import Foundation
import UIKit
import AsyncDisplayKit
@@ -663,6 +664,9 @@ open class ChatMessageItemView: ListViewItemNode, ChatMessageItemNodeProtocol {
public var playedEffectAnimation: Bool = false
public var effectAnimationNodes: [ChatMessageTransitionNode.DecorationItemNode] = []
private var wasFilteredKeywordTested: Bool = false
private var matchedFilterKeyword: String? = nil
public required init(rotated: Bool) {
super.init(layerBacked: false, rotated: rotated)
if rotated {
@@ -683,10 +687,23 @@ open class ChatMessageItemView: ListViewItemNode, ChatMessageItemNodeProtocol {
self.item = nil
self.frame = CGRect()
self.wasFilteredKeywordTested = false
self.matchedFilterKeyword = nil
}
open func setupItem(_ item: ChatMessageItem, synchronousLoad: Bool) {
self.item = item
if !self.wasFilteredKeywordTested && !SGSimpleSettings.shared.messageFilterKeywords.isEmpty && SGSimpleSettings.shared.ephemeralStatus > 1 {
let incomingMessage = item.message.effectivelyIncoming(item.context.account.peerId)
if incomingMessage {
if let matchedKeyword = SGSimpleSettings.shared.messageFilterKeywords.first(where: { item.message.text.contains($0) }) {
self.matchedFilterKeyword = matchedKeyword
self.alpha = item.presentationData.theme.theme.overallDarkAppearance ? 0.2 : 0.3
}
}
}
self.wasFilteredKeywordTested = true
}
open func updateAccessibilityData(_ accessibilityData: ChatMessageAccessibilityData) {
@@ -274,6 +274,7 @@ public class ChatMessageMapBubbleContentNode: ChatMessageBubbleContentNode {
context: item.context,
presentationData: item.presentationData,
edited: edited,
isDeleted: item.topMessage.ghostgramIsDeleted,
impressionCount: !item.presentationData.isPreview ? viewCount : nil,
dateText: dateText,
type: statusType,
@@ -1117,6 +1117,7 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
context: item.context,
presentationData: item.presentationData,
edited: edited,
isDeleted: item.topMessage.ghostgramIsDeleted,
impressionCount: viewCount,
dateText: dateText,
type: statusType,
@@ -130,6 +130,7 @@ public class ChatMessageRestrictedBubbleContentNode: ChatMessageBubbleContentNod
context: item.context,
presentationData: item.presentationData,
edited: edited,
isDeleted: message.ghostgramIsDeleted,
impressionCount: viewCount,
dateText: dateText,
type: statusType,
@@ -321,7 +321,7 @@ public final class ChatMessageSelectionInputPanelNode: ChatInputPanelNode {
if let actions = self.actions, actions.isCopyProtected {
self.interfaceInteraction?.displayCopyProtectionTip(self.forwardButton, false)
} else if !self.forwardButton.isImplicitlyDisabled {
self.interfaceInteraction?.forwardSelectedMessages()
self.interfaceInteraction?.forwardSelectedMessages(nil)
}
}
@@ -38,6 +38,7 @@ public class ChatMessageStickerItemNode: ChatMessageItemView {
public let contextSourceNode: ContextExtractedContentContainingNode
private let containerNode: ContextControllerSourceNode
public let imageNode: TransformImageNode
public var sizeCoefficient: Float = 1.0
private var backgroundNode: WallpaperBubbleBackgroundNode?
private var placeholderNode: StickerShimmerEffectNode
public var textNode: TextNode?
@@ -55,7 +56,7 @@ public class ChatMessageStickerItemNode: ChatMessageItemView {
private var suggestedPostInfoNode: ChatMessageSuggestedPostInfoNode?
private var viaBotNode: TextNode?
private let dateAndStatusNode: ChatMessageDateAndStatusNode
public let dateAndStatusNode: ChatMessageDateAndStatusNode
private var threadInfoNode: ChatMessageThreadInfoNode?
private var replyInfoNode: ChatMessageReplyInfoNode?
private var replyBackgroundContent: WallpaperBubbleBackgroundNode?
@@ -654,6 +655,7 @@ public class ChatMessageStickerItemNode: ChatMessageItemView {
context: item.context,
presentationData: item.presentationData,
edited: edited,
isDeleted: item.message.ghostgramIsDeleted,
impressionCount: viewCount,
dateText: dateText,
type: statusType,
@@ -897,11 +899,11 @@ public class ChatMessageStickerItemNode: ChatMessageItemView {
ReplyMarkupMessageAttribute(
rows: [
ReplyMarkupRow(buttons: [
ReplyMarkupButton(title: item.presentationData.strings.Chat_PostApproval_Message_ActionReject, titleWhenForwarded: nil, action: .callback(requiresPassword: false, data: buttonDecline)),
ReplyMarkupButton(title: item.presentationData.strings.Chat_PostApproval_Message_ActionApprove, titleWhenForwarded: nil, action: .callback(requiresPassword: false, data: buttonApprove))
ReplyMarkupButton(title: item.presentationData.strings.Chat_PostApproval_Message_ActionReject, titleWhenForwarded: nil, action: .callback(requiresPassword: false, data: buttonDecline), style: nil),
ReplyMarkupButton(title: item.presentationData.strings.Chat_PostApproval_Message_ActionApprove, titleWhenForwarded: nil, action: .callback(requiresPassword: false, data: buttonApprove), style: nil)
]),
ReplyMarkupRow(buttons: [
ReplyMarkupButton(title: item.presentationData.strings.Chat_PostApproval_Message_ActionSuggestChanges, titleWhenForwarded: nil, action: .callback(requiresPassword: false, data: buttonSuggestChanges))
ReplyMarkupButton(title: item.presentationData.strings.Chat_PostApproval_Message_ActionSuggestChanges, titleWhenForwarded: nil, action: .callback(requiresPassword: false, data: buttonSuggestChanges), style: nil)
])
],
flags: [],
@@ -725,6 +725,7 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
context: item.context,
presentationData: item.presentationData,
edited: edited && !item.presentationData.isPreview,
isDeleted: item.topMessage.ghostgramIsDeleted,
impressionCount: !item.presentationData.isPreview ? viewCount : nil,
dateText: dateText,
type: statusType,
@@ -742,7 +743,6 @@ 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
@@ -1256,7 +1256,7 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
return super.hitTest(point, with: event)
}
private func updateIsTranslating(_ isTranslating: Bool) {
public func updateIsTranslating(_ isTranslating: Bool) {
guard let item = self.item else {
return
}
@@ -1173,6 +1173,7 @@ public class ChatMessageTodoBubbleContentNode: ChatMessageBubbleContentNode {
context: item.context,
presentationData: item.presentationData,
edited: edited,
isDeleted: item.topMessage.ghostgramIsDeleted,
impressionCount: viewCount,
dateText: dateText,
type: statusType,
@@ -65,9 +65,9 @@ public final class ChatRecentActionsController: TelegramBaseController {
}, blockMessageAuthor: { _, _ in
}, deleteMessages: { _, _, f in
f(.default)
}, forwardSelectedMessages: {
}, forwardSelectedMessages: { _ in
}, forwardCurrentForwardMessages: {
}, forwardMessages: { _ in
}, forwardMessages: { _, _ in
}, updateForwardOptionsState: { _ in
}, presentForwardOptions: { _ in
}, presentReplyOptions: { _ in
@@ -1465,6 +1465,18 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode {
break
case .sendGift:
break
case .unknownDeepLink:
break
case .oauth:
break
case .chats:
break
case .compose:
break
case .postStory:
break
case .contacts:
break
}
}
}))
@@ -19,6 +19,14 @@ import GlassBackgroundComponent
import ComponentDisplayAdapters
import StarsParticleEffect
private func neutralActionButtonGlassTint(theme: PresentationTheme) -> GlassBackgroundView.TintColor {
if theme.overallDarkAppearance {
return .init(kind: .custom, color: UIColor(white: 0.0, alpha: 0.38))
} else {
return .init(kind: .custom, color: UIColor(white: 1.0, alpha: 0.68))
}
}
private final class EffectBadgeView: UIView {
private let context: AccountContext
private var currentEffectId: Int64?
@@ -345,7 +353,7 @@ public final class ChatTextInputActionButtonsNode: ASDisplayNode, ChatSendMessag
}
transition.updateFrame(view: self.micButtonBackgroundView, frame: CGRect(origin: CGPoint(), size: size))
self.micButtonBackgroundView.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))
self.micButtonBackgroundView.update(size: size, cornerRadius: size.height * 0.5, isDark: interfaceState.theme.overallDarkAppearance, tintColor: neutralActionButtonGlassTint(theme: interfaceState.theme), isInteractive: true, transition: ComponentTransition(transition))
transition.updateFrame(layer: self.micButton.layer, frame: CGRect(origin: CGPoint(), size: size))
self.micButton.layoutItems()
@@ -401,7 +409,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)), isInteractive: true, transition: ComponentTransition(transition))
self.expandMediaInputButtonBackgroundView.update(size: size, cornerRadius: size.height * 0.5, isDark: interfaceState.theme.overallDarkAppearance, tintColor: neutralActionButtonGlassTint(theme: interfaceState.theme), 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
@@ -418,11 +418,11 @@ public final class ChatTextInputPanelComponent: Component {
deleteMessages: { _, _, f in
f(.default)
},
forwardSelectedMessages: {
forwardSelectedMessages: { _ in
},
forwardCurrentForwardMessages: {
},
forwardMessages: { _ in
forwardMessages: { _, _ in
},
updateForwardOptionsState: { _ in
},
@@ -472,6 +472,7 @@ public final class ChatTextInputPanelComponent: Component {
var presentationInterfaceState = ChatPresentationInterfaceState(
chatWallpaper: .color(0),
theme: component.theme,
preferredGlassType: .default,
strings: component.strings,
dateTimeFormat: PresentationDateTimeFormat(),
nameDisplayOrder: .firstLast,
@@ -773,6 +774,7 @@ public final class ChatTextInputPanelComponent: Component {
var presentationInterfaceState = ChatPresentationInterfaceState(
chatWallpaper: .color(0),
theme: component.theme,
preferredGlassType: .default,
strings: component.strings,
dateTimeFormat: PresentationDateTimeFormat(),
nameDisplayOrder: .firstLast,
@@ -1,3 +1,9 @@
// MARK: Swiftgram
import TelegramUIPreferences
import SGSimpleSettings
import SwiftUI
import SGInputToolbar
import Foundation
import UniformTypeIdentifiers
import UIKit
@@ -65,6 +71,14 @@ public let chatTextInputMinFontSize: CGFloat = 5.0
private let minInputFontSize = chatTextInputMinFontSize
private func neutralChatInputGlassTint(theme: PresentationTheme, innerColor: UIColor? = nil) -> GlassBackgroundView.TintColor {
if theme.overallDarkAppearance {
return .init(kind: .custom, color: UIColor(white: 0.0, alpha: 0.38), innerColor: innerColor)
} else {
return .init(kind: .custom, color: UIColor(white: 1.0, alpha: 0.68), innerColor: innerColor)
}
}
private func calclulateTextFieldMinHeight(_ presentationInterfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics) -> CGFloat {
var baseFontSize = max(minInputFontSize, presentationInterfaceState.fontSize.baseDisplaySize)
if "".isEmpty {
@@ -333,6 +347,12 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
private let hapticFeedback = HapticFeedback()
// MARK: Swiftgram
private var sendWithReturnKey: Bool
private var sendWithReturnKeyDisposable: Disposable?
// private var toolbarHostingController: UIViewController? //Any? // UIHostingController<ChatToolbarView>?
private var toolbarNode: ASDisplayNode?
public var inputTextState: ChatTextInputState {
if let textInputNode = self.textInputNode {
let selectionRange: Range<Int> = textInputNode.selectedRange.location ..< (textInputNode.selectedRange.location + textInputNode.selectedRange.length)
@@ -619,6 +639,11 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
self.textInputViewInternalInsets = UIEdgeInsets(top: 5.0, left: 12.0, bottom: 4.0, right: 11.0)
// MARK: Swiftgram
self.sendWithReturnKey = SGSimpleSettings.shared.sendWithReturnKey // MARK: Swiftgram
//
var hasSpoilers = true
var hasQuotes = true
if presentationInterfaceState.chatLocation.peerId?.namespace == Namespaces.Peer.SecretChat {
@@ -755,7 +780,11 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
if let data = context.currentAppConfiguration.with({ $0 }).data, data["ios_killswitch_input_bounce"] != nil {
self.enableBounceAnimations = false
}*/
// MARK: Swiftgram
self.initToolbarIfNeeded(context: context)
//
self.sendAsAvatarContainerNode.activated = { [weak self] gesture, _ in
guard let strongSelf = self else {
return
@@ -818,6 +847,10 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
}
}
self.attachmentButtonDisabledNode.addTarget(self, action: #selector(self.attachmentButtonPressed), forControlEvents: .touchUpInside)
// MARK: Swiftgram
let longPressGesture = UILongPressGestureRecognizer(target: self, action: #selector(self.attachmentButtonLongPressed(_:)))
longPressGesture.minimumPressDuration = 1.0
self.attachmentButton.addGestureRecognizer(longPressGesture)
self.sendActionButtons.sendButtonLongPressed = { [weak self] node, gesture in
self?.interfaceInteraction?.displaySendMessageOptions(node, gesture)
@@ -1013,6 +1046,7 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
deinit {
self.statusDisposable.dispose()
self.sendWithReturnKeyDisposable?.dispose()
self.tooltipController?.dismiss()
self.currentEmojiSuggestion?.disposable.dispose()
}
@@ -1060,6 +1094,7 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
self.textInputNodeClippingContainer.addSubnode(textInputNode)
textInputNode.view.disablesInteractiveTransitionGestureRecognizer = true
textInputNode.isUserInteractionEnabled = !self.sendingTextDisabled
textInputNode.textView.returnKeyType = self.sendWithReturnKey ? .send : .default
self.textInputNode = textInputNode
if let textInputBackgroundTapRecognizer = self.textInputBackgroundTapRecognizer {
@@ -1421,6 +1456,16 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
let previousAdditionalSideInsets = self.validLayout?.4
self.validLayout = (width, leftInset, rightInset, bottomInset, additionalSideInsets, maxHeight, maxOverlayHeight, metrics, isSecondary, isMediaInputExpanded)
let defaultGlassTintColor: GlassBackgroundView.TintColor
let defaultGlassTintWithInnerColor: GlassBackgroundView.TintColor
if case .clear = interfaceState.preferredGlassType {
defaultGlassTintColor = .init(kind: .custom, color: UIColor.clear)
defaultGlassTintWithInnerColor = .init(kind: .custom, color: UIColor.clear, innerColor: interfaceState.theme.list.itemCheckColors.fillColor)
} else {
defaultGlassTintColor = neutralChatInputGlassTint(theme: interfaceState.theme)
defaultGlassTintWithInnerColor = neutralChatInputGlassTint(theme: interfaceState.theme, innerColor: interfaceState.theme.list.itemCheckColors.fillColor)
}
var leftInset = leftInset
var rightInset = rightInset
@@ -2205,6 +2250,9 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
if buttonTitleUpdated && !transition.isAnimated {
transition = .animated(duration: 0.3, curve: .easeInOut)
}
// MARK: Swiftgram
let originalLeftInset = leftInset
//
let textInputBackgroundWidthOffset: CGFloat = 0.0
var attachmentButtonX: CGFloat = hideOffset.x + leftInset + leftMenuInset + 8.0
@@ -2365,7 +2413,7 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
let menuButtonFrame = CGRect(x: leftInset + 8.0, y: menuButtonOriginY, width: menuButtonExpanded ? menuButtonWidth : menuCollapsedButtonWidth, height: menuButtonHeight)
transition.updateFrameAsPositionAndBounds(node: self.menuButton, frame: menuButtonFrame)
transition.updateFrame(view: self.menuButtonBackgroundView, frame: CGRect(origin: CGPoint(), size: menuButtonFrame.size))
self.menuButtonBackgroundView.update(size: menuButtonFrame.size, cornerRadius: menuButtonFrame.height * 0.5, isDark: interfaceState.theme.overallDarkAppearance, tintColor: .init(kind: .panel, color: interfaceState.theme.chat.inputPanel.inputBackgroundColor.withMultipliedAlpha(0.7), innerColor: interfaceState.theme.chat.inputPanel.actionControlFillColor), transition: ComponentTransition(transition))
self.menuButtonBackgroundView.update(size: menuButtonFrame.size, cornerRadius: menuButtonFrame.height * 0.5, isDark: interfaceState.theme.overallDarkAppearance, tintColor: defaultGlassTintWithInnerColor, transition: ComponentTransition(transition))
transition.updateFrame(node: self.menuButtonClippingNode, frame: CGRect(origin: CGPoint(x: 19.0, y: 0.0), size: CGSize(width: menuButtonWidth - 19.0, height: menuButtonFrame.height)))
var menuButtonTitleTransition = transition
if buttonTitleUpdated {
@@ -2385,7 +2433,7 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
if additionalSideInsets.right > 0.0 {
textFieldInsets.right += additionalSideInsets.right / 3.0
}
if inputHasText || self.extendedSearchLayout || hasMediaDraft || hasForward {
if SGSimpleSettings.shared.hideRecordingButton || inputHasText || self.extendedSearchLayout || hasMediaDraft || hasForward {
} else {
if let customRightAction = self.customRightAction, case .empty = customRightAction {
textFieldInsets.right = 8.0
@@ -2513,7 +2561,9 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
case let .audio(recorder, isLocked):
let hadAudioRecorder = self.mediaActionButtons.micButton.audioRecorder != nil
if !hadAudioRecorder, isLocked {
self.mediaActionButtons.micButton.lock()
DispatchQueue.main.async { [weak self] in
self?.mediaActionButtons.micButton.lock()
}
}
self.mediaActionButtons.micButton.audioRecorder = recorder
audioRecordingTimeNode.audioRecorder = recorder
@@ -2887,7 +2937,7 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
self.updateCounterTextNode(backgroundSize: textInputContainerBackgroundFrame.size, transition: transition)
let textInputContainerBackgroundTransition = ComponentTransition(transition)
self.textInputContainerBackgroundView.update(size: textInputContainerBackgroundFrame.size, cornerRadius: floor(minimalInputHeight * 0.5), isDark: interfaceState.theme.overallDarkAppearance, tintColor: .init(kind: .panel, color: interfaceState.theme.chat.inputPanel.inputBackgroundColor.withMultipliedAlpha(0.7)), isInteractive: true, transition: textInputContainerBackgroundTransition)
self.textInputContainerBackgroundView.update(size: textInputContainerBackgroundFrame.size, cornerRadius: floor(minimalInputHeight * 0.5), isDark: interfaceState.theme.overallDarkAppearance, tintColor: defaultGlassTintColor, isInteractive: true, transition: textInputContainerBackgroundTransition)
transition.updateFrame(layer: self.textInputBackgroundNode.layer, frame: textInputContainerBackgroundFrame)
transition.updateAlpha(node: self.textInputBackgroundNode, alpha: audioRecordingItemsAlpha)
@@ -3277,7 +3327,7 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
let attachmentButtonFrame = CGRect(origin: CGPoint(x: attachmentButtonX, y: textInputFrame.maxY - 40.0), size: CGSize(width: 40.0, height: 40.0))
attachmentButtonX += 40.0 + 6.0
self.attachmentButtonBackground.update(size: attachmentButtonFrame.size, cornerRadius: attachmentButtonFrame.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))
self.attachmentButtonBackground.update(size: attachmentButtonFrame.size, cornerRadius: attachmentButtonFrame.height * 0.5, isDark: interfaceState.theme.overallDarkAppearance, tintColor: defaultGlassTintColor, isInteractive: true, transition: ComponentTransition(transition))
transition.updateFrame(layer: self.attachmentButtonBackground.layer, frame: attachmentButtonFrame)
transition.updateFrame(layer: self.attachmentButton.layer, frame: CGRect(origin: CGPoint(), size: attachmentButtonFrame.size))
@@ -3505,7 +3555,11 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
transition.updateFrame(view: self.glassBackgroundContainer, frame: containerFrame)
self.glassBackgroundContainer.update(size: containerFrame.size, isDark: interfaceState.theme.overallDarkAppearance, transition: ComponentTransition(transition))
return contentHeight
// MARK: Swiftgram
var toolbarOffset: CGFloat = 0.0
toolbarOffset = layoutToolbar(transition: transition, panelHeight: contentHeight, width: width, leftInset: originalLeftInset, rightInset: rightInset, displayBotStartButton: displayBotStartButton)
return contentHeight + toolbarOffset
}
@objc private func slowModeButtonPressed() {
@@ -4046,7 +4100,7 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
}
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let controller = PeekController(presentationData: presentationData, content: content, sourceView: {
let controller = makePeekController(presentationData: presentationData, content: content, sourceView: {
return (sourceView, sourceRect)
})
//strongSelf.peekController = controller
@@ -4318,8 +4372,8 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
let blurTransitionOut: ComponentTransition = transition.isAnimated ? .easeInOut(duration: 0.18) : .immediate
let sendButtonBlurOut: CGFloat = 4.0
var hideMicButton = false
var hideMicButtonBackground = false
var hideMicButton = SGSimpleSettings.shared.hideRecordingButton
var hideMicButtonBackground = SGSimpleSettings.shared.hideRecordingButton
if self.customRightAction != nil {
self.mediaActionButtons.isHidden = true
@@ -4397,7 +4451,7 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
}
}
if (hasText || keepSendButtonEnabled && !mediaInputIsActive && !hasSlowModeButton) {
if (SGSimpleSettings.shared.hideRecordingButton || hasText || keepSendButtonEnabled && !mediaInputIsActive && !hasSlowModeButton) {
if self.sendActionButtons.sendContainerNode.alpha.isZero && self.rightSlowModeInset.isZero {
alphaTransition.updateAlpha(node: self.sendActionButtons.sendContainerNode, alpha: 1.0)
blurTransitionIn.animateBlur(layer: self.sendActionButtons.sendContainerNode.layer, fromRadius: sendButtonBlurOut, toRadius: 0.0)
@@ -4522,14 +4576,32 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
}
public func chatInputTextNodeShouldReturn() -> Bool {
return self.chatInputTextNodeShouldReturn(modifierFlags: [])
}
public func chatInputTextNodeShouldReturn(modifierFlags: UIKeyModifierFlags) -> Bool {
var shouldSendMessage = false
if self.sendActionButtons.sendButton.supernode != nil && !self.sendActionButtons.sendButton.isHidden && !self.sendActionButtons.sendContainerNode.alpha.isZero {
self.sendButtonPressed()
if let context = self.context, context.sharedContext.currentChatSettings.with({ $0 }).sendWithCmdEnter {
if modifierFlags.contains(.command) {
shouldSendMessage = true
}
} else {
if modifierFlags.isEmpty {
shouldSendMessage = true
}
}
}
return false
if shouldSendMessage {
self.sendButtonPressed()
return false
}
return true
}
@objc public func editableTextNodeShouldReturn(_ editableTextNode: ASEditableTextNode) -> Bool {
return self.chatInputTextNodeShouldReturn()
return self.chatInputTextNodeShouldReturn(modifierFlags: [])
}
private func applyUpdateSendButtonIcon() {
@@ -4943,6 +5015,13 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
}
self.updateActivity()
// MARK: Swiftgram
if self.sendWithReturnKey && text == "\n" {
self.sendButtonPressed()
return false
}
var cleanText = text
let removeSequences: [String] = ["\u{202d}", "\u{202c}"]
for sequence in removeSequences {
@@ -5169,6 +5248,11 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
}
}
// MARK: Swiftgram
@objc func attachmentButtonLongPressed(_ gesture: UILongPressGestureRecognizer) {
guard gesture.state == .began else { return }
}
@objc func searchLayoutClearButtonPressed() {
if let interfaceInteraction = self.interfaceInteraction {
interfaceInteraction.updateTextInputStateAndMode { textInputState, inputMode in
@@ -5477,3 +5561,114 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
return AttachmentInputPanelTransition(inputNode: self, accessoryPanelNode: accessoryPanelNode, menuButtonNode: self.menuButton, menuButtonBackgroundView: self.menuButtonBackgroundView, menuIconNode: self.menuButtonIconNode, menuTextNode: self.menuButtonTextNode, prepareForDismiss: { self.menuButtonIconNode.enqueueState(.app, animated: false) })
}
}
// MARK: Swiftgram
extension ChatTextInputPanelNode {
func initToolbarIfNeeded(context: AccountContext) {
guard #available(iOS 13.0, *) else { return }
guard SGSimpleSettings.shared.inputToolbar else { return }
guard context.sharedContext.immediateSGStatus.status > 1 else { return }
guard self.toolbarNode == nil else { return }
let toolbarView = ChatToolbarView(
onQuote: { [weak self] in
guard let strongSelf = self else { return }
strongSelf.interfaceInteraction?.sgSelectLastWordIfIdle()
strongSelf.formatAttributesQuote(strongSelf)
},
onSpoiler: { [weak self] in
guard let strongSelf = self else { return }
strongSelf.interfaceInteraction?.sgSelectLastWordIfIdle()
strongSelf.formatAttributesSpoiler(strongSelf)
},
onBold: { [weak self] in
guard let strongSelf = self else { return }
strongSelf.interfaceInteraction?.sgSelectLastWordIfIdle()
strongSelf.formatAttributesBold(strongSelf)
},
onItalic: { [weak self] in
guard let strongSelf = self else { return }
strongSelf.interfaceInteraction?.sgSelectLastWordIfIdle()
strongSelf.formatAttributesItalic(strongSelf)
},
onMonospace: { [weak self] in
guard let strongSelf = self else { return }
strongSelf.interfaceInteraction?.sgSelectLastWordIfIdle()
strongSelf.formatAttributesMonospace(strongSelf)
},
onLink: { [weak self] in
guard let strongSelf = self else { return }
strongSelf.interfaceInteraction?.sgSelectLastWordIfIdle()
strongSelf.formatAttributesLink(strongSelf)
},
onStrikethrough: { [weak self]
in guard let strongSelf = self else { return }
strongSelf.interfaceInteraction?.sgSelectLastWordIfIdle()
strongSelf.formatAttributesStrikethrough(strongSelf)
},
onUnderline: { [weak self] in
guard let strongSelf = self else { return }
strongSelf.interfaceInteraction?.sgSelectLastWordIfIdle()
strongSelf.formatAttributesUnderline(strongSelf)
},
onCode: { [weak self] in
guard let strongSelf = self else { return }
strongSelf.interfaceInteraction?.sgSelectLastWordIfIdle()
strongSelf.formatAttributesCodeBlock(strongSelf)
},
onNewLine: { [weak self] in
guard let strongSelf = self else { return }
strongSelf.interfaceInteraction?.sgSetNewLine()
},
// TODO(swiftgram): Binding
showNewLine: .constant(true), //.constant(self.sendWithReturnKey)
onClearFormatting: { [weak self] in
guard let strongSelf = self else { return }
strongSelf.interfaceInteraction?.updateTextInputStateAndMode { current, inputMode in
return (chatTextInputClearFormattingAttributes(current), inputMode)
}
}
)
let toolbarHostingController = UIHostingController(rootView: toolbarView)
toolbarHostingController.view.backgroundColor = UIColor.clear
let toolbarNode = ASDisplayNode { toolbarHostingController.view }
self.toolbarNode = toolbarNode
// assigning toolbarHostingController bugs responsivness and overrides layout
// self.toolbarHostingController = toolbarHostingController
// Disable "Swipe to go back" gesture when touching scrollview
self.view.interactiveTransitionGestureRecognizerTest = { [weak self] point in
if let self, let _ = self.toolbarNode?.view.hitTest(point, with: nil) {
return false
}
return true
}
self.addSubnode(toolbarNode)
}
func layoutToolbar(transition: ContainedViewLayoutTransition, panelHeight: CGFloat, width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, displayBotStartButton: Bool) -> CGFloat {
var toolbarHeight: CGFloat = 0.0
var toolbarSpacing: CGFloat = 0.0
if let toolbarNode = self.toolbarNode {
if displayBotStartButton {
toolbarNode.view.alpha = 0.0
// transition.updateAlpha(node: toolbarNode, alpha: 0.0)
/*} else if !self.isFocused {
transition.updateAlpha(node: toolbarNode, alpha: 0.0, completion: { _ in
toolbarNode.isHidden = true
})*/
} else {
if !self.isFocused {
transition.updateAlpha(node: toolbarNode, alpha: 0.0)
} else {
toolbarHeight = 44.0
toolbarSpacing = 6.0
transition.updateFrame(node: toolbarNode, frame: CGRect(origin: CGPoint(x: leftInset, y: panelHeight + toolbarSpacing), size: CGSize(width: width - rightInset - leftInset, height: toolbarHeight)))
transition.updateAlpha(node: toolbarNode, alpha: 1.0)
}
}
}
return toolbarHeight + toolbarSpacing
}
}
@@ -6,6 +6,14 @@ import ComponentFlow
import GlassBackgroundComponent
import AppBundle
private func neutralInputButtonGlassTint(theme: PresentationTheme) -> GlassBackgroundView.TintColor {
if theme.overallDarkAppearance {
return .init(kind: .custom, color: UIColor(white: 0.0, alpha: 0.38))
} else {
return .init(kind: .custom, color: UIColor(white: 1.0, alpha: 0.68))
}
}
final class InputIconButtonComponent: Component {
let theme: PresentationTheme
let name: String
@@ -87,7 +95,7 @@ final class InputIconButtonComponent: Component {
}
transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(), size: size))
self.backgroundView.update(size: size, cornerRadius: size.height * 0.5, isDark: component.theme.overallDarkAppearance, tintColor: .init(kind: .panel, color: component.theme.chat.inputPanel.inputBackgroundColor.withMultipliedAlpha(0.7)), isInteractive: true, transition: transition)
self.backgroundView.update(size: size, cornerRadius: size.height * 0.5, isDark: component.theme.overallDarkAppearance, tintColor: neutralInputButtonGlassTint(theme: component.theme), isInteractive: true, transition: transition)
self.button.frame = CGRect(origin: CGPoint(), size: size)
@@ -7,6 +7,14 @@ import GlassBackgroundComponent
import AnimatedTextComponent
import StarsParticleEffect
private func neutralStarReactionGlassTint(theme: PresentationTheme) -> GlassBackgroundView.TintColor {
if theme.overallDarkAppearance {
return .init(kind: .custom, color: UIColor(white: 0.0, alpha: 0.38))
} else {
return .init(kind: .custom, color: UIColor(white: 1.0, alpha: 0.68))
}
}
final class StarReactionButtonBadgeComponent: Component {
let theme: PresentationTheme
let count: Int
@@ -80,7 +88,7 @@ final class StarReactionButtonBadgeComponent: Component {
if component.isFilled {
backgroundTintColor = .init(kind: .custom, color: UIColor(rgb: 0xFFB10D))
} else {
backgroundTintColor = .init(kind: .panel, color: component.theme.chat.inputPanel.inputBackgroundColor.withMultipliedAlpha(0.7))
backgroundTintColor = neutralStarReactionGlassTint(theme: component.theme)
}
self.backgroundView.update(size: backgroundFrame.size, cornerRadius: backgroundFrame.height * 0.5, isDark: component.theme.overallDarkAppearance, tintColor: backgroundTintColor, isInteractive: true, transition: transition)
@@ -338,7 +346,7 @@ final class StarReactionButtonComponent: Component {
if component.isFilled {
backgroundTintColor = .init(kind: .custom, color: UIColor(rgb: 0xFFB10D))
} else {
backgroundTintColor = .init(kind: .panel, color: component.theme.chat.inputPanel.inputBackgroundColor.withMultipliedAlpha(0.7))
backgroundTintColor = neutralStarReactionGlassTint(theme: component.theme)
}
self.backgroundView.update(size: backgroundFrame.size, cornerRadius: backgroundFrame.height * 0.5, isDark: component.theme.overallDarkAppearance, tintColor: backgroundTintColor, isInteractive: false, transition: transition)