Merge branch 'master' of gitlab.com:peter-iakovlev/telegram-ios

This commit is contained in:
Ilya Laktyushin 2025-06-25 12:01:28 +02:00
commit 98c3b67f96
18 changed files with 321 additions and 35 deletions

View File

@ -1265,7 +1265,7 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate {
}, addDoNotTranslateLanguage: { _ in }, addDoNotTranslateLanguage: { _ in
}, hideTranslationPanel: { }, hideTranslationPanel: {
}, openPremiumGift: { }, openPremiumGift: {
}, openSuggestPost: { _ in }, openSuggestPost: { _, _ in
}, openPremiumRequiredForMessaging: { }, openPremiumRequiredForMessaging: {
}, openStarsPurchase: { _ in }, openStarsPurchase: { _ in
}, openMessagePayment: { }, openMessagePayment: {

View File

@ -62,6 +62,13 @@ public enum ChatTranslationDisplayType {
} }
public final class ChatPanelInterfaceInteraction { public final class ChatPanelInterfaceInteraction {
public enum OpenSuggestPostMode {
case `default`
case editMessage
case editTime
case editPrice
}
public let setupReplyMessage: (MessageId?, @escaping (ContainedViewLayoutTransition, @escaping () -> Void) -> Void) -> Void public let setupReplyMessage: (MessageId?, @escaping (ContainedViewLayoutTransition, @escaping () -> Void) -> Void) -> Void
public let setupEditMessage: (MessageId?, @escaping (ContainedViewLayoutTransition) -> Void) -> Void public let setupEditMessage: (MessageId?, @escaping (ContainedViewLayoutTransition) -> Void) -> Void
public let beginMessageSelection: ([MessageId], @escaping (ContainedViewLayoutTransition) -> Void) -> Void public let beginMessageSelection: ([MessageId], @escaping (ContainedViewLayoutTransition) -> Void) -> Void
@ -168,7 +175,7 @@ public final class ChatPanelInterfaceInteraction {
public let addDoNotTranslateLanguage: (String) -> Void public let addDoNotTranslateLanguage: (String) -> Void
public let hideTranslationPanel: () -> Void public let hideTranslationPanel: () -> Void
public let openPremiumGift: () -> Void public let openPremiumGift: () -> Void
public let openSuggestPost: (Message?) -> Void public let openSuggestPost: (Message?, OpenSuggestPostMode) -> Void
public let openPremiumRequiredForMessaging: () -> Void public let openPremiumRequiredForMessaging: () -> Void
public let openStarsPurchase: (Int64?) -> Void public let openStarsPurchase: (Int64?) -> Void
public let openMessagePayment: () -> Void public let openMessagePayment: () -> Void
@ -291,7 +298,7 @@ public final class ChatPanelInterfaceInteraction {
addDoNotTranslateLanguage: @escaping (String) -> Void, addDoNotTranslateLanguage: @escaping (String) -> Void,
hideTranslationPanel: @escaping () -> Void, hideTranslationPanel: @escaping () -> Void,
openPremiumGift: @escaping () -> Void, openPremiumGift: @escaping () -> Void,
openSuggestPost: @escaping (Message?) -> Void, openSuggestPost: @escaping (Message?, OpenSuggestPostMode) -> Void,
openPremiumRequiredForMessaging: @escaping () -> Void, openPremiumRequiredForMessaging: @escaping () -> Void,
openStarsPurchase: @escaping (Int64?) -> Void, openStarsPurchase: @escaping (Int64?) -> Void,
openMessagePayment: @escaping () -> Void, openMessagePayment: @escaping () -> Void,
@ -544,7 +551,7 @@ public final class ChatPanelInterfaceInteraction {
}, addDoNotTranslateLanguage: { _ in }, addDoNotTranslateLanguage: { _ in
}, hideTranslationPanel: { }, hideTranslationPanel: {
}, openPremiumGift: { }, openPremiumGift: {
}, openSuggestPost: { _ in }, openSuggestPost: { _, _ in
}, openPremiumRequiredForMessaging: { }, openPremiumRequiredForMessaging: {
}, openStarsPurchase: { _ in }, openStarsPurchase: { _ in
}, openMessagePayment: { }, openMessagePayment: {

View File

@ -107,6 +107,7 @@ func _internal_reinstateNoPaidMessagesException(account: Account, scopePeerId: P
} }
var flags: Int32 = 0 var flags: Int32 = 0
flags |= (1 << 2) flags |= (1 << 2)
flags |= (1 << 1)
return account.network.request(Api.functions.account.toggleNoPaidMessagesException(flags: flags, parentPeer: scopeInputPeer, userId: inputUser)) return account.network.request(Api.functions.account.toggleNoPaidMessagesException(flags: flags, parentPeer: scopeInputPeer, userId: inputUser))
|> `catch` { _ -> Signal<Api.Bool, NoError> in |> `catch` { _ -> Signal<Api.Bool, NoError> in
return .single(.boolFalse) return .single(.boolFalse)

View File

@ -53,6 +53,7 @@ swift_library(
"//submodules/TelegramUI/Components/Chat/ManagedDiceAnimationNode", "//submodules/TelegramUI/Components/Chat/ManagedDiceAnimationNode",
"//submodules/TelegramUI/Components/Chat/MessageHaptics", "//submodules/TelegramUI/Components/Chat/MessageHaptics",
"//submodules/TelegramUI/Components/Chat/ChatMessageTransitionNode", "//submodules/TelegramUI/Components/Chat/ChatMessageTransitionNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageSuggestedPostInfoNode",
], ],
visibility = [ visibility = [
"//visibility:public", "//visibility:public",

View File

@ -46,6 +46,7 @@ import ChatMessageReactionsFooterContentNode
import ManagedDiceAnimationNode import ManagedDiceAnimationNode
import MessageHaptics import MessageHaptics
import ChatMessageTransitionNode import ChatMessageTransitionNode
import ChatMessageSuggestedPostInfoNode
private let nameFont = Font.medium(14.0) private let nameFont = Font.medium(14.0)
private let inlineBotPrefixFont = Font.regular(14.0) private let inlineBotPrefixFont = Font.regular(14.0)
@ -143,6 +144,8 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
private var fetchEffectDisposable: Disposable? private var fetchEffectDisposable: Disposable?
private var suggestedPostInfoNode: ChatMessageSuggestedPostInfoNode?
required public init(rotated: Bool) { required public init(rotated: Bool) {
self.contextSourceNode = ContextExtractedContentContainingNode() self.contextSourceNode = ContextExtractedContentContainingNode()
self.containerNode = ContextControllerSourceNode() self.containerNode = ContextControllerSourceNode()
@ -823,6 +826,8 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
let textLayout = TextNodeWithEntities.asyncLayout(self.textNode) let textLayout = TextNodeWithEntities.asyncLayout(self.textNode)
let makeSuggestedPostInfoNodeLayout: ChatMessageSuggestedPostInfoNode.AsyncLayout = ChatMessageSuggestedPostInfoNode.asyncLayout(self.suggestedPostInfoNode)
func continueAsyncLayout(_ weakSelf: Weak<ChatMessageAnimatedStickerItemNode>, _ item: ChatMessageItem, _ params: ListViewItemLayoutParams, _ mergedTop: ChatMessageMerge, _ mergedBottom: ChatMessageMerge, _ dateHeaderAtBottom: ChatMessageHeaderSpec) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation, ListViewItemApply, Bool) -> Void) { func continueAsyncLayout(_ weakSelf: Weak<ChatMessageAnimatedStickerItemNode>, _ item: ChatMessageItem, _ params: ListViewItemLayoutParams, _ mergedTop: ChatMessageMerge, _ mergedBottom: ChatMessageMerge, _ dateHeaderAtBottom: ChatMessageHeaderSpec) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation, ListViewItemApply, Bool) -> Void) {
let accessibilityData = ChatMessageAccessibilityData(item: item, isSelected: nil) let accessibilityData = ChatMessageAccessibilityData(item: item, isSelected: nil)
let layoutConstants = chatMessageItemLayoutConstants(layoutConstants, params: params, presentationData: item.presentationData) let layoutConstants = chatMessageItemLayoutConstants(layoutConstants, params: params, presentationData: item.presentationData)
@ -1030,7 +1035,7 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
var innerImageSize = imageSize var innerImageSize = imageSize
imageSize = CGSize(width: imageSize.width + imageInset * 2.0, height: imageSize.height + imageInset * 2.0) imageSize = CGSize(width: imageSize.width + imageInset * 2.0, height: imageSize.height + imageInset * 2.0)
let imageFrame = CGRect(origin: CGPoint(x: 0.0 + (incoming ? (params.leftInset + layoutConstants.bubble.edgeInset + avatarInset + layoutConstants.bubble.contentInsets.left) : (params.width - params.rightInset - imageSize.width - layoutConstants.bubble.edgeInset - layoutConstants.bubble.contentInsets.left - deliveryFailedInset - imageHorizontalOffset)), y: imageVerticalInset + imageTopPadding), size: CGSize(width: imageSize.width, height: imageSize.height)) var imageFrame = CGRect(origin: CGPoint(x: 0.0 + (incoming ? (params.leftInset + layoutConstants.bubble.edgeInset + avatarInset + layoutConstants.bubble.contentInsets.left) : (params.width - params.rightInset - imageSize.width - layoutConstants.bubble.edgeInset - layoutConstants.bubble.contentInsets.left - deliveryFailedInset - imageHorizontalOffset)), y: imageVerticalInset + imageTopPadding), size: CGSize(width: imageSize.width, height: imageSize.height))
if isEmoji { if isEmoji {
innerImageSize = imageSize innerImageSize = imageSize
} }
@ -1281,10 +1286,48 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
needsForwardBackground = true needsForwardBackground = true
} }
let baseWidth = params.width - params.leftInset - params.rightInset
var maxContentWidth = imageSize.width var maxContentWidth = imageSize.width
var actionButtonsFinalize: ((CGFloat) -> (CGSize, (_ animation: ListViewItemUpdateAnimation) -> ChatMessageActionButtonsNode))? var actionButtonsFinalize: ((CGFloat) -> (CGSize, (_ animation: ListViewItemUpdateAnimation) -> ChatMessageActionButtonsNode))?
if let replyMarkup = replyMarkup { if let replyMarkup = replyMarkup {
let (minWidth, buttonsLayout) = actionButtonsLayout(item.context, item.presentationData.theme, item.presentationData.chatBubbleCorners, item.presentationData.strings, item.controllerInteraction.presentationContext.backgroundNode, replyMarkup, [:], item.message, maxContentWidth) let (minWidth, buttonsLayout) = actionButtonsLayout(item.context, item.presentationData.theme, item.presentationData.chatBubbleCorners, item.presentationData.strings, item.controllerInteraction.presentationContext.backgroundNode, replyMarkup, [:], item.message, baseWidth)
maxContentWidth = max(maxContentWidth, minWidth)
actionButtonsFinalize = buttonsLayout
} else if incoming, let attribute = item.message.attributes.first(where: { $0 is SuggestedPostMessageAttribute }) as? SuggestedPostMessageAttribute, attribute.state == nil {
//TODO:localize
var buttonDeclineValue: UInt8 = 0
let buttonDecline = MemoryBuffer(data: Data(bytes: &buttonDeclineValue, count: 1))
var buttonApproveValue: UInt8 = 1
let buttonApprove = MemoryBuffer(data: Data(bytes: &buttonApproveValue, count: 1))
var buttonSuggestChangesValue: UInt8 = 2
let buttonSuggestChanges = MemoryBuffer(data: Data(bytes: &buttonSuggestChangesValue, count: 1))
let customIcons: [MemoryBuffer: ChatMessageActionButtonsNode.CustomIcon] = [
buttonDecline: .suggestedPostReject,
buttonApprove: .suggestedPostApprove,
buttonSuggestChanges: .suggestedPostEdit
]
let (minWidth, buttonsLayout) = actionButtonsLayout(
item.context,
item.presentationData.theme,
item.presentationData.chatBubbleCorners,
item.presentationData.strings,
item.controllerInteraction.presentationContext.backgroundNode,
ReplyMarkupMessageAttribute(
rows: [
ReplyMarkupRow(buttons: [
ReplyMarkupButton(title: "Decline", titleWhenForwarded: nil, action: .callback(requiresPassword: false, data: buttonDecline)),
ReplyMarkupButton(title: "Approve", titleWhenForwarded: nil, action: .callback(requiresPassword: false, data: buttonApprove))
]),
ReplyMarkupRow(buttons: [
ReplyMarkupButton(title: "Suggest Changes", titleWhenForwarded: nil, action: .callback(requiresPassword: false, data: buttonSuggestChanges))
])
],
flags: [],
placeholder: nil
), customIcons, item.message, baseWidth)
maxContentWidth = max(maxContentWidth, minWidth) maxContentWidth = max(maxContentWidth, minWidth)
actionButtonsFinalize = buttonsLayout actionButtonsFinalize = buttonsLayout
} }
@ -1336,6 +1379,21 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
layoutSize.height += 4.0 + reactionButtonsSizeAndApply.0.height layoutSize.height += 4.0 + reactionButtonsSizeAndApply.0.height
} }
var suggestedPostInfoNodeLayout: (CGSize, () -> ChatMessageSuggestedPostInfoNode)?
for attribute in item.message.attributes {
if let _ = attribute as? SuggestedPostMessageAttribute {
let suggestedPostInfoNodeLayoutValue = makeSuggestedPostInfoNodeLayout(item, baseWidth)
suggestedPostInfoNodeLayout = suggestedPostInfoNodeLayoutValue
}
}
var additionalTopHeight: CGFloat = 0.0
if let suggestedPostInfoNodeLayout {
additionalTopHeight += 4.0 + suggestedPostInfoNodeLayout.0.height + 8.0
}
layoutSize.height += additionalTopHeight
imageFrame.origin.y += additionalTopHeight
var headersOffset: CGFloat = 0.0 var headersOffset: CGFloat = 0.0
if let (threadInfoSize, _) = threadInfoApply { if let (threadInfoSize, _) = threadInfoApply {
headersOffset += threadInfoSize.height + 10.0 headersOffset += threadInfoSize.height + 10.0
@ -1390,6 +1448,20 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
strongSelf.updateAttachedDateHeader(hasDate: dateHeaderAtBottom.hasDate, hasPeer: dateHeaderAtBottom.hasTopic) strongSelf.updateAttachedDateHeader(hasDate: dateHeaderAtBottom.hasDate, hasPeer: dateHeaderAtBottom.hasTopic)
if let (suggestedPostInfoSize, suggestedPostInfoApply) = suggestedPostInfoNodeLayout {
let suggestedPostInfoNode = suggestedPostInfoApply()
if suggestedPostInfoNode !== strongSelf.suggestedPostInfoNode {
strongSelf.suggestedPostInfoNode?.removeFromSupernode()
strongSelf.suggestedPostInfoNode = suggestedPostInfoNode
strongSelf.addSubnode(suggestedPostInfoNode)
}
let suggestedPostInfoFrame = CGRect(origin: CGPoint(x: floor((params.width - suggestedPostInfoSize.width) * 0.5), y: 4.0), size: suggestedPostInfoSize)
suggestedPostInfoNode.frame = suggestedPostInfoFrame
} else if let suggestedPostInfoNode = strongSelf.suggestedPostInfoNode {
strongSelf.suggestedPostInfoNode = nil
suggestedPostInfoNode.removeFromSupernode()
}
strongSelf.messageAccessibilityArea.frame = CGRect(origin: CGPoint(), size: layoutSize) strongSelf.messageAccessibilityArea.frame = CGRect(origin: CGPoint(), size: layoutSize)
strongSelf.containerNode.frame = CGRect(origin: CGPoint(), size: layoutSize) strongSelf.containerNode.frame = CGRect(origin: CGPoint(), size: layoutSize)
strongSelf.contextSourceNode.frame = CGRect(origin: CGPoint(), size: layoutSize) strongSelf.contextSourceNode.frame = CGRect(origin: CGPoint(), size: layoutSize)

View File

@ -2805,7 +2805,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
actionButtonsFinalize = buttonsLayout actionButtonsFinalize = buttonsLayout
lastNodeTopPosition = .None(.Both) lastNodeTopPosition = .None(.Both)
} else if incoming, /*let channel = item.message.peers[item.message.id.peerId] as? TelegramChannel, channel.isMonoForum, let linkedMonoforumId = channel.linkedMonoforumId, let mainChannel = item.message.peers[linkedMonoforumId] as? TelegramChannel, mainChannel.hasPermission(.manageDirect),*/ let attribute = item.message.attributes.first(where: { $0 is SuggestedPostMessageAttribute }) as? SuggestedPostMessageAttribute, attribute.state == nil { } else if incoming, let attribute = item.message.attributes.first(where: { $0 is SuggestedPostMessageAttribute }) as? SuggestedPostMessageAttribute, attribute.state == nil {
//TODO:localize //TODO:localize
var buttonDeclineValue: UInt8 = 0 var buttonDeclineValue: UInt8 = 0
let buttonDecline = MemoryBuffer(data: Data(bytes: &buttonDeclineValue, count: 1)) let buttonDecline = MemoryBuffer(data: Data(bytes: &buttonDeclineValue, count: 1))
@ -2838,7 +2838,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
], ],
flags: [], flags: [],
placeholder: nil placeholder: nil
), customIcons, item.message, maximumNodeWidth) ), customIcons, item.message, baseWidth)
maxContentWidth = max(maxContentWidth, minWidth) maxContentWidth = max(maxContentWidth, minWidth)
actionButtonsFinalize = buttonsLayout actionButtonsFinalize = buttonsLayout
@ -3542,7 +3542,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
if suggestedPostInfoNode !== strongSelf.suggestedPostInfoNode { if suggestedPostInfoNode !== strongSelf.suggestedPostInfoNode {
strongSelf.suggestedPostInfoNode?.removeFromSupernode() strongSelf.suggestedPostInfoNode?.removeFromSupernode()
strongSelf.suggestedPostInfoNode = suggestedPostInfoNode strongSelf.suggestedPostInfoNode = suggestedPostInfoNode
strongSelf.mainContextSourceNode.contentNode.addSubnode(suggestedPostInfoNode) strongSelf.addSubnode(suggestedPostInfoNode)
} }
let suggestedPostInfoFrame = CGRect(origin: CGPoint(x: floor((params.width - suggestedPostInfoSize.width) * 0.5), y: 4.0), size: suggestedPostInfoSize) let suggestedPostInfoFrame = CGRect(origin: CGPoint(x: floor((params.width - suggestedPostInfoSize.width) * 0.5), y: 4.0), size: suggestedPostInfoSize)
suggestedPostInfoNode.frame = suggestedPostInfoFrame suggestedPostInfoNode.frame = suggestedPostInfoFrame

