From 603d5754db8698e02e5a2279c427672f88d22a50 Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Wed, 23 Apr 2025 12:18:56 +0400 Subject: [PATCH] [WIP] Post suggestions --- .../Sources/AccountContext.swift | 3 +- .../Sources/ChatController.swift | 1 + .../Sources/AttachmentPanel.swift | 1 + .../ChatPanelInterfaceInteraction.swift | 4 + .../Sources/Account/AccountManager.swift | 1 + .../PendingMessages/EnqueueMessage.swift | 16 + .../PendingMessages/RequestEditMessage.swift | 3 + .../Sources/State/AccountViewTracker.swift | 36 + .../Sources/State/ApplyUpdateMessage.swift | 16 + .../CloudChatRemoveMessagesOperation.swift | 19 + ...gedCloudChatRemoveMessagesOperations.swift | 8 + .../Sources/State/PendingMessageManager.swift | 16 + .../SuggestedPostMessageAttribute.swift | 37 + ...ore_CloudChatRemoveMessagesOperation.swift | 1 + .../SyncCore/SyncCore_Namespaces.swift | 5 +- .../Messages/SearchMessages.swift | 3 + submodules/TelegramUI/BUILD | 3 +- .../ChatChannelSubscriberInputPanelNode.swift | 23 +- .../ChatEmptyNode/Sources/ChatEmptyNode.swift | 25 +- .../Chat/ChatMessageBubbleItemNode/BUILD | 1 + .../Sources/ChatMessageBubbleItemNode.swift | 51 +- .../ChatMessageSuggestedPostInfoNode.swift | 163 ++ .../StringForMessageTimestampStatus.swift | 11 + .../Sources/ChatMessageItemImpl.swift | 4 + .../Sources/ChatRecentActionsController.swift | 1 + .../Components/Chat/ChatSendStarsScreen/BUILD | 2 + .../Sources/ChatSendStarsScreen.swift | 1481 ++++++++++------- .../Sources/ChatEntityKeyboardInputNode.swift | 2 + .../Sources/ChatScheduleTimeController.swift | 7 +- .../ChatScheduleTimeControllerNode.swift | 70 +- .../ListItemSliderSelectorComponent.swift | 64 +- .../Components/ListSwitchItemComponent/BUILD | 22 + .../Sources/ListSwitchItemComponent.swift | 12 +- .../PeerAllowedReactionsScreen/BUILD | 1 + .../Sources/PeerAllowedReactionsScreen.swift | 1 + .../Sources/PeerInfoScreen.swift | 21 + .../PostSuggestionsSettingsScreen/BUILD | 38 + .../Sources/PostSuggestionsChatContents.swift | 153 ++ .../PostSuggestionsSettingsScreen.swift | 487 ++++++ .../Sources/PeerSelectionControllerNode.swift | 1 + ...aticBusinessMessageSetupChatContents.swift | 10 +- .../Resources/Animations/LampEmoji.tgs | Bin 0 -> 3835 bytes .../Chat/ChatControllerLoadDisplayNode.swift | 65 + ...UpdateChatPresentationInterfaceState.swift | 2 + .../ChatBusinessLinkTitlePanelNode.swift | 2 + .../TelegramUI/Sources/ChatController.swift | 65 +- .../Sources/ChatControllerNode.swift | 4 + ...rollerOpenMessageReactionContextMenu.swift | 196 +-- .../Sources/ChatInterfaceInputContexts.swift | 4 + .../ChatInterfaceStateContextMenus.swift | 3 + .../ChatInterfaceStateInputPanels.swift | 17 +- .../ChatInterfaceStateNavigationButtons.swift | 4 +- .../ChatInterfaceTitlePanelNodes.swift | 2 + .../ChatRestrictedInputPanelNode.swift | 2 + .../ChatTextInputActionButtonsNode.swift | 19 +- .../Sources/ChatTextInputPanelNode.swift | 27 +- .../Sources/SharedAccountContext.swift | 6 +- 57 files changed, 2500 insertions(+), 742 deletions(-) create mode 100644 submodules/TelegramCore/Sources/SyncCore/SuggestedPostMessageAttribute.swift create mode 100644 submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageSuggestedPostInfoNode.swift create mode 100644 submodules/TelegramUI/Components/ListSwitchItemComponent/BUILD rename submodules/TelegramUI/Components/{PeerAllowedReactionsScreen => ListSwitchItemComponent}/Sources/ListSwitchItemComponent.swift (90%) create mode 100644 submodules/TelegramUI/Components/PeerInfo/PostSuggestionsSettingsScreen/BUILD create mode 100644 submodules/TelegramUI/Components/PeerInfo/PostSuggestionsSettingsScreen/Sources/PostSuggestionsChatContents.swift create mode 100644 submodules/TelegramUI/Components/PeerInfo/PostSuggestionsSettingsScreen/Sources/PostSuggestionsSettingsScreen.swift create mode 100644 submodules/TelegramUI/Resources/Animations/LampEmoji.tgs diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index c1281c8448..77bebed23f 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -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) diff --git a/submodules/AccountContext/Sources/ChatController.swift b/submodules/AccountContext/Sources/ChatController.swift index 4cab8932e6..5931ae17b1 100644 --- a/submodules/AccountContext/Sources/ChatController.swift +++ b/submodules/AccountContext/Sources/ChatController.swift @@ -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 { diff --git a/submodules/AttachmentUI/Sources/AttachmentPanel.swift b/submodules/AttachmentUI/Sources/AttachmentPanel.swift index b74cbe5631..f99be460cd 100644 --- a/submodules/AttachmentUI/Sources/AttachmentPanel.swift +++ b/submodules/AttachmentUI/Sources/AttachmentPanel.swift @@ -1243,6 +1243,7 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate { }, joinGroupCall: { _ in }, presentInviteMembers: { }, presentGigagroupHelp: { + }, openSuggestPost: { }, editMessageMedia: { _, _ in }, updateShowCommands: { _ in }, updateShowSendAsPeers: { _ in diff --git a/submodules/ChatPresentationInterfaceState/Sources/ChatPanelInterfaceInteraction.swift b/submodules/ChatPresentationInterfaceState/Sources/ChatPanelInterfaceInteraction.swift index ec3c4cfd7e..aa89868121 100644 --- a/submodules/ChatPresentationInterfaceState/Sources/ChatPanelInterfaceInteraction.swift +++ b/submodules/ChatPresentationInterfaceState/Sources/ChatPanelInterfaceInteraction.swift @@ -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 diff --git a/submodules/TelegramCore/Sources/Account/AccountManager.swift b/submodules/TelegramCore/Sources/Account/AccountManager.swift index ef367cdca9..180790f157 100644 --- a/submodules/TelegramCore/Sources/Account/AccountManager.swift +++ b/submodules/TelegramCore/Sources/Account/AccountManager.swift @@ -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) }) diff --git a/submodules/TelegramCore/Sources/PendingMessages/EnqueueMessage.swift b/submodules/TelegramCore/Sources/PendingMessages/EnqueueMessage.swift index 8758b43e1c..8c1cf3caaf 100644 --- a/submodules/TelegramCore/Sources/PendingMessages/EnqueueMessage.swift +++ b/submodules/TelegramCore/Sources/PendingMessages/EnqueueMessage.swift @@ -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) diff --git a/submodules/TelegramCore/Sources/PendingMessages/RequestEditMessage.swift b/submodules/TelegramCore/Sources/PendingMessages/RequestEditMessage.swift index 1a131673d1..edf7c27b25 100644 --- a/submodules/TelegramCore/Sources/PendingMessages/RequestEditMessage.swift +++ b/submodules/TelegramCore/Sources/PendingMessages/RequestEditMessage.swift @@ -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)) diff --git a/submodules/TelegramCore/Sources/State/AccountViewTracker.swift b/submodules/TelegramCore/Sources/State/AccountViewTracker.swift index a30015757c..4b7b8db719 100644 --- a/submodules/TelegramCore/Sources/State/AccountViewTracker.swift +++ b/submodules/TelegramCore/Sources/State/AccountViewTracker.swift @@ -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? = nil, ignoreMessageIds: Set = 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 { diff --git a/submodules/TelegramCore/Sources/State/ApplyUpdateMessage.swift b/submodules/TelegramCore/Sources/State/ApplyUpdateMessage.swift index 0cdbec0fd6..b84f7f2577 100644 --- a/submodules/TelegramCore/Sources/State/ApplyUpdateMessage.swift +++ b/submodules/TelegramCore/Sources/State/ApplyUpdateMessage.swift @@ -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 } } } diff --git a/submodules/TelegramCore/Sources/State/CloudChatRemoveMessagesOperation.swift b/submodules/TelegramCore/Sources/State/CloudChatRemoveMessagesOperation.swift index acf9a30261..1ace711069 100644 --- a/submodules/TelegramCore/Sources/State/CloudChatRemoveMessagesOperation.swift +++ b/submodules/TelegramCore/Sources/State/CloudChatRemoveMessagesOperation.swift @@ -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 { diff --git a/submodules/TelegramCore/Sources/State/ManagedCloudChatRemoveMessagesOperations.swift b/submodules/TelegramCore/Sources/State/ManagedCloudChatRemoveMessagesOperations.swift index 18743c8450..b520f4ba7f 100644 --- a/submodules/TelegramCore/Sources/State/ManagedCloudChatRemoveMessagesOperations.swift +++ b/submodules/TelegramCore/Sources/State/ManagedCloudChatRemoveMessagesOperations.swift @@ -128,6 +128,7 @@ func managedCloudChatRemoveMessagesOperations(postbox: Postbox, network: Network private func removeMessages(postbox: Postbox, network: Network, stateManager: AccountStateManager, peer: Peer, operation: CloudChatRemoveMessagesOperation) -> Signal { 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 = .complete() diff --git a/submodules/TelegramCore/Sources/State/PendingMessageManager.swift b/submodules/TelegramCore/Sources/State/PendingMessageManager.swift index 2f1b40d99f..41a290a289 100644 --- a/submodules/TelegramCore/Sources/State/PendingMessageManager.swift +++ b/submodules/TelegramCore/Sources/State/PendingMessageManager.swift @@ -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 { diff --git a/submodules/TelegramCore/Sources/SyncCore/SuggestedPostMessageAttribute.swift b/submodules/TelegramCore/Sources/SyncCore/SuggestedPostMessageAttribute.swift new file mode 100644 index 0000000000..b442c2d157 --- /dev/null +++ b/submodules/TelegramCore/Sources/SyncCore/SuggestedPostMessageAttribute.swift @@ -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 + } +} diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_CloudChatRemoveMessagesOperation.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_CloudChatRemoveMessagesOperation.swift index 41e23ccdcf..f3a5be61bf 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_CloudChatRemoveMessagesOperation.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_CloudChatRemoveMessagesOperation.swift @@ -97,6 +97,7 @@ public enum CloudChatClearHistoryType: Int32 { case forEveryone case scheduledMessages case quickReplyMessages + case suggestedPostMessages } public enum InteractiveHistoryClearingType: Int32 { diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift index dcee84083e..835e78ee02 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift @@ -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 = Set([Namespaces.Message.ScheduledCloud, Namespaces.Message.ScheduledLocal]) public static let allQuickReply: Set = Set([Namespaces.Message.QuickReplyCloud, Namespaces.Message.QuickReplyLocal]) - public static let allNonRegular: Set = Set([Namespaces.Message.ScheduledCloud, Namespaces.Message.ScheduledLocal, Namespaces.Message.QuickReplyCloud, Namespaces.Message.QuickReplyLocal]) + public static let allSuggestedPost: Set = Set([Namespaces.Message.SuggestedPostCloud, Namespaces.Message.SuggestedPostLocal]) + public static let allNonRegular: Set = 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, diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/SearchMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/SearchMessages.swift index bb5ed2d240..83b1d94507 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/SearchMessages.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/SearchMessages.swift @@ -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)])) diff --git a/submodules/TelegramUI/BUILD b/submodules/TelegramUI/BUILD index 202428afd9..3f0e1ae3d6 100644 --- a/submodules/TelegramUI/BUILD +++ b/submodules/TelegramUI/BUILD @@ -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": [], diff --git a/submodules/TelegramUI/Components/Chat/ChatChannelSubscriberInputPanelNode/Sources/ChatChannelSubscriberInputPanelNode.swift b/submodules/TelegramUI/Components/Chat/ChatChannelSubscriberInputPanelNode/Sources/ChatChannelSubscriberInputPanelNode.swift index e07fa93515..a1add0ec6b 100644 --- a/submodules/TelegramUI/Components/Chat/ChatChannelSubscriberInputPanelNode/Sources/ChatChannelSubscriberInputPanelNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatChannelSubscriberInputPanelNode/Sources/ChatChannelSubscriberInputPanelNode.swift @@ -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) diff --git a/submodules/TelegramUI/Components/Chat/ChatEmptyNode/Sources/ChatEmptyNode.swift b/submodules/TelegramUI/Components/Chat/ChatEmptyNode/Sources/ChatEmptyNode.swift index 0a6f696f34..9bdc20deaf 100644 --- a/submodules/TelegramUI/Components/Chat/ChatEmptyNode/Sources/ChatEmptyNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatEmptyNode/Sources/ChatEmptyNode.swift @@ -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 diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/BUILD index a0c1bb4171..4f948fc37e 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/BUILD +++ b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/BUILD @@ -88,6 +88,7 @@ swift_library( "//submodules/AnimatedStickerNode", "//submodules/TelegramAnimatedStickerNode", "//submodules/TelegramUI/Components/LottieMetal", + "//submodules/TelegramStringFormatting", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift index 41f6bf61e0..a998ec96ff 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift @@ -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)) } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageSuggestedPostInfoNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageSuggestedPostInfoNode.swift new file mode 100644 index 0000000000..59430f93c2 --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageSuggestedPostInfoNode.swift @@ -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 + }) + } + } +} diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode/Sources/StringForMessageTimestampStatus.swift b/submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode/Sources/StringForMessageTimestampStatus.swift index a751806322..a9f0a5a929 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode/Sources/StringForMessageTimestampStatus.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode/Sources/StringForMessageTimestampStatus.swift @@ -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)" } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageItemImpl.swift b/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageItemImpl.swift index bc45a37839..b76046a703 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageItemImpl.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageItemImpl.swift @@ -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? diff --git a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsController.swift b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsController.swift index 8b65489d29..0d7f3737bf 100644 --- a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsController.swift +++ b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsController.swift @@ -147,6 +147,7 @@ public final class ChatRecentActionsController: TelegramBaseController { }, joinGroupCall: { _ in }, presentInviteMembers: { }, presentGigagroupHelp: { + }, openSuggestPost: { }, editMessageMedia: { _, _ in }, updateShowCommands: { _ in }, updateShowSendAsPeers: { _ in diff --git a/submodules/TelegramUI/Components/Chat/ChatSendStarsScreen/BUILD b/submodules/TelegramUI/Components/Chat/ChatSendStarsScreen/BUILD index 8aaaf9c69d..8a6876b050 100644 --- a/submodules/TelegramUI/Components/Chat/ChatSendStarsScreen/BUILD +++ b/submodules/TelegramUI/Components/Chat/ChatSendStarsScreen/BUILD @@ -35,6 +35,8 @@ swift_library( "//submodules/CheckNode", "//submodules/TextFormat", "//submodules/TelegramUI/Components/Stars/StarsBalanceOverlayComponent", + "//submodules/TelegramStringFormatting", + "//submodules/TelegramUI/Components/ChatScheduleTimeController", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Chat/ChatSendStarsScreen/Sources/ChatSendStarsScreen.swift b/submodules/TelegramUI/Components/Chat/ChatSendStarsScreen/Sources/ChatSendStarsScreen.swift index 3cabbce74b..bfafa2547f 100644 --- a/submodules/TelegramUI/Components/Chat/ChatSendStarsScreen/Sources/ChatSendStarsScreen.swift +++ b/submodules/TelegramUI/Components/Chat/ChatSendStarsScreen/Sources/ChatSendStarsScreen.swift @@ -23,6 +23,8 @@ import TextFormat import CheckComponent import ContextUI import StarsBalanceOverlayComponent +import TelegramStringFormatting +import ChatScheduleTimeController private final class BalanceComponent: CombinedComponent { let context: AccountContext @@ -166,6 +168,7 @@ private final class BadgeComponent: Component { private var badgeShapeArguments: (Double, Double, CGSize, CGFloat, CGFloat)? private var component: BadgeComponent? + private var isUpdating: Bool = false private var previousAvailableSize: CGSize? @@ -230,6 +233,11 @@ private final class BadgeComponent: Component { } func update(component: BadgeComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.isUpdating = true + defer { + self.isUpdating = false + } + if self.component == nil { self.badgeIcon.image = UIImage(bundleImageName: "Premium/SendStarsStarSliderIcon")?.withRenderingMode(.alwaysTemplate) } @@ -821,74 +829,17 @@ private final class ChatSendStarsScreenComponent: Component { typealias EnvironmentType = ViewControllerComponentContainer.Environment let context: AccountContext - let peer: EnginePeer - let myPeer: EnginePeer - let defaultPrivacyPeer: ChatSendStarsScreenComponent.PrivacyPeer - let channelsForPublicReaction: [EnginePeer] - let messageId: EngineMessage.Id - let maxAmount: Int - let balance: StarsAmount? - let currentSentAmount: Int? - let topPeers: [ChatSendStarsScreen.TopPeer] - let myTopPeer: ChatSendStarsScreen.TopPeer? - let completion: (Int64, TelegramPaidReactionPrivacy, Bool, ChatSendStarsScreen.TransitionOut) -> Void + let initialData: ChatSendStarsScreen.InitialData init( context: AccountContext, - peer: EnginePeer, - myPeer: EnginePeer, - defaultPrivacyPeer: ChatSendStarsScreenComponent.PrivacyPeer, - channelsForPublicReaction: [EnginePeer], - messageId: EngineMessage.Id, - maxAmount: Int, - balance: StarsAmount?, - currentSentAmount: Int?, - topPeers: [ChatSendStarsScreen.TopPeer], - myTopPeer: ChatSendStarsScreen.TopPeer?, - completion: @escaping (Int64, TelegramPaidReactionPrivacy, Bool, ChatSendStarsScreen.TransitionOut) -> Void + initialData: ChatSendStarsScreen.InitialData ) { self.context = context - self.peer = peer - self.myPeer = myPeer - self.defaultPrivacyPeer = defaultPrivacyPeer - self.channelsForPublicReaction = channelsForPublicReaction - self.messageId = messageId - self.maxAmount = maxAmount - self.balance = balance - self.currentSentAmount = currentSentAmount - self.topPeers = topPeers - self.myTopPeer = myTopPeer - self.completion = completion + self.initialData = initialData } static func ==(lhs: ChatSendStarsScreenComponent, rhs: ChatSendStarsScreenComponent) -> Bool { - if lhs.context !== rhs.context { - return false - } - if lhs.peer != rhs.peer { - return false - } - if lhs.myPeer != rhs.myPeer { - return false - } - if lhs.channelsForPublicReaction != rhs.channelsForPublicReaction { - return false - } - if lhs.maxAmount != rhs.maxAmount { - return false - } - if lhs.balance != rhs.balance { - return false - } - if lhs.currentSentAmount != rhs.currentSentAmount { - return false - } - if lhs.topPeers != rhs.topPeers { - return false - } - if lhs.myTopPeer != rhs.myTopPeer { - return false - } return true } @@ -1013,6 +964,7 @@ private final class ChatSendStarsScreenComponent: Component { private let title = ComponentView() private let subtitle = ComponentView() private let descriptionText = ComponentView() + private let timeSelectorButton = ComponentView() private let badgeStars = BadgeStarsView() private let sliderBackground = ComponentView() @@ -1038,6 +990,7 @@ private final class ChatSendStarsScreenComponent: Component { private var component: ChatSendStarsScreenComponent? private weak var state: EmptyComponentState? + private var isUpdating: Bool = false private var environment: ViewControllerComponentContainer.Environment? private var itemLayout: ItemLayout? @@ -1062,6 +1015,8 @@ private final class ChatSendStarsScreenComponent: Component { private var channelsForPublicReaction: [EnginePeer] = [] private var channelsForPublicReactionDisposable: Disposable? + private var currentSuggestPostTimestamp: Int32? + override init(frame: CGRect) { self.bottomOverscrollLimit = 200.0 @@ -1337,12 +1292,15 @@ private final class ChatSendStarsScreenComponent: Component { guard let component = self.component, let environment = self.environment, let controller = environment.controller() else { return } + guard case let .react(reactData) = component.initialData.subjectInitialData else { + return + } var items: [ContextMenuItem] = [] let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }) - var peers: [EnginePeer] = [component.myPeer] + var peers: [EnginePeer] = [reactData.myPeer] peers.append(contentsOf: self.channelsForPublicReaction) let avatarSize = CGSize(width: 30.0, height: 30.0) @@ -1396,6 +1354,9 @@ private final class ChatSendStarsScreenComponent: Component { guard let self, let component = self.component else { return } + guard case let .react(reactData) = component.initialData.subjectInitialData else { + return + } if self.currentMyPeer?.id == peer.id { return } @@ -1409,7 +1370,7 @@ private final class ChatSendStarsScreenComponent: Component { self.privacyPeer = .peer(peer) } - if component.myTopPeer != nil { + if reactData.myTopPeer != nil { let mappedPrivacy: TelegramPaidReactionPrivacy switch self.privacyPeer { case .account: @@ -1420,7 +1381,7 @@ private final class ChatSendStarsScreenComponent: Component { mappedPrivacy = .peer(peer.id) } - let _ = component.context.engine.messages.updateStarsReactionPrivacy(id: component.messageId, privacy: mappedPrivacy).startStandalone() + let _ = component.context.engine.messages.updateStarsReactionPrivacy(id: reactData.messageId, privacy: mappedPrivacy).startStandalone() } } @@ -1432,7 +1393,37 @@ private final class ChatSendStarsScreenComponent: Component { controller.presentInGlobalOverlay(contextController) } + private func displaySuggestTimeSelectionMenu(sourceView: UIView) { + guard let component = self.component else { + return + } + guard let environment = self.environment else { + return + } + guard case let .suggestPost(suggestPostData) = component.initialData.subjectInitialData else { + return + } + + let mode: ChatScheduleTimeControllerMode = .suggestPost + let theme = environment.theme + let controller = ChatScheduleTimeController(context: component.context, updatedPresentationData: (component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: theme), component.context.sharedContext.presentationData |> map { $0.withUpdated(theme: theme) }), peerId: suggestPostData.peer.id, mode: mode, style: .default, currentTime: self.currentSuggestPostTimestamp, minimalTime: nil, dismissByTapOutside: true, completion: { [weak self] time in + guard let self else { + return + } + self.currentSuggestPostTimestamp = time == 0 ? nil : time + if !self.isUpdating { + self.state?.updated(transition: .immediate) + } + }) + environment.controller()?.present(controller, in: .window(.root)) + } + func update(component: ChatSendStarsScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.isUpdating = true + defer { + self.isUpdating = false + } + let environment = environment[ViewControllerComponentContainer.Environment.self].value let themeUpdated = self.environment?.theme !== environment.theme @@ -1460,8 +1451,8 @@ private final class ChatSendStarsScreenComponent: Component { self.environment?.controller()?.dismiss() let _ = (context.engine.payments.starsTopUpOptions() - |> take(1) - |> deliverOnMainQueue).startStandalone(next: { options in + |> take(1) + |> deliverOnMainQueue).startStandalone(next: { options in let controller = context.sharedContext.makeStarsPurchaseScreen( context: context, starsContext: starsContext, @@ -1489,33 +1480,49 @@ private final class ChatSendStarsScreenComponent: Component { } if self.component == nil { - self.currentMyPeer = component.myPeer + self.balance = component.initialData.balance - self.balance = component.balance - var isLogarithmic = true - if let data = component.context.currentAppConfiguration.with({ $0 }).data, let value = data["ios_stars_reaction_logarithmic_scale"] as? Double { - isLogarithmic = Int(value) != 0 - } - self.amount = Amount(realValue: 50, maxRealValue: component.maxAmount, maxSliderValue: 999, isLogarithmic: isLogarithmic) - if let myTopPeer = component.myTopPeer { - if myTopPeer.isAnonymous { - self.privacyPeer = .anonymous - } else if myTopPeer.peer?.id == component.context.account.peerId { - self.privacyPeer = .account - } else if let peer = myTopPeer.peer { - self.privacyPeer = .peer(peer) - self.currentMyPeer = peer + switch component.initialData.subjectInitialData { + case let .react(reactData): + self.currentMyPeer = reactData.myPeer + self.amount = Amount(realValue: 50, maxRealValue: reactData.maxAmount, maxSliderValue: 999, isLogarithmic: true) + + if let myTopPeer = reactData.myTopPeer { + if myTopPeer.isAnonymous { + self.privacyPeer = .anonymous + } else if myTopPeer.peer?.id == component.context.account.peerId { + self.privacyPeer = .account + } else if let peer = myTopPeer.peer { + self.privacyPeer = .peer(peer) + self.currentMyPeer = peer + } else { + self.privacyPeer = .account + } } else { - self.privacyPeer = .account - } - } else { - self.privacyPeer = component.defaultPrivacyPeer - switch self.privacyPeer { - case .anonymous, .account: - break - case let .peer(peer): - self.currentMyPeer = peer + self.privacyPeer = reactData.defaultPrivacyPeer + switch self.privacyPeer { + case .anonymous, .account: + break + case let .peer(peer): + self.currentMyPeer = peer + } } + + self.channelsForPublicReactionDisposable = (component.context.engine.peers.channelsForPublicReaction(useLocalCache: false) + |> deliverOnMainQueue).startStrict(next: { [weak self] peers in + guard let self else { + return + } + if self.channelsForPublicReaction != peers { + self.channelsForPublicReaction = peers + if !self.isUpdating { + self.state?.updated(transition: .immediate) + } + } + }) + case let .suggestPost(suggestPostData): + self.currentSuggestPostTimestamp = suggestPostData.initialTimestamp + self.amount = Amount(realValue: 50, maxRealValue: 10000, maxSliderValue: 999, isLogarithmic: true) } if let starsContext = component.context.starsContext { @@ -1532,17 +1539,6 @@ private final class ChatSendStarsScreenComponent: Component { } }) } - - self.channelsForPublicReactionDisposable = (component.context.engine.peers.channelsForPublicReaction(useLocalCache: false) - |> deliverOnMainQueue).startStrict(next: { [weak self] peers in - guard let self else { - return - } - if self.channelsForPublicReaction != peers { - self.channelsForPublicReaction = peers - self.state?.updated(transition: .immediate) - } - }) } self.component = component @@ -1579,12 +1575,21 @@ private final class ChatSendStarsScreenComponent: Component { guard let self, let component = self.component else { return } + + let maxAmount: Int + switch component.initialData.subjectInitialData { + case let .react(reactData): + maxAmount = reactData.maxAmount + case let .suggestPost(suggestPostData): + maxAmount = suggestPostData.maxAmount + } + self.amount = self.amount.withSliderValue(value) self.didChangeAmount = true self.state?.updated(transition: ComponentTransition(animation: .none).withUserData(IsAdjustingAmountHint())) - let sliderValue = Float(value) / Float(component.maxAmount) + let sliderValue = Float(value) / Float(maxAmount) let currentTimestamp = CACurrentMediaTime() if let previousTimestamp { @@ -1627,31 +1632,37 @@ private final class ChatSendStarsScreenComponent: Component { let progressFraction: CGFloat = CGFloat(self.amount.sliderValue) / CGFloat(self.amount.maxSliderValue) - let topOthersCount: Int? = component.topPeers.filter({ !$0.isMy }).max(by: { $0.count < $1.count })?.count - var topCount: Int? - if let topOthersCount { - if let myTopPeer = component.myTopPeer { - topCount = max(0, topOthersCount - myTopPeer.count + 1) - } else { - topCount = topOthersCount - } - if topCount == 0 { - topCount = nil - } - } - var topCutoffFraction: CGFloat? - if let topCount { - let topCutoffFractionValue = CGFloat(topCount) / CGFloat(component.maxAmount - 1) - topCutoffFraction = topCutoffFractionValue - - let isPastCutoff = progressFraction >= topCutoffFractionValue - if let isPastTopCutoff = self.isPastTopCutoff, isPastTopCutoff != isPastCutoff { - HapticFeedback().tap() + + var topCount: Int? + switch component.initialData.subjectInitialData { + case let .react(reactData): + let topOthersCount: Int? = reactData.topPeers.filter({ !$0.isMy }).max(by: { $0.count < $1.count })?.count + if let topOthersCount { + if let myTopPeer = reactData.myTopPeer { + topCount = max(0, topOthersCount - myTopPeer.count + 1) + } else { + topCount = topOthersCount + } + if topCount == 0 { + topCount = nil + } } - self.isPastTopCutoff = isPastCutoff - } else { - self.isPastTopCutoff = nil + + if let topCount { + let topCutoffFractionValue = CGFloat(topCount) / CGFloat(reactData.maxAmount - 1) + topCutoffFraction = topCutoffFractionValue + + let isPastCutoff = progressFraction >= topCutoffFractionValue + if let isPastTopCutoff = self.isPastTopCutoff, isPastTopCutoff != isPastCutoff { + HapticFeedback().tap() + } + self.isPastTopCutoff = isPastCutoff + } else { + self.isPastTopCutoff = nil + } + case .suggestPost: + break } let _ = self.sliderBackground.update( @@ -1679,7 +1690,7 @@ private final class ChatSendStarsScreenComponent: Component { let badgeSize = self.badge.update( transition: transition, component: AnyComponent(BadgeComponent( - theme: environment.theme, + theme: environment.theme, title: "\(self.amount.realValue)" )), environment: {}, @@ -1721,55 +1732,62 @@ private final class ChatSendStarsScreenComponent: Component { contentHeight += 123.0 - var sendAsPeers: [EnginePeer] = [component.myPeer] - sendAsPeers.append(contentsOf: self.channelsForPublicReaction) - - let leftButtonSize = self.leftButton.update( - transition: transition, - component: AnyComponent(BalanceComponent( - context: component.context, - theme: environment.theme, - strings: environment.strings, - balance: self.balance - )), - environment: {}, - containerSize: CGSize(width: 120.0, height: 100.0) - ) - let leftButtonFrame = CGRect(origin: CGPoint(x: sideInset, y: floor((56.0 - leftButtonSize.height) * 0.5)), size: leftButtonSize) - if let leftButtonView = self.leftButton.view { - if leftButtonView.superview == nil { - self.navigationBarContainer.addSubview(leftButtonView) - } - transition.setFrame(view: leftButtonView, frame: leftButtonFrame) - leftButtonView.isHidden = sendAsPeers.count > 1 - } - - let currentMyPeer = self.currentMyPeer ?? component.myPeer - - let peerSelectorButtonSize = self.peerSelectorButton.update( - transition: transition, - component: AnyComponent(PeerSelectorBadgeComponent( - context: component.context, - theme: environment.theme, - strings: environment.strings, - peer: currentMyPeer, - action: { [weak self] sourceView in - guard let self else { - return - } - self.displayTargetSelectionMenu(sourceView: sourceView) + var leftButtonFrameValue: CGRect? + switch component.initialData.subjectInitialData { + case let .react(reactData): + var sendAsPeers: [EnginePeer] = [reactData.myPeer] + sendAsPeers.append(contentsOf: self.channelsForPublicReaction) + + let leftButtonSize = self.leftButton.update( + transition: transition, + component: AnyComponent(BalanceComponent( + context: component.context, + theme: environment.theme, + strings: environment.strings, + balance: self.balance + )), + environment: {}, + containerSize: CGSize(width: 120.0, height: 100.0) + ) + let leftButtonFrame = CGRect(origin: CGPoint(x: sideInset, y: floor((56.0 - leftButtonSize.height) * 0.5)), size: leftButtonSize) + if let leftButtonView = self.leftButton.view { + if leftButtonView.superview == nil { + self.navigationBarContainer.addSubview(leftButtonView) } - )), - environment: {}, - containerSize: CGSize(width: 120.0, height: 100.0) - ) - let peerSelectorButtonFrame = CGRect(origin: CGPoint(x: sideInset, y: 1.0 + floor((56.0 - peerSelectorButtonSize.height) * 0.5)), size: peerSelectorButtonSize) - if let peerSelectorButtonView = self.peerSelectorButton.view { - if peerSelectorButtonView.superview == nil { - self.navigationBarContainer.addSubview(peerSelectorButtonView) + transition.setFrame(view: leftButtonView, frame: leftButtonFrame) + leftButtonView.isHidden = sendAsPeers.count > 1 } - transition.setFrame(view: peerSelectorButtonView, frame: peerSelectorButtonFrame) - peerSelectorButtonView.isHidden = sendAsPeers.count <= 1 + leftButtonFrameValue = leftButtonFrame + + let currentMyPeer = self.currentMyPeer ?? reactData.myPeer + + let peerSelectorButtonSize = self.peerSelectorButton.update( + transition: transition, + component: AnyComponent(PeerSelectorBadgeComponent( + context: component.context, + theme: environment.theme, + strings: environment.strings, + peer: currentMyPeer, + action: { [weak self] sourceView in + guard let self else { + return + } + self.displayTargetSelectionMenu(sourceView: sourceView) + } + )), + environment: {}, + containerSize: CGSize(width: 120.0, height: 100.0) + ) + let peerSelectorButtonFrame = CGRect(origin: CGPoint(x: sideInset, y: 1.0 + floor((56.0 - peerSelectorButtonSize.height) * 0.5)), size: peerSelectorButtonSize) + if let peerSelectorButtonView = self.peerSelectorButton.view { + if peerSelectorButtonView.superview == nil { + self.navigationBarContainer.addSubview(peerSelectorButtonView) + } + transition.setFrame(view: peerSelectorButtonView, frame: peerSelectorButtonFrame) + peerSelectorButtonView.isHidden = sendAsPeers.count <= 1 + } + case .suggestPost: + break } if themeUpdated { @@ -1797,7 +1815,7 @@ private final class ChatSendStarsScreenComponent: Component { environment: {}, containerSize: CGSize(width: 30.0, height: 30.0) ) - let closeButtonFrame = CGRect(origin: CGPoint(x: availableSize.width - sideInset - closeButtonSize.width, y: floor((56.0 - leftButtonSize.height) * 0.5)), size: closeButtonSize) + let closeButtonFrame = CGRect(origin: CGPoint(x: availableSize.width - sideInset - closeButtonSize.width, y: floor((56.0 - 34.0) * 0.5)), size: closeButtonSize) if let closeButtonView = self.closeButton.view { if closeButtonView.superview == nil { self.navigationBarContainer.addSubview(closeButtonView) @@ -1816,25 +1834,51 @@ private final class ChatSendStarsScreenComponent: Component { let titleSubtitleSpacing: CGFloat = 1.0 - let subtitleSize = self.subtitle.update( - transition: .immediate, - component: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString(string: environment.strings.SendStarReactions_SubtitleFrom(currentMyPeer.compactDisplayTitle).string, font: Font.regular(12.0), textColor: environment.theme.list.itemSecondaryTextColor)) - )), - environment: {}, - containerSize: CGSize(width: availableSize.width - leftButtonFrame.maxX * 2.0, height: 100.0) - ) - + let subtitleText: String? + switch component.initialData.subjectInitialData { + case let .react(reactData): + let currentMyPeer = self.currentMyPeer ?? reactData.myPeer + subtitleText = environment.strings.SendStarReactions_SubtitleFrom(currentMyPeer.compactDisplayTitle).string + case .suggestPost: + subtitleText = nil + } + + var subtitleSize: CGSize? + if let subtitleText { + subtitleSize = self.subtitle.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: subtitleText, font: Font.regular(12.0), textColor: environment.theme.list.itemSecondaryTextColor)) + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - (leftButtonFrameValue?.maxX ?? sideInset) * 2.0, height: 100.0) + ) + } + + let titleText: String + switch component.initialData.subjectInitialData { + case .react: + titleText = environment.strings.SendStarReactions_Title + case .suggestPost: + //TODO:localize + titleText = "Suggest a Message" + } + let titleSize = title.update( transition: .immediate, component: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString(string: environment.strings.SendStarReactions_Title, font: Font.semibold(17.0), textColor: environment.theme.list.itemPrimaryTextColor)) + text: .plain(NSAttributedString(string: titleText, font: Font.semibold(17.0), textColor: environment.theme.list.itemPrimaryTextColor)) )), environment: {}, - containerSize: CGSize(width: availableSize.width - leftButtonFrame.maxX * 2.0, height: 100.0) + containerSize: CGSize(width: availableSize.width - (leftButtonFrameValue?.maxX ?? sideInset) * 2.0, height: 100.0) ) - let titleSubtitleHeight = titleSize.height + titleSubtitleSpacing + subtitleSize.height + let titleSubtitleHeight: CGFloat + if let subtitleSize { + titleSubtitleHeight = titleSize.height + titleSubtitleSpacing + subtitleSize.height + } else { + titleSubtitleHeight = titleSize.height + } let titleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) * 0.5), y: floor((56.0 - titleSubtitleHeight) * 0.5)), size: titleSize) if let titleView = title.view { @@ -1844,24 +1888,32 @@ private final class ChatSendStarsScreenComponent: Component { transition.setFrame(view: titleView, frame: titleFrame) } - let subtitleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - subtitleSize.width) * 0.5), y: titleFrame.maxY + titleSubtitleSpacing), size: subtitleSize) - if let subtitleView = subtitle.view { - if subtitleView.superview == nil { - self.navigationBarContainer.addSubview(subtitleView) + if let subtitleSize { + let subtitleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - subtitleSize.width) * 0.5), y: titleFrame.maxY + titleSubtitleSpacing), size: subtitleSize) + if let subtitleView = self.subtitle.view { + if subtitleView.superview == nil { + self.navigationBarContainer.addSubview(subtitleView) + } + transition.setFrame(view: subtitleView, frame: subtitleFrame) } - transition.setFrame(view: subtitleView, frame: subtitleFrame) } - + contentHeight += 56.0 contentHeight += 8.0 let text: String - if let currentSentAmount = component.currentSentAmount { - text = environment.strings.SendStarReactions_TextSentStars(Int32(currentSentAmount)) - } else { - text = environment.strings.SendStarReactions_TextGeneric(component.peer.debugDisplayTitle).string + switch component.initialData.subjectInitialData { + case let .react(reactData): + if let currentSentAmount = reactData.currentSentAmount { + text = environment.strings.SendStarReactions_TextSentStars(Int32(currentSentAmount)) + } else { + text = environment.strings.SendStarReactions_TextGeneric(reactData.peer.debugDisplayTitle).string + } + case let .suggestPost(suggestPostData): + //TODO:localize + text = "Choose how many stars you want to offer **\(suggestPostData.peer.compactDisplayTitle)** to publish this message." } - + let body = MarkdownAttributeSet(font: Font.regular(15.0), textColor: environment.theme.list.itemPrimaryTextColor) let bold = MarkdownAttributeSet(font: Font.semibold(15.0), textColor: environment.theme.list.itemPrimaryTextColor) @@ -1875,7 +1927,8 @@ private final class ChatSendStarsScreenComponent: Component { linkAttribute: { _ in nil } )), horizontalAlignment: .center, - maximumNumberOfLines: 0 + maximumNumberOfLines: 0, + lineSpacing: 0.2 )), environment: {}, containerSize: CGSize(width: availableSize.width - sideInset * 2.0 - 16.0 * 2.0, height: 1000.0) @@ -1887,262 +1940,292 @@ private final class ChatSendStarsScreenComponent: Component { } transition.setFrame(view: descriptionTextView, frame: descriptionTextFrame) } - + contentHeight += descriptionTextFrame.height contentHeight += 22.0 contentHeight += 2.0 - if !component.topPeers.isEmpty { + if case .suggestPost = component.initialData.subjectInitialData { contentHeight += 3.0 - let topPeersLeftSeparator: SimpleLayer - if let current = self.topPeersLeftSeparator { - topPeersLeftSeparator = current - } else { - topPeersLeftSeparator = SimpleLayer() - self.topPeersLeftSeparator = topPeersLeftSeparator - self.scrollContentView.layer.addSublayer(topPeersLeftSeparator) - } - - let topPeersRightSeparator: SimpleLayer - if let current = self.topPeersRightSeparator { - topPeersRightSeparator = current - } else { - topPeersRightSeparator = SimpleLayer() - self.topPeersRightSeparator = topPeersRightSeparator - self.scrollContentView.layer.addSublayer(topPeersRightSeparator) - } - - let topPeersTitleBackground: SimpleLayer - if let current = self.topPeersTitleBackground { - topPeersTitleBackground = current - } else { - topPeersTitleBackground = SimpleLayer() - self.topPeersTitleBackground = topPeersTitleBackground - self.scrollContentView.layer.addSublayer(topPeersTitleBackground) - } - - let topPeersTitle: ComponentView - if let current = self.topPeersTitle { - topPeersTitle = current - } else { - topPeersTitle = ComponentView() - self.topPeersTitle = topPeersTitle - } - - topPeersLeftSeparator.backgroundColor = environment.theme.list.itemPlainSeparatorColor.cgColor - topPeersRightSeparator.backgroundColor = environment.theme.list.itemPlainSeparatorColor.cgColor - - let topPeersTitleSize = topPeersTitle.update( - transition: .immediate, - component: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString(string: environment.strings.SendStarReactions_SectionTop, font: Font.semibold(15.0), textColor: .white)) + let timeSelectorButtonSize = self.timeSelectorButton.update( + transition: transition, + component: AnyComponent(TimeSelectorBadgeComponent( + context: component.context, + theme: environment.theme, + strings: environment.strings, + timestamp: self.currentSuggestPostTimestamp, + action: { [weak self] sourceView in + guard let self else { + return + } + self.displaySuggestTimeSelectionMenu(sourceView: sourceView) + } )), environment: {}, - containerSize: CGSize(width: 300.0, height: 100.0) + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100.0) ) - let topPeersBackgroundSize = CGSize(width: topPeersTitleSize.width + 16.0 * 2.0, height: topPeersTitleSize.height + 9.0 * 2.0) - let topPeersBackgroundFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - topPeersBackgroundSize.width) * 0.5), y: contentHeight), size: topPeersBackgroundSize) - - topPeersTitleBackground.backgroundColor = UIColor(rgb: 0xFFB10D).cgColor - topPeersTitleBackground.cornerRadius = topPeersBackgroundFrame.height * 0.5 - transition.setFrame(layer: topPeersTitleBackground, frame: topPeersBackgroundFrame) - - let topPeersTitleFrame = CGRect(origin: CGPoint(x: topPeersBackgroundFrame.minX + floor((topPeersBackgroundFrame.width - topPeersTitleSize.width) * 0.5), y: topPeersBackgroundFrame.minY + floor((topPeersBackgroundFrame.height - topPeersTitleSize.height) * 0.5)), size: topPeersTitleSize) - if let topPeersTitleView = topPeersTitle.view { - if topPeersTitleView.superview == nil { - self.scrollContentView.addSubview(topPeersTitleView) + let timeSelectorButtonFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - timeSelectorButtonSize.width) * 0.5), y: contentHeight), size: timeSelectorButtonSize) + if let timeSelectorButtonView = self.timeSelectorButton.view { + if timeSelectorButtonView.superview == nil { + self.navigationBarContainer.addSubview(timeSelectorButtonView) } - transition.setFrame(view: topPeersTitleView, frame: topPeersTitleFrame) + transition.setFrame(view: timeSelectorButtonView, frame: timeSelectorButtonFrame) } + contentHeight += timeSelectorButtonSize.height - let separatorY = topPeersBackgroundFrame.midY - let separatorSpacing: CGFloat = 10.0 - transition.setFrame(layer: topPeersLeftSeparator, frame: CGRect(origin: CGPoint(x: sideInset, y: separatorY), size: CGSize(width: max(0.0, topPeersBackgroundFrame.minX - separatorSpacing - sideInset), height: UIScreenPixel))) - transition.setFrame(layer: topPeersRightSeparator, frame: CGRect(origin: CGPoint(x: topPeersBackgroundFrame.maxX + separatorSpacing, y: separatorY), size: CGSize(width: max(0.0, availableSize.width - sideInset - (topPeersBackgroundFrame.maxX + separatorSpacing)), height: UIScreenPixel))) - - var mappedTopPeers = component.topPeers - if let index = mappedTopPeers.firstIndex(where: { $0.isMy }) { - mappedTopPeers.remove(at: index) - } - - var myCount = 0 - if let myTopPeer = component.myTopPeer { - myCount += myTopPeer.count - } - var myCountAddition = 0 - if self.didChangeAmount { - myCountAddition = Int(self.amount.realValue) - } - myCount += myCountAddition - if myCount != 0 { - var topPeer: EnginePeer? - switch self.privacyPeer { - case .anonymous: - topPeer = nil - case .account: - topPeer = component.myPeer - case let .peer(peer): - topPeer = peer - } - - mappedTopPeers.append(ChatSendStarsScreen.TopPeer( - randomIndex: -1, - peer: topPeer, - isMy: true, - count: myCount - )) - } - mappedTopPeers.sort(by: { $0.count > $1.count }) - if mappedTopPeers.count > 3 { - mappedTopPeers = Array(mappedTopPeers.prefix(3)) - } - - var animateItems = false - var itemPositionTransition = transition - var itemAlphaTransition = transition - if transition.userData(IsAdjustingAmountHint.self) != nil { - animateItems = true - itemPositionTransition = .spring(duration: 0.3) - itemAlphaTransition = .easeInOut(duration: 0.15) - } - - var validIds: [ChatSendStarsScreen.TopPeer.Id] = [] - var items: [(itemView: ComponentView, size: CGSize)] = [] - for topPeer in mappedTopPeers { - validIds.append(topPeer.id) - - let itemView: ComponentView - if let current = self.topPeerItems[topPeer.id] { - itemView = current - } else { - itemView = ComponentView() - self.topPeerItems[topPeer.id] = itemView - } - - let itemCountString = presentationStringsFormattedNumber(Int32(topPeer.count), environment.dateTimeFormat.groupingSeparator) - /*if topPeer.isMy && myCountAddition != 0 && topPeer.count > myCountAddition { - itemCountString = "\(topPeer.count - myCountAddition) +\(myCountAddition)" - }*/ - - let itemSize = itemView.update( - transition: .immediate, - component: AnyComponent(PlainButtonComponent( - content: AnyComponent(PeerComponent( - context: component.context, - theme: environment.theme, - strings: environment.strings, - peer: topPeer.peer, - count: itemCountString - )), - effectAlignment: .center, - action: { [weak self] in - guard let self, let component = self.component, let peer = topPeer.peer else { - return - } - guard let controller = self.environment?.controller() else { - return - } - guard let navigationController = controller.navigationController as? NavigationController else { - return - } - var viewControllers = navigationController.viewControllers - guard let index = viewControllers.firstIndex(where: { $0 === controller }) else { - return - } - - let context = component.context - - if case .user = peer { - if let peerInfoController = context.sharedContext.makePeerInfoController( - context: context, - updatedPresentationData: nil, - peer: peer._asPeer(), - mode: .generic, - avatarInitiallyExpanded: false, - fromChat: false, - requestsContext: nil - ) { - viewControllers.insert(peerInfoController, at: index) - } - } else { - let chatController = context.sharedContext.makeChatController(context: context, chatLocation: .peer(id: peer.id), subject: nil, botStart: nil, mode: .standard(.default), params: nil) - viewControllers.insert(chatController, at: index) - } - navigationController.setViewControllers(viewControllers, animated: true) - controller.dismiss() - }, - isEnabled: topPeer.peer != nil && topPeer.peer?.id != component.context.account.peerId, - animateAlpha: false - )), - environment: {}, - containerSize: CGSize(width: 200.0, height: 200.0) - ) - items.append((itemView, itemSize)) - } - var removedIds: [ChatSendStarsScreen.TopPeer.Id] = [] - for (id, itemView) in self.topPeerItems { - if !validIds.contains(id) { - removedIds.append(id) - - if animateItems { - if let itemComponentView = itemView.view { - itemPositionTransition.setScale(view: itemComponentView, scale: 0.001) - itemAlphaTransition.setAlpha(view: itemComponentView, alpha: 0.0, completion: { [weak itemComponentView] _ in - itemComponentView?.removeFromSuperview() - }) - } - } else { - itemView.view?.removeFromSuperview() - } - } - } - for id in removedIds { - self.topPeerItems.removeValue(forKey: id) - } - - var itemsWidth: CGFloat = 0.0 - for (_, itemSize) in items { - itemsWidth += itemSize.width - } - - let maxItemSpacing = 48.0 - var itemSpacing = floor((availableSize.width - itemsWidth) / CGFloat(items.count + 1)) - itemSpacing = min(itemSpacing, maxItemSpacing) - - let totalWidth = itemsWidth + itemSpacing * CGFloat(items.count + 1) - var itemX: CGFloat = floor((availableSize.width - totalWidth) * 0.5) + itemSpacing - for (itemView, itemSize) in items { - if let itemComponentView = itemView.view { - var animateItem = animateItems - if itemComponentView.superview == nil { - self.scrollContentView.addSubview(itemComponentView) - animateItem = false - ComponentTransition.immediate.setScale(view: itemComponentView, scale: 0.001) - itemComponentView.alpha = 0.0 - } - - let itemFrame = CGRect(origin: CGPoint(x: itemX, y: contentHeight + 56.0), size: itemSize) - - if animateItem { - itemPositionTransition.setPosition(view: itemComponentView, position: itemFrame.center) - itemPositionTransition.setBounds(view: itemComponentView, bounds: CGRect(origin: CGPoint(), size: itemFrame.size)) - } else { - itemComponentView.center = itemFrame.center - itemComponentView.bounds = CGRect(origin: CGPoint(), size: itemFrame.size) - } - - itemPositionTransition.setScale(view: itemComponentView, scale: 1.0) - itemAlphaTransition.setAlpha(view: itemComponentView, alpha: 1.0) - } - itemX += itemSize.width + itemSpacing - } - - contentHeight += 161.0 + contentHeight += 32.0 } - do { - if !component.topPeers.isEmpty { + switch component.initialData.subjectInitialData { + case let .react(reactData): + if !reactData.topPeers.isEmpty { + contentHeight += 3.0 + + let topPeersLeftSeparator: SimpleLayer + if let current = self.topPeersLeftSeparator { + topPeersLeftSeparator = current + } else { + topPeersLeftSeparator = SimpleLayer() + self.topPeersLeftSeparator = topPeersLeftSeparator + self.scrollContentView.layer.addSublayer(topPeersLeftSeparator) + } + + let topPeersRightSeparator: SimpleLayer + if let current = self.topPeersRightSeparator { + topPeersRightSeparator = current + } else { + topPeersRightSeparator = SimpleLayer() + self.topPeersRightSeparator = topPeersRightSeparator + self.scrollContentView.layer.addSublayer(topPeersRightSeparator) + } + + let topPeersTitleBackground: SimpleLayer + if let current = self.topPeersTitleBackground { + topPeersTitleBackground = current + } else { + topPeersTitleBackground = SimpleLayer() + self.topPeersTitleBackground = topPeersTitleBackground + self.scrollContentView.layer.addSublayer(topPeersTitleBackground) + } + + let topPeersTitle: ComponentView + if let current = self.topPeersTitle { + topPeersTitle = current + } else { + topPeersTitle = ComponentView() + self.topPeersTitle = topPeersTitle + } + + topPeersLeftSeparator.backgroundColor = environment.theme.list.itemPlainSeparatorColor.cgColor + topPeersRightSeparator.backgroundColor = environment.theme.list.itemPlainSeparatorColor.cgColor + + let topPeersTitleSize = topPeersTitle.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: environment.strings.SendStarReactions_SectionTop, font: Font.semibold(15.0), textColor: .white)) + )), + environment: {}, + containerSize: CGSize(width: 300.0, height: 100.0) + ) + let topPeersBackgroundSize = CGSize(width: topPeersTitleSize.width + 16.0 * 2.0, height: topPeersTitleSize.height + 9.0 * 2.0) + let topPeersBackgroundFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - topPeersBackgroundSize.width) * 0.5), y: contentHeight), size: topPeersBackgroundSize) + + topPeersTitleBackground.backgroundColor = UIColor(rgb: 0xFFB10D).cgColor + topPeersTitleBackground.cornerRadius = topPeersBackgroundFrame.height * 0.5 + transition.setFrame(layer: topPeersTitleBackground, frame: topPeersBackgroundFrame) + + let topPeersTitleFrame = CGRect(origin: CGPoint(x: topPeersBackgroundFrame.minX + floor((topPeersBackgroundFrame.width - topPeersTitleSize.width) * 0.5), y: topPeersBackgroundFrame.minY + floor((topPeersBackgroundFrame.height - topPeersTitleSize.height) * 0.5)), size: topPeersTitleSize) + if let topPeersTitleView = topPeersTitle.view { + if topPeersTitleView.superview == nil { + self.scrollContentView.addSubview(topPeersTitleView) + } + transition.setFrame(view: topPeersTitleView, frame: topPeersTitleFrame) + } + + let separatorY = topPeersBackgroundFrame.midY + let separatorSpacing: CGFloat = 10.0 + transition.setFrame(layer: topPeersLeftSeparator, frame: CGRect(origin: CGPoint(x: sideInset, y: separatorY), size: CGSize(width: max(0.0, topPeersBackgroundFrame.minX - separatorSpacing - sideInset), height: UIScreenPixel))) + transition.setFrame(layer: topPeersRightSeparator, frame: CGRect(origin: CGPoint(x: topPeersBackgroundFrame.maxX + separatorSpacing, y: separatorY), size: CGSize(width: max(0.0, availableSize.width - sideInset - (topPeersBackgroundFrame.maxX + separatorSpacing)), height: UIScreenPixel))) + + var mappedTopPeers = reactData.topPeers + if let index = mappedTopPeers.firstIndex(where: { $0.isMy }) { + mappedTopPeers.remove(at: index) + } + + var myCount = 0 + if let myTopPeer = reactData.myTopPeer { + myCount += myTopPeer.count + } + var myCountAddition = 0 + if self.didChangeAmount { + myCountAddition = Int(self.amount.realValue) + } + myCount += myCountAddition + if myCount != 0 { + var topPeer: EnginePeer? + switch self.privacyPeer { + case .anonymous: + topPeer = nil + case .account: + topPeer = reactData.myPeer + case let .peer(peer): + topPeer = peer + } + + mappedTopPeers.append(ChatSendStarsScreen.TopPeer( + randomIndex: -1, + peer: topPeer, + isMy: true, + count: myCount + )) + } + mappedTopPeers.sort(by: { $0.count > $1.count }) + if mappedTopPeers.count > 3 { + mappedTopPeers = Array(mappedTopPeers.prefix(3)) + } + + var animateItems = false + var itemPositionTransition = transition + var itemAlphaTransition = transition + if transition.userData(IsAdjustingAmountHint.self) != nil { + animateItems = true + itemPositionTransition = .spring(duration: 0.3) + itemAlphaTransition = .easeInOut(duration: 0.15) + } + + var validIds: [ChatSendStarsScreen.TopPeer.Id] = [] + var items: [(itemView: ComponentView, size: CGSize)] = [] + for topPeer in mappedTopPeers { + validIds.append(topPeer.id) + + let itemView: ComponentView + if let current = self.topPeerItems[topPeer.id] { + itemView = current + } else { + itemView = ComponentView() + self.topPeerItems[topPeer.id] = itemView + } + + let itemCountString = presentationStringsFormattedNumber(Int32(topPeer.count), environment.dateTimeFormat.groupingSeparator) + + let itemSize = itemView.update( + transition: .immediate, + component: AnyComponent(PlainButtonComponent( + content: AnyComponent(PeerComponent( + context: component.context, + theme: environment.theme, + strings: environment.strings, + peer: topPeer.peer, + count: itemCountString + )), + effectAlignment: .center, + action: { [weak self] in + guard let self, let component = self.component, let peer = topPeer.peer else { + return + } + guard let controller = self.environment?.controller() else { + return + } + guard let navigationController = controller.navigationController as? NavigationController else { + return + } + var viewControllers = navigationController.viewControllers + guard let index = viewControllers.firstIndex(where: { $0 === controller }) else { + return + } + + let context = component.context + + if case .user = peer { + if let peerInfoController = context.sharedContext.makePeerInfoController( + context: context, + updatedPresentationData: nil, + peer: peer._asPeer(), + mode: .generic, + avatarInitiallyExpanded: false, + fromChat: false, + requestsContext: nil + ) { + viewControllers.insert(peerInfoController, at: index) + } + } else { + let chatController = context.sharedContext.makeChatController(context: context, chatLocation: .peer(id: peer.id), subject: nil, botStart: nil, mode: .standard(.default), params: nil) + viewControllers.insert(chatController, at: index) + } + navigationController.setViewControllers(viewControllers, animated: true) + controller.dismiss() + }, + isEnabled: topPeer.peer != nil && topPeer.peer?.id != component.context.account.peerId, + animateAlpha: false + )), + environment: {}, + containerSize: CGSize(width: 200.0, height: 200.0) + ) + items.append((itemView, itemSize)) + } + var removedIds: [ChatSendStarsScreen.TopPeer.Id] = [] + for (id, itemView) in self.topPeerItems { + if !validIds.contains(id) { + removedIds.append(id) + + if animateItems { + if let itemComponentView = itemView.view { + itemPositionTransition.setScale(view: itemComponentView, scale: 0.001) + itemAlphaTransition.setAlpha(view: itemComponentView, alpha: 0.0, completion: { [weak itemComponentView] _ in + itemComponentView?.removeFromSuperview() + }) + } + } else { + itemView.view?.removeFromSuperview() + } + } + } + for id in removedIds { + self.topPeerItems.removeValue(forKey: id) + } + + var itemsWidth: CGFloat = 0.0 + for (_, itemSize) in items { + itemsWidth += itemSize.width + } + + let maxItemSpacing = 48.0 + var itemSpacing = floor((availableSize.width - itemsWidth) / CGFloat(items.count + 1)) + itemSpacing = min(itemSpacing, maxItemSpacing) + + let totalWidth = itemsWidth + itemSpacing * CGFloat(items.count + 1) + var itemX: CGFloat = floor((availableSize.width - totalWidth) * 0.5) + itemSpacing + for (itemView, itemSize) in items { + if let itemComponentView = itemView.view { + var animateItem = animateItems + if itemComponentView.superview == nil { + self.scrollContentView.addSubview(itemComponentView) + animateItem = false + ComponentTransition.immediate.setScale(view: itemComponentView, scale: 0.001) + itemComponentView.alpha = 0.0 + } + + let itemFrame = CGRect(origin: CGPoint(x: itemX, y: contentHeight + 56.0), size: itemSize) + + if animateItem { + itemPositionTransition.setPosition(view: itemComponentView, position: itemFrame.center) + itemPositionTransition.setBounds(view: itemComponentView, bounds: CGRect(origin: CGPoint(), size: itemFrame.size)) + } else { + itemComponentView.center = itemFrame.center + itemComponentView.bounds = CGRect(origin: CGPoint(), size: itemFrame.size) + } + + itemPositionTransition.setScale(view: itemComponentView, scale: 1.0) + itemAlphaTransition.setAlpha(view: itemComponentView, alpha: 1.0) + } + itemX += itemSize.width + itemSpacing + } + + contentHeight += 161.0 + } + + if !reactData.topPeers.isEmpty { contentHeight += 2.0 } @@ -2163,7 +2246,7 @@ private final class ChatSendStarsScreenComponent: Component { let anonymousContentsSize = self.anonymousContents.update( transition: transition, component: AnyComponent(PlainButtonComponent( - content: AnyComponent(HStack([ + content: AnyComponent(HStack([ AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(CheckComponent( theme: checkTheme, selected: self.privacyPeer != .anonymous @@ -2171,9 +2254,7 @@ private final class ChatSendStarsScreenComponent: Component { AnyComponentWithIdentity(id: AnyHashable(1), component: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString(string: environment.strings.SendStarReactions_ShowMyselfInTop, font: Font.regular(16.0), textColor: environment.theme.list.itemPrimaryTextColor)) ))) - ], - spacing: 10.0 - )), + ], spacing: 10.0)), effectAlignment: .center, action: { [weak self] in guard let self, let component = self.component else { @@ -2194,7 +2275,7 @@ private final class ChatSendStarsScreenComponent: Component { } self.state?.updated(transition: .easeInOut(duration: 0.2)) - if component.myTopPeer != nil { + if reactData.myTopPeer != nil { let mappedPrivacy: TelegramPaidReactionPrivacy switch self.privacyPeer { case .account: @@ -2205,7 +2286,7 @@ private final class ChatSendStarsScreenComponent: Component { mappedPrivacy = .peer(peer.id) } - let _ = component.context.engine.messages.updateStarsReactionPrivacy(id: component.messageId, privacy: mappedPrivacy).startStandalone() + let _ = component.context.engine.messages.updateStarsReactionPrivacy(id: reactData.messageId, privacy: mappedPrivacy).startStandalone() } }, animateAlpha: false, @@ -2228,6 +2309,8 @@ private final class ChatSendStarsScreenComponent: Component { } contentHeight += anonymousContentsSize.height + 27.0 + case .suggestPost: + break } initialContentHeight = contentHeight @@ -2236,7 +2319,13 @@ private final class ChatSendStarsScreenComponent: Component { self.cachedStarImage = (generateTintedImage(image: UIImage(bundleImageName: "Item List/PremiumIcon"), color: .white)!, environment.theme) } - let buttonString = environment.strings.SendStarReactions_SendButtonTitle("\(self.amount.realValue)").string + let buttonString: String + switch component.initialData.subjectInitialData { + case .react: + buttonString = environment.strings.SendStarReactions_SendButtonTitle("\(self.amount.realValue)").string + case .suggestPost: + buttonString = "Offer # \(self.amount.realValue)" + } let buttonAttributedString = NSMutableAttributedString(string: buttonString, font: Font.semibold(17.0), textColor: .white, paragraphAlignment: .center) if let range = buttonAttributedString.string.range(of: "#"), let starImage = self.cachedStarImage?.0 { buttonAttributedString.addAttribute(.attachment, value: starImage, range: NSRange(range, in: buttonAttributedString.string)) @@ -2278,7 +2367,16 @@ private final class ChatSendStarsScreenComponent: Component { return } - let purchaseScreen = component.context.sharedContext.makeStarsPurchaseScreen(context: component.context, starsContext: starsContext, options: options, purpose: .reactions(peerId: component.peer.id, requiredStars: Int64(self.amount.realValue)), completion: { result in + let purchasePurpose: StarsPurchasePurpose + switch component.initialData.subjectInitialData { + case let .react(reactData): + purchasePurpose = .reactions(peerId: reactData.peer.id, requiredStars: Int64(self.amount.realValue)) + case let .suggestPost(suggestPost): + //TODO:release + purchasePurpose = .reactions(peerId: suggestPost.peer.id, requiredStars: Int64(self.amount.realValue)) + } + + let purchaseScreen = component.context.sharedContext.makeStarsPurchaseScreen(context: component.context, starsContext: starsContext, options: options, purpose: purchasePurpose, completion: { result in let _ = result //TODO:release }) @@ -2309,14 +2407,19 @@ private final class ChatSendStarsScreenComponent: Component { mappedPrivacy = .peer(peer.id) } - component.completion( - Int64(self.amount.realValue), - mappedPrivacy, - isBecomingTop, - ChatSendStarsScreen.TransitionOut( - sourceView: badgeView.badgeIcon + switch component.initialData.subjectInitialData { + case let .react(reactData): + reactData.completion( + Int64(self.amount.realValue), + mappedPrivacy, + isBecomingTop, + ChatSendStarsScreen.TransitionOut( + sourceView: badgeView.badgeIcon + ) ) - ) + case let .suggestPost(suggestPostData): + suggestPostData.completion(Int64(self.amount.realValue), self.currentSuggestPostTimestamp) + } self.environment?.controller()?.dismiss() } )), @@ -2324,40 +2427,48 @@ private final class ChatSendStarsScreenComponent: Component { containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 50.0) ) - let buttonDescriptionTextSize = self.buttonDescriptionText.update( - transition: .immediate, - component: AnyComponent(MultilineTextComponent( - text: .markdown(text: environment.strings.SendStarReactions_TermsOfServiceFooter, attributes: MarkdownAttributes( - body: MarkdownAttributeSet(font: Font.regular(13.0), textColor: environment.theme.list.itemSecondaryTextColor), - bold: MarkdownAttributeSet(font: Font.semibold(13.0), textColor: environment.theme.list.itemSecondaryTextColor), - link: MarkdownAttributeSet(font: Font.regular(13.0), textColor: environment.theme.list.itemAccentColor), - linkAttribute: { contents in - return (TelegramTextAttributes.URL, contents) + var buttonDescriptionTextSize: CGSize? + if case .react = component.initialData.subjectInitialData { + buttonDescriptionTextSize = self.buttonDescriptionText.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .markdown(text: environment.strings.SendStarReactions_TermsOfServiceFooter, attributes: MarkdownAttributes( + body: MarkdownAttributeSet(font: Font.regular(13.0), textColor: environment.theme.list.itemSecondaryTextColor), + bold: MarkdownAttributeSet(font: Font.semibold(13.0), textColor: environment.theme.list.itemSecondaryTextColor), + link: MarkdownAttributeSet(font: Font.regular(13.0), textColor: environment.theme.list.itemAccentColor), + linkAttribute: { contents in + return (TelegramTextAttributes.URL, contents) + } + )), + horizontalAlignment: .center, + maximumNumberOfLines: 0, + highlightColor: environment.theme.list.itemAccentColor.withAlphaComponent(0.2), + highlightAction: { attributes in + if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] { + return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL) + } else { + return nil + } + }, + tapAction: { [weak self] attributes, _ in + if let controller = self?.environment?.controller(), let navigationController = controller.navigationController as? NavigationController, let url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String { + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + component.context.sharedContext.openExternalUrl(context: component.context, urlContext: .generic, url: url, forceExternal: false, presentationData: presentationData, navigationController: navigationController, dismissInput: {}) + } } )), - horizontalAlignment: .center, - maximumNumberOfLines: 0, - highlightColor: environment.theme.list.itemAccentColor.withAlphaComponent(0.2), - highlightAction: { attributes in - if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] { - return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL) - } else { - return nil - } - }, - tapAction: { [weak self] attributes, _ in - if let controller = self?.environment?.controller(), let navigationController = controller.navigationController as? NavigationController, let url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String { - let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } - component.context.sharedContext.openExternalUrl(context: component.context, urlContext: .generic, url: url, forceExternal: false, presentationData: presentationData, navigationController: navigationController, dismissInput: {}) - } - } - )), - environment: {}, - containerSize: CGSize(width: availableSize.width - sideInset, height: 1000.0) - ) + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset, height: 1000.0) + ) + } let buttonDescriptionSpacing: CGFloat = 14.0 - let bottomPanelHeight = 13.0 + environment.safeInsets.bottom + actionButtonSize.height + buttonDescriptionSpacing + buttonDescriptionTextSize.height + var bottomPanelHeight = 13.0 + environment.safeInsets.bottom + actionButtonSize.height + if let buttonDescriptionTextSize { + bottomPanelHeight += buttonDescriptionSpacing + buttonDescriptionTextSize.height + } else { + bottomPanelHeight -= 1.0 + } let actionButtonFrame = CGRect(origin: CGPoint(x: sideInset, y: availableSize.height - bottomPanelHeight), size: actionButtonSize) if let actionButtonView = actionButton.view { if actionButtonView.superview == nil { @@ -2366,12 +2477,14 @@ private final class ChatSendStarsScreenComponent: Component { transition.setFrame(view: actionButtonView, frame: actionButtonFrame) } - let buttonDescriptionTextFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - buttonDescriptionTextSize.width) * 0.5), y: actionButtonFrame.maxY + buttonDescriptionSpacing), size: buttonDescriptionTextSize) - if let buttonDescriptionTextView = buttonDescriptionText.view { - if buttonDescriptionTextView.superview == nil { - self.addSubview(buttonDescriptionTextView) + if let buttonDescriptionTextSize { + let buttonDescriptionTextFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - buttonDescriptionTextSize.width) * 0.5), y: actionButtonFrame.maxY + buttonDescriptionSpacing), size: buttonDescriptionTextSize) + if let buttonDescriptionTextView = buttonDescriptionText.view { + if buttonDescriptionTextView.superview == nil { + self.addSubview(buttonDescriptionTextView) + } + transition.setFrame(view: buttonDescriptionTextView, frame: buttonDescriptionTextFrame) } - transition.setFrame(view: buttonDescriptionTextView, frame: buttonDescriptionTextFrame) } contentHeight += bottomPanelHeight @@ -2422,37 +2535,61 @@ private final class ChatSendStarsScreenComponent: Component { } public class ChatSendStarsScreen: ViewControllerComponentContainer { + fileprivate enum SubjectInitialData { + final class React { + let peer: EnginePeer + let myPeer: EnginePeer + let defaultPrivacyPeer: ChatSendStarsScreenComponent.PrivacyPeer + let channelsForPublicReaction: [EnginePeer] + let messageId: EngineMessage.Id + let currentSentAmount: Int? + let topPeers: [ChatSendStarsScreen.TopPeer] + let myTopPeer: ChatSendStarsScreen.TopPeer? + let maxAmount: Int + let completion: (Int64, TelegramPaidReactionPrivacy, Bool, ChatSendStarsScreen.TransitionOut) -> Void + + init(peer: EnginePeer, myPeer: EnginePeer, defaultPrivacyPeer: ChatSendStarsScreenComponent.PrivacyPeer, channelsForPublicReaction: [EnginePeer], messageId: EngineMessage.Id, currentSentAmount: Int?, topPeers: [ChatSendStarsScreen.TopPeer], myTopPeer: ChatSendStarsScreen.TopPeer?, maxAmount: Int, completion: @escaping (Int64, TelegramPaidReactionPrivacy, Bool, ChatSendStarsScreen.TransitionOut) -> Void) { + self.peer = peer + self.myPeer = myPeer + self.defaultPrivacyPeer = defaultPrivacyPeer + self.channelsForPublicReaction = channelsForPublicReaction + self.messageId = messageId + self.currentSentAmount = currentSentAmount + self.topPeers = topPeers + self.myTopPeer = myTopPeer + self.maxAmount = maxAmount + self.completion = completion + } + } + + class SuggestPost { + let peer: EnginePeer + let initialTimestamp: Int32? + let maxAmount: Int + let completion: (Int64, Int32?) -> Void + + init(peer: EnginePeer, initialTimestamp: Int32?, maxAmount: Int, completion: @escaping (Int64, Int32?) -> Void) { + self.peer = peer + self.initialTimestamp = initialTimestamp + self.maxAmount = maxAmount + self.completion = completion + } + } + + case react(React) + case suggestPost(SuggestPost) + } + public final class InitialData { - fileprivate let peer: EnginePeer - fileprivate let myPeer: EnginePeer - fileprivate let defaultPrivacyPeer: ChatSendStarsScreenComponent.PrivacyPeer - fileprivate let channelsForPublicReaction: [EnginePeer] - fileprivate let messageId: EngineMessage.Id + fileprivate let subjectInitialData: SubjectInitialData fileprivate let balance: StarsAmount? - fileprivate let currentSentAmount: Int? - fileprivate let topPeers: [ChatSendStarsScreen.TopPeer] - fileprivate let myTopPeer: ChatSendStarsScreen.TopPeer? fileprivate init( - peer: EnginePeer, - myPeer: EnginePeer, - defaultPrivacyPeer: ChatSendStarsScreenComponent.PrivacyPeer, - channelsForPublicReaction: [EnginePeer], - messageId: EngineMessage.Id, - balance: StarsAmount?, - currentSentAmount: Int?, - topPeers: [ChatSendStarsScreen.TopPeer], - myTopPeer: ChatSendStarsScreen.TopPeer? + subjectInitialData: SubjectInitialData, + balance: StarsAmount? ) { - self.peer = peer - self.myPeer = myPeer - self.defaultPrivacyPeer = defaultPrivacyPeer - self.channelsForPublicReaction = channelsForPublicReaction - self.messageId = messageId + self.subjectInitialData = subjectInitialData self.balance = balance - self.currentSentAmount = currentSentAmount - self.topPeers = topPeers - self.myTopPeer = myTopPeer } } @@ -2521,27 +2658,12 @@ public class ChatSendStarsScreen: ViewControllerComponentContainer { private var presenceDisposable: Disposable? - public init(context: AccountContext, initialData: InitialData, completion: @escaping (Int64, TelegramPaidReactionPrivacy, Bool, TransitionOut) -> Void) { + public init(context: AccountContext, initialData: InitialData) { self.context = context - var maxAmount = 2500 - if let data = context.currentAppConfiguration.with({ $0 }).data, let value = data["stars_paid_reaction_amount_max"] as? Double { - maxAmount = Int(value) - } - super.init(context: context, component: ChatSendStarsScreenComponent( context: context, - peer: initialData.peer, - myPeer: initialData.myPeer, - defaultPrivacyPeer: initialData.defaultPrivacyPeer, - channelsForPublicReaction: initialData.channelsForPublicReaction, - messageId: initialData.messageId, - maxAmount: maxAmount, - balance: initialData.balance, - currentSentAmount: initialData.currentSentAmount, - topPeers: initialData.topPeers, - myTopPeer: initialData.myTopPeer, - completion: completion + initialData: initialData ), navigationBarAppearance: .none) self.statusBar.statusBarStyle = .Ignore @@ -2571,7 +2693,7 @@ public class ChatSendStarsScreen: ViewControllerComponentContainer { } } - public static func initialData(context: AccountContext, peerId: EnginePeer.Id, messageId: EngineMessage.Id, topPeers: [ReactionsMessageAttribute.TopPeer]) -> Signal { + public static func initialData(context: AccountContext, peerId: EnginePeer.Id, messageId: EngineMessage.Id, topPeers: [ReactionsMessageAttribute.TopPeer], completion: @escaping (Int64, TelegramPaidReactionPrivacy, Bool, TransitionOut) -> Void) -> Signal { let balance: Signal if let starsContext = context.starsContext { balance = starsContext.state @@ -2622,6 +2744,11 @@ public class ChatSendStarsScreen: ViewControllerComponentContainer { } } + var maxAmount = 2500 + if let data = context.currentAppConfiguration.with({ $0 }).data, let value = data["stars_paid_reaction_amount_max"] as? Double { + maxAmount = Int(value) + } + return combineLatest( context.engine.data.get( TelegramEngine.EngineData.Item.Peer.Peer(id: peerId), @@ -2640,61 +2767,105 @@ public class ChatSendStarsScreen: ViewControllerComponentContainer { var nextRandomIndex = 0 return InitialData( - peer: peer, - myPeer: myPeer, - defaultPrivacyPeer: defaultPrivacyPeer, - channelsForPublicReaction: channelsForPublicReaction, - messageId: messageId, - balance: balance, - currentSentAmount: currentSentAmount, - topPeers: topPeers.compactMap { topPeer -> ChatSendStarsScreen.TopPeer? in - guard let topPeerId = topPeer.peerId else { + subjectInitialData: .react(SubjectInitialData.React( + peer: peer, + myPeer: myPeer, + defaultPrivacyPeer: defaultPrivacyPeer, + channelsForPublicReaction: channelsForPublicReaction, + messageId: messageId, + currentSentAmount: currentSentAmount, + topPeers: topPeers.compactMap { topPeer -> ChatSendStarsScreen.TopPeer? in + guard let topPeerId = topPeer.peerId else { + let randomIndex = nextRandomIndex + nextRandomIndex += 1 + return ChatSendStarsScreen.TopPeer( + randomIndex: randomIndex, + peer: nil, + isMy: topPeer.isMy, + count: Int(topPeer.count) + ) + } + guard let topPeerValue = topPeerMap[topPeerId] else { + return nil + } + guard let topPeerValue else { + return nil + } let randomIndex = nextRandomIndex nextRandomIndex += 1 return ChatSendStarsScreen.TopPeer( randomIndex: randomIndex, - peer: nil, + peer: topPeer.isAnonymous ? nil : topPeerValue, isMy: topPeer.isMy, count: Int(topPeer.count) ) - } - guard let topPeerValue = topPeerMap[topPeerId] else { - return nil - } - guard let topPeerValue else { - return nil - } - let randomIndex = nextRandomIndex - nextRandomIndex += 1 - return ChatSendStarsScreen.TopPeer( - randomIndex: randomIndex, - peer: topPeer.isAnonymous ? nil : topPeerValue, - isMy: topPeer.isMy, - count: Int(topPeer.count) - ) - }, - myTopPeer: myTopPeer.flatMap { topPeer -> ChatSendStarsScreen.TopPeer? in - guard let topPeerId = topPeer.peerId else { + }, + myTopPeer: myTopPeer.flatMap { topPeer -> ChatSendStarsScreen.TopPeer? in + guard let topPeerId = topPeer.peerId else { + return ChatSendStarsScreen.TopPeer( + randomIndex: -1, + peer: nil, + isMy: topPeer.isMy, + count: Int(topPeer.count) + ) + } + guard let topPeerValue = topPeerMap[topPeerId] else { + return nil + } + guard let topPeerValue else { + return nil + } return ChatSendStarsScreen.TopPeer( randomIndex: -1, - peer: nil, + peer: topPeer.isAnonymous ? nil : topPeerValue, isMy: topPeer.isMy, count: Int(topPeer.count) ) - } - guard let topPeerValue = topPeerMap[topPeerId] else { - return nil - } - guard let topPeerValue else { - return nil - } - return ChatSendStarsScreen.TopPeer( - randomIndex: -1, - peer: topPeer.isAnonymous ? nil : topPeerValue, - isMy: topPeer.isMy, - count: Int(topPeer.count) - ) - } + }, + maxAmount: maxAmount, + completion: completion + )), + balance: balance + ) + } + } + + public static func initialData(context: AccountContext, peerId: EnginePeer.Id, suggestMessageAmount: StarsAmount, completion: @escaping (Int64, Int32?) -> Void) -> Signal { + let balance: Signal + if let starsContext = context.starsContext { + balance = starsContext.state + |> map { state in + return state?.balance + } + |> take(1) + } else { + balance = .single(nil) + } + + var maxAmount = 2500 + if let data = context.currentAppConfiguration.with({ $0 }).data, let value = data["stars_suggest_post_amount_max"] as? Double { + maxAmount = Int(value) + } + + return combineLatest( + context.engine.data.get( + TelegramEngine.EngineData.Item.Peer.Peer(id: peerId) + ), + balance + ) + |> map { peer, balance -> InitialData? in + guard let peer else { + return nil + } + + return InitialData( + subjectInitialData: .suggestPost(SubjectInitialData.SuggestPost( + peer: peer, + initialTimestamp: nil, + maxAmount: maxAmount, + completion: completion + )), + balance: balance ) } } @@ -3083,6 +3254,166 @@ private final class PeerSelectorBadgeComponent: Component { } } +private final class TimeSelectorBadgeComponent: Component { + let context: AccountContext + let theme: PresentationTheme + let strings: PresentationStrings + let timestamp: Int32? + let action: ((UIView) -> Void)? + + init( + context: AccountContext, + theme: PresentationTheme, + strings: PresentationStrings, + timestamp: Int32?, + action: ((UIView) -> Void)? + ) { + self.context = context + self.theme = theme + self.strings = strings + self.timestamp = timestamp + self.action = action + } + + static func ==(lhs: TimeSelectorBadgeComponent, rhs: TimeSelectorBadgeComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.theme !== rhs.theme { + return false + } + if lhs.strings !== rhs.strings { + return false + } + if lhs.timestamp != rhs.timestamp { + return false + } + if (lhs.action == nil) != (rhs.action == nil) { + return false + } + return true + } + + final class View: HighlightableButton { + private let background = ComponentView() + private let title = ComponentView() + private var selectorIcon: ComponentView? + + private var component: TimeSelectorBadgeComponent? + + override init(frame: CGRect) { + super.init(frame: frame) + + self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc private func pressed() { + guard let component = self.component else { + return + } + component.action?(self) + } + + func update(component: TimeSelectorBadgeComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.component = component + + self.isEnabled = component.action != nil + + let height: CGFloat = 32.0 + let leftTextInset: CGFloat = 12.0 + let rightTextInset: CGFloat = component.action != nil ? (leftTextInset + 14.0) : leftTextInset + + var titleString: String + //TODO:localize + if let timestamp = component.timestamp { + titleString = humanReadableStringForTimestamp(strings: component.strings, dateTimeFormat: PresentationDateTimeFormat(), timestamp: timestamp, alwaysShowTime: true).string + if titleString.count > 1 { + titleString = String(titleString[titleString.startIndex]).capitalized + titleString[titleString.index(after: titleString.startIndex)...] + } + } else { + titleString = "Anytime" + } + let titleSize = self.title.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: titleString, font: Font.medium(15.0), textColor: component.theme.list.itemPrimaryTextColor)) + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - leftTextInset - rightTextInset, height: 100.0) + ) + if let titleView = self.title.view { + if titleView.superview == nil { + titleView.isUserInteractionEnabled = false + self.addSubview(titleView) + } + titleView.frame = CGRect(origin: CGPoint(x: leftTextInset, y: floorToScreenPixels((height - titleSize.height) * 0.5)), size: titleSize) + } + + let size = CGSize(width: leftTextInset + rightTextInset + titleSize.width, height: height) + + if component.action != nil { + let selectorIcon: ComponentView + if let current = self.selectorIcon { + selectorIcon = current + } else { + selectorIcon = ComponentView() + self.selectorIcon = selectorIcon + } + let selectorIconSize = selectorIcon.update( + transition: transition, + component: AnyComponent(BundleIconComponent( + name: "Item List/ExpandableSelectorArrows", tintColor: component.theme.list.itemInputField.primaryColor.withMultipliedAlpha(0.5))), + environment: {}, + containerSize: CGSize(width: 100.0, height: 100.0) + ) + let selectorIconFrame = CGRect(origin: CGPoint(x: size.width - 8.0 - selectorIconSize.width, y: floorToScreenPixels((size.height - selectorIconSize.height) * 0.5)), size: selectorIconSize) + if let selectorIconView = selectorIcon.view { + if selectorIconView.superview == nil { + selectorIconView.isUserInteractionEnabled = false + self.addSubview(selectorIconView) + } + transition.setFrame(view: selectorIconView, frame: selectorIconFrame) + } + } else if let selectorIcon = self.selectorIcon { + self.selectorIcon = nil + selectorIcon.view?.removeFromSuperview() + } + + let _ = self.background.update( + transition: transition, + component: AnyComponent(FilledRoundedRectangleComponent( + color: component.theme.list.itemInputField.backgroundColor, + cornerRadius: .minEdge, + smoothCorners: false + )), + environment: {}, + containerSize: size + ) + if let backgroundView = self.background.view { + if backgroundView.superview == nil { + backgroundView.isUserInteractionEnabled = false + self.insertSubview(backgroundView, at: 0) + } + transition.setFrame(view: backgroundView, frame: CGRect(origin: CGPoint(), size: size)) + } + + return size + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + final class HeaderContextReferenceContentSource: ContextReferenceContentSource { private let controller: ViewController private let sourceView: UIView diff --git a/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift index 2413db2468..cea4348d69 100644 --- a/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift +++ b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift @@ -1816,6 +1816,8 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { case .businessLinkSetup: stickerContent = nil gifContent = nil + case .postSuggestions: + break } } diff --git a/submodules/TelegramUI/Components/ChatScheduleTimeController/Sources/ChatScheduleTimeController.swift b/submodules/TelegramUI/Components/ChatScheduleTimeController/Sources/ChatScheduleTimeController.swift index 22964531cb..5b8b8655f0 100644 --- a/submodules/TelegramUI/Components/ChatScheduleTimeController/Sources/ChatScheduleTimeController.swift +++ b/submodules/TelegramUI/Components/ChatScheduleTimeController/Sources/ChatScheduleTimeController.swift @@ -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 diff --git a/submodules/TelegramUI/Components/ChatScheduleTimeController/Sources/ChatScheduleTimeControllerNode.swift b/submodules/TelegramUI/Components/ChatScheduleTimeController/Sources/ChatScheduleTimeControllerNode.swift index 3ddddcd8ba..88c6b9a864 100644 --- a/submodules/TelegramUI/Components/ChatScheduleTimeController/Sources/ChatScheduleTimeControllerNode.swift +++ b/submodules/TelegramUI/Components/ChatScheduleTimeController/Sources/ChatScheduleTimeControllerNode.swift @@ -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 diff --git a/submodules/TelegramUI/Components/ListItemSliderSelectorComponent/Sources/ListItemSliderSelectorComponent.swift b/submodules/TelegramUI/Components/ListItemSliderSelectorComponent/Sources/ListItemSliderSelectorComponent.swift index 3cb174722b..c7709e6a76 100644 --- a/submodules/TelegramUI/Components/ListItemSliderSelectorComponent/Sources/ListItemSliderSelectorComponent.swift +++ b/submodules/TelegramUI/Components/ListItemSliderSelectorComponent/Sources/ListItemSliderSelectorComponent.swift @@ -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] = [:] private var mainTitle: ComponentView? + private var secondaryTitle: ComponentView? private var slider = ComponentView() 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? + var secondaryTitleSize: CGSize? + if let secondaryTitleValue { + let secondaryTitle: ComponentView + 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 diff --git a/submodules/TelegramUI/Components/ListSwitchItemComponent/BUILD b/submodules/TelegramUI/Components/ListSwitchItemComponent/BUILD new file mode 100644 index 0000000000..7236c2c1b0 --- /dev/null +++ b/submodules/TelegramUI/Components/ListSwitchItemComponent/BUILD @@ -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", + ], +) diff --git a/submodules/TelegramUI/Components/PeerAllowedReactionsScreen/Sources/ListSwitchItemComponent.swift b/submodules/TelegramUI/Components/ListSwitchItemComponent/Sources/ListSwitchItemComponent.swift similarity index 90% rename from submodules/TelegramUI/Components/PeerAllowedReactionsScreen/Sources/ListSwitchItemComponent.swift rename to submodules/TelegramUI/Components/ListSwitchItemComponent/Sources/ListSwitchItemComponent.swift index 8a079687ea..f72667e980 100644 --- a/submodules/TelegramUI/Components/PeerAllowedReactionsScreen/Sources/ListSwitchItemComponent.swift +++ b/submodules/TelegramUI/Components/ListSwitchItemComponent/Sources/ListSwitchItemComponent.swift @@ -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() private let switchView = ComponentView() @@ -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, transition: ComponentTransition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/PeerAllowedReactionsScreen/BUILD b/submodules/TelegramUI/Components/PeerAllowedReactionsScreen/BUILD index 99f9217acf..12a157c72b 100644 --- a/submodules/TelegramUI/Components/PeerAllowedReactionsScreen/BUILD +++ b/submodules/TelegramUI/Components/PeerAllowedReactionsScreen/BUILD @@ -35,6 +35,7 @@ swift_library( "//submodules/Components/HierarchyTrackingLayer", "//submodules/TelegramUI/Components/ListSectionComponent", "//submodules/TelegramUI/Components/ListItemSliderSelectorComponent", + "//submodules/TelegramUI/Components/ListSwitchItemComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/PeerAllowedReactionsScreen/Sources/PeerAllowedReactionsScreen.swift b/submodules/TelegramUI/Components/PeerAllowedReactionsScreen/Sources/PeerAllowedReactionsScreen.swift index 8c757c98e9..9987a52958 100644 --- a/submodules/TelegramUI/Components/PeerAllowedReactionsScreen/Sources/PeerAllowedReactionsScreen.swift +++ b/submodules/TelegramUI/Components/PeerAllowedReactionsScreen/Sources/PeerAllowedReactionsScreen.swift @@ -23,6 +23,7 @@ import AudioToolbox import PremiumLockButtonSubtitleComponent import ListSectionComponent import ListItemSliderSelectorComponent +import ListSwitchItemComponent final class PeerAllowedReactionsScreenComponent: Component { typealias EnvironmentType = ViewControllerComponentContainer.Environment diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift index b3a328a3b1..2a7a6e720b 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift @@ -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 diff --git a/submodules/TelegramUI/Components/PeerInfo/PostSuggestionsSettingsScreen/BUILD b/submodules/TelegramUI/Components/PeerInfo/PostSuggestionsSettingsScreen/BUILD new file mode 100644 index 0000000000..eb9501ea4d --- /dev/null +++ b/submodules/TelegramUI/Components/PeerInfo/PostSuggestionsSettingsScreen/BUILD @@ -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", + ], +) diff --git a/submodules/TelegramUI/Components/PeerInfo/PostSuggestionsSettingsScreen/Sources/PostSuggestionsChatContents.swift b/submodules/TelegramUI/Components/PeerInfo/PostSuggestionsSettingsScreen/Sources/PostSuggestionsChatContents.swift new file mode 100644 index 0000000000..462c656fd4 --- /dev/null +++ b/submodules/TelegramUI/Components/PeerInfo/PostSuggestionsSettingsScreen/Sources/PostSuggestionsChatContents.swift @@ -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 + + 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 } +} diff --git a/submodules/TelegramUI/Components/PeerInfo/PostSuggestionsSettingsScreen/Sources/PostSuggestionsSettingsScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PostSuggestionsSettingsScreen/Sources/PostSuggestionsSettingsScreen.swift new file mode 100644 index 0000000000..7f29cb8160 --- /dev/null +++ b/submodules/TelegramUI/Components/PeerInfo/PostSuggestionsSettingsScreen/Sources/PostSuggestionsSettingsScreen.swift @@ -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() + private let icon = ComponentView() + private let subtitle = ComponentView() + private let switchSection = ComponentView() + private let contentSection = ComponentView() + + 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, 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] = [] + 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] = [] + + 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, 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) + } +} diff --git a/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift b/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift index bf71406a8c..3e311386e4 100644 --- a/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift +++ b/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift @@ -761,6 +761,7 @@ final class PeerSelectionControllerNode: ASDisplayNode { }, joinGroupCall: { _ in }, presentInviteMembers: { }, presentGigagroupHelp: { + }, openSuggestPost: { }, editMessageMedia: { _, _ in }, updateShowCommands: { _ in }, updateShowSendAsPeers: { _ in diff --git a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/AutomaticBusinessMessageSetupChatContents.swift b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/AutomaticBusinessMessageSetupChatContents.swift index 66e706dddf..54794848fd 100644 --- a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/AutomaticBusinessMessageSetupChatContents.swift +++ b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/AutomaticBusinessMessageSetupChatContents.swift @@ -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 } } diff --git a/submodules/TelegramUI/Resources/Animations/LampEmoji.tgs b/submodules/TelegramUI/Resources/Animations/LampEmoji.tgs new file mode 100644 index 0000000000000000000000000000000000000000..e3c35414ad18b7cbbe33eb0d7838132a46d7b347 GIT binary patch literal 3835 zcmVttd0zvrv!CfR(+Bbj!_5)U2?o6YX(?y9ez9Dmx~y*cd8WVQRaJKNdDHfmSxF88}L zC#v1mhusAzyylU7=<7 z`|Zt#Uj;f`pZ|HWPdD1WSMOi*+;@NO&P*QepKs4UT)g<1I=}FE?8Eu~;{Dz3EL6M0 zo_oK;AfI-7`Z(ud-l19B&*(d7ds*woL|@f1m`c3D{qxa%%lENT+@;&MeCt3IBtnU> z-J3ngxVlSAdY>rg8;7^MmYQx*bV0|LG-Sn}udsiUmXFj@+p5+cjr2ttNjHAqon4+^ zA1;U--i}|+@7}(UnCI>5_EweYy1PMhJG)Fv;^{x_{)?}o(FP-;=SxY8zb$U)e1mrd zE3=|eF7RmF_K6yuO$}0@i8@|9W-jS`I~jjZ6Pod4HZ(FuC4(Q&dOO%(ftDL4fw$*h z`>UH5v`2@9O*-7|cvs7FmzAtUL$g^Im9jxsaykb6)N> zh=pif|C|5(cEjW($++9!zPngr24d0?|BG3!&#i~io(NI#mRBRm5(^u1csJ>GIG;4- zLppzdxV+up>^{F5NI5MfudrXytAsnWQ3O2BRdk4F+ULHsZ1}O^ z$1}taw~Qa^cmreukPSdK0C|=G;+FwL-UA>%Dp;6C8jvYOW8;Py8)j^n@d(TaNh)<7 z`nU7L#m()<3)+wYNK6@Nf7IblrDrTYqGaY`4A-&1aZ+I)KG2jH7tZapf$Y)4NUbtOs!_6KdrssReE(tTEHD+1CyRM_8g3PX6HwMwItD`A-Q8_s_H=R8 zU6kwK{g@eD=2reTJR6z<+ws*?j27U^71)Oiug#Z72g3Elt1pjE=OgWSPd}H+a$J*f zNT)C_qH(C!Nkku^%@wqijq@5k_fc(+b+x)~oUxrAr%!I_uIp)sy6iEu1-{g~TAZ)7 zAhc|pu$6SO5sobh9biJZvc*_vRJ2&r7#9XD7)+KA8Q%y=E$z5{LuiyR{UhCV8Z=of zcl$q%GFawxF{InN;FR~$N>a<#SP_G84jhat`mUoDKKL&y>C_go7L`eh;?K=B^?bV8 z^r!vpA0OtmY4w!2Uc+1t1$Wj40IlPo#WkK=L}R2XTXY1g+43o^n#e8fz^n_$hLB{asuk5R7C)(u+7*^T8>GBkAE4x2A=*Em`9 z?;6sO8_%B*&+GMhUWqv3L9~=SKkiVMJtjPVZ|`bxzK-W3EIHXRC!x49J+szUHG;TX z!pjL|OLR^2x_Bf?81Xt`I1Sld!yFRe@9u$o-440&`w8*8S)1R*fcKVTcGXP$PQu#2 z3HG+O@{J*@c-%m&#$cSE$kz|!tjp^;&I+*P!8lfj05NKO2ufRx*$rJ8DZMkn2{>_! zoRwUzl5hTYb$$JmxW=9m*VNJx@RTl^ZKsNA@@_67n_oAK;@- z_T<5Fps!wUy?%#5Trg@Hu$l(7#(~-t%ngHJZf+?8 zzLTIZ=Q;c!Ogv77OtU5JL%lIJPP&YeAv4*MNjha5!5+nx=OWLO*z`CWIEvHFMDj+F zzENyt6!#bhM%I9RuOgX}K?KcqCFuYWgo5RXOXr1!K7bxj@j1i36yM%hsAU87ONmm? z447LjFeA*`4vb9*$B^=A%Q&xk{iJK!4?A#8*I@4nIa+Tu1T7#Ee3@=5;phq zDhL=&n@L+Ez$ht#s4$|CWKWT|24RZINfInct{p6@D2)D&5yp88P$r4F#-EBVH5-3c zb7)j)F2@QkO8=}e;J%uuqw6iC7#3Vyz5iI&U(;`9e6ivJ%Fs7pt>tCCN8 z<}?~qMLIJkR|uyV7S23TQg@^f!+0vfB5+`Pn2Ky^GL^R2(;k7f9ZluP`>#mzLO~@n zL3vu2H%YxhkI`ux<;JJ&VG_IIw6Xa@1v>4#3LFblLY6FV19l14W#VO>t_AH&$j4nn zSx2k-1FtoRRkRqN2_nu6Q1E1#C!fJJA+CPrZ8`AZ%(4Hj5Oj&n!DqFxgd z#zNK;bonDr{^;sbsb`N}LrhIC>yi1slXZxf^}VRqv|+xNbvVYtb)pV$k#2U9Zg9SZ zoYy4i3*erp=;fUDjG(%cb8M;zp%ZlMmtw~*2|C1;j-(TGjz8#L(&;S-8x}+zsNf1< zhncQrFX(`IZA~fX;0MTLFXswsN%wLNR+KPUy3@v_WRmw%jx*DRh+~C%S;WgS=1nvI zc$)duL;bF`-U4Y$->LkTTr_J9xxhpsc}3UjJbe7xX!&gjG(H_FQ9Tt5#<(m`khUt+nfq>B!izkKTA1!mpqQ9u_ox`Y&$Y^Qy^% zgOsBm%>3<->)(F8eP6^W9;DH=Qb;^xwucx%0@_xL0jJKVK{U%*Zsd_XHL`#+pu z%K0WZCeogk>@1Z_zvZwcm*zbT(rDs$i1U=TF^oC_S<^vdPA{@6Cp7;q1EepSRWe{PRnaRKsScv?oB^YPMdB^;G3loA6v5bP?C%Hl z)n6(hiGt^D$=BC8@}Md3=SlQg^c+QJlx>3J%7aEpGmt$63^G(zk%Kps$c+Fdm1K`N zi`moC(1B#nB{#p5J!d%%lOh#7grp%-ZkQ%8Jb^7mPmz_8M9(o)Sy&*x4df7bP)svF zL+cA17@>u78LjiC6TN5wGX_!s*j4ldeTitGat)4II<9-M00`a`AO(P-a->^Q$ON+s z%MH0$MK{4nej-JaDCbNeU`JWG6!cMzq0 zJWz6ZG7V{GXn$gG^ywnoaz6bYhiB_cd+udfEsVqh$Ds+zJr?72`I98MLj?9w;Jfc| z$JW@}i6|@x$4~q7*H^GTUOc(HviSzWy{XkFpjHk0BV=CRS!CHQit$eTT zIah1k`-ainl3L?8g|C;5_dcbVjqt>WwHIbZMc^2Oo9#qalcd_RJ7p#d2%KaBAy zHLD1$B&Clz+X7>PQms(UIn6Bw)p3G;_p0hFBq^L9Fl6C4Jiy5pXyC|tJoFXB zfSmvQ6Z(6Zz-G&@7W`FpX85#q}veNSQ>-Ng9>v5;UQp;U`ia z4`9a$d)v75(`gY_lS&4TOC}&5j%=AJm}Y7n6wC#5PB=DBjB!!Jz_gs+4BFw)t!CF_ zfP!n~iP{v6T{F|7#44ltHA#yr4%VhT)>)@AE0WeGE$o`P2h{~<+9Cp*A~T!*-Sls( zf6cu9{lQi5=72oexL$fOIZAD!>0eBCPMhQ3Oy1&p-z=`TN#2hwdF6k3dEZfwBdCKA z=P>#?jg3yjr+sKOAIdNQJB-hEg>0iR@+jCl2n0`J#DfTOnzWBHkK@my0QF)BcN|Tf zjkr#NvXijwIIug8{Ej2T!^rW{S*!2%VxE2`Q69Ge=mwyhP;IW;_i)`_QIDnnq&k<( zv`le*_2%szP4r@a@zMMh)9o)V|33KduJ7~d=%<|(|CaT1H~F4*Lxx$1YgNT(TdOko zovAL2Q5BzUNF)F1bWs;ACB3Y!s`)CG)n@BUcYKxpebeda^`_bRQd!N+(n}Y5{C}Po xJY-B_p;}Vy=)M2B=rZH9VF2|Q9 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) diff --git a/submodules/TelegramUI/Sources/Chat/UpdateChatPresentationInterfaceState.swift b/submodules/TelegramUI/Sources/Chat/UpdateChatPresentationInterfaceState.swift index 3aff2f15bd..a40e9caac5 100644 --- a/submodules/TelegramUI/Sources/Chat/UpdateChatPresentationInterfaceState.swift +++ b/submodules/TelegramUI/Sources/Chat/UpdateChatPresentationInterfaceState.swift @@ -230,6 +230,8 @@ func updateChatPresentationInterfaceStateImpl( break case .businessLinkSetup: canHaveUrlPreview = false + case .postSuggestions: + break } } diff --git a/submodules/TelegramUI/Sources/ChatBusinessLinkTitlePanelNode.swift b/submodules/TelegramUI/Sources/ChatBusinessLinkTitlePanelNode.swift index 4fe8585ec1..993979ecc4 100644 --- a/submodules/TelegramUI/Sources/ChatBusinessLinkTitlePanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatBusinessLinkTitlePanelNode.swift @@ -196,6 +196,8 @@ final class ChatBusinessLinkTitlePanelNode: ChatTitleAccessoryPanelNode { self.link = link case .hashTagSearch: break + case .postSuggestions: + break } default: break diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 6657465b55..e6c1fbc74d 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -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 = .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() + 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)) + } + })) } } diff --git a/submodules/TelegramUI/Sources/ChatControllerNode.swift b/submodules/TelegramUI/Sources/ChatControllerNode.swift index 169a288c11..34c5c34c27 100644 --- a/submodules/TelegramUI/Sources/ChatControllerNode.swift +++ b/submodules/TelegramUI/Sources/ChatControllerNode.swift @@ -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 diff --git a/submodules/TelegramUI/Sources/ChatControllerOpenMessageReactionContextMenu.swift b/submodules/TelegramUI/Sources/ChatControllerOpenMessageReactionContextMenu.swift index 7faf9b1ec1..df04ef1521 100644 --- a/submodules/TelegramUI/Sources/ChatControllerOpenMessageReactionContextMenu.swift +++ b/submodules/TelegramUI/Sources/ChatControllerOpenMessageReactionContextMenu.swift @@ -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)) }) }) } diff --git a/submodules/TelegramUI/Sources/ChatInterfaceInputContexts.swift b/submodules/TelegramUI/Sources/ChatInterfaceInputContexts.swift index 43afa00b7a..0488f21816 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceInputContexts.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceInputContexts.swift @@ -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 } } diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift index a6cee571bb..3115665dee 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift @@ -2108,6 +2108,9 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState } case .businessLinkSetup: actions.removeAll() + case .postSuggestions: + //TODO:release + actions.removeAll() } } diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateInputPanels.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateInputPanels.swift index f69d9ad7a9..554e65229c 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateInputPanels.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateInputPanels.swift @@ -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 { diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateNavigationButtons.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateNavigationButtons.swift index 8e194084e2..ffacc12883 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateNavigationButtons.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateNavigationButtons.swift @@ -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 } } diff --git a/submodules/TelegramUI/Sources/ChatInterfaceTitlePanelNodes.swift b/submodules/TelegramUI/Sources/ChatInterfaceTitlePanelNodes.swift index 9ba210ead3..65345b97f3 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceTitlePanelNodes.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceTitlePanelNodes.swift @@ -66,6 +66,8 @@ func titlePanelForChatPresentationInterfaceState(_ chatPresentationInterfaceStat panel.interfaceInteraction = interfaceInteraction return panel } + case .postSuggestions: + break } default: break diff --git a/submodules/TelegramUI/Sources/ChatRestrictedInputPanelNode.swift b/submodules/TelegramUI/Sources/ChatRestrictedInputPanelNode.swift index 2c1c834767..f1a51a5c8a 100644 --- a/submodules/TelegramUI/Sources/ChatRestrictedInputPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatRestrictedInputPanelNode.swift @@ -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) } diff --git a/submodules/TelegramUI/Sources/ChatTextInputActionButtonsNode.swift b/submodules/TelegramUI/Sources/ChatTextInputActionButtonsNode.swift index 40722128c5..816584b0c4 100644 --- a/submodules/TelegramUI/Sources/ChatTextInputActionButtonsNode.swift +++ b/submodules/TelegramUI/Sources/ChatTextInputActionButtonsNode.swift @@ -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) diff --git a/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift b/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift index f6b6715f03..193ccfa97b 100644 --- a/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift @@ -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 } } diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index 564282e927..84ee0d6047 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -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)?, peer: Peer, mode: PeerInfoControllerMode, avatarInitiallyExpanded: Bool, isOpenedFromChat: Bool, requestsContext: PeerInvitationImportersContext? = nil) -> ViewController? {