[WIP] Post suggestions

This commit is contained in:
Isaac 2025-04-23 12:18:56 +04:00
parent 96a5df0b68
commit 603d5754db
57 changed files with 2500 additions and 742 deletions

View File

@ -1212,9 +1212,10 @@ public protocol SharedAccountContext: AnyObject {
func makeGalleryController(context: AccountContext, source: GalleryControllerItemSource, streamSingleVideo: Bool, isPreview: Bool) -> ViewController
func makeAccountFreezeInfoScreen(context: AccountContext) -> ViewController
func makeSendInviteLinkScreen(context: AccountContext, subject: SendInviteLinkScreenSubject, peers: [TelegramForbiddenInvitePeer], theme: PresentationTheme?) -> ViewController
func makePostSuggestionsSettingsScreen(context: AccountContext) -> ViewController
func makeDebugSettingsController(context: AccountContext?) -> ViewController?
func openCreateGroupCallUI(context: AccountContext, peerIds: [EnginePeer.Id], parentController: ViewController)

View File

@ -1166,6 +1166,7 @@ public enum ChatCustomContentsKind: Equatable {
case quickReplyMessageInput(shortcut: String, shortcutType: ChatQuickReplyShortcutType)
case businessLinkSetup(link: TelegramBusinessChatLinks.Link)
case hashTagSearch(publicPosts: Bool)
case postSuggestions(price: StarsAmount)
}
public protocol ChatCustomContentsProtocol: AnyObject {

View File

@ -1243,6 +1243,7 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate {
}, joinGroupCall: { _ in
}, presentInviteMembers: {
}, presentGigagroupHelp: {
}, openSuggestPost: {
}, editMessageMedia: { _, _ in
}, updateShowCommands: { _ in
}, updateShowSendAsPeers: { _ in

View File

@ -151,6 +151,7 @@ public final class ChatPanelInterfaceInteraction {
public let joinGroupCall: (CachedChannelData.ActiveCall) -> Void
public let presentInviteMembers: () -> Void
public let presentGigagroupHelp: () -> Void
public let openSuggestPost: () -> Void
public let updateShowCommands: ((Bool) -> Bool) -> Void
public let updateShowSendAsPeers: ((Bool) -> Bool) -> Void
public let openInviteRequests: () -> Void
@ -267,6 +268,7 @@ public final class ChatPanelInterfaceInteraction {
joinGroupCall: @escaping (CachedChannelData.ActiveCall) -> Void,
presentInviteMembers: @escaping () -> Void,
presentGigagroupHelp: @escaping () -> Void,
openSuggestPost: @escaping () -> Void,
editMessageMedia: @escaping (MessageId, Bool) -> Void,
updateShowCommands: @escaping ((Bool) -> Bool) -> Void,
updateShowSendAsPeers: @escaping ((Bool) -> Bool) -> Void,
@ -384,6 +386,7 @@ public final class ChatPanelInterfaceInteraction {
self.joinGroupCall = joinGroupCall
self.presentInviteMembers = presentInviteMembers
self.presentGigagroupHelp = presentGigagroupHelp
self.openSuggestPost = openSuggestPost
self.updateShowCommands = updateShowCommands
self.updateShowSendAsPeers = updateShowSendAsPeers
self.openInviteRequests = openInviteRequests
@ -507,6 +510,7 @@ public final class ChatPanelInterfaceInteraction {
}, joinGroupCall: { _ in
}, presentInviteMembers: {
}, presentGigagroupHelp: {
}, openSuggestPost: {
}, editMessageMedia: { _, _ in
}, updateShowCommands: { _ in
}, updateShowSendAsPeers: { _ in

View File

@ -228,6 +228,7 @@ private var declaredEncodables: Void = {
declareEncodable(DerivedDataMessageAttribute.self, f: { DerivedDataMessageAttribute(decoder: $0) })
declareEncodable(TelegramApplicationIcons.self, f: { TelegramApplicationIcons(decoder: $0) })
declareEncodable(OutgoingQuickReplyMessageAttribute.self, f: { OutgoingQuickReplyMessageAttribute(decoder: $0) })
declareEncodable(OutgoingSuggestedPostMessageAttribute.self, f: { OutgoingSuggestedPostMessageAttribute(decoder: $0) })
declareEncodable(EffectMessageAttribute.self, f: { EffectMessageAttribute(decoder: $0) })
declareEncodable(FactCheckMessageAttribute.self, f: { FactCheckMessageAttribute(decoder: $0) })
declareEncodable(TelegramMediaPaidContent.self, f: { TelegramMediaPaidContent(decoder: $0) })

View File

@ -236,6 +236,8 @@ private func filterMessageAttributesForOutgoingMessage(_ attributes: [MessageAtt
return true
case _ as OutgoingQuickReplyMessageAttribute:
return true
case _ as OutgoingSuggestedPostMessageAttribute:
return true
case _ as EmbeddedMediaStickersMessageAttribute:
return true
case _ as EmojiSearchQueryMessageAttribute:
@ -275,6 +277,8 @@ private func filterMessageAttributesForForwardedMessage(_ attributes: [MessageAt
return true
case _ as OutgoingQuickReplyMessageAttribute:
return true
case _ as OutgoingSuggestedPostMessageAttribute:
return true
case _ as ForwardOptionsMessageAttribute:
return true
case _ as SendAsMessageAttribute:
@ -733,6 +737,9 @@ func enqueueMessages(transaction: Transaction, account: Account, peerId: PeerId,
} else if attribute is OutgoingQuickReplyMessageAttribute {
messageNamespace = Namespaces.Message.QuickReplyLocal
effectiveTimestamp = 0
} else if attribute is OutgoingSuggestedPostMessageAttribute {
messageNamespace = Namespaces.Message.SuggestedPostLocal
effectiveTimestamp = 0
} else if let attribute = attribute as? SendAsMessageAttribute {
if let peer = transaction.getPeer(attribute.peerId) {
sendAsPeer = peer
@ -769,6 +776,9 @@ func enqueueMessages(transaction: Transaction, account: Account, peerId: PeerId,
if messageNamespace != Namespaces.Message.QuickReplyLocal {
attributes.removeAll(where: { $0 is OutgoingQuickReplyMessageAttribute })
}
if messageNamespace != Namespaces.Message.SuggestedPostLocal {
attributes.removeAll(where: { $0 is OutgoingSuggestedPostMessageAttribute })
}
if let peer = peer as? TelegramChannel {
switch peer.info {
@ -1003,6 +1013,9 @@ func enqueueMessages(transaction: Transaction, account: Account, peerId: PeerId,
} else if attribute is OutgoingQuickReplyMessageAttribute {
messageNamespace = Namespaces.Message.QuickReplyLocal
effectiveTimestamp = 0
} else if attribute is OutgoingSuggestedPostMessageAttribute {
messageNamespace = Namespaces.Message.SuggestedPostLocal
effectiveTimestamp = 0
} else if let attribute = attribute as? ReplyMessageAttribute {
if let threadMessageId = attribute.threadMessageId {
threadId = Int64(threadMessageId.id)
@ -1035,6 +1048,9 @@ func enqueueMessages(transaction: Transaction, account: Account, peerId: PeerId,
if messageNamespace != Namespaces.Message.QuickReplyLocal {
attributes.removeAll(where: { $0 is OutgoingQuickReplyMessageAttribute })
}
if messageNamespace != Namespaces.Message.SuggestedPostLocal {
attributes.removeAll(where: { $0 is OutgoingSuggestedPostMessageAttribute })
}
let (tags, globalTags) = tagsForStoreMessage(incoming: false, attributes: attributes, media: sourceMessage.media, textEntities: entitiesAttribute?.entities, isPinned: false)

View File

@ -179,6 +179,9 @@ private func requestEditMessageInternal(accountPeerId: PeerId, postbox: Postbox,
if messageId.namespace == Namespaces.Message.QuickReplyCloud {
quickReplyShortcutId = Int32(clamping: message.threadId ?? 0)
flags |= Int32(1 << 17)
} else if messageId.namespace == Namespaces.Message.SuggestedPostLocal {
//TODO:release
preconditionFailure()
}
return network.request(Api.functions.messages.editMessage(flags: flags, peer: inputPeer, id: messageId.id, message: text, media: inputMedia, replyMarkup: nil, entities: apiEntities, scheduleDate: effectiveScheduleTime, quickReplyShortcutId: quickReplyShortcutId))

View File

@ -72,6 +72,8 @@ private func fetchWebpage(account: Account, messageId: MessageId, threadId: Int6
targetMessageNamespace = Namespaces.Message.ScheduledCloud
} else if Namespaces.Message.allQuickReply.contains(messageId.namespace) {
targetMessageNamespace = Namespaces.Message.QuickReplyCloud
} else if Namespaces.Message.allSuggestedPost.contains(messageId.namespace) {
targetMessageNamespace = Namespaces.Message.SuggestedPostCloud
} else {
targetMessageNamespace = Namespaces.Message.Cloud
}
@ -1071,6 +1073,10 @@ public final class AccountViewTracker {
} else {
fetchSignal = .never()
}
} else if let messageId = messageIds.first, messageId.namespace == Namespaces.Message.SuggestedPostCloud {
//TODO:release
assertionFailure()
fetchSignal = .never()
} else if peerIdAndThreadId.peerId.namespace == Namespaces.Peer.CloudUser || peerIdAndThreadId.peerId.namespace == Namespaces.Peer.CloudGroup {
fetchSignal = account.network.request(Api.functions.messages.getMessages(id: messageIds.map { Api.InputMessage.inputMessageID(id: $0.id) }))
} else if peerIdAndThreadId.peerId.namespace == Namespaces.Peer.CloudChannel {
@ -2120,6 +2126,36 @@ public final class AccountViewTracker {
}
return signal
}
public func postSuggestionsViewForLocation(peerId: EnginePeer.Id, additionalData: [AdditionalMessageHistoryViewData] = []) -> Signal<(MessageHistoryView, ViewUpdateType, InitialMessageHistoryData?), NoError> {
guard let account = self.account else {
return .never()
}
let chatLocation: ChatLocationInput = .peer(peerId: peerId, threadId: nil)
let signal = account.postbox.aroundMessageHistoryViewForLocation(chatLocation, anchor: .upperBound, ignoreMessagesInTimestampRange: nil, ignoreMessageIds: Set(), count: 200, fixedCombinedReadStates: nil, topTaggedMessageIdNamespaces: [], tag: nil, appendMessagesFromTheSameGroup: false, namespaces: .just(Namespaces.Message.allSuggestedPost), orderStatistics: [], additionalData: additionalData)
return withState(signal, { [weak self] () -> Int32 in
if let strongSelf = self {
return OSAtomicIncrement32(&strongSelf.nextViewId)
} else {
return -1
}
}, next: { [weak self] next, viewId in
if let strongSelf = self {
strongSelf.queue.async {
let (messageIds, localWebpages) = pendingWebpages(entries: next.0.entries)
strongSelf.updatePendingWebpages(viewId: viewId, threadId: nil, messageIds: messageIds, localWebpages: localWebpages)
strongSelf.historyViewStateValidationContexts.updateView(id: viewId, view: next.0, location: chatLocation)
}
}
}, disposed: { [weak self] viewId in
if let strongSelf = self {
strongSelf.queue.async {
strongSelf.updatePendingWebpages(viewId: viewId, threadId: nil, messageIds: [], localWebpages: [:])
strongSelf.historyViewStateValidationContexts.updateView(id: viewId, view: nil, location: nil)
}
}
})
}
public func aroundMessageOfInterestHistoryViewForLocation(_ chatLocation: ChatLocationInput, ignoreMessagesInTimestampRange: ClosedRange<Int32>? = nil, ignoreMessageIds: Set<MessageId> = Set(), count: Int, tag: HistoryViewInputTag? = nil, appendMessagesFromTheSameGroup: Bool = false, orderStatistics: MessageHistoryViewOrderStatistics = [], additionalData: [AdditionalMessageHistoryViewData] = [], useRootInterfaceStateForThread: Bool = false) -> Signal<(MessageHistoryView, ViewUpdateType, InitialMessageHistoryData?), NoError> {
if let account = self.account {

View File

@ -198,6 +198,14 @@ func applyUpdateMessage(postbox: Postbox, stateManager: AccountStateManager, mes
}
}
}
if Namespaces.Message.allSuggestedPost.contains(message.id.namespace) {
for i in 0 ..< updatedAttributes.count {
if updatedAttributes[i] is OutgoingSuggestedPostMessageAttribute {
updatedAttributes.remove(at: i)
break
}
}
}
attributes = updatedAttributes
text = currentMessage.text
@ -220,6 +228,8 @@ func applyUpdateMessage(postbox: Postbox, stateManager: AccountStateManager, mes
}
if Namespaces.Message.allQuickReply.contains(message.id.namespace) {
namespace = Namespaces.Message.QuickReplyCloud
} else if Namespaces.Message.allSuggestedPost.contains(message.id.namespace) {
namespace = Namespaces.Message.SuggestedPostCloud
} else if let updatedTimestamp = updatedTimestamp {
if attributes.contains(where: { $0 is PendingProcessingMessageAttribute }) {
namespace = Namespaces.Message.ScheduledCloud
@ -243,6 +253,8 @@ func applyUpdateMessage(postbox: Postbox, stateManager: AccountStateManager, mes
if let threadId {
_internal_applySentQuickReplyMessage(transaction: transaction, shortcut: attribute.shortcut, quickReplyId: Int32(clamping: threadId))
}
} else if attribute is OutgoingSuggestedPostMessageAttribute {
//TODO:release
}
}
@ -397,6 +409,8 @@ func applyUpdateGroupMessages(postbox: Postbox, stateManager: AccountStateManage
var namespace = Namespaces.Message.Cloud
if Namespaces.Message.allQuickReply.contains(messages[0].id.namespace) {
namespace = Namespaces.Message.QuickReplyCloud
} else if Namespaces.Message.allSuggestedPost.contains(messages[0].id.namespace) {
namespace = Namespaces.Message.SuggestedPostCloud
} else if let message = messages.first, let apiMessage = result.messages.first {
if message.scheduleTime != nil && message.scheduleTime == apiMessage.timestamp {
namespace = Namespaces.Message.ScheduledCloud
@ -474,6 +488,8 @@ func applyUpdateGroupMessages(postbox: Postbox, stateManager: AccountStateManage
if let threadId = updatedMessage.threadId {
_internal_applySentQuickReplyMessage(transaction: transaction, shortcut: attribute.shortcut, quickReplyId: Int32(clamping: threadId))
}
} else if attribute is OutgoingSuggestedPostMessageAttribute {
//TODO:release
}
}
}

View File

@ -35,6 +35,25 @@ func cloudChatAddClearHistoryOperation(transaction: Transaction, peerId: PeerId,
} else if case .forEveryone = type {
transaction.operationLogAddEntry(peerId: peerId, tag: OperationLogTags.CloudChatRemoveMessages, tagLocalIndex: .automatic, tagMergedIndex: .automatic, contents: CloudChatClearHistoryOperation(peerId: peerId, topMessageId: MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: .max), threadId: threadId, minTimestamp: minTimestamp, maxTimestamp: maxTimestamp, type: type))
}
} else if type == .suggestedPostMessages {
var messageIds: [MessageId] = []
transaction.withAllMessages(peerId: peerId, namespace: Namespaces.Message.SuggestedPostCloud) { message -> Bool in
messageIds.append(message.id)
return true
}
cloudChatAddRemoveMessagesOperation(transaction: transaction, peerId: peerId, threadId: threadId, messageIds: messageIds, type: .forLocalPeer)
let topMessageId: MessageId?
if let explicitTopMessageId = explicitTopMessageId {
topMessageId = explicitTopMessageId
} else {
topMessageId = transaction.getTopPeerMessageId(peerId: peerId, namespace: Namespaces.Message.SuggestedPostCloud)
}
if let topMessageId = topMessageId {
transaction.operationLogAddEntry(peerId: peerId, tag: OperationLogTags.CloudChatRemoveMessages, tagLocalIndex: .automatic, tagMergedIndex: .automatic, contents: CloudChatClearHistoryOperation(peerId: peerId, topMessageId: topMessageId, threadId: threadId, minTimestamp: minTimestamp, maxTimestamp: maxTimestamp, type: type))
} else if case .forEveryone = type {
transaction.operationLogAddEntry(peerId: peerId, tag: OperationLogTags.CloudChatRemoveMessages, tagLocalIndex: .automatic, tagMergedIndex: .automatic, contents: CloudChatClearHistoryOperation(peerId: peerId, topMessageId: MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: .max), threadId: threadId, minTimestamp: minTimestamp, maxTimestamp: maxTimestamp, type: type))
}
} else {
let topMessageId: MessageId?
if let explicitTopMessageId = explicitTopMessageId {

View File

@ -128,6 +128,7 @@ func managedCloudChatRemoveMessagesOperations(postbox: Postbox, network: Network
private func removeMessages(postbox: Postbox, network: Network, stateManager: AccountStateManager, peer: Peer, operation: CloudChatRemoveMessagesOperation) -> Signal<Void, NoError> {
var isScheduled = false
var isQuickReply = false
var isSuggestedPost = false
for id in operation.messageIds {
if id.namespace == Namespaces.Message.ScheduledCloud {
isScheduled = true
@ -135,6 +136,9 @@ private func removeMessages(postbox: Postbox, network: Network, stateManager: Ac
} else if id.namespace == Namespaces.Message.QuickReplyCloud {
isQuickReply = true
break
} else if id.namespace == Namespaces.Message.SuggestedPostCloud {
isSuggestedPost = true
break
}
}
@ -190,6 +194,10 @@ private func removeMessages(postbox: Postbox, network: Network, stateManager: Ac
} else {
return .complete()
}
} else if isSuggestedPost {
//TODO:release
assertionFailure()
return .complete()
} else if peer.id.namespace == Namespaces.Peer.CloudChannel {
if let inputChannel = apiInputChannel(peer) {
var signal: Signal<Void, NoError> = .complete()

View File

@ -858,11 +858,15 @@ public final class PendingMessageManager {
var videoTimestamp: Int32?
var sendAsPeerId: PeerId?
var quickReply: OutgoingQuickReplyMessageAttribute?
var suggestedPost: OutgoingSuggestedPostMessageAttribute?
var messageEffect: EffectMessageAttribute?
var allowPaidStars: Int64?
var flags: Int32 = 0
//TODO:release
let _ = suggestedPost
for attribute in messages[0].0.attributes {
if let replyAttribute = attribute as? ReplyMessageAttribute {
replyMessageId = replyAttribute.messageId.id
@ -890,6 +894,8 @@ public final class PendingMessageManager {
sendAsPeerId = attribute.peerId
} else if let attribute = attribute as? OutgoingQuickReplyMessageAttribute {
quickReply = attribute
} else if let attribute = attribute as? OutgoingSuggestedPostMessageAttribute {
suggestedPost = attribute
} else if let attribute = attribute as? EffectMessageAttribute {
messageEffect = attribute
} else if let _ = attribute as? InvertMediaMessageAttribute {
@ -1322,10 +1328,14 @@ public final class PendingMessageManager {
var sendAsPeerId: PeerId?
var bubbleUpEmojiOrStickersets = false
var quickReply: OutgoingQuickReplyMessageAttribute?
var suggestedPost: OutgoingSuggestedPostMessageAttribute?
var messageEffect: EffectMessageAttribute?
var allowPaidStars: Int64?
var flags: Int32 = 0
//TODO:release
let _ = suggestedPost
for attribute in message.attributes {
if let replyAttribute = attribute as? ReplyMessageAttribute {
@ -1360,6 +1370,8 @@ public final class PendingMessageManager {
sendAsPeerId = attribute.peerId
} else if let attribute = attribute as? OutgoingQuickReplyMessageAttribute {
quickReply = attribute
} else if let attribute = attribute as? OutgoingSuggestedPostMessageAttribute {
suggestedPost = attribute
} else if let attribute = attribute as? EffectMessageAttribute {
messageEffect = attribute
} else if let attribute = attribute as? ForwardVideoTimestampAttribute {
@ -1803,6 +1815,8 @@ public final class PendingMessageManager {
targetNamespace = Namespaces.Message.ScheduledCloud
} else if Namespaces.Message.allQuickReply.contains(message.id.namespace) {
targetNamespace = Namespaces.Message.QuickReplyCloud
} else if Namespaces.Message.allSuggestedPost.contains(message.id.namespace) {
targetNamespace = Namespaces.Message.SuggestedPostCloud
} else {
targetNamespace = Namespaces.Message.Cloud
}
@ -1854,6 +1868,8 @@ public final class PendingMessageManager {
if let message = messages.first {
if message.id.namespace == Namespaces.Message.QuickReplyLocal {
namespace = Namespaces.Message.QuickReplyCloud
} else if Namespaces.Message.allSuggestedPost.contains(message.id.namespace) {
namespace = Namespaces.Message.SuggestedPostCloud
} else if let apiMessage = result.messages.first, message.scheduleTime != nil && message.scheduleTime == apiMessage.timestamp {
namespace = Namespaces.Message.ScheduledCloud
} else if let apiMessage = result.messages.first, case let .message(_, flags2, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _) = apiMessage, (flags2 & (1 << 4)) != 0 {

View File

@ -0,0 +1,37 @@
import Foundation
import Postbox
import TelegramApi
public final class OutgoingSuggestedPostMessageAttribute: Equatable, MessageAttribute {
public let price: StarsAmount
public let timestamp: Int32?
public init(price: StarsAmount, timestamp: Int32?) {
self.price = price
self.timestamp = timestamp
}
required public init(decoder: PostboxDecoder) {
self.price = decoder.decodeCodable(StarsAmount.self, forKey: "s") ?? StarsAmount(value: 0, nanos: 0)
self.timestamp = decoder.decodeOptionalInt32ForKey("t")
}
public func encode(_ encoder: PostboxEncoder) {
encoder.encodeCodable(self.price, forKey: "s")
if let timestamp = self.timestamp {
encoder.encodeInt32(timestamp, forKey: "t")
} else {
encoder.encodeNil(forKey: "t")
}
}
public static func ==(lhs: OutgoingSuggestedPostMessageAttribute, rhs: OutgoingSuggestedPostMessageAttribute) -> Bool {
if lhs.price != rhs.price {
return false
}
if lhs.timestamp != rhs.timestamp {
return false
}
return true
}
}

View File

@ -97,6 +97,7 @@ public enum CloudChatClearHistoryType: Int32 {
case forEveryone
case scheduledMessages
case quickReplyMessages
case suggestedPostMessages
}
public enum InteractiveHistoryClearingType: Int32 {

View File

@ -10,10 +10,13 @@ public struct Namespaces {
public static let ScheduledLocal: Int32 = 4
public static let QuickReplyCloud: Int32 = 5
public static let QuickReplyLocal: Int32 = 6
public static let SuggestedPostLocal: Int32 = 7
public static let SuggestedPostCloud: Int32 = 8
public static let allScheduled: Set<Int32> = Set([Namespaces.Message.ScheduledCloud, Namespaces.Message.ScheduledLocal])
public static let allQuickReply: Set<Int32> = Set([Namespaces.Message.QuickReplyCloud, Namespaces.Message.QuickReplyLocal])
public static let allNonRegular: Set<Int32> = Set([Namespaces.Message.ScheduledCloud, Namespaces.Message.ScheduledLocal, Namespaces.Message.QuickReplyCloud, Namespaces.Message.QuickReplyLocal])
public static let allSuggestedPost: Set<Int32> = Set([Namespaces.Message.SuggestedPostCloud, Namespaces.Message.SuggestedPostLocal])
public static let allNonRegular: Set<Int32> = Set([Namespaces.Message.ScheduledCloud, Namespaces.Message.ScheduledLocal, Namespaces.Message.QuickReplyCloud, Namespaces.Message.QuickReplyLocal, Namespaces.Message.SuggestedPostCloud, Namespaces.Message.SuggestedPostLocal])
public static let allLocal: [Int32] = [
Namespaces.Message.Local,
Namespaces.Message.SecretIncoming,

View File

@ -701,6 +701,9 @@ func fetchRemoteMessage(accountPeerId: PeerId, postbox: Postbox, source: FetchMe
} else {
signal = .never()
}
} else if id.namespace == Namespaces.Message.SuggestedPostCloud {
//TODO:release
signal = .never()
} else if id.peerId.namespace == Namespaces.Peer.CloudChannel {
if let channel = peer.inputChannel {
signal = source.request(Api.functions.channels.getMessages(channel: channel, id: [Api.InputMessage.inputMessageID(id: id.id)]))

View File

@ -49,6 +49,7 @@ swift_library(
"-warnings-as-errors",
],
deps = [
"//third-party/recaptcha:RecaptchaEnterprise",
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
"//submodules/SSignalKit/SSignalKit:SSignalKit",
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
@ -474,7 +475,7 @@ swift_library(
"//submodules/Components/BlurredBackgroundComponent",
"//submodules/TelegramUI/Components/CheckComponent",
"//submodules/TelegramUI/Components/MarqueeComponent",
"//third-party/recaptcha:RecaptchaEnterprise",
"//submodules/TelegramUI/Components/PeerInfo/PostSuggestionsSettingsScreen",
] + select({
"@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets,
"//build-system:ios_sim_arm64": [],

View File

@ -149,6 +149,7 @@ public final class ChatChannelSubscriberInputPanelNode: ChatInputPanelNode {
private let helpButton: HighlightableButtonNode
private let giftButton: HighlightableButtonNode
private let suggestedPostButton: HighlightableButtonNode
private var action: SubscriberAction?
@ -182,6 +183,8 @@ public final class ChatChannelSubscriberInputPanelNode: ChatInputPanelNode {
self.helpButton.isHidden = true
self.giftButton = HighlightableButtonNode()
self.giftButton.isHidden = true
self.suggestedPostButton = HighlightableButtonNode()
self.suggestedPostButton.isHidden = true
self.discussButton.addSubnode(self.discussButtonText)
self.discussButton.addSubnode(self.badgeBackground)
@ -196,11 +199,12 @@ public final class ChatChannelSubscriberInputPanelNode: ChatInputPanelNode {
self.view.addSubview(self.activityIndicator)
self.addSubnode(self.helpButton)
self.addSubnode(self.giftButton)
self.addSubnode(self.suggestedPostButton)
self.button.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside)
self.discussButton.addTarget(self, action: #selector(self.discussPressed), forControlEvents: .touchUpInside)
self.helpButton.addTarget(self, action: #selector(self.helpPressed), forControlEvents: .touchUpInside)
self.giftButton.addTarget(self, action: #selector(self.giftPressed), forControlEvents: .touchUpInside)
self.suggestedPostButton.addTarget(self, action: #selector(self.suggestedPostPressed), forControlEvents: .touchUpInside)
}
deinit {
@ -222,6 +226,10 @@ public final class ChatChannelSubscriberInputPanelNode: ChatInputPanelNode {
@objc private func helpPressed() {
self.interfaceInteraction?.presentGigagroupHelp()
}
@objc private func suggestedPostPressed() {
self.interfaceInteraction?.openSuggestPost()
}
@objc private func buttonPressed() {
guard let context = self.context, let action = self.action, let presentationInterfaceState = self.presentationInterfaceState, let peer = presentationInterfaceState.renderedPeer?.peer else {
@ -369,6 +377,7 @@ public final class ChatChannelSubscriberInputPanelNode: ChatInputPanelNode {
if previousState?.theme !== interfaceState.theme {
self.badgeBackground.image = PresentationResourcesChatList.badgeBackgroundActive(interfaceState.theme, diameter: 20.0)
self.helpButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/Help"), color: interfaceState.theme.chat.inputPanel.panelControlAccentColor), for: .normal)
self.suggestedPostButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/Gift"), color: interfaceState.theme.chat.inputPanel.panelControlAccentColor), for: .normal)
self.giftButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/Gift"), color: interfaceState.theme.chat.inputPanel.panelControlAccentColor), for: .normal)
}
@ -420,18 +429,26 @@ public final class ChatChannelSubscriberInputPanelNode: ChatInputPanelNode {
self.giftButton.isHidden = false
self.helpButton.isHidden = true
//TODO:release
self.suggestedPostButton.isHidden = false
self.presentGiftTooltip()
} else if case .broadcast = peer.info {
self.giftButton.isHidden = true
self.helpButton.isHidden = true
self.suggestedPostButton.isHidden = false
} else if peer.flags.contains(.isGigagroup), self.action == .muteNotifications || self.action == .unmuteNotifications {
self.giftButton.isHidden = true
self.helpButton.isHidden = false
self.suggestedPostButton.isHidden = true
} else {
self.giftButton.isHidden = true
self.helpButton.isHidden = true
self.suggestedPostButton.isHidden = true
}
} else {
self.giftButton.isHidden = true
self.helpButton.isHidden = true
self.suggestedPostButton.isHidden = true
}
if let action = self.action, action == .muteNotifications || action == .unmuteNotifications {
let buttonWidth = self.button.calculateSizeThatFits(CGSize(width: width, height: panelHeight)).width + 24.0
@ -441,9 +458,11 @@ public final class ChatChannelSubscriberInputPanelNode: ChatInputPanelNode {
}
self.giftButton.frame = CGRect(x: width - rightInset - panelHeight - 5.0, y: 0.0, width: panelHeight, height: panelHeight)
self.helpButton.frame = CGRect(x: width - rightInset - panelHeight, y: 0.0, width: panelHeight, height: panelHeight)
self.suggestedPostButton.frame = CGRect(x: leftInset + 5.0, y: 0.0, width: panelHeight, height: panelHeight)
} else {
self.giftButton.isHidden = true
self.helpButton.isHidden = true
self.suggestedPostButton.isHidden = true
let availableWidth = min(600.0, width - leftInset - rightInset)
let leftOffset = floor((width - availableWidth) / 2.0)

View File

@ -780,6 +780,10 @@ private final class ChatEmptyNodeCloudChatContent: ASDisplayNode, ChatEmptyNodeC
insets.top = -9.0
imageSpacing = 4.0
titleSpacing = 5.0
case .postSuggestions:
insets.top = 10.0
imageSpacing = 5.0
titleSpacing = 5.0
case .hashTagSearch:
break
}
@ -841,7 +845,7 @@ private final class ChatEmptyNodeCloudChatContent: ASDisplayNode, ChatEmptyNodeC
}
self.businessLink = link
case .hashTagSearch:
case .hashTagSearch, .postSuggestions:
titleString = ""
strings = []
}
@ -1297,7 +1301,11 @@ public final class ChatEmptyNodePremiumRequiredChatContent: ASDisplayNode, ChatE
if let amount = self.stars {
let starsString = presentationStringsFormattedNumber(Int32(amount), interfaceState.dateTimeFormat.groupingSeparator)
let rawText: String
if self.isPremiumDisabled {
if case let .customChatContents(customChatContents) = interfaceState.subject, case .postSuggestions = customChatContents.kind {
//TODO:localize
rawText = "\(peerTitle) charges $ \(starsString) per message suggestion."
} else if self.isPremiumDisabled {
rawText = interfaceState.strings.Chat_EmptyStatePaidMessagingDisabled_Text(peerTitle, " $ \(starsString)").string
} else {
rawText = interfaceState.strings.Chat_EmptyStatePaidMessaging_Text(peerTitle, " $ \(starsString)").string
@ -1427,6 +1435,7 @@ private enum ChatEmptyNodeContentType: Equatable {
case topic
case premiumRequired
case starsRequired(Int64)
case postSuggestions(Int64)
}
private final class EmptyAttachedDescriptionNode: HighlightTrackingButtonNode {
@ -1795,8 +1804,12 @@ public final class ChatEmptyNode: ASDisplayNode {
case let .emptyChat(emptyType):
if case .customGreeting = emptyType {
contentType = .greeting
} else if case .customChatContents = interfaceState.subject {
contentType = .cloud
} else if case let .customChatContents(customChatContents) = interfaceState.subject {
if case let .postSuggestions(postSuggestions) = customChatContents.kind {
contentType = .postSuggestions(postSuggestions.value)
} else {
contentType = .cloud
}
} else if case .replyThread = interfaceState.chatLocation {
if case .topic = emptyType {
contentType = .topic
@ -1883,6 +1896,8 @@ public final class ChatEmptyNode: ASDisplayNode {
node = ChatEmptyNodePremiumRequiredChatContent(context: self.context, interaction: self.interaction, stars: nil)
case let .starsRequired(stars):
node = ChatEmptyNodePremiumRequiredChatContent(context: self.context, interaction: self.interaction, stars: stars)
case let .postSuggestions(stars):
node = ChatEmptyNodePremiumRequiredChatContent(context: self.context, interaction: self.interaction, stars: stars)
}
self.content = (contentType, node)
self.addSubnode(node)
@ -1894,7 +1909,7 @@ public final class ChatEmptyNode: ASDisplayNode {
}
}
switch contentType {
case .peerNearby, .greeting, .premiumRequired, .starsRequired, .cloud:
case .peerNearby, .greeting, .premiumRequired, .starsRequired, .cloud, .postSuggestions:
self.isUserInteractionEnabled = true
default:
self.isUserInteractionEnabled = false

View File

@ -88,6 +88,7 @@ swift_library(
"//submodules/AnimatedStickerNode",
"//submodules/TelegramAnimatedStickerNode",
"//submodules/TelegramUI/Components/LottieMetal",
"//submodules/TelegramStringFormatting",
],
visibility = [
"//visibility:public",

View File

@ -618,6 +618,8 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
private let shadowNode: ChatMessageShadowNode
private var clippingNode: ChatMessageBubbleClippingNode
private var suggestedPostInfoNode: ChatMessageSuggestedPostInfoNode?
override public var extractedBackgroundNode: ASDisplayNode? {
return self.shadowNode
}
@ -1422,6 +1424,8 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
let weakSelf = Weak(self)
let makeSuggestedPostInfoNodeLayout: ChatMessageSuggestedPostInfoNode.AsyncLayout = ChatMessageSuggestedPostInfoNode.asyncLayout(self.suggestedPostInfoNode)
return { item, params, mergedTop, mergedBottom, dateHeaderAtBottom in
let layoutConstants = chatMessageItemLayoutConstants(layoutConstants, params: params, presentationData: item.presentationData)
return ChatMessageBubbleItemNode.beginLayout(selfReference: weakSelf, item, params, mergedTop, mergedBottom, dateHeaderAtBottom,
@ -1438,6 +1442,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
unlockButtonLayout: unlockButtonLayout,
mediaInfoLayout: mediaInfoLayout,
mosaicStatusLayout: mosaicStatusLayout,
makeSuggestedPostInfoNodeLayout: makeSuggestedPostInfoNodeLayout,
layoutConstants: layoutConstants,
currentItem: currentItem,
currentForwardInfo: currentForwardInfo,
@ -1466,6 +1471,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
unlockButtonLayout: (ChatMessageUnlockMediaNode.Arguments) -> (CGSize, (Bool) -> ChatMessageUnlockMediaNode),
mediaInfoLayout: (ChatMessageStarsMediaInfoNode.Arguments) -> (CGSize, (Bool) -> ChatMessageStarsMediaInfoNode),
mosaicStatusLayout: (ChatMessageDateAndStatusNode.Arguments) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> ChatMessageDateAndStatusNode)),
makeSuggestedPostInfoNodeLayout: ChatMessageSuggestedPostInfoNode.AsyncLayout,
layoutConstants: ChatMessageItemLayoutConstants,
currentItem: ChatMessageItem?,
currentForwardInfo: (Peer?, String?)?,
@ -2935,6 +2941,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
var totalContentNodesHeight: CGFloat = 0.0
var currentContainerGroupOverlap: CGFloat = 0.0
var detachedContentNodesHeight: CGFloat = 0.0
var additionalTopHeight: CGFloat = 0.0
var mosaicStatusOrigin: CGPoint?
var unlockButtonPosition: CGPoint?
@ -3110,6 +3117,19 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
}
reactionButtonsSizeAndApply = reactionButtonsFinalize(maxContentWidth)
}
var suggestedPostInfoNodeLayout: (CGSize, () -> ChatMessageSuggestedPostInfoNode)?
for attribute in item.message.attributes {
if let attribute = attribute as? OutgoingSuggestedPostMessageAttribute {
let _ = attribute
let suggestedPostInfoNodeLayoutValue = makeSuggestedPostInfoNodeLayout(item, baseWidth)
suggestedPostInfoNodeLayout = suggestedPostInfoNodeLayoutValue
}
}
if let suggestedPostInfoNodeLayout {
additionalTopHeight += 4.0 + suggestedPostInfoNodeLayout.0.height + 8.0
}
let minimalContentSize: CGSize
if hideBackground {
@ -3130,7 +3150,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
let contentUpperRightCorner: CGPoint
switch alignment {
case .none:
backgroundFrame = CGRect(origin: CGPoint(x: incoming ? (params.leftInset + layoutConstants.bubble.edgeInset + avatarInset) : (params.width - params.rightInset - layoutBubbleSize.width - layoutConstants.bubble.edgeInset - deliveryFailedInset), y: detachedContentNodesHeight), size: layoutBubbleSize)
backgroundFrame = CGRect(origin: CGPoint(x: incoming ? (params.leftInset + layoutConstants.bubble.edgeInset + avatarInset) : (params.width - params.rightInset - layoutBubbleSize.width - layoutConstants.bubble.edgeInset - deliveryFailedInset), y: detachedContentNodesHeight + additionalTopHeight), size: layoutBubbleSize)
contentOrigin = CGPoint(x: backgroundFrame.origin.x + (incoming ? layoutConstants.bubble.contentInsets.left : layoutConstants.bubble.contentInsets.right), y: backgroundFrame.origin.y + layoutConstants.bubble.contentInsets.top + headerSize.height + contentVerticalOffset)
contentUpperRightCorner = CGPoint(x: backgroundFrame.maxX - (incoming ? layoutConstants.bubble.contentInsets.right : layoutConstants.bubble.contentInsets.left), y: backgroundFrame.origin.y + layoutConstants.bubble.contentInsets.top + headerSize.height)
case .center:
@ -3147,7 +3167,8 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
let bubbleContentWidth = maxContentWidth - layoutConstants.bubble.edgeInset * 2.0 - (layoutConstants.bubble.contentInsets.right + layoutConstants.bubble.contentInsets.left)
var layoutSize = CGSize(width: params.width, height: layoutBubbleSize.height + detachedContentNodesHeight)
var layoutSize = CGSize(width: params.width, height: layoutBubbleSize.height + detachedContentNodesHeight + additionalTopHeight)
if let reactionButtonsSizeAndApply = reactionButtonsSizeAndApply {
layoutSize.height += 4.0 + reactionButtonsSizeAndApply.0.height + 2.0
}
@ -3207,7 +3228,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
nameNodeSizeApply: nameNodeSizeApply,
viaWidth: viaWidth,
contentOrigin: contentOrigin,
nameNodeOriginY: nameNodeOriginY + detachedContentNodesHeight,
nameNodeOriginY: nameNodeOriginY + detachedContentNodesHeight + additionalTopHeight,
authorNameColor: authorNameColor,
layoutConstants: layoutConstants,
currentCredibilityIcon: currentCredibilityIcon,
@ -3215,11 +3236,11 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
boostNodeSizeApply: boostNodeSizeApply,
contentUpperRightCorner: contentUpperRightCorner,
threadInfoSizeApply: threadInfoSizeApply,
threadInfoOriginY: threadInfoOriginY + detachedContentNodesHeight,
threadInfoOriginY: threadInfoOriginY + detachedContentNodesHeight + additionalTopHeight,
forwardInfoSizeApply: forwardInfoSizeApply,
forwardInfoOriginY: forwardInfoOriginY + detachedContentNodesHeight,
forwardInfoOriginY: forwardInfoOriginY + detachedContentNodesHeight + additionalTopHeight,
replyInfoSizeApply: replyInfoSizeApply,
replyInfoOriginY: replyInfoOriginY + detachedContentNodesHeight,
replyInfoOriginY: replyInfoOriginY + detachedContentNodesHeight + additionalTopHeight,
removedContentNodeIndices: removedContentNodeIndices,
updatedContentNodeOrder: updatedContentNodeOrder,
addedContentNodes: addedContentNodes,
@ -3237,6 +3258,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
avatarOffset: avatarOffset,
hidesHeaders: hidesHeaders,
disablesComments: disablesComments,
suggestedPostInfoNodeLayout: suggestedPostInfoNodeLayout,
alignment: alignment
)
})
@ -3297,6 +3319,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
avatarOffset: CGFloat?,
hidesHeaders: Bool,
disablesComments: Bool,
suggestedPostInfoNodeLayout: (CGSize, () -> ChatMessageSuggestedPostInfoNode)?,
alignment: ChatMessageBubbleContentAlignment
) -> Void {
guard let strongSelf = selfReference.value else {
@ -3379,6 +3402,22 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
strongSelf.backgroundNode.backgroundFrame = backgroundFrame
if let (suggestedPostInfoSize, suggestedPostInfoApply) = suggestedPostInfoNodeLayout {
let suggestedPostInfoNode = suggestedPostInfoApply()
if suggestedPostInfoNode !== strongSelf.suggestedPostInfoNode {
strongSelf.suggestedPostInfoNode?.removeFromSupernode()
strongSelf.suggestedPostInfoNode = suggestedPostInfoNode
strongSelf.mainContextSourceNode.contentNode.addSubnode(suggestedPostInfoNode)
let suggestedPostInfoFrame = CGRect(origin: CGPoint(x: floor((params.width - suggestedPostInfoSize.width) * 0.5), y: 4.0), size: suggestedPostInfoSize)
suggestedPostInfoNode.frame = suggestedPostInfoFrame
//animation.animator.updateFrame(layer: suggestedPostInfoNode.layer, frame: suggestedPostInfoFrame, completion: nil)
}
} else if let suggestedPostInfoNode = strongSelf.suggestedPostInfoNode {
strongSelf.suggestedPostInfoNode = nil
suggestedPostInfoNode.removeFromSupernode()
}
if let avatarOffset = avatarOffset {
strongSelf.updateAttachedAvatarNodeOffset(offset: avatarOffset, transition: .animated(duration: 0.3, curve: .spring))
}

View File

@ -0,0 +1,163 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import SwiftSignalKit
import Postbox
import TelegramCore
import TelegramPresentationData
import TelegramUIPreferences
import TextFormat
import AccountContext
import WallpaperBackgroundNode
import ChatMessageItem
import TelegramStringFormatting
public final class ChatMessageSuggestedPostInfoNode: ASDisplayNode {
private var titleNode: TextNode?
private var priceLabelNode: TextNode?
private var priceValueNode: TextNode?
private var timeLabelNode: TextNode?
private var timeValueNode: TextNode?
private var backgroundNode: WallpaperBubbleBackgroundNode?
override public init() {
super.init()
}
public typealias AsyncLayout = (ChatMessageItem, CGFloat) -> (CGSize, () -> ChatMessageSuggestedPostInfoNode)
public static func asyncLayout(_ node: ChatMessageSuggestedPostInfoNode?) -> (ChatMessageItem, CGFloat) -> (CGSize, () -> ChatMessageSuggestedPostInfoNode) {
let makeTitleLayout = TextNode.asyncLayout(node?.titleNode)
let makePriceLabelLayout = TextNode.asyncLayout(node?.priceLabelNode)
let makePriceValueLayout = TextNode.asyncLayout(node?.priceValueNode)
let makeTimeLabelLayout = TextNode.asyncLayout(node?.timeLabelNode)
let makeTimeValueLayout = TextNode.asyncLayout(node?.timeValueNode)
return { item, maxWidth in
let insets = UIEdgeInsets(
top: 12.0,
left: 12.0,
bottom: 12.0,
right: 12.0
)
let titleSpacing: CGFloat = 8.0
let labelSpacing: CGFloat = 8.0
let valuesVerticalSpacing: CGFloat = 2.0
var amount: Int64 = 0
var timestamp: Int32?
for attribute in item.message.attributes {
if let attribute = attribute as? OutgoingSuggestedPostMessageAttribute {
amount = attribute.price.value
timestamp = attribute.timestamp
}
}
//TODO:localize
let amountString: String
if amount == 0 {
amountString = "Free"
} else if amount == 1 {
amountString = "1 Star"
} else {
amountString = "\(amount) Stars"
}
var timestampString: String
if let timestamp {
timestampString = humanReadableStringForTimestamp(strings: item.presentationData.strings, dateTimeFormat: PresentationDateTimeFormat(), timestamp: timestamp, alwaysShowTime: true).string
if timestampString.count > 1 {
timestampString = String(timestampString[timestampString.startIndex]).capitalized + timestampString[timestampString.index(after: timestampString.startIndex)...]
}
} else {
timestampString = "Anytime"
}
let serviceColor = serviceMessageColorComponents(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper)
//TODO:localize
let titleLayout = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: "You suggest to post\nthis message.", font: Font.regular(13.0), textColor: serviceColor.primaryText), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: maxWidth - insets.left - insets.right, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
let priceLabelLayout = makePriceLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: "Price", font: Font.regular(13.0), textColor: serviceColor.primaryText.withMultipliedAlpha(0.5)), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: maxWidth - insets.left - insets.right, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let timeLabelLayout = makeTimeLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: "Time", font: Font.regular(13.0), textColor: serviceColor.primaryText.withMultipliedAlpha(0.5)), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: maxWidth - insets.left - insets.right, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let priceValueLayout = makePriceValueLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: amountString, font: Font.semibold(13.0), textColor: serviceColor.primaryText), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: maxWidth - insets.left - insets.right, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let timeValueLayout = makeTimeValueLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: timestampString, font: Font.semibold(13.0), textColor: serviceColor.primaryText), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: maxWidth - insets.left - insets.right, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
var maxContentWidth: CGFloat = 0.0
var contentHeight: CGFloat = 0.0
maxContentWidth = max(maxContentWidth, titleLayout.0.size.width)
contentHeight += titleLayout.0.size.height
contentHeight += titleSpacing
maxContentWidth = max(maxContentWidth, priceLabelLayout.0.size.width + labelSpacing + priceValueLayout.0.size.width)
contentHeight += priceLabelLayout.0.size.height + valuesVerticalSpacing
maxContentWidth = max(maxContentWidth, timeLabelLayout.0.size.width + labelSpacing + timeValueLayout.0.size.width)
contentHeight += timeLabelLayout.0.size.height
let size = CGSize(width: insets.left + insets.right + maxContentWidth, height: insets.top + insets.bottom + contentHeight)
return (size, {
let node = node ?? ChatMessageSuggestedPostInfoNode()
if node.backgroundNode == nil {
if let backgroundNode = item.controllerInteraction.presentationContext.backgroundNode?.makeBubbleBackground(for: .free) {
node.backgroundNode = backgroundNode
backgroundNode.layer.masksToBounds = true
backgroundNode.layer.cornerRadius = 15.0
node.insertSubnode(backgroundNode, at: 0)
}
}
if let backgroundNode = node.backgroundNode {
backgroundNode.frame = CGRect(origin: CGPoint(), size: size)
}
let titleNode = titleLayout.1()
if node.titleNode !== titleNode {
node.titleNode = titleNode
node.addSubnode(titleNode)
}
let priceLabelNode = priceLabelLayout.1()
if node.priceLabelNode !== priceLabelNode {
node.priceLabelNode = priceLabelNode
node.addSubnode(priceLabelNode)
}
let priceValueNode = priceValueLayout.1()
if node.priceValueNode !== priceValueNode {
node.priceValueNode = priceValueNode
node.addSubnode(priceValueNode)
}
let timeLabelNode = timeLabelLayout.1()
if node.timeLabelNode !== timeLabelNode {
node.timeLabelNode = timeLabelNode
node.addSubnode(timeLabelNode)
}
let timeValueNode = timeValueLayout.1()
if node.timeValueNode !== timeValueNode {
node.timeValueNode = timeValueNode
node.addSubnode(timeValueNode)
}
let titleFrame = CGRect(origin: CGPoint(x: floor((size.width - titleLayout.0.size.width) * 0.5), y: insets.top), size: titleLayout.0.size)
titleNode.frame = titleFrame
let priceLabelFrame = CGRect(origin: CGPoint(x: insets.left, y: titleFrame.maxY + titleSpacing), size: priceLabelLayout.0.size)
priceLabelNode.frame = priceLabelFrame
priceValueNode.frame = CGRect(origin: CGPoint(x: priceLabelFrame.maxX + labelSpacing, y: priceLabelFrame.minY), size: priceValueLayout.0.size)
let timeLabelFrame = CGRect(origin: CGPoint(x: insets.left, y: priceLabelFrame.maxY + valuesVerticalSpacing), size: timeLabelLayout.0.size)
timeLabelNode.frame = timeLabelFrame
timeValueNode.frame = CGRect(origin: CGPoint(x: timeLabelFrame.maxX + labelSpacing, y: timeLabelFrame.minY), size: timeValueLayout.0.size)
return node
})
}
}
}

View File

@ -95,6 +95,17 @@ public func stringForMessageTimestampStatus(accountPeerId: PeerId, message: Mess
dateText = " "
}
for attribute in message.attributes {
if let attribute = attribute as? OutgoingSuggestedPostMessageAttribute {
if let timestamp = attribute.timestamp {
dateText = stringForMessageTimestamp(timestamp: timestamp, dateTimeFormat: dateTimeFormat)
} else {
//TODO:localize
dateText = "Anytime"
}
}
}
if message.id.namespace == Namespaces.Message.ScheduledCloud, let _ = message.pendingProcessingAttribute {
return "appx. \(dateText)"
}

View File

@ -367,6 +367,10 @@ public final class ChatMessageItemImpl: ChatMessageItem, CustomStringConvertible
}
}
if let subject = associatedData.subject, case let .customChatContents(contents) = subject, case .postSuggestions = contents.kind {
hasAvatar = false
}
if hasAvatar {
if let effectiveAuthor = effectiveAuthor {
var storyStats: PeerStoryStats?

View File

@ -147,6 +147,7 @@ public final class ChatRecentActionsController: TelegramBaseController {
}, joinGroupCall: { _ in
}, presentInviteMembers: {
}, presentGigagroupHelp: {
}, openSuggestPost: {
}, editMessageMedia: { _, _ in
}, updateShowCommands: { _ in
}, updateShowSendAsPeers: { _ in

View File

@ -35,6 +35,8 @@ swift_library(
"//submodules/CheckNode",
"//submodules/TextFormat",
"//submodules/TelegramUI/Components/Stars/StarsBalanceOverlayComponent",
"//submodules/TelegramStringFormatting",
"//submodules/TelegramUI/Components/ChatScheduleTimeController",
],
visibility = [
"//visibility:public",

View File

@ -1816,6 +1816,8 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
case .businessLinkSetup:
stickerContent = nil
gifContent = nil
case .postSuggestions:
break
}
}

View File

@ -11,6 +11,7 @@ import TelegramPresentationData
public enum ChatScheduleTimeControllerMode {
case scheduledMessages(sendWhenOnlineAvailable: Bool)
case reminders
case suggestPost
}
public enum ChatScheduleTimeControllerStyle {
@ -82,7 +83,11 @@ public final class ChatScheduleTimeController: ViewController {
guard let strongSelf = self else {
return
}
strongSelf.completion(time == scheduleWhenOnlineTimestamp ? time : time + 5)
if time == 0 {
strongSelf.completion(time)
} else {
strongSelf.completion(time == scheduleWhenOnlineTimestamp ? time : time + 5)
}
strongSelf.dismiss()
}
self.controllerNode.dismiss = { [weak self] in

View File

@ -94,10 +94,13 @@ class ChatScheduleTimeControllerNode: ViewControllerTracingNode, ASScrollViewDel
let title: String
switch mode {
case .scheduledMessages:
title = self.presentationData.strings.Conversation_ScheduleMessage_Title
case .reminders:
title = self.presentationData.strings.Conversation_SetReminder_Title
case .scheduledMessages:
title = self.presentationData.strings.Conversation_ScheduleMessage_Title
case .reminders:
title = self.presentationData.strings.Conversation_SetReminder_Title
case .suggestPost:
//TODO:localize
title = "Time"
}
self.titleNode = ASTextNode()
@ -113,7 +116,13 @@ class ChatScheduleTimeControllerNode: ViewControllerTracingNode, ASScrollViewDel
self.doneButton = SolidRoundedButtonNode(theme: SolidRoundedButtonTheme(theme: self.presentationData.theme), height: 52.0, cornerRadius: 11.0, gloss: false)
self.onlineButton = SolidRoundedButtonNode(theme: SolidRoundedButtonTheme(backgroundColor: buttonColor, foregroundColor: buttonTextColor), font: .regular, height: 52.0, cornerRadius: 11.0, gloss: false)
self.onlineButton.title = self.presentationData.strings.Conversation_ScheduleMessage_SendWhenOnline
switch mode {
case .suggestPost:
//TODO:localize
self.onlineButton.title = "Send Anytime"
default:
self.onlineButton.title = self.presentationData.strings.Conversation_ScheduleMessage_SendWhenOnline
}
self.dateFormatter = DateFormatter()
self.dateFormatter.timeStyle = .none
@ -141,6 +150,8 @@ class ChatScheduleTimeControllerNode: ViewControllerTracingNode, ASScrollViewDel
self.contentContainerNode.addSubnode(self.doneButton)
if case .scheduledMessages(true) = self.mode {
self.contentContainerNode.addSubnode(self.onlineButton)
} else if case .suggestPost = self.mode {
self.contentContainerNode.addSubnode(self.onlineButton)
}
self.cancelButton.addTarget(self, action: #selector(self.cancelButtonPressed), forControlEvents: .touchUpInside)
@ -159,7 +170,12 @@ class ChatScheduleTimeControllerNode: ViewControllerTracingNode, ASScrollViewDel
self.onlineButton.pressed = { [weak self] in
if let strongSelf = self {
strongSelf.onlineButton.isUserInteractionEnabled = false
strongSelf.completion?(scheduleWhenOnlineTimestamp)
switch strongSelf.mode {
case .suggestPost:
strongSelf.completion?(0)
default:
strongSelf.completion?(scheduleWhenOnlineTimestamp)
}
}
}
@ -273,22 +289,30 @@ class ChatScheduleTimeControllerNode: ViewControllerTracingNode, ASScrollViewDel
let time = stringForMessageTimestamp(timestamp: Int32(date.timeIntervalSince1970), dateTimeFormat: self.presentationData.dateTimeFormat)
switch mode {
case .scheduledMessages:
if calendar.isDateInToday(date) {
self.doneButton.title = self.presentationData.strings.Conversation_ScheduleMessage_SendToday(time).string
} else if calendar.isDateInTomorrow(date) {
self.doneButton.title = self.presentationData.strings.Conversation_ScheduleMessage_SendTomorrow(time).string
} else {
self.doneButton.title = self.presentationData.strings.Conversation_ScheduleMessage_SendOn(self.dateFormatter.string(from: date), time).string
}
case .reminders:
if calendar.isDateInToday(date) {
self.doneButton.title = self.presentationData.strings.Conversation_SetReminder_RemindToday(time).string
} else if calendar.isDateInTomorrow(date) {
self.doneButton.title = self.presentationData.strings.Conversation_SetReminder_RemindTomorrow(time).string
} else {
self.doneButton.title = self.presentationData.strings.Conversation_SetReminder_RemindOn(self.dateFormatter.string(from: date), time).string
}
case .scheduledMessages:
if calendar.isDateInToday(date) {
self.doneButton.title = self.presentationData.strings.Conversation_ScheduleMessage_SendToday(time).string
} else if calendar.isDateInTomorrow(date) {
self.doneButton.title = self.presentationData.strings.Conversation_ScheduleMessage_SendTomorrow(time).string
} else {
self.doneButton.title = self.presentationData.strings.Conversation_ScheduleMessage_SendOn(self.dateFormatter.string(from: date), time).string
}
case .reminders:
if calendar.isDateInToday(date) {
self.doneButton.title = self.presentationData.strings.Conversation_SetReminder_RemindToday(time).string
} else if calendar.isDateInTomorrow(date) {
self.doneButton.title = self.presentationData.strings.Conversation_SetReminder_RemindTomorrow(time).string
} else {
self.doneButton.title = self.presentationData.strings.Conversation_SetReminder_RemindOn(self.dateFormatter.string(from: date), time).string
}
case .suggestPost:
if calendar.isDateInToday(date) {
self.doneButton.title = self.presentationData.strings.Conversation_ScheduleMessage_SendToday(time).string
} else if calendar.isDateInTomorrow(date) {
self.doneButton.title = self.presentationData.strings.Conversation_ScheduleMessage_SendTomorrow(time).string
} else {
self.doneButton.title = self.presentationData.strings.Conversation_ScheduleMessage_SendOn(self.dateFormatter.string(from: date), time).string
}
}
}
@ -382,6 +406,8 @@ class ChatScheduleTimeControllerNode: ViewControllerTracingNode, ASScrollViewDel
var buttonOffset: CGFloat = 0.0
if case .scheduledMessages(true) = self.mode {
buttonOffset += 64.0
} else if case .suggestPost = self.mode {
buttonOffset += 64.0
}
let bottomInset: CGFloat = 10.0 + cleanInsets.bottom

View File

@ -15,14 +15,16 @@ public final class ListItemSliderSelectorComponent: Component {
public let selectedIndex: Int
public let minSelectedIndex: Int?
public let title: String?
public let secondaryTitle: String?
public let selectedIndexUpdated: (Int) -> Void
public init(values: [String], markPositions: Bool, selectedIndex: Int, minSelectedIndex: Int? = nil, title: String?, selectedIndexUpdated: @escaping (Int) -> Void) {
public init(values: [String], markPositions: Bool, selectedIndex: Int, minSelectedIndex: Int? = nil, title: String?, secondaryTitle: String? = nil, selectedIndexUpdated: @escaping (Int) -> Void) {
self.values = values
self.markPositions = markPositions
self.selectedIndex = selectedIndex
self.minSelectedIndex = minSelectedIndex
self.title = title
self.secondaryTitle = secondaryTitle
self.selectedIndexUpdated = selectedIndexUpdated
}
@ -42,6 +44,9 @@ public final class ListItemSliderSelectorComponent: Component {
if lhs.title != rhs.title {
return false
}
if lhs.secondaryTitle != rhs.secondaryTitle {
return false
}
return true
}
}
@ -112,6 +117,7 @@ public final class ListItemSliderSelectorComponent: Component {
public final class View: UIView, ListSectionComponent.ChildView {
private var titles: [Int: ComponentView<Empty>] = [:]
private var mainTitle: ComponentView<Empty>?
private var secondaryTitle: ComponentView<Empty>?
private var slider = ComponentView<Empty>()
private var component: ListItemSliderSelectorComponent?
@ -140,10 +146,12 @@ public final class ListItemSliderSelectorComponent: Component {
var validIds: [Int] = []
var mainTitleValue: String?
var secondaryTitleValue: String?
switch component.content {
case let .discrete(discrete):
mainTitleValue = discrete.title
secondaryTitleValue = discrete.secondaryTitle
for i in 0 ..< discrete.values.count {
if discrete.title != nil {
@ -254,7 +262,44 @@ public final class ListItemSliderSelectorComponent: Component {
environment: {},
containerSize: CGSize(width: 100.0, height: 100.0)
)
let mainTitleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - mainTitleSize.width) * 0.5), y: 10.0), size: mainTitleSize)
var secondaryTitleView: ComponentView<Empty>?
var secondaryTitleSize: CGSize?
if let secondaryTitleValue {
let secondaryTitle: ComponentView<Empty>
if let current = self.secondaryTitle {
secondaryTitle = current
} else {
secondaryTitle = ComponentView()
mainTitleTransition = mainTitleTransition.withAnimation(.none)
self.secondaryTitle = secondaryTitle
}
secondaryTitleView = secondaryTitle
secondaryTitleSize = secondaryTitle.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: secondaryTitleValue, font: Font.regular(12.0), textColor: component.theme.list.itemSecondaryTextColor))
)),
environment: {},
containerSize: CGSize(width: 100.0, height: 100.0)
)
} else {
mainTitleTransition = mainTitleTransition.withAnimation(.none)
if let secondaryTitle = self.secondaryTitle {
self.secondaryTitle = nil
secondaryTitle.view?.removeFromSuperview()
}
}
var mainTitleContentWidth = mainTitleSize.width
let secondaryTitleSpacing: CGFloat = 2.0
if let secondaryTitleSize {
mainTitleContentWidth += secondaryTitleSpacing + secondaryTitleSize.width
}
let mainTitleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - mainTitleContentWidth) * 0.5), y: 10.0), size: mainTitleSize)
if let mainTitleView = mainTitle.view {
if mainTitleView.superview == nil {
self.addSubview(mainTitleView)
@ -262,11 +307,26 @@ public final class ListItemSliderSelectorComponent: Component {
mainTitleView.bounds = CGRect(origin: CGPoint(), size: mainTitleFrame.size)
mainTitleTransition.setPosition(view: mainTitleView, position: mainTitleFrame.center)
}
if let secondaryTitleView, let secondaryTitleSize {
let secondaryTitleFrame = CGRect(origin: CGPoint(x: mainTitleFrame.maxX + secondaryTitleSpacing, y: mainTitleFrame.minY + floorToScreenPixels((mainTitleFrame.height - secondaryTitleSize.height) * 0.5)), size: secondaryTitleSize)
if let secondaryTitleComponentView = secondaryTitleView.view {
if secondaryTitleComponentView.superview == nil {
self.addSubview(secondaryTitleComponentView)
}
secondaryTitleComponentView.bounds = CGRect(origin: CGPoint(), size: secondaryTitleFrame.size)
mainTitleTransition.setPosition(view: secondaryTitleComponentView, position: secondaryTitleFrame.center)
}
}
} else {
if let mainTitle = self.mainTitle {
self.mainTitle = nil
mainTitle.view?.removeFromSuperview()
}
if let secondaryTitle = self.secondaryTitle {
self.secondaryTitle = nil
secondaryTitle.view?.removeFromSuperview()
}
}
let sliderSize: CGSize

View File

@ -0,0 +1,22 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "ListSwitchItemComponent",
module_name = "ListSwitchItemComponent",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/Display",
"//submodules/ComponentFlow",
"//submodules/TelegramPresentationData",
"//submodules/Components/ComponentDisplayAdapters",
"//submodules/TelegramUI/Components/SwitchComponent",
],
visibility = [
"//visibility:public",
],
)

View File

@ -6,13 +6,13 @@ import ComponentFlow
import ComponentDisplayAdapters
import SwitchComponent
final class ListSwitchItemComponent: Component {
public final class ListSwitchItemComponent: Component {
let theme: PresentationTheme
let title: String
let value: Bool
let valueUpdated: (Bool) -> Void
init(
public init(
theme: PresentationTheme,
title: String,
value: Bool,
@ -24,7 +24,7 @@ final class ListSwitchItemComponent: Component {
self.valueUpdated = valueUpdated
}
static func ==(lhs: ListSwitchItemComponent, rhs: ListSwitchItemComponent) -> Bool {
public static func ==(lhs: ListSwitchItemComponent, rhs: ListSwitchItemComponent) -> Bool {
if lhs.theme !== rhs.theme {
return false
}
@ -37,7 +37,7 @@ final class ListSwitchItemComponent: Component {
return true
}
final class View: UIView {
public final class View: UIView {
private let title = ComponentView<Empty>()
private let switchView = ComponentView<Empty>()
@ -106,11 +106,11 @@ final class ListSwitchItemComponent: Component {
}
}
func makeView() -> View {
public func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}

View File

@ -35,6 +35,7 @@ swift_library(
"//submodules/Components/HierarchyTrackingLayer",
"//submodules/TelegramUI/Components/ListSectionComponent",
"//submodules/TelegramUI/Components/ListItemSliderSelectorComponent",
"//submodules/TelegramUI/Components/ListSwitchItemComponent",
],
visibility = [
"//visibility:public",

View File

@ -23,6 +23,7 @@ import AudioToolbox
import PremiumLockButtonSubtitleComponent
import ListSectionComponent
import ListItemSliderSelectorComponent
import ListSwitchItemComponent
final class PeerAllowedReactionsScreenComponent: Component {
typealias EnvironmentType = ViewControllerComponentContainer.Environment

View File

@ -411,6 +411,7 @@ final class PeerInfoSelectionPanelNode: ASDisplayNode {
}, joinGroupCall: { _ in
}, presentInviteMembers: {
}, presentGigagroupHelp: {
}, openSuggestPost: {
}, editMessageMedia: { _, _ in
}, updateShowCommands: { _ in
}, updateShowSendAsPeers: { _ in
@ -566,6 +567,7 @@ private final class PeerInfoInteraction {
let editingOpenNameColorSetup: () -> Void
let editingOpenInviteLinksSetup: () -> Void
let editingOpenDiscussionGroupSetup: () -> Void
let editingOpenPostSuggestionsSetup: () -> Void
let editingOpenRevenue: () -> Void
let editingOpenStars: () -> Void
let openParticipantsSection: (PeerInfoParticipantsSection) -> Void
@ -636,6 +638,7 @@ private final class PeerInfoInteraction {
editingOpenNameColorSetup: @escaping () -> Void,
editingOpenInviteLinksSetup: @escaping () -> Void,
editingOpenDiscussionGroupSetup: @escaping () -> Void,
editingOpenPostSuggestionsSetup: @escaping () -> Void,
editingOpenRevenue: @escaping () -> Void,
editingOpenStars: @escaping () -> Void,
openParticipantsSection: @escaping (PeerInfoParticipantsSection) -> Void,
@ -705,6 +708,7 @@ private final class PeerInfoInteraction {
self.editingOpenNameColorSetup = editingOpenNameColorSetup
self.editingOpenInviteLinksSetup = editingOpenInviteLinksSetup
self.editingOpenDiscussionGroupSetup = editingOpenDiscussionGroupSetup
self.editingOpenPostSuggestionsSetup = editingOpenPostSuggestionsSetup
self.editingOpenRevenue = editingOpenRevenue
self.editingOpenStars = editingOpenStars
self.openParticipantsSection = openParticipantsSection
@ -2154,6 +2158,7 @@ private func editingItems(data: PeerInfoScreenData?, state: PeerInfoState, chatL
let ItemBanned = 11
let ItemRecentActions = 12
let ItemAffiliatePrograms = 13
let ItemPostSuggestionsSettings = 14
let isCreator = channel.flags.contains(.isCreator)
@ -2200,6 +2205,11 @@ private func editingItems(data: PeerInfoScreenData?, state: PeerInfoState, chatL
items[.peerSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemDiscussionGroup, label: .text(discussionGroupTitle), text: presentationData.strings.Channel_DiscussionGroup, icon: UIImage(bundleImageName: "Chat/Info/GroupDiscussionIcon"), action: {
interaction.editingOpenDiscussionGroupSetup()
}))
//TODO:localize
items[.peerSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemPostSuggestionsSettings, label: .text("Off"), additionalBadgeLabel: presentationData.strings.Settings_New, text: "Post Suggestions", icon: UIImage(bundleImageName: "Chat/Info/GroupDiscussionIcon"), action: {
interaction.editingOpenPostSuggestionsSetup()
}))
}
if isCreator || (channel.adminRights?.rights.contains(.canChangeInfo) == true) {
@ -2996,6 +3006,9 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
editingOpenDiscussionGroupSetup: { [weak self] in
self?.editingOpenDiscussionGroupSetup()
},
editingOpenPostSuggestionsSetup: { [weak self] in
self?.editingOpenPostSuggestionsSetup()
},
editingOpenRevenue: { [weak self] in
self?.editingOpenRevenue()
},
@ -9093,6 +9106,14 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
self.controller?.push(channelDiscussionGroupSetupController(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, peerId: peer.id))
}
private func editingOpenPostSuggestionsSetup() {
guard let data = self.data, let peer = data.peer else {
return
}
let _ = peer
self.controller?.push(self.context.sharedContext.makePostSuggestionsSettingsScreen(context: self.context))
}
private func editingOpenRevenue() {
guard let revenueContext = self.data?.revenueStatsContext else {
return

View File

@ -0,0 +1,38 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "PostSuggestionsSettingsScreen",
module_name = "PostSuggestionsSettingsScreen",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/Display",
"//submodules/TelegramPresentationData",
"//submodules/ComponentFlow",
"//submodules/Components/ComponentDisplayAdapters",
"//submodules/AppBundle",
"//submodules/Components/ViewControllerComponent",
"//submodules/SSignalKit/SwiftSignalKit",
"//submodules/TelegramCore",
"//submodules/Postbox",
"//submodules/AccountContext",
"//submodules/TelegramUI/Components/SwitchComponent",
"//submodules/Components/MultilineTextComponent",
"//submodules/Markdown",
"//submodules/TelegramUI/Components/ButtonComponent",
"//submodules/Components/BundleIconComponent",
"//submodules/TextFormat",
"//submodules/TelegramUI/Components/ListSectionComponent",
"//submodules/TelegramUI/Components/ListItemSliderSelectorComponent",
"//submodules/TelegramUI/Components/ListSwitchItemComponent",
"//submodules/TelegramUI/Components/ListActionItemComponent",
"//submodules/TelegramStringFormatting",
],
visibility = [
"//visibility:public",
],
)

View File

@ -0,0 +1,153 @@
import Foundation
import UIKit
import SwiftSignalKit
import Postbox
import TelegramCore
import AccountContext
public final class PostSuggestionsChatContents: ChatCustomContentsProtocol {
private final class Impl {
let queue: Queue
let context: AccountContext
private var peerId: EnginePeer.Id
private(set) var mergedHistoryView: MessageHistoryView?
private var sourceHistoryView: MessageHistoryView?
private var historyViewDisposable: Disposable?
private var pendingHistoryViewDisposable: Disposable?
let historyViewStream = ValuePipe<(MessageHistoryView, ViewUpdateType)>()
private var nextUpdateIsHoleFill: Bool = false
init(queue: Queue, context: AccountContext, peerId: EnginePeer.Id) {
self.queue = queue
self.context = context
self.peerId = peerId
self.updateHistoryViewRequest(reload: false)
}
deinit {
self.historyViewDisposable?.dispose()
self.pendingHistoryViewDisposable?.dispose()
}
private func updateHistoryViewRequest(reload: Bool) {
self.pendingHistoryViewDisposable?.dispose()
self.pendingHistoryViewDisposable = nil
if self.historyViewDisposable == nil || reload {
self.historyViewDisposable?.dispose()
self.historyViewDisposable = (self.context.account.viewTracker.postSuggestionsViewForLocation(peerId: self.peerId)
|> deliverOn(self.queue)).start(next: { [weak self] view, update, _ in
guard let self else {
return
}
if update == .FillHole {
self.nextUpdateIsHoleFill = true
self.updateHistoryViewRequest(reload: true)
return
}
let nextUpdateIsHoleFill = self.nextUpdateIsHoleFill
self.nextUpdateIsHoleFill = false
self.sourceHistoryView = view
self.updateHistoryView(updateType: nextUpdateIsHoleFill ? .FillHole : .Generic)
})
}
}
private func updateHistoryView(updateType: ViewUpdateType) {
var entries = self.sourceHistoryView?.entries ?? []
entries.sort(by: { $0.message.index < $1.message.index })
let mergedHistoryView = MessageHistoryView(tag: nil, namespaces: .just(Namespaces.Message.allSuggestedPost), entries: entries, holeEarlier: false, holeLater: false, isLoading: false)
self.mergedHistoryView = mergedHistoryView
self.historyViewStream.putNext((mergedHistoryView, updateType))
}
func enqueueMessages(messages: [EnqueueMessage]) {
let _ = (TelegramCore.enqueueMessages(account: self.context.account, peerId: self.peerId, messages: messages.compactMap { message -> EnqueueMessage? in
if !message.attributes.contains(where: { $0 is OutgoingSuggestedPostMessageAttribute }) {
return nil
}
return message
})
|> deliverOn(self.queue)).startStandalone()
}
func deleteMessages(ids: [EngineMessage.Id]) {
let _ = self.context.engine.messages.deleteMessagesInteractively(messageIds: ids, type: .forEveryone).startStandalone()
}
func editMessage(id: EngineMessage.Id, text: String, media: RequestEditMessageMedia, entities: TextEntitiesMessageAttribute?, webpagePreviewAttribute: WebpagePreviewMessageAttribute?, disableUrlPreview: Bool) {
}
}
public let peerId: EnginePeer.Id
public var kind: ChatCustomContentsKind
public var historyView: Signal<(MessageHistoryView, ViewUpdateType), NoError> {
return self.impl.signalWith({ impl, subscriber in
if let mergedHistoryView = impl.mergedHistoryView {
subscriber.putNext((mergedHistoryView, .Initial))
}
return impl.historyViewStream.signal().start(next: subscriber.putNext)
})
}
public var messageLimit: Int? {
return 20
}
private let queue: Queue
private let impl: QueueLocalObject<Impl>
public init(context: AccountContext, peerId: EnginePeer.Id) {
self.peerId = peerId
self.kind = .postSuggestions(price: StarsAmount(value: 250, nanos: 0))
let queue = Queue()
self.queue = queue
self.impl = QueueLocalObject(queue: queue, generate: {
return Impl(queue: queue, context: context, peerId: peerId)
})
}
public func enqueueMessages(messages: [EnqueueMessage]) {
self.impl.with { impl in
impl.enqueueMessages(messages: messages)
}
}
public func deleteMessages(ids: [EngineMessage.Id]) {
self.impl.with { impl in
impl.deleteMessages(ids: ids)
}
}
public func editMessage(id: EngineMessage.Id, text: String, media: RequestEditMessageMedia, entities: TextEntitiesMessageAttribute?, webpagePreviewAttribute: WebpagePreviewMessageAttribute?, disableUrlPreview: Bool) {
self.impl.with { impl in
impl.editMessage(id: id, text: text, media: media, entities: entities, webpagePreviewAttribute: webpagePreviewAttribute, disableUrlPreview: disableUrlPreview)
}
}
public func quickReplyUpdateShortcut(value: String) {
}
public func businessLinkUpdate(message: String, entities: [MessageTextEntity], title: String?) {
}
public func loadMore() {
}
public func hashtagSearchUpdate(query: String) {
}
public var hashtagSearchResultsUpdate: ((SearchMessagesResult, SearchMessagesState)) -> Void = { _ in }
}

View File

@ -0,0 +1,487 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import Postbox
import TelegramCore
import TelegramPresentationData
import TelegramUIPreferences
import PresentationDataUtils
import AccountContext
import ComponentFlow
import ViewControllerComponent
import MultilineTextComponent
import BalancedTextComponent
import ListSectionComponent
import BundleIconComponent
import LottieComponent
import ListSwitchItemComponent
import ListItemSliderSelectorComponent
import ListSwitchItemComponent
import ListActionItemComponent
import Markdown
import TelegramStringFormatting
final class PostSuggestionsSettingsScreenComponent: Component {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let context: AccountContext
let completion: () -> Void
init(
context: AccountContext,
completion: @escaping () -> Void
) {
self.context = context
self.completion = completion
}
static func ==(lhs: PostSuggestionsSettingsScreenComponent, rhs: PostSuggestionsSettingsScreenComponent) -> Bool {
return true
}
private final class ScrollView: UIScrollView {
override func touchesShouldCancel(in view: UIView) -> Bool {
return true
}
}
final class View: UIView, UIScrollViewDelegate {
private let topOverscrollLayer = SimpleLayer()
private let scrollView: ScrollView
private let navigationTitle = ComponentView<Empty>()
private let icon = ComponentView<Empty>()
private let subtitle = ComponentView<Empty>()
private let switchSection = ComponentView<Empty>()
private let contentSection = ComponentView<Empty>()
private var isUpdating: Bool = false
private var component: PostSuggestionsSettingsScreenComponent?
private(set) weak var state: EmptyComponentState?
private var environment: EnvironmentType?
private var areSuggestionsEnabled: Bool = false
private var starCount: Int = 0
override init(frame: CGRect) {
self.scrollView = ScrollView()
self.scrollView.showsVerticalScrollIndicator = true
self.scrollView.showsHorizontalScrollIndicator = false
self.scrollView.scrollsToTop = false
self.scrollView.delaysContentTouches = false
self.scrollView.canCancelContentTouches = true
self.scrollView.contentInsetAdjustmentBehavior = .never
if #available(iOS 13.0, *) {
self.scrollView.automaticallyAdjustsScrollIndicatorInsets = false
}
self.scrollView.alwaysBounceVertical = true
super.init(frame: frame)
self.scrollView.delegate = self
self.addSubview(self.scrollView)
self.scrollView.layer.addSublayer(self.topOverscrollLayer)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
}
func scrollToTop() {
self.scrollView.setContentOffset(CGPoint(), animated: true)
}
func attemptNavigation(complete: @escaping () -> Void) -> Bool {
guard let component = self.component, let environment = self.environment else {
return true
}
let _ = component
let _ = environment
return true
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
self.updateScrolling(transition: .immediate)
}
var scrolledUp = true
private func updateScrolling(transition: ComponentTransition) {
let navigationRevealOffsetY: CGFloat = 0.0
let navigationAlphaDistance: CGFloat = 16.0
let navigationAlpha: CGFloat = max(0.0, min(1.0, (self.scrollView.contentOffset.y - navigationRevealOffsetY) / navigationAlphaDistance))
if let controller = self.environment?.controller(), let navigationBar = controller.navigationBar {
transition.setAlpha(layer: navigationBar.backgroundNode.layer, alpha: navigationAlpha)
transition.setAlpha(layer: navigationBar.stripeNode.layer, alpha: navigationAlpha)
}
var scrolledUp = false
if navigationAlpha < 0.5 {
scrolledUp = true
} else if navigationAlpha > 0.5 {
scrolledUp = false
}
if self.scrolledUp != scrolledUp {
self.scrolledUp = scrolledUp
if !self.isUpdating {
self.state?.updated()
}
}
if let navigationTitleView = self.navigationTitle.view {
transition.setAlpha(view: navigationTitleView, alpha: 1.0)
}
}
func update(component: PostSuggestionsSettingsScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
self.isUpdating = true
defer {
self.isUpdating = false
}
if self.component == nil {
self.starCount = 20
}
let environment = environment[EnvironmentType.self].value
let themeUpdated = self.environment?.theme !== environment.theme
self.environment = environment
self.component = component
self.state = state
let alphaTransition: ComponentTransition
if !transition.animation.isImmediate {
alphaTransition = .easeInOut(duration: 0.25)
} else {
alphaTransition = .immediate
}
if themeUpdated {
self.backgroundColor = environment.theme.list.blocksBackgroundColor
}
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
//TODO:localize
let navigationTitleSize = self.navigationTitle.update(
transition: transition,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: "Post Suggestion", font: Font.semibold(17.0), textColor: environment.theme.rootController.navigationBar.primaryTextColor)),
horizontalAlignment: .center
)),
environment: {},
containerSize: CGSize(width: availableSize.width, height: 100.0)
)
let navigationTitleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - navigationTitleSize.width) / 2.0), y: environment.statusBarHeight + floor((environment.navigationHeight - environment.statusBarHeight - navigationTitleSize.height) / 2.0)), size: navigationTitleSize)
if let navigationTitleView = self.navigationTitle.view {
if navigationTitleView.superview == nil {
if let controller = self.environment?.controller(), let navigationBar = controller.navigationBar {
navigationBar.view.addSubview(navigationTitleView)
}
}
transition.setFrame(view: navigationTitleView, frame: navigationTitleFrame)
}
let bottomContentInset: CGFloat = 24.0
let sideInset: CGFloat = 16.0 + environment.safeInsets.left
let sectionSpacing: CGFloat = 24.0
var contentHeight: CGFloat = 0.0
contentHeight += environment.navigationHeight
let iconSize = self.icon.update(
transition: .immediate,
component: AnyComponent(LottieComponent(
content: LottieComponent.AppBundleContent(name: "LampEmoji"),
loop: false
)),
environment: {},
containerSize: CGSize(width: 100.0, height: 100.0)
)
let iconFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - iconSize.width) * 0.5), y: contentHeight + 11.0), size: iconSize)
if let iconView = self.icon.view as? LottieComponent.View {
if iconView.superview == nil {
self.scrollView.addSubview(iconView)
iconView.playOnce()
}
transition.setPosition(view: iconView, position: iconFrame.center)
iconView.bounds = CGRect(origin: CGPoint(), size: iconFrame.size)
}
contentHeight += 129.0
//TODO:localize
let subtitleString = NSMutableAttributedString(attributedString: parseMarkdownIntoAttributedString("Allow users to suggest posts for your channel.", attributes: MarkdownAttributes(
body: MarkdownAttributeSet(font: Font.regular(15.0), textColor: environment.theme.list.freeTextColor),
bold: MarkdownAttributeSet(font: Font.semibold(15.0), textColor: environment.theme.list.freeTextColor),
link: MarkdownAttributeSet(font: Font.regular(15.0), textColor: environment.theme.list.itemAccentColor),
linkAttribute: { attributes in
return ("URL", "")
}), textAlignment: .center
))
let subtitleSize = self.subtitle.update(
transition: .immediate,
component: AnyComponent(BalancedTextComponent(
text: .plain(subtitleString),
horizontalAlignment: .center,
maximumNumberOfLines: 0,
lineSpacing: 0.25,
highlightColor: environment.theme.list.itemAccentColor.withMultipliedAlpha(0.1),
highlightAction: { attributes in
if let _ = attributes[NSAttributedString.Key(rawValue: "URL")] {
return NSAttributedString.Key(rawValue: "URL")
} else {
return nil
}
},
tapAction: { [weak self] _, _ in
guard let self, let component = self.component else {
return
}
let _ = component
}
)),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 1000.0)
)
let subtitleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - subtitleSize.width) * 0.5), y: contentHeight), size: subtitleSize)
if let subtitleView = self.subtitle.view {
if subtitleView.superview == nil {
self.scrollView.addSubview(subtitleView)
}
transition.setPosition(view: subtitleView, position: subtitleFrame.center)
subtitleView.bounds = CGRect(origin: CGPoint(), size: subtitleFrame.size)
}
contentHeight += subtitleSize.height
contentHeight += 27.0
var switchSectionItems: [AnyComponentWithIdentity<Empty>] = []
switchSectionItems.append(AnyComponentWithIdentity(id: 0, component: AnyComponent(ListActionItemComponent(
theme: environment.theme,
title: AnyComponent(VStack([
AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: "Allow Post Suggestions",
font: Font.regular(presentationData.listsFontSize.baseDisplaySize),
textColor: environment.theme.list.itemPrimaryTextColor
)),
maximumNumberOfLines: 1
))),
], alignment: .left, spacing: 2.0)),
accessory: .toggle(ListActionItemComponent.Toggle(style: .regular, isOn: self.areSuggestionsEnabled, isInteractive: false)),
action: { [weak self] _ in
guard let self else {
return
}
self.areSuggestionsEnabled = !self.areSuggestionsEnabled
self.state?.updated(transition: .spring(duration: 0.4))
}
))))
let switchSectionSize = self.switchSection.update(
transition: transition,
component: AnyComponent(ListSectionComponent(
theme: environment.theme,
header: nil,
footer: nil,
items: switchSectionItems
)),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0)
)
let switchSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: switchSectionSize)
if let switchSectionView = self.switchSection.view {
if switchSectionView.superview == nil {
self.scrollView.addSubview(switchSectionView)
self.switchSection.parentState = state
}
transition.setFrame(view: switchSectionView, frame: switchSectionFrame)
}
contentHeight += switchSectionSize.height
contentHeight += sectionSpacing
var contentSectionItems: [AnyComponentWithIdentity<Empty>] = []
let sliderValueList = (0 ... 10000).map { i -> String in
return "\(i)"
}
//TODO:localize
let sliderTitle: String
let sliderSecondaryTitle: String?
let usdAmount = Double(self.starCount) * 0.013
let usdAmountString = formatCurrencyAmount(Int64(usdAmount * 100.0), currency: "USD")
if self.starCount == 0 {
sliderTitle = "Free"
sliderSecondaryTitle = nil
} else if self.starCount == 1 {
sliderTitle = "\(self.starCount) Star"
sliderSecondaryTitle = "~\(usdAmountString)"
} else {
sliderTitle = "\(self.starCount) Stars"
sliderSecondaryTitle = "~\(usdAmountString)"
}
contentSectionItems.append(AnyComponentWithIdentity(id: 0, component: AnyComponent(ListItemSliderSelectorComponent(
theme: environment.theme,
content: .discrete(ListItemSliderSelectorComponent.Discrete(
values: sliderValueList.map { item in
return item
},
markPositions: false,
selectedIndex: max(0, min(sliderValueList.count - 1, self.starCount - 1)),
title: sliderTitle,
secondaryTitle: sliderSecondaryTitle,
selectedIndexUpdated: { [weak self] index in
guard let self else {
return
}
let index = max(0, min(sliderValueList.count, index))
self.starCount = index
self.state?.updated(transition: .immediate)
}
))
))))
let contentSectionSize = self.contentSection.update(
transition: transition,
component: AnyComponent(ListSectionComponent(
theme: environment.theme,
header: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: "PRICE FOR EACH SUGGESTION",
font: Font.regular(13.0),
textColor: environment.theme.list.freeTextColor
)),
maximumNumberOfLines: 0
)),
footer: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: "Charge users for the ability to suggest one post for your channel. You're not required to publish any suggestions by charging this. You'll receive 85% of the selected fee for each incoming suggestion.",
font: Font.regular(13.0),
textColor: environment.theme.list.freeTextColor
)),
maximumNumberOfLines: 0
)),
items: contentSectionItems,
displaySeparators: false
)),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0)
)
let contentSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: contentSectionSize)
if let contentSectionView = self.contentSection.view {
if contentSectionView.superview == nil {
self.scrollView.addSubview(contentSectionView)
}
transition.setFrame(view: contentSectionView, frame: contentSectionFrame)
alphaTransition.setAlpha(view: contentSectionView, alpha: self.areSuggestionsEnabled ? 1.0 : 0.0)
}
if self.areSuggestionsEnabled {
contentHeight += contentSectionSize.height
}
contentHeight += bottomContentInset
contentHeight += environment.safeInsets.bottom
let previousBounds = self.scrollView.bounds
let contentSize = CGSize(width: availableSize.width, height: contentHeight)
if self.scrollView.frame != CGRect(origin: CGPoint(), size: availableSize) {
self.scrollView.frame = CGRect(origin: CGPoint(), size: availableSize)
}
if self.scrollView.contentSize != contentSize {
self.scrollView.contentSize = contentSize
}
let scrollInsets = UIEdgeInsets(top: environment.navigationHeight, left: 0.0, bottom: 0.0, right: 0.0)
if self.scrollView.scrollIndicatorInsets != scrollInsets {
self.scrollView.scrollIndicatorInsets = scrollInsets
}
if !previousBounds.isEmpty, !transition.animation.isImmediate {
let bounds = self.scrollView.bounds
if bounds.maxY != previousBounds.maxY {
let offsetY = previousBounds.maxY - bounds.maxY
transition.animateBoundsOrigin(view: self.scrollView, from: CGPoint(x: 0.0, y: offsetY), to: CGPoint(), additive: true)
}
}
self.topOverscrollLayer.frame = CGRect(origin: CGPoint(x: 0.0, y: -3000.0), size: CGSize(width: availableSize.width, height: 3000.0))
self.updateScrolling(transition: transition)
return availableSize
}
}
func makeView() -> View {
return View()
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
public final class PostSuggestionsSettingsScreen: ViewControllerComponentContainer {
private let context: AccountContext
public init(
context: AccountContext,
completion: @escaping () -> Void
) {
self.context = context
super.init(context: context, component: PostSuggestionsSettingsScreenComponent(
context: context,
completion: completion
), navigationBarAppearance: .default, theme: .default, updatedPresentationData: nil)
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
self.title = ""
self.navigationItem.backBarButtonItem = UIBarButtonItem(title: presentationData.strings.Common_Back, style: .plain, target: nil, action: nil)
self.scrollToTop = { [weak self] in
guard let self, let componentView = self.node.hostView.componentView as? PostSuggestionsSettingsScreenComponent.View else {
return
}
componentView.scrollToTop()
}
self.attemptNavigation = { [weak self] complete in
guard let self, let componentView = self.node.hostView.componentView as? PostSuggestionsSettingsScreenComponent.View else {
return true
}
return componentView.attemptNavigation(complete: complete)
}
}
required public init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
}
@objc private func cancelPressed() {
self.dismiss()
}
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
}
}

View File

@ -761,6 +761,7 @@ final class PeerSelectionControllerNode: ASDisplayNode {
}, joinGroupCall: { _ in
}, presentInviteMembers: {
}, presentGigagroupHelp: {
}, openSuggestPost: {
}, editMessageMedia: { _, _ in
}, updateShowCommands: { _ in
}, updateShowSendAsPeers: { _ in

View File

@ -95,12 +95,6 @@ final class AutomaticBusinessMessageSetupChatContents: ChatCustomContentsProtoco
self.updateHistoryView(updateType: nextUpdateIsHoleFill ? .FillHole : .Generic)
})
/*if self.sourceHistoryView == nil {
let sourceHistoryView = MessageHistoryView(tag: nil, namespaces: .just(Namespaces.Message.allQuickReply), entries: [], holeEarlier: false, holeLater: false, isLoading: false)
self.sourceHistoryView = sourceHistoryView
self.updateHistoryView(updateType: .Initial)
}*/
}
}
@ -217,6 +211,8 @@ final class AutomaticBusinessMessageSetupChatContents: ChatCustomContentsProtoco
initialShortcut = ""
case .hashTagSearch:
initialShortcut = ""
case .postSuggestions:
initialShortcut = ""
}
let queue = Queue()
@ -255,6 +251,8 @@ final class AutomaticBusinessMessageSetupChatContents: ChatCustomContentsProtoco
break
case .hashTagSearch:
break
case .postSuggestions:
break
}
}

View File

@ -123,6 +123,8 @@ import PeerNameColorScreen
import ChatEmptyNode
import ChatMediaInputStickerGridItem
import AdsInfoScreen
import PostSuggestionsSettingsScreen
import ChatSendStarsScreen
extension ChatControllerImpl {
func loadDisplayNodeImpl() {
@ -1417,6 +1419,46 @@ extension ChatControllerImpl {
}
strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .succeed(text: strongSelf.presentationData.strings.Business_Links_EditLinkToastSaved, timeout: nil, customUndoText: nil), elevatedLayout: false, action: { _ in return false }), in: .current)
case let .postSuggestions(postSuggestions):
if let customChatContents = customChatContents as? PostSuggestionsChatContents {
//TODO:release
strongSelf.chatDisplayNode.dismissInput()
let _ = (ChatSendStarsScreen.initialData(context: strongSelf.context, peerId: customChatContents.peerId, suggestMessageAmount: postSuggestions, completion: { [weak strongSelf] amount, timestamp in
guard let strongSelf else {
return
}
guard case let .customChatContents(customChatContents) = strongSelf.subject else {
return
}
if amount == 0 {
return
}
let messages = messages.map { message in
return message.withUpdatedAttributes { attributes in
var attributes = attributes
attributes.removeAll(where: { $0 is OutgoingSuggestedPostMessageAttribute })
attributes.append(OutgoingSuggestedPostMessageAttribute(
price: StarsAmount(value: amount, nanos: 0),
timestamp: timestamp
))
return attributes
}
}
customChatContents.enqueueMessages(messages: messages)
strongSelf.chatDisplayNode.historyNode.scrollToEndOfHistory()
})
|> deliverOnMainQueue).startStandalone(next: { [weak strongSelf] initialData in
guard let strongSelf, let initialData else {
return
}
let sendStarsScreen = ChatSendStarsScreen(
context: strongSelf.context,
initialData: initialData
)
strongSelf.push(sendStarsScreen)
})
}
}
}
strongSelf.updateChatPresentationInterfaceState(interactive: true, { $0.updatedShowCommands(false) })
@ -4082,6 +4124,29 @@ extension ChatControllerImpl {
if let strongSelf = self {
strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .info(title: nil, text: strongSelf.presentationData.strings.Conversation_GigagroupDescription, timeout: nil, customUndoText: nil), elevatedLayout: false, action: { _ in return true }), in: .current)
}
}, openSuggestPost: { [weak self] in
guard let self else {
return
}
guard let peerId = self.chatLocation.peerId else {
return
}
let contents = PostSuggestionsChatContents(
context: self.context,
peerId: peerId
)
let chatController = self.context.sharedContext.makeChatController(
context: self.context,
chatLocation: .customChatContents,
subject: .customChatContents(contents: contents),
botStart: nil,
mode: .standard(.default),
params: nil
)
chatController.navigationPresentation = .modal
self.push(chatController)
}, editMessageMedia: { [weak self] messageId, draw in
if let strongSelf = self {
strongSelf.controllerInteraction?.editMessageMedia(messageId, draw)

View File

@ -230,6 +230,8 @@ func updateChatPresentationInterfaceStateImpl(
break
case .businessLinkSetup:
canHaveUrlPreview = false
case .postSuggestions:
break
}
}

View File

@ -196,6 +196,8 @@ final class ChatBusinessLinkTitlePanelNode: ChatTitleAccessoryPanelNode {
self.link = link
case .hashTagSearch:
break
case .postSuggestions:
break
}
default:
break

View File

@ -135,6 +135,7 @@ import AdUI
import ChatMessagePaymentAlertController
import TelegramCallsUI
import QuickShareScreen
import PostSuggestionsSettingsScreen
public enum ChatControllerPeekActions {
case standard
@ -776,6 +777,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}
case .hashTagSearch:
break
case .postSuggestions:
break
}
}
@ -882,6 +885,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
return false
}
case .postSuggestions:
break
}
}
@ -5222,7 +5227,14 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
chatInfoButtonItem = UIBarButtonItem(customDisplayNode: avatarNode)!
self.avatarNode = avatarNode
case .customChatContents:
chatInfoButtonItem = UIBarButtonItem(title: "", style: .plain, target: nil, action: nil)
if case let .customChatContents(customChatContents) = self.subject, case .postSuggestions = customChatContents.kind {
let avatarNode = ChatAvatarNavigationNode()
chatInfoButtonItem = UIBarButtonItem(customDisplayNode: avatarNode)!
chatInfoButtonItem.isEnabled = false
self.avatarNode = avatarNode
} else {
chatInfoButtonItem = UIBarButtonItem(title: "", style: .plain, target: nil, action: nil)
}
}
chatInfoButtonItem.target = self
chatInfoButtonItem.action = #selector(self.rightNavigationButtonAction)
@ -6805,6 +6817,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
self.reportIrrelvantGeoNoticePromise.set(.single(nil))
self.titleDisposable.set(nil)
var peerView: Signal<PeerView?, NoError> = .single(nil)
if case let .customChatContents(customChatContents) = self.subject {
switch customChatContents.kind {
case .hashTagSearch:
@ -6827,15 +6841,56 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}
self.chatTitleView?.titleContent = .custom(link.title ?? self.presentationData.strings.Business_Links_EditLinkTitle, linkUrl, false)
case .postSuggestions:
if let customChatContents = customChatContents as? PostSuggestionsChatContents {
peerView = context.account.viewTracker.peerView(customChatContents.peerId) |> map(Optional.init)
}
//TODO:localize
self.chatTitleView?.titleContent = .custom("Message Suggestions", nil, false)
}
} else {
self.chatTitleView?.titleContent = .custom(" ", nil, false)
}
if !self.didSetChatLocationInfoReady {
self.didSetChatLocationInfoReady = true
self._chatLocationInfoReady.set(.single(true))
}
self.peerDisposable.set((peerView
|> deliverOnMainQueue).startStrict(next: { [weak self] peerView in
guard let self else {
return
}
var renderedPeer: RenderedPeer?
if let peerView, let peer = peerView.peers[peerView.peerId] {
var peers = SimpleDictionary<PeerId, Peer>()
peers[peer.id] = peer
if let associatedPeerId = peer.associatedPeerId, let associatedPeer = peerView.peers[associatedPeerId] {
peers[associatedPeer.id] = associatedPeer
}
renderedPeer = RenderedPeer(peerId: peer.id, peers: peers, associatedMedia: peerView.media)
(self.chatInfoNavigationButton?.buttonItem.customDisplayNode as? ChatAvatarNavigationNode)?.setPeer(context: self.context, theme: self.presentationData.theme, peer: EnginePeer(peer), overrideImage: nil)
}
self.peerView = peerView
if self.isNodeLoaded {
self.chatDisplayNode.overlayTitle = self.overlayTitle
}
(self.chatInfoNavigationButton?.buttonItem.customDisplayNode as? ChatAvatarNavigationNode)?.contextActionIsEnabled = false
self.updateChatPresentationInterfaceState(animated: false, interactive: false, {
return $0.updatedPeer { _ in
return renderedPeer
}.updatedInterfaceState { interfaceState in
return interfaceState
}
})
if !self.didSetChatLocationInfoReady {
self.didSetChatLocationInfoReady = true
self._chatLocationInfoReady.set(.single(true))
}
}))
}
}

View File

@ -4286,6 +4286,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
}
var postEmptyMessages = false
var isPostSuggestions = false
if case let .customChatContents(customChatContents) = self.chatPresentationInterfaceState.subject {
switch customChatContents.kind {
case .hashTagSearch:
@ -4294,8 +4295,11 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
break
case .businessLinkSetup:
postEmptyMessages = true
case .postSuggestions:
isPostSuggestions = true
}
}
let _ = isPostSuggestions
if !messages.isEmpty, let messageEffect {
messages[0] = messages[0].withUpdatedAttributes { attributes in

View File

@ -385,109 +385,109 @@ extension ChatControllerImpl {
}
let reactionsAttribute = mergedMessageReactions(attributes: message.attributes, isTags: false)
let _ = (ChatSendStarsScreen.initialData(context: self.context, peerId: message.id.peerId, messageId: message.id, topPeers: reactionsAttribute?.topPeers ?? [])
let _ = (ChatSendStarsScreen.initialData(context: self.context, peerId: message.id.peerId, messageId: message.id, topPeers: reactionsAttribute?.topPeers ?? [], completion: { [weak self] amount, privacy, isBecomingTop, transitionOut in
guard let self, amount > 0 else {
return
}
if case let .known(reactionSettings) = reactionSettings, let starsAllowed = reactionSettings.starsAllowed, !starsAllowed {
if let peer = self.presentationInterfaceState.renderedPeer?.chatMainPeer {
self.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: self.presentationData), title: nil, text: self.presentationData.strings.Chat_ToastStarsReactionsDisabled(peer.debugDisplayTitle).string, actions: [
TextAlertAction(type: .genericAction, title: self.presentationData.strings.Common_OK, action: {})
]), in: .window(.root))
}
return
}
var sourceItemNode: ChatMessageItemView?
self.chatDisplayNode.historyNode.forEachItemNode { itemNode in
if let itemNode = itemNode as? ChatMessageItemView {
if itemNode.item?.message.id == message.id {
sourceItemNode = itemNode
return
}
}
}
if let itemNode = sourceItemNode, let item = itemNode.item, let availableReactions = item.associatedData.availableReactions, let targetView = itemNode.targetReactionView(value: .stars) {
var reactionItem: ReactionItem?
for reaction in availableReactions.reactions {
guard let centerAnimation = reaction.centerAnimation else {
continue
}
guard let aroundAnimation = reaction.aroundAnimation else {
continue
}
if reaction.value == .stars {
reactionItem = ReactionItem(
reaction: ReactionItem.Reaction(rawValue: reaction.value),
appearAnimation: reaction.appearAnimation,
stillAnimation: reaction.selectAnimation,
listAnimation: centerAnimation,
largeListAnimation: reaction.activateAnimation,
applicationAnimation: aroundAnimation,
largeApplicationAnimation: reaction.effectAnimation,
isCustom: false
)
break
}
}
if let reactionItem {
let standaloneReactionAnimation = StandaloneReactionAnimation(genericReactionEffect: self.chatDisplayNode.historyNode.takeGenericReactionEffect())
self.chatDisplayNode.messageTransitionNode.addMessageStandaloneReactionAnimation(messageId: item.message.id, standaloneReactionAnimation: standaloneReactionAnimation)
self.view.window?.addSubview(standaloneReactionAnimation.view)
standaloneReactionAnimation.frame = self.chatDisplayNode.bounds
standaloneReactionAnimation.animateOutToReaction(
context: self.context,
theme: self.presentationData.theme,
item: reactionItem,
value: .stars,
sourceView: transitionOut.sourceView,
targetView: targetView,
hideNode: false,
forceSwitchToInlineImmediately: false,
animateTargetContainer: nil,
addStandaloneReactionAnimation: { [weak self] standaloneReactionAnimation in
guard let self else {
return
}
self.chatDisplayNode.messageTransitionNode.addMessageStandaloneReactionAnimation(messageId: item.message.id, standaloneReactionAnimation: standaloneReactionAnimation)
standaloneReactionAnimation.frame = self.chatDisplayNode.bounds
self.chatDisplayNode.addSubnode(standaloneReactionAnimation)
},
onHit: { [weak self, weak itemNode] in
guard let self else {
return
}
if isBecomingTop {
self.chatDisplayNode.playConfettiAnimation()
}
if let itemNode, let targetView = itemNode.targetReactionView(value: .stars), self.context.sharedContext.energyUsageSettings.fullTranslucency {
self.chatDisplayNode.wrappingNode.triggerRipple(at: targetView.convert(targetView.bounds.center, to: self.chatDisplayNode.view))
}
},
completion: { [weak standaloneReactionAnimation] in
standaloneReactionAnimation?.view.removeFromSuperview()
}
)
}
}
let _ = self.context.engine.messages.sendStarsReaction(id: message.id, count: Int(amount), privacy: privacy).startStandalone()
self.displayOrUpdateSendStarsUndo(messageId: message.id, count: Int(amount), privacy: privacy)
})
|> deliverOnMainQueue).start(next: { [weak self] initialData in
guard let self, let initialData else {
return
}
HapticFeedback().tap()
self.push(ChatSendStarsScreen(context: self.context, initialData: initialData, completion: { [weak self] amount, privacy, isBecomingTop, transitionOut in
guard let self, amount > 0 else {
return
}
if case let .known(reactionSettings) = reactionSettings, let starsAllowed = reactionSettings.starsAllowed, !starsAllowed {
if let peer = self.presentationInterfaceState.renderedPeer?.chatMainPeer {
self.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: self.presentationData), title: nil, text: self.presentationData.strings.Chat_ToastStarsReactionsDisabled(peer.debugDisplayTitle).string, actions: [
TextAlertAction(type: .genericAction, title: self.presentationData.strings.Common_OK, action: {})
]), in: .window(.root))
}
return
}
var sourceItemNode: ChatMessageItemView?
self.chatDisplayNode.historyNode.forEachItemNode { itemNode in
if let itemNode = itemNode as? ChatMessageItemView {
if itemNode.item?.message.id == message.id {
sourceItemNode = itemNode
return
}
}
}
if let itemNode = sourceItemNode, let item = itemNode.item, let availableReactions = item.associatedData.availableReactions, let targetView = itemNode.targetReactionView(value: .stars) {
var reactionItem: ReactionItem?
for reaction in availableReactions.reactions {
guard let centerAnimation = reaction.centerAnimation else {
continue
}
guard let aroundAnimation = reaction.aroundAnimation else {
continue
}
if reaction.value == .stars {
reactionItem = ReactionItem(
reaction: ReactionItem.Reaction(rawValue: reaction.value),
appearAnimation: reaction.appearAnimation,
stillAnimation: reaction.selectAnimation,
listAnimation: centerAnimation,
largeListAnimation: reaction.activateAnimation,
applicationAnimation: aroundAnimation,
largeApplicationAnimation: reaction.effectAnimation,
isCustom: false
)
break
}
}
if let reactionItem {
let standaloneReactionAnimation = StandaloneReactionAnimation(genericReactionEffect: self.chatDisplayNode.historyNode.takeGenericReactionEffect())
self.chatDisplayNode.messageTransitionNode.addMessageStandaloneReactionAnimation(messageId: item.message.id, standaloneReactionAnimation: standaloneReactionAnimation)
self.view.window?.addSubview(standaloneReactionAnimation.view)
standaloneReactionAnimation.frame = self.chatDisplayNode.bounds
standaloneReactionAnimation.animateOutToReaction(
context: self.context,
theme: self.presentationData.theme,
item: reactionItem,
value: .stars,
sourceView: transitionOut.sourceView,
targetView: targetView,
hideNode: false,
forceSwitchToInlineImmediately: false,
animateTargetContainer: nil,
addStandaloneReactionAnimation: { [weak self] standaloneReactionAnimation in
guard let self else {
return
}
self.chatDisplayNode.messageTransitionNode.addMessageStandaloneReactionAnimation(messageId: item.message.id, standaloneReactionAnimation: standaloneReactionAnimation)
standaloneReactionAnimation.frame = self.chatDisplayNode.bounds
self.chatDisplayNode.addSubnode(standaloneReactionAnimation)
},
onHit: { [weak self, weak itemNode] in
guard let self else {
return
}
if isBecomingTop {
self.chatDisplayNode.playConfettiAnimation()
}
if let itemNode, let targetView = itemNode.targetReactionView(value: .stars), self.context.sharedContext.energyUsageSettings.fullTranslucency {
self.chatDisplayNode.wrappingNode.triggerRipple(at: targetView.convert(targetView.bounds.center, to: self.chatDisplayNode.view))
}
},
completion: { [weak standaloneReactionAnimation] in
standaloneReactionAnimation?.view.removeFromSuperview()
}
)
}
}
let _ = self.context.engine.messages.sendStarsReaction(id: message.id, count: Int(amount), privacy: privacy).startStandalone()
self.displayOrUpdateSendStarsUndo(messageId: message.id, count: Int(amount), privacy: privacy)
}))
self.push(ChatSendStarsScreen(context: self.context, initialData: initialData))
})
})
}