View File

@ -291,7 +291,7 @@ public func canAddMessageReactions(message: Message) -> Bool {
return true return true
} else { } else {
switch action.action { switch action.action {
case .unknown, .groupCreated, .channelMigratedFromGroup, .groupMigratedToChannel, .historyCleared, .customText, .botDomainAccessGranted, .botAppAccessGranted, .botSentSecureValues, .phoneNumberRequest, .webViewData, .topicCreated, .attachMenuBotAllowed, .requestedPeer, .giveawayLaunched: case .unknown, .groupCreated, .channelMigratedFromGroup, .groupMigratedToChannel, .historyCleared, .customText, .botDomainAccessGranted, .botAppAccessGranted, .botSentSecureValues, .phoneNumberRequest, .webViewData, .topicCreated, .attachMenuBotAllowed, .requestedPeer, .giveawayLaunched, .suggestedPostApprovalStatus:
return false return false
default: default:
return true return true

View File

@ -38,6 +38,7 @@ swift_library(
"//submodules/TelegramUI/Components/Chat/ChatMessageActionButtonsNode", "//submodules/TelegramUI/Components/Chat/ChatMessageActionButtonsNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageReactionsFooterContentNode", "//submodules/TelegramUI/Components/Chat/ChatMessageReactionsFooterContentNode",
"//submodules/TelegramUI/Components/Chat/ChatSwipeToReplyRecognizer", "//submodules/TelegramUI/Components/Chat/ChatSwipeToReplyRecognizer",
"//submodules/TelegramUI/Components/Chat/ChatMessageSuggestedPostInfoNode",
], ],
visibility = [ visibility = [
"//visibility:public", "//visibility:public",

View File

@ -28,6 +28,7 @@ import ChatMessageThreadInfoNode
import ChatMessageActionButtonsNode import ChatMessageActionButtonsNode
import ChatMessageReactionsFooterContentNode import ChatMessageReactionsFooterContentNode
import ChatSwipeToReplyRecognizer import ChatSwipeToReplyRecognizer
import ChatMessageSuggestedPostInfoNode
private let nameFont = Font.medium(14.0) private let nameFont = Font.medium(14.0)
private let inlineBotPrefixFont = Font.regular(14.0) private let inlineBotPrefixFont = Font.regular(14.0)
@ -51,6 +52,8 @@ public class ChatMessageStickerItemNode: ChatMessageItemView {
public var telegramFile: TelegramMediaFile? public var telegramFile: TelegramMediaFile?
private let fetchDisposable = MetaDisposable() private let fetchDisposable = MetaDisposable()
private var suggestedPostInfoNode: ChatMessageSuggestedPostInfoNode?
private var viaBotNode: TextNode? private var viaBotNode: TextNode?
private let dateAndStatusNode: ChatMessageDateAndStatusNode private let dateAndStatusNode: ChatMessageDateAndStatusNode
private var threadInfoNode: ChatMessageThreadInfoNode? private var threadInfoNode: ChatMessageThreadInfoNode?
@ -435,6 +438,8 @@ public class ChatMessageStickerItemNode: ChatMessageItemView {
let currentShareButtonNode = self.shareButtonNode let currentShareButtonNode = self.shareButtonNode
let currentForwardInfo = self.appliedForwardInfo let currentForwardInfo = self.appliedForwardInfo
let makeSuggestedPostInfoNodeLayout: ChatMessageSuggestedPostInfoNode.AsyncLayout = ChatMessageSuggestedPostInfoNode.asyncLayout(self.suggestedPostInfoNode)
func continueAsyncLayout(_ weakSelf: Weak<ChatMessageStickerItemNode>, _ item: ChatMessageItem, _ params: ListViewItemLayoutParams, _ mergedTop: ChatMessageMerge, _ mergedBottom: ChatMessageMerge, _ dateHeaderAtBottom: ChatMessageHeaderSpec) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation, ListViewItemApply, Bool) -> Void) { func continueAsyncLayout(_ weakSelf: Weak<ChatMessageStickerItemNode>, _ item: ChatMessageItem, _ params: ListViewItemLayoutParams, _ mergedTop: ChatMessageMerge, _ mergedBottom: ChatMessageMerge, _ dateHeaderAtBottom: ChatMessageHeaderSpec) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation, ListViewItemApply, Bool) -> Void) {
let accessibilityData = ChatMessageAccessibilityData(item: item, isSelected: nil) let accessibilityData = ChatMessageAccessibilityData(item: item, isSelected: nil)
@ -591,7 +596,7 @@ public class ChatMessageStickerItemNode: ChatMessageItemView {
let innerImageInset: CGFloat = 10.0 let innerImageInset: CGFloat = 10.0
let innerImageSize = CGSize(width: imageSize.width + innerImageInset * 2.0, height: imageSize.height + innerImageInset * 2.0) let innerImageSize = CGSize(width: imageSize.width + innerImageInset * 2.0, height: imageSize.height + innerImageInset * 2.0)
let imageFrame = CGRect(origin: CGPoint(x: 0.0 + (incoming ? (params.leftInset + layoutConstants.bubble.edgeInset + avatarInset + layoutConstants.bubble.contentInsets.left) : (params.width - params.rightInset - innerImageSize.width - layoutConstants.bubble.edgeInset - layoutConstants.bubble.contentInsets.left - deliveryFailedInset)), y: -innerImageInset), size: innerImageSize) var imageFrame = CGRect(origin: CGPoint(x: 0.0 + (incoming ? (params.leftInset + layoutConstants.bubble.edgeInset + avatarInset + layoutConstants.bubble.contentInsets.left) : (params.width - params.rightInset - innerImageSize.width - layoutConstants.bubble.edgeInset - layoutConstants.bubble.contentInsets.left - deliveryFailedInset)), y: -innerImageInset), size: innerImageSize)
let arguments = TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets(top: innerImageInset, left: innerImageInset, bottom: innerImageInset, right: innerImageInset)) let arguments = TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets(top: innerImageInset, left: innerImageInset, bottom: innerImageInset, right: innerImageInset))
@ -842,10 +847,48 @@ public class ChatMessageStickerItemNode: ChatMessageItemView {
needsForwardBackground = true needsForwardBackground = true
} }
let baseWidth = params.width - params.leftInset - params.rightInset
var maxContentWidth = imageSize.width var maxContentWidth = imageSize.width
var actionButtonsFinalize: ((CGFloat) -> (CGSize, (_ animation: ListViewItemUpdateAnimation) -> ChatMessageActionButtonsNode))? var actionButtonsFinalize: ((CGFloat) -> (CGSize, (_ animation: ListViewItemUpdateAnimation) -> ChatMessageActionButtonsNode))?
if let replyMarkup = replyMarkup { if let replyMarkup = replyMarkup {
let (minWidth, buttonsLayout) = actionButtonsLayout(item.context, item.presentationData.theme, item.presentationData.chatBubbleCorners, item.presentationData.strings, item.controllerInteraction.presentationContext.backgroundNode, replyMarkup, [:], item.message, maxContentWidth) let (minWidth, buttonsLayout) = actionButtonsLayout(item.context, item.presentationData.theme, item.presentationData.chatBubbleCorners, item.presentationData.strings, item.controllerInteraction.presentationContext.backgroundNode, replyMarkup, [:], item.message, baseWidth)
maxContentWidth = max(maxContentWidth, minWidth)
actionButtonsFinalize = buttonsLayout
} else if incoming, let attribute = item.message.attributes.first(where: { $0 is SuggestedPostMessageAttribute }) as? SuggestedPostMessageAttribute, attribute.state == nil {
//TODO:localize
var buttonDeclineValue: UInt8 = 0
let buttonDecline = MemoryBuffer(data: Data(bytes: &buttonDeclineValue, count: 1))
var buttonApproveValue: UInt8 = 1
let buttonApprove = MemoryBuffer(data: Data(bytes: &buttonApproveValue, count: 1))
var buttonSuggestChangesValue: UInt8 = 2
let buttonSuggestChanges = MemoryBuffer(data: Data(bytes: &buttonSuggestChangesValue, count: 1))
let customIcons: [MemoryBuffer: ChatMessageActionButtonsNode.CustomIcon] = [
buttonDecline: .suggestedPostReject,
buttonApprove: .suggestedPostApprove,
buttonSuggestChanges: .suggestedPostEdit
]
let (minWidth, buttonsLayout) = actionButtonsLayout(
item.context,
item.presentationData.theme,
item.presentationData.chatBubbleCorners,
item.presentationData.strings,
item.controllerInteraction.presentationContext.backgroundNode,
ReplyMarkupMessageAttribute(
rows: [
ReplyMarkupRow(buttons: [
ReplyMarkupButton(title: "Decline", titleWhenForwarded: nil, action: .callback(requiresPassword: false, data: buttonDecline)),
ReplyMarkupButton(title: "Approve", titleWhenForwarded: nil, action: .callback(requiresPassword: false, data: buttonApprove))
]),
ReplyMarkupRow(buttons: [
ReplyMarkupButton(title: "Suggest Changes", titleWhenForwarded: nil, action: .callback(requiresPassword: false, data: buttonSuggestChanges))
])
],
flags: [],
placeholder: nil
), customIcons, item.message, baseWidth)
maxContentWidth = max(maxContentWidth, minWidth) maxContentWidth = max(maxContentWidth, minWidth)
actionButtonsFinalize = buttonsLayout actionButtonsFinalize = buttonsLayout
} }
@ -901,6 +944,21 @@ public class ChatMessageStickerItemNode: ChatMessageItemView {
layoutSize.height += actionButtonsSizeAndApply.0.height layoutSize.height += actionButtonsSizeAndApply.0.height
} }
var suggestedPostInfoNodeLayout: (CGSize, () -> ChatMessageSuggestedPostInfoNode)?
for attribute in item.message.attributes {
if let _ = attribute as? SuggestedPostMessageAttribute {
let suggestedPostInfoNodeLayoutValue = makeSuggestedPostInfoNodeLayout(item, baseWidth)
suggestedPostInfoNodeLayout = suggestedPostInfoNodeLayoutValue
}
}
var additionalTopHeight: CGFloat = 0.0
if let suggestedPostInfoNodeLayout {
additionalTopHeight += 4.0 + suggestedPostInfoNodeLayout.0.height + 8.0
}
layoutSize.height += additionalTopHeight
imageFrame.origin.y += additionalTopHeight
var updatedImageFrame = imageFrame.offsetBy(dx: 0.0, dy: floor((contentHeight - imageSize.height) / 2.0)) var updatedImageFrame = imageFrame.offsetBy(dx: 0.0, dy: floor((contentHeight - imageSize.height) / 2.0))
var dateOffset = CGPoint(x: dateAndStatusSize.width + 4.0, y: dateAndStatusSize.height + 16.0) var dateOffset = CGPoint(x: dateAndStatusSize.width + 4.0, y: dateAndStatusSize.height + 16.0)
@ -1002,6 +1060,20 @@ public class ChatMessageStickerItemNode: ChatMessageItemView {
animation.animator.updateFrame(layer: strongSelf.placeholderNode.layer, frame: placeholderFrame, completion: nil) animation.animator.updateFrame(layer: strongSelf.placeholderNode.layer, frame: placeholderFrame, completion: nil)
} }
if let (suggestedPostInfoSize, suggestedPostInfoApply) = suggestedPostInfoNodeLayout {
let suggestedPostInfoNode = suggestedPostInfoApply()
if suggestedPostInfoNode !== strongSelf.suggestedPostInfoNode {
strongSelf.suggestedPostInfoNode?.removeFromSupernode()
strongSelf.suggestedPostInfoNode = suggestedPostInfoNode
strongSelf.addSubnode(suggestedPostInfoNode)
}
let suggestedPostInfoFrame = CGRect(origin: CGPoint(x: floor((params.width - suggestedPostInfoSize.width) * 0.5), y: 4.0), size: suggestedPostInfoSize)
suggestedPostInfoNode.frame = suggestedPostInfoFrame
} else if let suggestedPostInfoNode = strongSelf.suggestedPostInfoNode {
strongSelf.suggestedPostInfoNode = nil
suggestedPostInfoNode.removeFromSupernode()
}
strongSelf.messageAccessibilityArea.frame = CGRect(origin: CGPoint(), size: layoutSize) strongSelf.messageAccessibilityArea.frame = CGRect(origin: CGPoint(), size: layoutSize)
strongSelf.containerNode.frame = CGRect(origin: CGPoint(), size: layoutSize) strongSelf.containerNode.frame = CGRect(origin: CGPoint(), size: layoutSize)
strongSelf.contextSourceNode.frame = CGRect(origin: CGPoint(), size: layoutSize) strongSelf.contextSourceNode.frame = CGRect(origin: CGPoint(), size: layoutSize)