View File

@ -63,6 +63,8 @@ func inputContextQueriesForChatPresentationIntefaceState(_ chatPresentationInter
break
case .businessLinkSetup:
return []
case .postSuggestions:
return []
}
}
@ -241,6 +243,8 @@ func inputTextPanelStateForChatPresentationInterfaceState(_ chatPresentationInte
break
case .businessLinkSetup:
stickersEnabled = false
case .postSuggestions:
break
}
}

View File

@ -2108,6 +2108,9 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
}
case .businessLinkSetup:
actions.removeAll()
case .postSuggestions:
//TODO:release
actions.removeAll()
}
}

View File

@ -10,7 +10,13 @@ import ChatChannelSubscriberInputPanelNode
import ChatMessageSelectionInputPanelNode
func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState: ChatPresentationInterfaceState, context: AccountContext, currentPanel: ChatInputPanelNode?, currentSecondaryPanel: ChatInputPanelNode?, textInputPanelNode: ChatTextInputPanelNode?, interfaceInteraction: ChatPanelInterfaceInteraction?) -> (primary: ChatInputPanelNode?, secondary: ChatInputPanelNode?) {
if let renderedPeer = chatPresentationInterfaceState.renderedPeer, renderedPeer.peer?.restrictionText(platform: "ios", contentSettings: context.currentContentSettings.with { $0 }) != nil {
var isPostSuggestions = false
if case let .customChatContents(customChatContents) = chatPresentationInterfaceState.subject, case .postSuggestions = customChatContents.kind {
isPostSuggestions = true
}
if isPostSuggestions {
} else if let renderedPeer = chatPresentationInterfaceState.renderedPeer, renderedPeer.peer?.restrictionText(platform: "ios", contentSettings: context.currentContentSettings.with { $0 }) != nil {
return (nil, nil)
}
if chatPresentationInterfaceState.isNotAccessible {
@ -132,7 +138,8 @@ func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState
}
}
if chatPresentationInterfaceState.peerIsBlocked, let peer = chatPresentationInterfaceState.renderedPeer?.peer as? TelegramUser, peer.botInfo == nil {
if isPostSuggestions {
} else if chatPresentationInterfaceState.peerIsBlocked, let peer = chatPresentationInterfaceState.renderedPeer?.peer as? TelegramUser, peer.botInfo == nil {
if let currentPanel = (currentPanel as? ChatUnblockInputPanelNode) ?? (currentSecondaryPanel as? ChatUnblockInputPanelNode) {
currentPanel.interfaceInteraction = interfaceInteraction
currentPanel.updateThemeAndStrings(theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings)
@ -147,7 +154,9 @@ func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState
var displayInputTextPanel = false
if let peer = chatPresentationInterfaceState.renderedPeer?.peer {
if isPostSuggestions {
displayInputTextPanel = true
} else if let peer = chatPresentationInterfaceState.renderedPeer?.peer {
if peer.id.isRepliesOrVerificationCodes {
if let currentPanel = (currentPanel as? ChatChannelSubscriberInputPanelNode) ?? (currentSecondaryPanel as? ChatChannelSubscriberInputPanelNode) {
return (currentPanel, nil)
@ -423,6 +432,8 @@ func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState
displayInputTextPanel = false
case .quickReplyMessageInput, .businessLinkSetup:
displayInputTextPanel = true
case .postSuggestions:
displayInputTextPanel = true
}
if let chatHistoryState = chatPresentationInterfaceState.chatHistoryState, case .loaded(_, true) = chatHistoryState {

View File

@ -59,7 +59,7 @@ func leftNavigationButtonForChatInterfaceState(_ presentationInterfaceState: Cha
switch customChatContents.kind {
case .hashTagSearch:
break
case .quickReplyMessageInput, .businessLinkSetup:
case .quickReplyMessageInput, .businessLinkSetup, .postSuggestions:
if let currentButton = currentButton, currentButton.action == .dismiss {
return currentButton
} else {
@ -149,6 +149,8 @@ func rightNavigationButtonForChatInterfaceState(context: AccountContext, present
buttonItem.accessibilityLabel = strings.Common_Done
return ChatNavigationButton(action: .edit, buttonItem: buttonItem)
}
case .postSuggestions:
return chatInfoNavigationButton
}
}

View File

@ -66,6 +66,8 @@ func titlePanelForChatPresentationInterfaceState(_ chatPresentationInterfaceStat
panel.interfaceInteraction = interfaceInteraction
return panel
}
case .postSuggestions:
break
}
default:
break

View File

@ -128,6 +128,8 @@ final class ChatRestrictedInputPanelNode: ChatInputPanelNode {
displayCount = customChatContents.messageLimit ?? 20
case .businessLinkSetup:
displayCount = 0
case .postSuggestions:
displayCount = 0
}
self.textNode.attributedText = NSAttributedString(string: interfaceState.strings.Chat_QuickReplyMessageLimitReachedText(Int32(displayCount)), font: Font.regular(13.0), textColor: interfaceState.theme.chat.inputPanel.secondaryTextColor)
}

View File

@ -272,10 +272,9 @@ final class ChatTextInputActionButtonsNode: ASDisplayNode, ChatSendMessageAction
self.validLayout = size
var innerSize = size
var starsAmount: Int64?
if let sendPaidMessageStars = interfaceState.sendPaidMessageStars, interfaceState.interfaceState.editMessage == nil {
self.sendButton.imageNode.alpha = 0.0
self.textNode.isHidden = false
var amount: Int64
if let forwardedCount = interfaceState.interfaceState.forwardMessageIds?.count, forwardedCount > 0 {
amount = sendPaidMessageStars.value * Int64(forwardedCount)
@ -290,7 +289,19 @@ final class ChatTextInputActionButtonsNode: ASDisplayNode, ChatSendMessageAction
amount = sendPaidMessageStars.value
}
}
starsAmount = amount
} else if case let .customChatContents(customChatContents) = interfaceState.subject {
switch customChatContents.kind {
case let .postSuggestions(postSuggestions):
starsAmount = postSuggestions.value
default:
break
}
}
if let amount = starsAmount {
self.sendButton.imageNode.alpha = 0.0
self.textNode.isHidden = false
let text = "\(amount)"
let font = Font.with(size: 17.0, design: .round, weight: .semibold, traits: .monospacedNumbers)
let badgeString = NSMutableAttributedString(string: "⭐️ ", font: font, textColor: interfaceState.theme.chat.inputPanel.actionControlForegroundColor)

View File

@ -1558,6 +1558,8 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch
break
case .businessLinkSetup:
displayMediaButton = false
case .postSuggestions:
break
}
}
@ -1944,6 +1946,10 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch
}
case .businessLinkSetup:
placeholder = interfaceState.strings.Chat_Placeholder_BusinessLinkPreset
case let .postSuggestions(postSuggestions):
//TODO:localize
placeholder = "Suggest for # \(postSuggestions)"
placeholderHasStar = true
}
}
@ -1971,6 +1977,8 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch
break
case .businessLinkSetup:
sendButtonHasApplyIcon = true
case .postSuggestions:
break
}
}
}
@ -2493,8 +2501,17 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch
var actionButtonsSize = CGSize(width: 44.0, height: minimalHeight)
if let presentationInterfaceState = self.presentationInterfaceState {
var showTitle = false
if let _ = presentationInterfaceState.sendPaidMessageStars, !self.actionButtons.sendContainerNode.alpha.isZero {
showTitle = true
if !self.actionButtons.sendContainerNode.alpha.isZero {
if let _ = presentationInterfaceState.sendPaidMessageStars {
showTitle = true
} else if case let .customChatContents(customChatContents) = interfaceState.subject {
switch customChatContents.kind {
case .postSuggestions:
showTitle = true
default:
break
}
}
}
actionButtonsSize = self.actionButtons.updateLayout(size: CGSize(width: 44.0, height: minimalHeight), isMediaInputExpanded: isMediaInputExpanded, showTitle: showTitle, currentMessageEffectId: presentationInterfaceState.interfaceState.sendMessageEffect, transition: transition, interfaceState: presentationInterfaceState)
}
@ -3768,6 +3785,8 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch
break
case .businessLinkSetup:
keepSendButtonEnabled = true
case .postSuggestions:
break
}
}
}
@ -3888,6 +3907,8 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch
break
case .businessLinkSetup:
hideMicButton = true
case .postSuggestions:
break
}
}
}
@ -3993,6 +4014,8 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch
break
case .businessLinkSetup:
sendButtonHasApplyIcon = true
case .postSuggestions:
break
}
}

View File

@ -83,7 +83,7 @@ import OldChannelsController
import InviteLinksUI
import GiftStoreScreen
import SendInviteLinkScreen
import PostSuggestionsSettingsScreen
private final class AccountUserInterfaceInUseContext {
let subscribers = Bag<(Bool) -> Void>()
@ -3839,6 +3839,10 @@ public final class SharedAccountContextImpl: SharedAccountContext {
public func makeSendInviteLinkScreen(context: AccountContext, subject: SendInviteLinkScreenSubject, peers: [TelegramForbiddenInvitePeer], theme: PresentationTheme?) -> ViewController {
return SendInviteLinkScreen(context: context, subject: subject, peers: peers, theme: theme)
}
public func makePostSuggestionsSettingsScreen(context: AccountContext) -> ViewController {
return PostSuggestionsSettingsScreen(context: context, completion: {})
}
}
private func peerInfoControllerImpl(context: AccountContext, updatedPresentationData: (PresentationData, Signal<PresentationData, NoError>)?, peer: Peer, mode: PeerInfoControllerMode, avatarInitiallyExpanded: Bool, isOpenedFromChat: Bool, requestsContext: PeerInvitationImportersContext? = nil) -> ViewController? {