View File

@ -93,15 +93,82 @@ public final class ChatMessageSuggestedPostInfoNode: ASDisplayNode {
//TODO:localize //TODO:localize
let titleText: String let titleText: String
if !item.message.effectivelyIncoming(item.context.account.peerId) { if let attribute = item.message.attributes.first(where: { $0 is ReplyMessageAttribute }) as? ReplyMessageAttribute {
if item.message.attributes.contains(where: { $0 is ReplyMessageAttribute }) { var changedText = false
titleText = "You suggest a new price,\ntime and text for this message." var changedMedia = false
var changedTime = false
var changedPrice = false
if let previousMessage = item.message.associatedMessages[attribute.messageId] {
if previousMessage.text != item.message.text {
changedText = true
}
let filteredMediaIds = item.message.media.compactMap { media -> EngineMedia.Id? in
if media is TelegramMediaImage || media is TelegramMediaFile {
return media.id
} else {
return nil
}
}
let filteredPreviousMediaIds = previousMessage.media.compactMap { media -> EngineMedia.Id? in
if media is TelegramMediaImage || media is TelegramMediaFile {
return media.id
} else {
return nil
}
}
if Set(filteredPreviousMediaIds) != Set(filteredMediaIds) {
changedMedia = true
}
if let previousAttribute = previousMessage.attributes.first(where: { $0 is SuggestedPostMessageAttribute }) as? SuggestedPostMessageAttribute {
if previousAttribute.amount != amount {
changedPrice = true
}
if previousAttribute.timestamp != timestamp {
changedTime = true
}
}
}
if !item.message.effectivelyIncoming(item.context.account.peerId) {
if changedText && changedMedia && changedPrice && changedTime {
titleText = "You suggest a new price,\ntime and contents for this message."
} else if changedText && changedPrice && changedTime {
titleText = "You suggest a new price,\ntime and text for this message."
} else if changedMedia && changedPrice && changedTime {
titleText = "You suggest a new price,\ntime and attachments for this message."
} else if changedPrice && changedTime {
titleText = "You suggest a new price and time\nfor this message."
} else if changedPrice {
titleText = "You suggest a new price\nfor this message."
} else if changedTime {
titleText = "You suggest a new time\nfor this message."
} else {
titleText = "You suggest changes\nfor this message."
}
} else { } else {
titleText = "You suggest to post\nthis message." var channelName = ""
if item.message.author is TelegramChannel {
channelName = item.message.author.flatMap(EnginePeer.init)?.compactDisplayTitle ?? " "
}
if changedText && changedMedia && changedPrice && changedTime {
titleText = "**\(channelName)** suggests a new price,\ntime and contents for this message."
} else if changedText && changedPrice && changedTime {
titleText = "**\(channelName)** suggests a new price,\ntime and text for this message."
} else if changedMedia && changedPrice && changedTime {
titleText = "**\(channelName)** suggests a new price,\ntime and attachments for this message."
} else if changedPrice && changedTime {
titleText = "**\(channelName)** suggests a new price and time\nfor this message."
} else if changedPrice {
titleText = "**\(channelName)** suggests a new price\nfor this message."
} else if changedTime {
titleText = "**\(channelName)** suggests a new time\nfor this message."
} else {
titleText = "**\(channelName)** suggests changes\nfor this message."
}
} }
} else { } else {
if item.message.author is TelegramChannel { if !item.message.effectivelyIncoming(item.context.account.peerId) {
titleText = "**\(item.message.author.flatMap(EnginePeer.init)?.compactDisplayTitle ?? " ")** suggests a new price,\ntime, and text for your message." titleText = "You suggest to post\nthis message."
} else { } else {
titleText = "**\(item.message.author.flatMap(EnginePeer.init)?.compactDisplayTitle ?? " ")** suggests to post\nthis message." titleText = "**\(item.message.author.flatMap(EnginePeer.init)?.compactDisplayTitle ?? " ")** suggests to post\nthis message."
} }

View File

@ -165,7 +165,7 @@ public final class ChatRecentActionsController: TelegramBaseController {
}, addDoNotTranslateLanguage: { _ in }, addDoNotTranslateLanguage: { _ in
}, hideTranslationPanel: { }, hideTranslationPanel: {
}, openPremiumGift: { }, openPremiumGift: {
}, openSuggestPost: { _ in }, openSuggestPost: { _, _ in
}, openPremiumRequiredForMessaging: { }, openPremiumRequiredForMessaging: {
}, openStarsPurchase: { _ in }, openStarsPurchase: { _ in
}, openMessagePayment: { }, openMessagePayment: {

View File

@ -430,7 +430,7 @@ final class PeerInfoSelectionPanelNode: ASDisplayNode {
}, addDoNotTranslateLanguage: { _ in }, addDoNotTranslateLanguage: { _ in
}, hideTranslationPanel: { }, hideTranslationPanel: {
}, openPremiumGift: { }, openPremiumGift: {
}, openSuggestPost: { _ in }, openSuggestPost: { _, _ in
}, openPremiumRequiredForMessaging: { }, openPremiumRequiredForMessaging: {
}, openStarsPurchase: { _ in }, openStarsPurchase: { _ in
}, openMessagePayment: { }, openMessagePayment: {

View File

@ -818,7 +818,7 @@ final class PeerSelectionControllerNode: ASDisplayNode {
}, addDoNotTranslateLanguage: { _ in }, addDoNotTranslateLanguage: { _ in
}, hideTranslationPanel: { }, hideTranslationPanel: {
}, openPremiumGift: { }, openPremiumGift: {
}, openSuggestPost: { _ in }, openSuggestPost: { _, _ in
}, openPremiumRequiredForMessaging: { }, openPremiumRequiredForMessaging: {
}, openStarsPurchase: { _ in }, openStarsPurchase: { _ in
}, openMessagePayment: { }, openMessagePayment: {

View File

@ -4141,7 +4141,7 @@ extension ChatControllerImpl {
}) })
self.push(controller) self.push(controller)
} }
}, openSuggestPost: { [weak self] message in }, openSuggestPost: { [weak self] message, mode in
guard let self else { guard let self else {
return return
} }
@ -4198,6 +4198,13 @@ extension ChatControllerImpl {
} }
return updated return updated
}) })
switch mode {
case .default, .editMessage:
break
case .editTime, .editPrice:
self.presentSuggestPostOptions()
}
} else { } else {
self.updateChatPresentationInterfaceState(interactive: true, { state in self.updateChatPresentationInterfaceState(interactive: true, { state in
var state = state var state = state
@ -4212,8 +4219,8 @@ extension ChatControllerImpl {
} }
return state return state
}) })
self.presentSuggestPostOptions()
} }
self.presentSuggestPostOptions()
}, openPremiumRequiredForMessaging: { [weak self] in }, openPremiumRequiredForMessaging: { [weak self] in
guard let self else { guard let self else {
return return

View File

@ -2361,10 +2361,20 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
timestamp = Int32(Date().timeIntervalSince1970) + 1 * 60 * 60 timestamp = Int32(Date().timeIntervalSince1970) + 1 * 60 * 60
} else { } else {
let _ = strongSelf.context.engine.messages.monoforumPerformSuggestedPostAction(id: message.id, action: .approve(timestamp: timestamp)).startStandalone() //TODO:localize
let textString = "Publish this message now?"
strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: textString, actions: [
TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {}),
TextAlertAction(type: .defaultAction, title: "Publish", action: { [weak strongSelf] in
guard let strongSelf else {
return
}
let _ = strongSelf.context.engine.messages.monoforumPerformSuggestedPostAction(id: message.id, action: .approve(timestamp: timestamp)).startStandalone()
})
]), in: .window(.root))
} }
case 2: case 2:
strongSelf.interfaceInteraction?.openSuggestPost(message) strongSelf.interfaceInteraction?.openSuggestPost(message, .default)
default: default:
break break
} }

View File

@ -819,13 +819,23 @@ extension ChatControllerImpl {
if let channel = peerView.peers[peerView.peerId] as? TelegramChannel { if let channel = peerView.peers[peerView.peerId] as? TelegramChannel {
if channel.isMonoForum { if channel.isMonoForum {
if let linkedMonoforumId = channel.linkedMonoforumId, let mainChannel = peerView.peers[linkedMonoforumId] as? TelegramChannel, mainChannel.hasPermission(.manageDirect) { if let linkedMonoforumId = channel.linkedMonoforumId, let mainChannel = peerView.peers[linkedMonoforumId] as? TelegramChannel, mainChannel.hasPermission(.manageDirect) {
} else if let sendPaidMessageStarsValue = cachedData.sendPaidMessageStars, sendPaidMessageStarsValue == .zero {
sendPaidMessageStars = nil
} else { } else {
sendPaidMessageStars = channel.sendPaidMessageStars sendPaidMessageStars = channel.sendPaidMessageStars
} }
} else { } else {
if channel.flags.contains(.isCreator) || channel.adminRights != nil { if channel.flags.contains(.isCreator) || channel.adminRights != nil {
} else { } else {
sendPaidMessageStars = channel.sendPaidMessageStars if let personalSendPaidMessageStars = cachedData.sendPaidMessageStars {
if personalSendPaidMessageStars == .zero {
sendPaidMessageStars = nil
} else {
sendPaidMessageStars = personalSendPaidMessageStars
}
} else {
sendPaidMessageStars = channel.sendPaidMessageStars
}
} }
} }
} }

View File

@ -1512,14 +1512,47 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
} }
if let message = messages.first, message.id.namespace == Namespaces.Message.Cloud, let channel = message.peers[message.id.peerId] as? TelegramChannel, channel.isMonoForum { if let message = messages.first, message.id.namespace == Namespaces.Message.Cloud, let channel = message.peers[message.id.peerId] as? TelegramChannel, channel.isMonoForum {
//TODO:localize var canSuggestPost = true
actions.append(.action(ContextMenuActionItem(text: "Suggest a Post", icon: { theme in for media in message.media {
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Customize"), color: theme.actionSheet.primaryTextColor) if media is TelegramMediaAction {
}, action: { c, _ in canSuggestPost = false
c?.dismiss(completion: { }
interfaceInteraction.openSuggestPost(message) }
})
}))) if canSuggestPost {
//TODO:localize
if message.attributes.contains(where: { $0 is SuggestedPostMessageAttribute }) {
actions.append(.action(ContextMenuActionItem(text: "Edit Message", icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Edit"), color: theme.actionSheet.primaryTextColor)
}, action: { c, _ in
c?.dismiss(completion: {
interfaceInteraction.openSuggestPost(message, .editMessage)
})
})))
actions.append(.action(ContextMenuActionItem(text: "Edit Time", icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Calendar"), color: theme.actionSheet.primaryTextColor)
}, action: { c, _ in
c?.dismiss(completion: {
interfaceInteraction.openSuggestPost(message, .editTime)
})
})))
actions.append(.action(ContextMenuActionItem(text: "Edit Price", icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/PriceTag"), color: theme.actionSheet.primaryTextColor)
}, action: { c, _ in
c?.dismiss(completion: {
interfaceInteraction.openSuggestPost(message, .editPrice)
})
})))
} else {
actions.append(.action(ContextMenuActionItem(text: "Suggest a Post", icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Customize"), color: theme.actionSheet.primaryTextColor)
}, action: { c, _ in
c?.dismiss(completion: {
interfaceInteraction.openSuggestPost(message, .default)
})
})))
}
}
} }
if let activePoll = activePoll, let voters = activePoll.results.voters { if let activePoll = activePoll, let voters = activePoll.results.voters {
@ -1935,6 +1968,11 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
if let channel = chatPresentationInterfaceState.renderedPeer?.peer as? TelegramChannel, channel.isMonoForum, let associatedPeerId = channel.associatedPeerId { if let channel = chatPresentationInterfaceState.renderedPeer?.peer as? TelegramChannel, channel.isMonoForum, let associatedPeerId = channel.associatedPeerId {
if message.effectivelyIncoming(context.account.peerId), message.author?.id == associatedPeerId { if message.effectivelyIncoming(context.account.peerId), message.author?.id == associatedPeerId {
canViewAuthor = true canViewAuthor = true
for media in message.media {
if media is TelegramMediaAction {
canViewAuthor = false
}
}
} }
} else if let messageReadStatsAreHidden = infoSummaryData.messageReadStatsAreHidden, !messageReadStatsAreHidden { } else if let messageReadStatsAreHidden = infoSummaryData.messageReadStatsAreHidden, !messageReadStatsAreHidden {
canViewStats = canViewReadStats(message: message, participantCount: infoSummaryData.participantCount, isMessageRead: isMessageRead, isPremium: isPremium, appConfig: appConfig) canViewStats = canViewReadStats(message: message, participantCount: infoSummaryData.participantCount, isMessageRead: isMessageRead, isPremium: isPremium, appConfig: appConfig)

View File

@ -4773,7 +4773,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch
case .gift: case .gift:
self.interfaceInteraction?.openPremiumGift() self.interfaceInteraction?.openPremiumGift()
case .suggestPost: case .suggestPost:
self.interfaceInteraction?.openSuggestPost(nil) self.interfaceInteraction?.openSuggestPost(nil, .default)
} }
break break
} }