diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index 733f79ef20..cee7c241f6 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -1038,7 +1038,7 @@ public enum StarsWithdrawalScreenSubject { case withdraw(completion: (Int64) -> Void) case enterAmount(current: StarsAmount, minValue: StarsAmount, fractionAfterCommission: Int, kind: PaidMessageKind, completion: (Int64) -> Void) - case postSuggestion(channel: EnginePeer, current: CurrencyAmount, timestamp: Int32?, completion: (CurrencyAmount, Int32?) -> Void) + case postSuggestion(channel: EnginePeer, isFromAdmin: Bool, current: CurrencyAmount, timestamp: Int32?, completion: (CurrencyAmount, Int32?) -> Void) case postSuggestionModification(current: CurrencyAmount, timestamp: Int32?, completion: (CurrencyAmount, Int32?) -> Void) } @@ -1506,7 +1506,10 @@ public struct StarsSubscriptionConfiguration { paidMessagesAvailable: false, starGiftResaleMinAmount: 125, starGiftResaleMaxAmount: 3500, - starGiftCommissionPermille: 80 + starGiftCommissionPermille: 80, + channelMessageSuggestionCommissionPermille: 850, + channelMessageSuggestionMaxStarsAmount: 10000, + channelMessageSuggestionMaxTonAmount: 10000000000000 ) } @@ -1518,6 +1521,9 @@ public struct StarsSubscriptionConfiguration { public let starGiftResaleMinAmount: Int64 public let starGiftResaleMaxAmount: Int64 public let starGiftCommissionPermille: Int32 + public let channelMessageSuggestionCommissionPermille: Int32 + public let channelMessageSuggestionMaxStarsAmount: Int64 + public let channelMessageSuggestionMaxTonAmount: Int64 fileprivate init( maxFee: Int64, @@ -1527,7 +1533,10 @@ public struct StarsSubscriptionConfiguration { paidMessagesAvailable: Bool, starGiftResaleMinAmount: Int64, starGiftResaleMaxAmount: Int64, - starGiftCommissionPermille: Int32 + starGiftCommissionPermille: Int32, + channelMessageSuggestionCommissionPermille: Int32, + channelMessageSuggestionMaxStarsAmount: Int64, + channelMessageSuggestionMaxTonAmount: Int64 ) { self.maxFee = maxFee self.usdWithdrawRate = usdWithdrawRate @@ -1537,6 +1546,9 @@ public struct StarsSubscriptionConfiguration { self.starGiftResaleMinAmount = starGiftResaleMinAmount self.starGiftResaleMaxAmount = starGiftResaleMaxAmount self.starGiftCommissionPermille = starGiftCommissionPermille + self.channelMessageSuggestionCommissionPermille = channelMessageSuggestionCommissionPermille + self.channelMessageSuggestionMaxStarsAmount = channelMessageSuggestionMaxStarsAmount + self.channelMessageSuggestionMaxTonAmount = channelMessageSuggestionMaxTonAmount } public static func with(appConfiguration: AppConfiguration) -> StarsSubscriptionConfiguration { @@ -1550,6 +1562,10 @@ public struct StarsSubscriptionConfiguration { let starGiftResaleMaxAmount = (data["stars_stargift_resale_amount_max"] as? Double).flatMap(Int64.init) ?? StarsSubscriptionConfiguration.defaultValue.starGiftResaleMaxAmount let starGiftCommissionPermille = (data["stars_stargift_resale_commission_permille"] as? Double).flatMap(Int32.init) ?? StarsSubscriptionConfiguration.defaultValue.starGiftCommissionPermille + let channelMessageSuggestionCommissionPermille = (data["stars_suggested_post_commission_permille"] as? Double).flatMap(Int32.init) ?? StarsSubscriptionConfiguration.defaultValue.channelMessageSuggestionCommissionPermille + let channelMessageSuggestionMaxStarsAmount = (data["stars_suggested_post_amount_max"] as? Double).flatMap(Int64.init) ?? StarsSubscriptionConfiguration.defaultValue.channelMessageSuggestionMaxStarsAmount + let channelMessageSuggestionMaxTonAmount = (data["ton_suggested_post_amount_max"] as? Double).flatMap(Int64.init) ?? StarsSubscriptionConfiguration.defaultValue.channelMessageSuggestionMaxTonAmount + return StarsSubscriptionConfiguration( maxFee: maxFee, usdWithdrawRate: usdWithdrawRate, @@ -1558,7 +1574,10 @@ public struct StarsSubscriptionConfiguration { paidMessagesAvailable: paidMessagesAvailable, starGiftResaleMinAmount: starGiftResaleMinAmount, starGiftResaleMaxAmount: starGiftResaleMaxAmount, - starGiftCommissionPermille: starGiftCommissionPermille + starGiftCommissionPermille: starGiftCommissionPermille, + channelMessageSuggestionCommissionPermille: channelMessageSuggestionCommissionPermille, + channelMessageSuggestionMaxStarsAmount: channelMessageSuggestionMaxStarsAmount, + channelMessageSuggestionMaxTonAmount: channelMessageSuggestionMaxTonAmount ) } else { return .defaultValue diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index 0b9865c4a6..858ccf4ab0 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -3797,6 +3797,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController controller.completion = { [weak controller] title, fileId, iconColor, _ in controller?.isInProgress = true + controller?.view.endEditing(true) let _ = (context.engine.peers.createForumChannelTopic(id: peerId, title: title, iconColor: iconColor, iconFileId: fileId) |> deliverOnMainQueue).startStandalone(next: { topicId in @@ -7127,7 +7128,7 @@ private final class ChatListLocationContext { strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, - content: .peer(peerView: ChatTitleContent.PeerData(peerView: peerView), customTitle: nil, onlineMemberCount: onlineMemberCount, isScheduledMessages: false, isMuted: nil, customMessageCount: nil, isEnabled: true), + content: .peer(peerView: ChatTitleContent.PeerData(peerView: peerView), customTitle: nil, customSubtitle: nil, onlineMemberCount: onlineMemberCount, isScheduledMessages: false, isMuted: nil, customMessageCount: nil, isEnabled: true), tapped: { [weak self] in guard let self else { return diff --git a/submodules/ChatListUI/Sources/Node/ChatListItem.swift b/submodules/ChatListUI/Sources/Node/ChatListItem.swift index f04e0edc59..ad74d8d1b4 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListItem.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListItem.swift @@ -3220,7 +3220,14 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { if displayAsMessage { switch item.content { case let .peer(peerData): - if let peer = peerData.messages.last?.author { + var iconPeer: EnginePeer? + if case let .chat(itemPeer) = contentPeer, let peer = itemPeer.chatOrMonoforumMainPeer { + iconPeer = peer + } else { + iconPeer = peerData.messages.last?.author + } + + if let peer = iconPeer { if case let .peer(peerData) = item.content, peerData.customMessageListData != nil { currentCredibilityIconContent = nil } else if case .savedMessagesChats = item.chatListLocation, peer.id == item.context.account.peerId { diff --git a/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift b/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift index f374dc79e3..3eeeb0943b 100644 --- a/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift +++ b/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift @@ -182,6 +182,7 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att private let chatLocation: ChatLocation? private let bannedSendPhotos: (Int32, Bool)? private let bannedSendVideos: (Int32, Bool)? + private let enableMultiselection: Bool private let canBoostToUnrestrict: Bool fileprivate let paidMediaAllowed: Bool fileprivate let subject: Subject @@ -1845,6 +1846,7 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att isScheduledMessages: Bool = false, bannedSendPhotos: (Int32, Bool)? = nil, bannedSendVideos: (Int32, Bool)? = nil, + enableMultiselection: Bool = true, canBoostToUnrestrict: Bool = false, paidMediaAllowed: Bool = false, subject: Subject, @@ -1868,6 +1870,7 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att self.isScheduledMessages = isScheduledMessages self.bannedSendPhotos = bannedSendPhotos self.bannedSendVideos = bannedSendVideos + self.enableMultiselection = enableMultiselection self.canBoostToUnrestrict = canBoostToUnrestrict self.paidMediaAllowed = paidMediaAllowed self.subject = subject @@ -1877,7 +1880,7 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att self.mainButtonAction = mainButtonAction self.secondaryButtonAction = secondaryButtonAction - let selectionContext = selectionContext ?? TGMediaSelectionContext() + let selectionContext = selectionContext ?? TGMediaSelectionContext(groupingAllowed: false, selectionLimit: enableMultiselection ? 100 : 1)! self.titleView = MediaPickerTitleView(theme: self.presentationData.theme, segments: [self.presentationData.strings.Attachment_AllMedia, self.presentationData.strings.Attachment_SelectedMedia(1)], selectedIndex: 0) @@ -1924,11 +1927,12 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationData: presentationData)) self.statusBar.statusBarStyle = .Ignore - + selectionContext.attemptSelectingItem = { [weak self] item in guard let self else { return false } + if let _ = item as? TGMediaPickerGalleryPhotoItem { if self.bannedSendPhotos != nil { self.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: self.presentationData), title: nil, text: self.presentationData.strings.Chat_SendNotAllowedPhoto, actions: [TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) @@ -2807,7 +2811,7 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att return } if let selectionContext = self.interaction?.selectionState, let editingContext = self.interaction?.editingState { - selectionContext.selectionLimit = 10 + selectionContext.selectionLimit = self.enableMultiselection ? 10 : 1 for case let item as TGMediaEditableItem in selectionContext.selectedItems() { editingContext.setPrice(NSNumber(value: amount), for: item) } diff --git a/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift index 3a58f9ad9c..c3c28bd13e 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift @@ -264,10 +264,10 @@ func telegramMediaActionFromApiAction(_ action: Api.MessageAction) -> TelegramMe return TelegramMediaAction(action: .suggestedPostApprovalStatus(status: status)) case let .messageActionGiftTon(_, currency, amount, cryptoCurrency, cryptoAmount, transactionId): return TelegramMediaAction(action: .giftTon(currency: currency, amount: amount, cryptoCurrency: cryptoCurrency, cryptoAmount: cryptoAmount, transactionId: transactionId)) - case .messageActionSuggestedPostSuccess: - return nil - case .messageActionSuggestedPostRefund: - return nil + case let .messageActionSuggestedPostSuccess(price): + return TelegramMediaAction(action: .suggestedPostSuccess(amount: CurrencyAmount(apiAmount: price))) + case let .messageActionSuggestedPostRefund(flags): + return TelegramMediaAction(action: .suggestedPostRefund(TelegramMediaActionType.SuggestedPostRefund(isUserInitiated: (flags & (1 << 0)) != 0))) } } diff --git a/submodules/TelegramCore/Sources/PendingMessages/StandaloneSendMessage.swift b/submodules/TelegramCore/Sources/PendingMessages/StandaloneSendMessage.swift index 2cac48b3b5..b3749f0605 100644 --- a/submodules/TelegramCore/Sources/PendingMessages/StandaloneSendMessage.swift +++ b/submodules/TelegramCore/Sources/PendingMessages/StandaloneSendMessage.swift @@ -384,7 +384,7 @@ private func sendUploadedMessageContent( } else if let attribute = attribute as? PaidStarsMessageAttribute { allowPaidStars = attribute.stars.value } else if let attribute = attribute as? SuggestedPostMessageAttribute { - suggestedPost = attribute.apiSuggestedPost() + suggestedPost = attribute.apiSuggestedPost(fixMinTime: Int32(Date().timeIntervalSince1970 + 10)) } } @@ -656,7 +656,7 @@ private func sendMessageContent(account: Account, peerId: PeerId, attributes: [M } else if let attribute = attribute as? PaidStarsMessageAttribute { allowPaidStars = attribute.stars.value } else if let attribute = attribute as? SuggestedPostMessageAttribute { - suggestedPost = attribute.apiSuggestedPost() + suggestedPost = attribute.apiSuggestedPost(fixMinTime: Int32(Date().timeIntervalSince1970 + 10)) } } diff --git a/submodules/TelegramCore/Sources/State/ConferenceCallE2EContext.swift b/submodules/TelegramCore/Sources/State/ConferenceCallE2EContext.swift index ad71c3ea4f..d947507fb5 100644 --- a/submodules/TelegramCore/Sources/State/ConferenceCallE2EContext.swift +++ b/submodules/TelegramCore/Sources/State/ConferenceCallE2EContext.swift @@ -240,12 +240,12 @@ public final class ConferenceCallE2EContext { var delayPoll = true if let result { if subChainId == 0 { - if self.e2ePoll0Offset != result.nextOffset { + if let e2ePoll0Offset = self.e2ePoll0Offset, e2ePoll0Offset < result.nextOffset { self.e2ePoll0Offset = result.nextOffset delayPoll = false } } else if subChainId == 1 { - if self.e2ePoll1Offset != result.nextOffset { + if let e2ePoll1Offset = self.e2ePoll1Offset, e2ePoll1Offset < result.nextOffset { self.e2ePoll1Offset = result.nextOffset delayPoll = false } diff --git a/submodules/TelegramCore/Sources/State/PendingMessageManager.swift b/submodules/TelegramCore/Sources/State/PendingMessageManager.swift index 770abaa3e0..df88ea29d2 100644 --- a/submodules/TelegramCore/Sources/State/PendingMessageManager.swift +++ b/submodules/TelegramCore/Sources/State/PendingMessageManager.swift @@ -900,7 +900,7 @@ public final class PendingMessageManager { } else if let attribute = attribute as? PaidStarsMessageAttribute { allowPaidStars = attribute.stars.value * Int64(messages.count) } else if let attribute = attribute as? SuggestedPostMessageAttribute { - suggestedPost = attribute.apiSuggestedPost() + suggestedPost = attribute.apiSuggestedPost(fixMinTime: Int32(Date().timeIntervalSince1970 + 10)) } } @@ -1412,7 +1412,7 @@ public final class PendingMessageManager { } else if let attribute = attribute as? PaidStarsMessageAttribute { allowPaidStars = attribute.stars.value } else if let attribute = attribute as? SuggestedPostMessageAttribute { - suggestedPost = attribute.apiSuggestedPost() + suggestedPost = attribute.apiSuggestedPost(fixMinTime: Int32(Date().timeIntervalSince1970 + 10)) } } diff --git a/submodules/TelegramCore/Sources/SyncCore/SuggestedPostMessageAttribute.swift b/submodules/TelegramCore/Sources/SyncCore/SuggestedPostMessageAttribute.swift index 390710fb04..4602bd7ccc 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SuggestedPostMessageAttribute.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SuggestedPostMessageAttribute.swift @@ -70,7 +70,7 @@ extension SuggestedPostMessageAttribute { } } - func apiSuggestedPost() -> Api.SuggestedPost { + func apiSuggestedPost(fixMinTime: Int32?) -> Api.SuggestedPost { var flags: Int32 = 0 if let state = self.state { switch state { @@ -80,7 +80,14 @@ extension SuggestedPostMessageAttribute { flags |= 1 << 2 } } - if self.timestamp != nil { + var timestamp = self.timestamp + if let timestampValue = timestamp, let fixMinTime { + if timestampValue < fixMinTime { + timestamp = fixMinTime + } + } + + if timestamp != nil { flags |= 1 << 0 } var price: Api.StarsAmount? @@ -88,7 +95,7 @@ extension SuggestedPostMessageAttribute { flags |= 1 << 3 price = amount.apiAmount } - return .suggestedPost(flags: flags, price: price, scheduleDate: self.timestamp) + return .suggestedPost(flags: flags, price: price, scheduleDate: timestamp) } } diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift index 78b964a97f..a079328a77 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift @@ -177,6 +177,28 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { } } + public struct SuggestedPostRefund: Codable, Equatable { + private enum CodingKeys: String, CodingKey { + case isUserInitiated = "iui" + } + + public var isUserInitiated: Bool + + public init(isUserInitiated: Bool) { + self.isUserInitiated = isUserInitiated + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.isUserInitiated = try container.decode(Bool.self, forKey: .isUserInitiated) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(self.isUserInitiated, forKey: .isUserInitiated) + } + } + case unknown case groupCreated(title: String) case addedMembers(peerIds: [PeerId]) @@ -230,6 +252,8 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { case todoAppendTasks([TelegramMediaTodo.Item]) case suggestedPostApprovalStatus(status: SuggestedPostApprovalStatus) case giftTon(currency: String, amount: Int64, cryptoCurrency: String?, cryptoAmount: Int64?, transactionId: String?) + case suggestedPostSuccess(amount: CurrencyAmount) + case suggestedPostRefund(SuggestedPostRefund) public init(decoder: PostboxDecoder) { let rawValue: Int32 = decoder.decodeInt32ForKey("_rawValue", orElse: 0) @@ -379,6 +403,10 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { self = .suggestedPostApprovalStatus(status: status ?? .rejected(reason: .generic, comment: nil)) case 52: self = .giftTon(currency: decoder.decodeStringForKey("currency", orElse: ""), amount: decoder.decodeInt64ForKey("amount", orElse: 0), cryptoCurrency: decoder.decodeOptionalStringForKey("cryptoCurrency"), cryptoAmount: decoder.decodeOptionalInt64ForKey("cryptoAmount"), transactionId: decoder.decodeOptionalStringForKey("transactionId")) + case 53: + self = .suggestedPostSuccess(amount: decoder.decodeCodable(CurrencyAmount.self, forKey: "amt") ?? CurrencyAmount(amount: .zero, currency: .stars)) + case 54: + self = .suggestedPostRefund(decoder.decodeCodable(SuggestedPostRefund.self, forKey: "s") ?? SuggestedPostRefund(isUserInitiated: true)) default: self = .unknown } @@ -808,6 +836,12 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { } else { encoder.encodeNil(forKey: "transactionId") } + case let .suggestedPostSuccess(amount): + encoder.encodeInt32(53, forKey: "_rawValue") + encoder.encodeCodable(amount, forKey: "amt") + case let .suggestedPostRefund(status): + encoder.encodeInt32(54, forKey: "_rawValue") + encoder.encodeCodable(status, forKey: "s") } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/TelegramEnginePayments.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/TelegramEnginePayments.swift index 262cd8a681..80b066523a 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/TelegramEnginePayments.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/TelegramEnginePayments.swift @@ -160,6 +160,10 @@ public extension TelegramEngine { public func updateStarGiftResalePrice(reference: StarGiftReference, price: Int64?) -> Signal { return _internal_updateStarGiftResalePrice(account: self.account, reference: reference, price: price) } + + public func getStarsTransaction(reference: StarsTransactionReference) -> Signal { + return _internal_getStarsTransaction(accountPeerId: self.account.peerId, postbox: self.account.postbox, network: self.account.network, transactionReference: reference) + } } } diff --git a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift index fd1a7213c5..24ab1b6267 100644 --- a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift +++ b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift @@ -127,6 +127,7 @@ public enum PresentationResourceKey: Int32 { case chatListLocationIcon case chatListGeneralTopicIcon + case chatListGeneralTopicTemplateIcon case chatListGeneralTopicSmallIcon case searchAdIcon diff --git a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChatList.swift b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChatList.swift index f281093841..a152ad6a35 100644 --- a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChatList.swift +++ b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChatList.swift @@ -445,6 +445,12 @@ public struct PresentationResourcesChatList { }) } + public static func generalTopicTemplateIcon(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatListGeneralTopicTemplateIcon.rawValue, { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat List/GeneralTopicIcon"), color: .white)?.withRenderingMode(.alwaysTemplate) + }) + } + public static func statusAutoremoveIcon(_ theme: PresentationTheme, isActive: Bool) -> UIImage? { return theme.image(PresentationResourceParameterKey.statusAutoremoveIcon(isActive: isActive), { theme in return generateTintedImage(image: UIImage(bundleImageName: isActive ? "Chat List/StatusIconAutoremoveOn" : "Chat List/StatusIconAutoremoveOff"), color: isActive ? theme.list.itemAccentColor : theme.list.itemSecondaryTextColor) diff --git a/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift b/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift index 3ec7c3169e..11111d9eb3 100644 --- a/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift +++ b/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift @@ -1259,8 +1259,24 @@ public func universalServiceMessageString(presentationData: (PresentationTheme, messagePeer = EnginePeer(messagePeerValue) } else if message.id.peerId.namespace == Namespaces.Peer.CloudChannel, let peer = message.peers[message.id.peerId] as? TelegramChannel, peer.isMonoForum { if let author = message.author, let threadId = message.threadId, let threadPeer = message.peers[PeerId(threadId)], author.id != threadPeer.id { - isOutgoing = true - messagePeer = EnginePeer(threadPeer) + if case .channel = author { + var isUser = true + if let peer = message.peers[message.id.peerId] as? TelegramChannel { + if peer.isMonoForum, let linkedMonoforumId = peer.linkedMonoforumId, let mainChannel = message.peers[linkedMonoforumId] as? TelegramChannel, mainChannel.hasPermission(.manageDirect) { + isUser = false + } + } + + if isUser { + messagePeer = author + } else { + messagePeer = EnginePeer(threadPeer) + isOutgoing = true + } + } else { + isOutgoing = true + messagePeer = EnginePeer(threadPeer) + } } } @@ -1450,6 +1466,51 @@ public func universalServiceMessageString(presentationData: (PresentationTheme, } } attributedString = NSAttributedString(string: string, font: titleFont, textColor: primaryTextColor) + case let .suggestedPostSuccess(amount): + var isUser = true + var channelName: String = "" + if let peer = message.peers[message.id.peerId] as? TelegramChannel { + channelName = peer.title + if peer.isMonoForum, let linkedMonoforumId = peer.linkedMonoforumId, let mainChannel = message.peers[linkedMonoforumId] as? TelegramChannel, mainChannel.hasPermission(.manageDirect) { + isUser = false + } + } + let _ = isUser + + //TODO:localize + let amountString: String + switch amount.currency { + case .stars: + if amount.amount.value == 1 { + amountString = "1 Star" + } else { + amountString = "\(amount.amount.value) Stars" + } + case .ton: + amountString = "\(formatTonAmountText(amount.amount.value, dateTimeFormat: dateTimeFormat)) TON" + } + attributedString = parseMarkdownIntoAttributedString("**\(channelName)** received **\(amountString)** for publishing this post", attributes: MarkdownAttributes(body: bodyAttributes, bold: boldAttributes, link: bodyAttributes, linkAttribute: { _ in return nil })) + case let .suggestedPostRefund(info): + var isUser = true + var channelName: String = "" + if let peer = message.peers[message.id.peerId] as? TelegramChannel { + channelName = peer.title + if peer.isMonoForum, let linkedMonoforumId = peer.linkedMonoforumId, let mainChannel = message.peers[linkedMonoforumId] as? TelegramChannel, mainChannel.hasPermission(.manageDirect) { + isUser = false + } + } + let _ = channelName + + //TODO:localize + if info.isUserInitiated { + if isUser { + attributedString = NSAttributedString(string: "Suggested post was refunded because you didn't have enough funds", font: titleFont, textColor: primaryTextColor) + } else { + attributedString = NSAttributedString(string: "Suggested post was refunded because the user didn't have enough funds", font: titleFont, textColor: primaryTextColor) + } + } else { + attributedString = NSAttributedString(string: "Suggested post was refunded because the message was deleted", font: titleFont, textColor: primaryTextColor) + } case let .giftTon(currency, amount, _, _, _): attributedString = nil if !forAdditionalServiceMessage { diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageActionBubbleContentNode/Sources/ChatMessageActionBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageActionBubbleContentNode/Sources/ChatMessageActionBubbleContentNode.swift index 3eecc1b6f1..c32acf3826 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageActionBubbleContentNode/Sources/ChatMessageActionBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageActionBubbleContentNode/Sources/ChatMessageActionBubbleContentNode.swift @@ -228,6 +228,11 @@ public class ChatMessageActionBubbleContentNode: ChatMessageBubbleContentNode { } } + var isUser = true + if let peer = item.message.peers[item.message.id.peerId] as? TelegramChannel, peer.isMonoForum, let linkedMonoforumId = peer.linkedMonoforumId, let mainChannel = item.message.peers[linkedMonoforumId] as? TelegramChannel, mainChannel.hasPermission(.manageDirect) { + isUser = false + } + let imageSize = CGSize(width: 212.0, height: 212.0) var updatedAttributedString = attributedString @@ -281,13 +286,13 @@ public class ChatMessageActionBubbleContentNode: ChatMessageBubbleContentNode { switch amount.currency { case .stars: - if !item.message.effectivelyIncoming(item.context.account.peerId) { + if !isUser { pricePart = "\n\nšŸ’° The user have been charged \(amountString).\n\nāŒ› **\(channelName)** will receive the Stars once the post has been live for 24 hours.\n\nšŸ”„ If your remove the post before it has been live for 24 hours, the user's Stars will be refunded." } else { pricePart = "\n\nšŸ’° You have been charged \(amountString).\n\nāŒ› **\(channelName)** will receive your Stars once the post has been live for 24 hours.\n\nšŸ”„ If **\(channelName)** removes the post before it has been live for 24 hours, your Stars will be refunded." } case .ton: - if !item.message.effectivelyIncoming(item.context.account.peerId) { + if !isUser { pricePart = "\n\nšŸ’° The user have been charged \(amountString).\n\nāŒ› **\(channelName)** will receive TON once the post has been live for 24 hours.\n\nšŸ”„ If your remove the post before it has been live for 24 hours, the user's TON will be refunded." } else { pricePart = "\n\nšŸ’° You have been charged \(amountString).\n\nāŒ› **\(channelName)** will receive your TON once the post has been live for 24 hours.\n\nšŸ”„ If **\(channelName)** removes the post before it has been live for 24 hours, your TON will be refunded." @@ -298,20 +303,20 @@ public class ChatMessageActionBubbleContentNode: ChatMessageBubbleContentNode { let rawString: String if let timestamp { if Int32(Date().timeIntervalSince1970) >= timestamp { - if !item.message.effectivelyIncoming(item.context.account.peerId) { + if !isUser { rawString = "šŸ“… The post has been automatically published in **\(channelName)** **\(timeString)**." + pricePart } else { rawString = "šŸ“… Your post has been automatically published in **\(channelName)** **\(timeString)**." + pricePart } } else { - if !item.message.effectivelyIncoming(item.context.account.peerId) { + if !isUser { rawString = "šŸ“… The post will be automatically published in **\(channelName)** **\(timeString)**." + pricePart } else { rawString = "šŸ“… Your post will be automatically published in **\(channelName)** **\(timeString)**." + pricePart } } } else { - if !item.message.effectivelyIncoming(item.context.account.peerId) { + if !isUser { rawString = "šŸ“… The post has been automatically published in **\(channelName)**." + pricePart } else { rawString = "šŸ“… Your post has been automatically published in **\(channelName)**." + pricePart diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageActionButtonsNode/Sources/ChatMessageActionButtonsNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageActionButtonsNode/Sources/ChatMessageActionButtonsNode.swift index ade800e580..9aa7f29a9b 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageActionButtonsNode/Sources/ChatMessageActionButtonsNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageActionButtonsNode/Sources/ChatMessageActionButtonsNode.swift @@ -213,10 +213,10 @@ private final class ChatMessageActionButtonNode: ASDisplayNode { } } - class func asyncLayout(_ maybeNode: ChatMessageActionButtonNode?) -> (_ context: AccountContext, _ theme: ChatPresentationThemeData, _ bubbleCorners: PresentationChatBubbleCorners, _ strings: PresentationStrings, _ backgroundNode: WallpaperBackgroundNode?, _ message: Message, _ button: ReplyMarkupButton, _ customIcon: ChatMessageActionButtonsNode.CustomIcon?, _ constrainedWidth: CGFloat, _ position: MessageBubbleActionButtonPosition) -> (minimumWidth: CGFloat, layout: ((CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> ChatMessageActionButtonNode))) { + class func asyncLayout(_ maybeNode: ChatMessageActionButtonNode?) -> (_ context: AccountContext, _ theme: ChatPresentationThemeData, _ bubbleCorners: PresentationChatBubbleCorners, _ strings: PresentationStrings, _ backgroundNode: WallpaperBackgroundNode?, _ message: Message, _ button: ReplyMarkupButton, _ customInfo: ChatMessageActionButtonsNode.CustomInfo?, _ constrainedWidth: CGFloat, _ position: MessageBubbleActionButtonPosition) -> (minimumWidth: CGFloat, layout: ((CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> ChatMessageActionButtonNode))) { let titleLayout = TextNode.asyncLayout(maybeNode?.titleNode) - return { context, theme, bubbleCorners, strings, backgroundNode, message, button, customIcon, constrainedWidth, position in + return { context, theme, bubbleCorners, strings, backgroundNode, message, button, customInfo, constrainedWidth, position in let incoming = message.effectivelyIncoming(context.account.peerId) let graphics = PresentationResourcesChat.additionalGraphics(theme.theme, wallpaper: theme.wallpaper, bubbleCorners: bubbleCorners) @@ -227,7 +227,7 @@ private final class ChatMessageActionButtonNode: ASDisplayNode { var isStarsPayment = false let iconImage: UIImage? var tintColor: UIColor? - if let customIcon { + if let customIcon = customInfo?.icon { switch customIcon { case .suggestedPostReject: iconImage = PresentationResourcesChat.messageButtonsPostReject(theme.theme) @@ -314,7 +314,7 @@ private final class ChatMessageActionButtonNode: ASDisplayNode { } var customIconSpaceWidth: CGFloat = 0.0 - if let iconImage, customIcon != nil { + if let iconImage, customInfo?.icon != nil { customIconSpaceWidth = 3.0 + iconImage.size.width } @@ -334,14 +334,13 @@ private final class ChatMessageActionButtonNode: ASDisplayNode { } node.wallpaperBackgroundNode = backgroundNode - node.button = button switch button.action { - case .url: - node.longTapRecognizer?.isEnabled = true - default: - node.longTapRecognizer?.isEnabled = false + case .url: + node.longTapRecognizer?.isEnabled = true + default: + node.longTapRecognizer?.isEnabled = false } //animation.animator.updateFrame(layer: node.backgroundBlurNode.layer, frame: CGRect(origin: CGPoint(), size: CGSize(width: max(0.0, width), height: 42.0)), completion: nil) @@ -453,7 +452,7 @@ private final class ChatMessageActionButtonNode: ASDisplayNode { } var titleFrame = CGRect(origin: CGPoint(x: floor((width - titleSize.size.width) / 2.0), y: floor((42.0 - titleSize.size.height) / 2.0) + 1.0), size: titleSize.size) - if let image = node.iconNode?.image, customIcon != nil { + if let image = node.iconNode?.image, customInfo?.icon != nil { titleFrame.origin.x = floorToScreenPixels((width - titleSize.size.width - image.size.width - 3.0) * 0.5) + 3.0 + image.size.width } titleNode.layer.bounds = CGRect(origin: CGPoint(), size: titleFrame.size) @@ -464,7 +463,7 @@ private final class ChatMessageActionButtonNode: ASDisplayNode { } if let iconNode = node.iconNode { let iconFrame: CGRect - if customIcon != nil, let image = iconNode.image { + if customInfo?.icon != nil, let image = iconNode.image { iconFrame = CGRect(x: titleFrame.minX - 3.0 - image.size.width, y: titleFrame.minY + floorToScreenPixels((titleFrame.height - image.size.height) * 0.5) - 1.0, width: image.size.width, height: image.size.height) } else { iconFrame = CGRect(x: width - 16.0, y: 4.0, width: 12.0, height: 12.0) @@ -479,6 +478,18 @@ private final class ChatMessageActionButtonNode: ASDisplayNode { node.accessibilityArea.accessibilityLabel = title node.accessibilityArea.frame = CGRect(origin: CGPoint(), size: CGSize(width: width, height: 42.0)) + if let buttonView = node.buttonView { + let isEnabled = customInfo?.isEnabled ?? true + if buttonView.isEnabled != isEnabled { + buttonView.isEnabled = isEnabled + + if let backgroundBlurView = node.backgroundBlurView { + backgroundBlurView.view.alpha = isEnabled ? 1.0 : 0.55 + } + node.backgroundContent?.alpha = isEnabled ? 1.0 : 0.55 + } + } + return node }) }) @@ -493,6 +504,16 @@ public final class ChatMessageActionButtonsNode: ASDisplayNode { case suggestedPostEdit } + public struct CustomInfo { + var isEnabled: Bool + var icon: CustomIcon? + + public init(isEnabled: Bool, icon: CustomIcon?) { + self.isEnabled = isEnabled + self.icon = icon + } + } + private var buttonNodes: [ChatMessageActionButtonNode] = [] private var buttonPressedWrapper: ((ReplyMarkupButton, Promise) -> Void)? @@ -529,10 +550,10 @@ public final class ChatMessageActionButtonsNode: ASDisplayNode { } } - public class func asyncLayout(_ maybeNode: ChatMessageActionButtonsNode?) -> (_ context: AccountContext, _ theme: ChatPresentationThemeData, _ chatBubbleCorners: PresentationChatBubbleCorners, _ strings: PresentationStrings, _ backgroundNode: WallpaperBackgroundNode?, _ replyMarkup: ReplyMarkupMessageAttribute, _ customIcons: [MemoryBuffer: CustomIcon], _ message: Message, _ constrainedWidth: CGFloat) -> (minWidth: CGFloat, layout: (CGFloat) -> (CGSize, (_ animation: ListViewItemUpdateAnimation) -> ChatMessageActionButtonsNode)) { + public class func asyncLayout(_ maybeNode: ChatMessageActionButtonsNode?) -> (_ context: AccountContext, _ theme: ChatPresentationThemeData, _ chatBubbleCorners: PresentationChatBubbleCorners, _ strings: PresentationStrings, _ backgroundNode: WallpaperBackgroundNode?, _ replyMarkup: ReplyMarkupMessageAttribute, _ customInfos: [MemoryBuffer: CustomInfo], _ message: Message, _ constrainedWidth: CGFloat) -> (minWidth: CGFloat, layout: (CGFloat) -> (CGSize, (_ animation: ListViewItemUpdateAnimation) -> ChatMessageActionButtonsNode)) { let currentButtonLayouts = maybeNode?.buttonNodes.map { ChatMessageActionButtonNode.asyncLayout($0) } ?? [] - return { context, theme, chatBubbleCorners, strings, backgroundNode, replyMarkup, customIcons, message, constrainedWidth in + return { context, theme, chatBubbleCorners, strings, backgroundNode, replyMarkup, customInfos, message, constrainedWidth in let buttonHeight: CGFloat = 42.0 let buttonSpacing: CGFloat = 2.0 @@ -548,9 +569,9 @@ public final class ChatMessageActionButtonsNode: ASDisplayNode { var finalizeRowButtonLayouts: [((CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> ChatMessageActionButtonNode))] = [] var rowButtonIndex = 0 for button in row.buttons { - var customIcon: CustomIcon? + var customInfo: CustomInfo? if case let .callback(_, data) = button.action { - customIcon = customIcons[data] + customInfo = customInfos[data] } let buttonPosition: MessageBubbleActionButtonPosition @@ -570,9 +591,9 @@ public final class ChatMessageActionButtonsNode: ASDisplayNode { let prepareButtonLayout: (minimumWidth: CGFloat, layout: ((CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> ChatMessageActionButtonNode))) if buttonIndex < currentButtonLayouts.count { - prepareButtonLayout = currentButtonLayouts[buttonIndex](context, theme, chatBubbleCorners, strings, backgroundNode, message, button, customIcon, maximumButtonWidth, buttonPosition) + prepareButtonLayout = currentButtonLayouts[buttonIndex](context, theme, chatBubbleCorners, strings, backgroundNode, message, button, customInfo, maximumButtonWidth, buttonPosition) } else { - prepareButtonLayout = ChatMessageActionButtonNode.asyncLayout(nil)(context, theme, chatBubbleCorners, strings, backgroundNode, message, button, customIcon, maximumButtonWidth, buttonPosition) + prepareButtonLayout = ChatMessageActionButtonNode.asyncLayout(nil)(context, theme, chatBubbleCorners, strings, backgroundNode, message, button, customInfo, maximumButtonWidth, buttonPosition) } maximumRowButtonWidth = max(maximumRowButtonWidth, prepareButtonLayout.minimumWidth) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift index 8fd93f0bcf..2e54b862be 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift @@ -1295,6 +1295,11 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { maxContentWidth = max(maxContentWidth, minWidth) actionButtonsFinalize = buttonsLayout } else if incoming, let attribute = item.message.attributes.first(where: { $0 is SuggestedPostMessageAttribute }) as? SuggestedPostMessageAttribute, attribute.state == nil { + var canApprove = true + if let peer = item.message.peers[item.message.id.peerId] as? TelegramChannel, peer.isMonoForum, let linkedMonoforumId = peer.linkedMonoforumId, let mainChannel = item.message.peers[linkedMonoforumId] as? TelegramChannel, mainChannel.hasPermission(.manageDirect), !mainChannel.hasPermission(.sendSomething) { + canApprove = false + } + //TODO:localize var buttonDeclineValue: UInt8 = 0 let buttonDecline = MemoryBuffer(data: Data(bytes: &buttonDeclineValue, count: 1)) @@ -1303,10 +1308,19 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { var buttonSuggestChangesValue: UInt8 = 2 let buttonSuggestChanges = MemoryBuffer(data: Data(bytes: &buttonSuggestChangesValue, count: 1)) - let customIcons: [MemoryBuffer: ChatMessageActionButtonsNode.CustomIcon] = [ - buttonDecline: .suggestedPostReject, - buttonApprove: .suggestedPostApprove, - buttonSuggestChanges: .suggestedPostEdit + let customInfos: [MemoryBuffer: ChatMessageActionButtonsNode.CustomInfo] = [ + buttonDecline: ChatMessageActionButtonsNode.CustomInfo( + isEnabled: true, + icon: .suggestedPostReject + ), + buttonApprove: ChatMessageActionButtonsNode.CustomInfo( + isEnabled: canApprove, + icon: .suggestedPostApprove + ), + buttonSuggestChanges: ChatMessageActionButtonsNode.CustomInfo( + isEnabled: canApprove, + icon: .suggestedPostEdit + ) ] let (minWidth, buttonsLayout) = actionButtonsLayout( @@ -1327,7 +1341,7 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { ], flags: [], placeholder: nil - ), customIcons, item.message, baseWidth) + ), customInfos, item.message, baseWidth) maxContentWidth = max(maxContentWidth, minWidth) actionButtonsFinalize = buttonsLayout } @@ -1394,7 +1408,7 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { layoutSize.height += additionalTopHeight imageFrame.origin.y += additionalTopHeight - var headersOffset: CGFloat = 0.0 + var headersOffset: CGFloat = additionalTopHeight if let (threadInfoSize, _) = threadInfoApply { headersOffset += threadInfoSize.height + 10.0 } @@ -1611,7 +1625,7 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { } } - var headersOffset: CGFloat = 0.0 + var headersOffset: CGFloat = additionalTopHeight if let (threadInfoSize, threadInfoApply) = threadInfoApply { let threadInfoNode = threadInfoApply(synchronousLoads) if strongSelf.threadInfoNode == nil { diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift index cfd5d507aa..1f786cf66a 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift @@ -1491,7 +1491,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI threadInfoLayout: (ChatMessageThreadInfoNode.Arguments) -> (CGSize, (Bool) -> ChatMessageThreadInfoNode), forwardInfoLayout: (AccountContext, ChatPresentationData, PresentationStrings, ChatMessageForwardInfoType, Peer?, String?, String?, ChatMessageForwardInfoNode.StoryData?, CGSize) -> (CGSize, (CGFloat) -> ChatMessageForwardInfoNode), replyInfoLayout: (ChatMessageReplyInfoNode.Arguments) -> (CGSize, (CGSize, Bool, ListViewItemUpdateAnimation) -> ChatMessageReplyInfoNode), - actionButtonsLayout: (AccountContext, ChatPresentationThemeData, PresentationChatBubbleCorners, PresentationStrings, WallpaperBackgroundNode?, ReplyMarkupMessageAttribute, [MemoryBuffer: ChatMessageActionButtonsNode.CustomIcon], Message, CGFloat) -> (minWidth: CGFloat, layout: (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> ChatMessageActionButtonsNode)), + actionButtonsLayout: (AccountContext, ChatPresentationThemeData, PresentationChatBubbleCorners, PresentationStrings, WallpaperBackgroundNode?, ReplyMarkupMessageAttribute, [MemoryBuffer: ChatMessageActionButtonsNode.CustomInfo], Message, CGFloat) -> (minWidth: CGFloat, layout: (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> ChatMessageActionButtonsNode)), reactionButtonsLayout: (ChatMessageReactionButtonsNode.Arguments) -> (minWidth: CGFloat, layout: (CGFloat) -> (size: CGSize, apply: (ListViewItemUpdateAnimation) -> ChatMessageReactionButtonsNode)), unlockButtonLayout: (ChatMessageUnlockMediaNode.Arguments) -> (CGSize, (Bool) -> ChatMessageUnlockMediaNode), mediaInfoLayout: (ChatMessageStarsMediaInfoNode.Arguments) -> (CGSize, (Bool) -> ChatMessageStarsMediaInfoNode), @@ -2806,6 +2806,11 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI lastNodeTopPosition = .None(.Both) } else if incoming, let attribute = item.message.attributes.first(where: { $0 is SuggestedPostMessageAttribute }) as? SuggestedPostMessageAttribute, attribute.state == nil { + var canApprove = true + if let peer = item.message.peers[item.message.id.peerId] as? TelegramChannel, peer.isMonoForum, let linkedMonoforumId = peer.linkedMonoforumId, let mainChannel = item.message.peers[linkedMonoforumId] as? TelegramChannel, mainChannel.hasPermission(.manageDirect), !mainChannel.hasPermission(.sendSomething) { + canApprove = false + } + //TODO:localize var buttonDeclineValue: UInt8 = 0 let buttonDecline = MemoryBuffer(data: Data(bytes: &buttonDeclineValue, count: 1)) @@ -2814,10 +2819,19 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI var buttonSuggestChangesValue: UInt8 = 2 let buttonSuggestChanges = MemoryBuffer(data: Data(bytes: &buttonSuggestChangesValue, count: 1)) - let customIcons: [MemoryBuffer: ChatMessageActionButtonsNode.CustomIcon] = [ - buttonDecline: .suggestedPostReject, - buttonApprove: .suggestedPostApprove, - buttonSuggestChanges: .suggestedPostEdit + let customInfos: [MemoryBuffer: ChatMessageActionButtonsNode.CustomInfo] = [ + buttonDecline: ChatMessageActionButtonsNode.CustomInfo( + isEnabled: true, + icon: .suggestedPostReject + ), + buttonApprove: ChatMessageActionButtonsNode.CustomInfo( + isEnabled: canApprove, + icon: .suggestedPostApprove + ), + buttonSuggestChanges: ChatMessageActionButtonsNode.CustomInfo( + isEnabled: canApprove, + icon: .suggestedPostEdit + ) ] let (minWidth, buttonsLayout) = actionButtonsLayout( @@ -2838,7 +2852,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI ], flags: [], placeholder: nil - ), customIcons, item.message, baseWidth) + ), customInfos, item.message, baseWidth) maxContentWidth = max(maxContentWidth, minWidth) actionButtonsFinalize = buttonsLayout diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageItemCommon/Sources/ChatMessageItemCommon.swift b/submodules/TelegramUI/Components/Chat/ChatMessageItemCommon/Sources/ChatMessageItemCommon.swift index bbdb19ca9a..0eab89f7bb 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageItemCommon/Sources/ChatMessageItemCommon.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageItemCommon/Sources/ChatMessageItemCommon.swift @@ -291,7 +291,7 @@ public func canAddMessageReactions(message: Message) -> Bool { return true } else { switch action.action { - case .unknown, .groupCreated, .channelMigratedFromGroup, .groupMigratedToChannel, .historyCleared, .customText, .botDomainAccessGranted, .botAppAccessGranted, .botSentSecureValues, .phoneNumberRequest, .webViewData, .topicCreated, .attachMenuBotAllowed, .requestedPeer, .giveawayLaunched, .suggestedPostApprovalStatus: + case .unknown, .groupCreated, .channelMigratedFromGroup, .groupMigratedToChannel, .historyCleared, .customText, .botDomainAccessGranted, .botAppAccessGranted, .botSentSecureValues, .phoneNumberRequest, .webViewData, .topicCreated, .attachMenuBotAllowed, .requestedPeer, .giveawayLaunched, .suggestedPostApprovalStatus, .suggestedPostSuccess, .suggestedPostRefund: return false default: return true diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageStickerItemNode/Sources/ChatMessageStickerItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageStickerItemNode/Sources/ChatMessageStickerItemNode.swift index ad8c362519..55ad3cc6a7 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageStickerItemNode/Sources/ChatMessageStickerItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageStickerItemNode/Sources/ChatMessageStickerItemNode.swift @@ -856,6 +856,11 @@ public class ChatMessageStickerItemNode: ChatMessageItemView { maxContentWidth = max(maxContentWidth, minWidth) actionButtonsFinalize = buttonsLayout } else if incoming, let attribute = item.message.attributes.first(where: { $0 is SuggestedPostMessageAttribute }) as? SuggestedPostMessageAttribute, attribute.state == nil { + var canApprove = true + if let peer = item.message.peers[item.message.id.peerId] as? TelegramChannel, peer.isMonoForum, let linkedMonoforumId = peer.linkedMonoforumId, let mainChannel = item.message.peers[linkedMonoforumId] as? TelegramChannel, mainChannel.hasPermission(.manageDirect), !mainChannel.hasPermission(.sendSomething) { + canApprove = false + } + //TODO:localize var buttonDeclineValue: UInt8 = 0 let buttonDecline = MemoryBuffer(data: Data(bytes: &buttonDeclineValue, count: 1)) @@ -864,10 +869,19 @@ public class ChatMessageStickerItemNode: ChatMessageItemView { var buttonSuggestChangesValue: UInt8 = 2 let buttonSuggestChanges = MemoryBuffer(data: Data(bytes: &buttonSuggestChangesValue, count: 1)) - let customIcons: [MemoryBuffer: ChatMessageActionButtonsNode.CustomIcon] = [ - buttonDecline: .suggestedPostReject, - buttonApprove: .suggestedPostApprove, - buttonSuggestChanges: .suggestedPostEdit + let customInfos: [MemoryBuffer: ChatMessageActionButtonsNode.CustomInfo] = [ + buttonDecline: ChatMessageActionButtonsNode.CustomInfo( + isEnabled: true, + icon: .suggestedPostReject + ), + buttonApprove: ChatMessageActionButtonsNode.CustomInfo( + isEnabled: canApprove, + icon: .suggestedPostApprove + ), + buttonSuggestChanges: ChatMessageActionButtonsNode.CustomInfo( + isEnabled: canApprove, + icon: .suggestedPostEdit + ) ] let (minWidth, buttonsLayout) = actionButtonsLayout( @@ -888,7 +902,7 @@ public class ChatMessageStickerItemNode: ChatMessageItemView { ], flags: [], placeholder: nil - ), customIcons, item.message, baseWidth) + ), customInfos, item.message, baseWidth) maxContentWidth = max(maxContentWidth, minWidth) actionButtonsFinalize = buttonsLayout } @@ -977,7 +991,7 @@ public class ChatMessageStickerItemNode: ChatMessageItemView { baseShareButtonFrame.origin.x = dateAndStatusFrame.maxX + 8.0 } - var headersOffset: CGFloat = 0.0 + var headersOffset: CGFloat = additionalTopHeight if let (threadInfoSize, _) = threadInfoApply { headersOffset += threadInfoSize.height + 10.0 } @@ -1135,7 +1149,7 @@ public class ChatMessageStickerItemNode: ChatMessageItemView { } } - var headersOffset: CGFloat = 0.0 + var headersOffset: CGFloat = additionalTopHeight if let (threadInfoSize, threadInfoApply) = threadInfoApply { let threadInfoNode = threadInfoApply(synchronousLoads) if strongSelf.threadInfoNode == nil { diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageSuggestedPostInfoNode/Sources/ChatMessageSuggestedPostInfoNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageSuggestedPostInfoNode/Sources/ChatMessageSuggestedPostInfoNode.swift index 8c9f2407a3..573baf3a9a 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageSuggestedPostInfoNode/Sources/ChatMessageSuggestedPostInfoNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageSuggestedPostInfoNode/Sources/ChatMessageSuggestedPostInfoNode.swift @@ -199,10 +199,14 @@ public final class ChatMessageSuggestedPostInfoNode: ASDisplayNode { 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 + var tableContentWidth: CGFloat = 0.0 + tableContentWidth = max(tableContentWidth, priceLabelLayout.0.size.width + labelSpacing + priceValueLayout.0.size.width) + tableContentWidth = max(tableContentWidth, timeLabelLayout.0.size.width + labelSpacing + timeValueLayout.0.size.width) - maxContentWidth = max(maxContentWidth, timeLabelLayout.0.size.width + labelSpacing + timeValueLayout.0.size.width) + let labelValueOffset = labelSpacing + max(priceLabelLayout.0.size.width, timeLabelLayout.0.size.width) + + maxContentWidth = max(maxContentWidth, tableContentWidth) + contentHeight += priceLabelLayout.0.size.height + valuesVerticalSpacing contentHeight += timeLabelLayout.0.size.height let size = CGSize(width: insets.left + insets.right + maxContentWidth, height: insets.top + insets.bottom + contentHeight) @@ -252,13 +256,15 @@ public final class ChatMessageSuggestedPostInfoNode: ASDisplayNode { 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) + let tableX: CGFloat = floor((size.width - tableContentWidth) * 0.5) + + let priceLabelFrame = CGRect(origin: CGPoint(x: tableX, 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) + priceValueNode.frame = CGRect(origin: CGPoint(x: tableX + labelValueOffset, y: priceLabelFrame.minY), size: priceValueLayout.0.size) - let timeLabelFrame = CGRect(origin: CGPoint(x: insets.left, y: priceLabelFrame.maxY + valuesVerticalSpacing), size: timeLabelLayout.0.size) + let timeLabelFrame = CGRect(origin: CGPoint(x: tableX, 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) + timeValueNode.frame = CGRect(origin: CGPoint(x: tableX + labelValueOffset, y: timeLabelFrame.minY), size: timeValueLayout.0.size) return node }) diff --git a/submodules/TelegramUI/Components/Chat/ChatSendStarsScreen/Sources/ChatSendStarsScreen.swift b/submodules/TelegramUI/Components/Chat/ChatSendStarsScreen/Sources/ChatSendStarsScreen.swift index 654807dc07..673a9092d4 100644 --- a/submodules/TelegramUI/Components/Chat/ChatSendStarsScreen/Sources/ChatSendStarsScreen.swift +++ b/submodules/TelegramUI/Components/Chat/ChatSendStarsScreen/Sources/ChatSendStarsScreen.swift @@ -1015,8 +1015,6 @@ 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 @@ -1393,28 +1391,6 @@ 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 - } - - let mode: ChatScheduleTimeControllerMode = .suggestPost(needsTime: false) - 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) }), 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 { @@ -1518,9 +1494,6 @@ private final class ChatSendStarsScreenComponent: Component { } } }) - 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 { @@ -1578,8 +1551,6 @@ private final class ChatSendStarsScreenComponent: Component { switch component.initialData.subjectInitialData { case let .react(reactData): maxAmount = reactData.maxAmount - case let .suggestPost(suggestPostData): - maxAmount = suggestPostData.maxAmount } self.amount = self.amount.withSliderValue(value) @@ -1659,8 +1630,6 @@ private final class ChatSendStarsScreenComponent: Component { } else { self.isPastTopCutoff = nil } - case .suggestPost: - break } let _ = self.sliderBackground.update( @@ -1784,8 +1753,6 @@ private final class ChatSendStarsScreenComponent: Component { transition.setFrame(view: peerSelectorButtonView, frame: peerSelectorButtonFrame) peerSelectorButtonView.isHidden = sendAsPeers.count <= 1 } - case .suggestPost: - break } if themeUpdated { @@ -1837,8 +1804,6 @@ private final class ChatSendStarsScreenComponent: Component { 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? @@ -1857,9 +1822,6 @@ private final class ChatSendStarsScreenComponent: Component { switch component.initialData.subjectInitialData { case .react: titleText = environment.strings.SendStarReactions_Title - case .suggestPost: - //TODO:localize - titleText = "Suggest a Message" } let titleSize = title.update( @@ -1907,9 +1869,6 @@ private final class ChatSendStarsScreenComponent: Component { } 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) @@ -1943,38 +1902,6 @@ private final class ChatSendStarsScreenComponent: Component { contentHeight += 22.0 contentHeight += 2.0 - if case .suggestPost = component.initialData.subjectInitialData { - contentHeight += 3.0 - - 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: availableSize.width - sideInset * 2.0, height: 100.0) - ) - 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: timeSelectorButtonView, frame: timeSelectorButtonFrame) - } - contentHeight += timeSelectorButtonSize.height - - contentHeight += 32.0 - } - switch component.initialData.subjectInitialData { case let .react(reactData): if !reactData.topPeers.isEmpty { @@ -2307,8 +2234,6 @@ private final class ChatSendStarsScreenComponent: Component { } contentHeight += anonymousContentsSize.height + 27.0 - case .suggestPost: - break } initialContentHeight = contentHeight @@ -2321,8 +2246,6 @@ private final class ChatSendStarsScreenComponent: Component { 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 { @@ -2369,9 +2292,6 @@ private final class ChatSendStarsScreenComponent: Component { 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 @@ -2415,8 +2335,6 @@ private final class ChatSendStarsScreenComponent: Component { sourceView: badgeView.badgeIcon ) ) - case let .suggestPost(suggestPostData): - suggestPostData.completion(Int64(self.amount.realValue), self.currentSuggestPostTimestamp) } self.environment?.controller()?.dismiss() } @@ -2560,22 +2478,7 @@ public class ChatSendStarsScreen: ViewControllerComponentContainer { } } - 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 { @@ -2828,46 +2731,6 @@ public class ChatSendStarsScreen: ViewControllerComponentContainer { } } - 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 - ) - } - } - override public func dismiss(completion: (() -> Void)? = nil) { if !self.isDismissed { self.isDismissed = true diff --git a/submodules/TelegramUI/Components/Chat/ChatSideTopicsPanel/Sources/ChatSideTopicsPanel.swift b/submodules/TelegramUI/Components/Chat/ChatSideTopicsPanel/Sources/ChatSideTopicsPanel.swift index a30ed226fb..f2f63e0eac 100644 --- a/submodules/TelegramUI/Components/Chat/ChatSideTopicsPanel/Sources/ChatSideTopicsPanel.swift +++ b/submodules/TelegramUI/Components/Chat/ChatSideTopicsPanel/Sources/ChatSideTopicsPanel.swift @@ -360,6 +360,7 @@ public final class ChatSideTopicsPanel: Component { } func update(component: VerticalItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + let previousComponent = self.component self.component = component self.tapRecognizer?.isEnabled = component.action != nil @@ -381,12 +382,12 @@ public final class ChatSideTopicsPanel: Component { if case let .forum(topicId) = component.item.item.id { if topicId != 1, let threadData = component.item.item.threadData { if let fileId = threadData.info.icon, fileId != 0 { - avatarIconContent = .animation(content: .customEmoji(fileId: fileId), size: iconSize, placeholderColor: component.theme.list.mediaPlaceholderColor, themeColor: component.theme.list.itemAccentColor, loopMode: .count(0)) + avatarIconContent = .animation(content: .customEmoji(fileId: fileId), size: iconSize, placeholderColor: component.theme.list.mediaPlaceholderColor, themeColor: component.isSelected ? component.theme.rootController.navigationBar.accentTextColor : component.theme.rootController.navigationBar.controlColor, loopMode: .count(0)) } else { avatarIconContent = .topic(title: String(threadData.info.title.prefix(1)), color: threadData.info.iconColor, size: iconSize) } } else { - avatarIconContent = .image(image: PresentationResourcesChatList.generalTopicIcon(component.theme), tintColor: component.theme.rootController.navigationBar.secondaryTextColor) + avatarIconContent = .image(image: PresentationResourcesChatList.generalTopicTemplateIcon(component.theme), tintColor: component.isSelected ? component.theme.rootController.navigationBar.accentTextColor : component.theme.rootController.navigationBar.controlColor) } } @@ -406,8 +407,14 @@ public final class ChatSideTopicsPanel: Component { icon = ComponentView() self.icon = icon } + + var iconTransition = transition + if iconTransition.animation.isImmediate, let previousComponent, previousComponent.isSelected != component.isSelected { + iconTransition = .easeInOut(duration: 0.2) + } + let _ = icon.update( - transition: .immediate, + transition: iconTransition, component: AnyComponent(avatarIconComponent), environment: {}, containerSize: iconSize @@ -813,12 +820,12 @@ public final class ChatSideTopicsPanel: Component { if case let .forum(topicId) = component.item.item.id { if topicId != 1, let threadData = component.item.item.threadData { if let fileId = threadData.info.icon, fileId != 0 { - avatarIconContent = .animation(content: .customEmoji(fileId: fileId), size: iconSize, placeholderColor: component.theme.list.mediaPlaceholderColor, themeColor: component.theme.list.itemAccentColor, loopMode: .count(0)) + avatarIconContent = .animation(content: .customEmoji(fileId: fileId), size: iconSize, placeholderColor: component.theme.list.mediaPlaceholderColor, themeColor: component.isSelected ? component.theme.rootController.navigationBar.accentTextColor : component.theme.rootController.navigationBar.controlColor, loopMode: .count(0)) } else { avatarIconContent = .topic(title: String(threadData.info.title.prefix(1)), color: threadData.info.iconColor, size: iconSize) } } else { - avatarIconContent = .image(image: PresentationResourcesChatList.generalTopicIcon(component.theme), tintColor: component.theme.rootController.navigationBar.secondaryTextColor) + avatarIconContent = .image(image: PresentationResourcesChatList.generalTopicTemplateIcon(component.theme), tintColor: component.isSelected ? component.theme.rootController.navigationBar.accentTextColor : component.theme.rootController.navigationBar.controlColor) } } @@ -1845,7 +1852,7 @@ public final class ChatSideTopicsPanel: Component { case .side: scrollSize = CGSize(width: availableSize.width, height: availableSize.height - directionContainerInset) scrollFrame = CGRect(origin: CGPoint(x: 0.0, y: directionContainerInset), size: scrollSize) - listContentInsets = UIEdgeInsets(top: 8.0, left: 0.0, bottom: 8.0, right: 0.0) + listContentInsets = UIEdgeInsets(top: 8.0 + environment.insets.top, left: 0.0, bottom: 8.0 + environment.insets.bottom, right: 0.0) case .top: scrollSize = CGSize(width: availableSize.width - directionContainerInset, height: availableSize.height) scrollFrame = CGRect(origin: CGPoint(x: directionContainerInset, y: 0.0), size: scrollSize) diff --git a/submodules/TelegramUI/Components/ChatTitleView/Sources/ChatTitleView.swift b/submodules/TelegramUI/Components/ChatTitleView/Sources/ChatTitleView.swift index 4c39ade9f7..d1226299bd 100644 --- a/submodules/TelegramUI/Components/ChatTitleView/Sources/ChatTitleView.swift +++ b/submodules/TelegramUI/Components/ChatTitleView/Sources/ChatTitleView.swift @@ -92,20 +92,23 @@ public enum ChatTitleContent: Equatable { case replies } - case peer(peerView: PeerData, customTitle: String?, onlineMemberCount: (total: Int32?, recent: Int32?), isScheduledMessages: Bool, isMuted: Bool?, customMessageCount: Int?, isEnabled: Bool) + case peer(peerView: PeerData, customTitle: String?, customSubtitle: String?, onlineMemberCount: (total: Int32?, recent: Int32?), isScheduledMessages: Bool, isMuted: Bool?, customMessageCount: Int?, isEnabled: Bool) case replyThread(type: ReplyThreadType, count: Int) case custom(String, String?, Bool) public static func ==(lhs: ChatTitleContent, rhs: ChatTitleContent) -> Bool { switch lhs { - case let .peer(peerView, customTitle, onlineMemberCount, isScheduledMessages, isMuted, customMessageCount, isEnabled): - if case let .peer(rhsPeerView, rhsCustomTitle, rhsOnlineMemberCount, rhsIsScheduledMessages, rhsIsMuted, rhsCustomMessageCount, rhsIsEnabled) = rhs { + case let .peer(peerView, customTitle, customSubtitle, onlineMemberCount, isScheduledMessages, isMuted, customMessageCount, isEnabled): + if case let .peer(rhsPeerView, rhsCustomTitle, rhsCustomSubtitle, rhsOnlineMemberCount, rhsIsScheduledMessages, rhsIsMuted, rhsCustomMessageCount, rhsIsEnabled) = rhs { if peerView != rhsPeerView { return false } if customTitle != rhsCustomTitle { return false } + if customSubtitle != rhsCustomSubtitle { + return false + } if onlineMemberCount.0 != rhsOnlineMemberCount.0 || onlineMemberCount.1 != rhsOnlineMemberCount.1 { return false } @@ -246,7 +249,7 @@ public final class ChatTitleView: UIView, NavigationBarTitleView { var titleStatusIcon: ChatTitleCredibilityIcon = .none var isEnabled = true switch titleContent { - case let .peer(peerView, customTitle, _, isScheduledMessages, isMuted, _, isEnabledValue): + case let .peer(peerView, customTitle, _, _, isScheduledMessages, isMuted, _, isEnabledValue): if peerView.peerId.isReplies { let typeText: String = self.strings.DialogList_Replies segments = [.text(0, NSAttributedString(string: typeText, font: titleFont, textColor: titleTheme.rootController.navigationBar.primaryTextColor))] @@ -260,7 +263,7 @@ public final class ChatTitleView: UIView, NavigationBarTitleView { isEnabled = false } else { if let peer = peerView.peer { - if let customTitle = customTitle { + if let customTitle { segments = [.text(0, NSAttributedString(string: customTitle, font: titleFont, textColor: titleTheme.rootController.navigationBar.primaryTextColor))] } else if peerView.peerId == self.context.account.peerId { if peerView.isSavedMessages { @@ -444,8 +447,8 @@ public final class ChatTitleView: UIView, NavigationBarTitleView { var enableAnimation = false switch titleContent { - case let .peer(_, customTitle, _, _, _, _, _): - if case let .peer(_, previousCustomTitle, _, _, _, _, _) = oldValue { + case let .peer(_, customTitle, _, _, _, _, _, _): + if case let .peer(_, previousCustomTitle, _, _, _, _, _, _) = oldValue { if customTitle != previousCustomTitle { enableAnimation = false } @@ -471,7 +474,7 @@ public final class ChatTitleView: UIView, NavigationBarTitleView { var inputActivitiesAllowed = true if let titleContent = self.titleContent { switch titleContent { - case let .peer(peerView, _, _, isScheduledMessages, _, _, _): + case let .peer(peerView, _, _, _, isScheduledMessages, _, _, _): if let peer = peerView.peer { if peer.id == self.context.account.peerId || isScheduledMessages || peer.id.isRepliesOrVerificationCodes { inputActivitiesAllowed = false @@ -572,8 +575,11 @@ public final class ChatTitleView: UIView, NavigationBarTitleView { } else { if let titleContent = self.titleContent { switch titleContent { - case let .peer(peerView, customTitle, onlineMemberCount, isScheduledMessages, _, customMessageCount, _): - if let customMessageCount = customMessageCount, customMessageCount != 0 { + case let .peer(peerView, customTitle, customSubtitle, onlineMemberCount, isScheduledMessages, _, customMessageCount, _): + if let customSubtitle { + let string = NSAttributedString(string: customSubtitle, font: subtitleFont, textColor: titleTheme.rootController.navigationBar.secondaryTextColor) + state = .info(string, .generic) + } else if let customMessageCount = customMessageCount, customMessageCount != 0 { let string = NSAttributedString(string: self.strings.Conversation_Messages(Int32(customMessageCount)), font: subtitleFont, textColor: titleTheme.rootController.navigationBar.secondaryTextColor) state = .info(string, .generic) } else if let peer = peerView.peer { diff --git a/submodules/TelegramUI/Components/EmojiStatusComponent/Sources/EmojiStatusComponent.swift b/submodules/TelegramUI/Components/EmojiStatusComponent/Sources/EmojiStatusComponent.swift index aa6e7296df..2a3ce4872f 100644 --- a/submodules/TelegramUI/Components/EmojiStatusComponent/Sources/EmojiStatusComponent.swift +++ b/submodules/TelegramUI/Components/EmojiStatusComponent/Sources/EmojiStatusComponent.swift @@ -233,7 +233,10 @@ public final class EmojiStatusComponent: Component { private weak var state: EmptyComponentState? private var component: EmojiStatusComponent? private var starsLayer: StarsEffectLayer? - private var iconView: UIImageView? + + private var iconLayer: SimpleLayer? + private var iconLayerImage: UIImage? + private var animationLayer: InlineStickerItemLayer? private var lottieAnimationView: AnimationView? private let hierarchyTrackingLayer: HierarchyTrackingLayer @@ -323,7 +326,7 @@ public final class EmojiStatusComponent: Component { case let .premium(color): iconTintColor = color - if case .premium = self.component?.content, let image = self.iconView?.image { + if case .premium = self.component?.content, let image = self.iconLayerImage { iconImage = image } else { if let sourceImage = UIImage(bundleImageName: "Chat/Input/Media/EntityInputPremiumIcon") { @@ -454,7 +457,7 @@ public final class EmojiStatusComponent: Component { } } } else { - iconImage = self.iconView?.image + iconImage = self.iconLayerImage if case let .animation(animationContent, size, placeholderColor, themeColor, loopMode) = component.content { emojiFileId = animationContent.fileId.id emojiPlaceholderColor = placeholderColor @@ -471,31 +474,28 @@ public final class EmojiStatusComponent: Component { var size = CGSize() if let iconImage = iconImage { - let iconView: UIImageView - if let current = self.iconView { - iconView = current + let iconLayer: SimpleLayer + if let current = self.iconLayer { + iconLayer = current } else { - iconView = UIImageView() - self.iconView = iconView - self.addSubview(iconView) + iconLayer = SimpleLayer() + self.iconLayer = iconLayer + self.layer.addSublayer(iconLayer) if !transition.animation.isImmediate { - iconView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) - iconView.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.5) + iconLayer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + iconLayer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.5) } } - if iconView.image !== iconImage { - iconView.image = iconImage + if self.iconLayerImage !== iconImage { + self.iconLayerImage = iconImage + iconLayer.contents = iconImage.cgImage } if let iconTintColor { - if transition.animation.isImmediate { - iconView.tintColor = iconTintColor - } else { - transition.setTintColor(view: iconView, color: iconTintColor) - } + transition.setTintColor(layer: iconLayer, color: iconTintColor) } else { - iconView.tintColor = nil + iconLayer.layerTintColor = nil } var useFit = false @@ -509,24 +509,25 @@ public final class EmojiStatusComponent: Component { } if useFit { size = CGSize(width: iconImage.size.width, height: availableSize.height) - iconView.frame = CGRect(origin: CGPoint(x: floor((size.width - iconImage.size.width) / 2.0), y: floor((size.height - iconImage.size.height) / 2.0)), size: iconImage.size) + iconLayer.frame = CGRect(origin: CGPoint(x: floor((size.width - iconImage.size.width) / 2.0), y: floor((size.height - iconImage.size.height) / 2.0)), size: iconImage.size) } else { size = iconImage.size.aspectFilled(availableSize) - iconView.frame = CGRect(origin: CGPoint(), size: size) + iconLayer.frame = CGRect(origin: CGPoint(), size: size) } } else { - if let iconView = self.iconView { - self.iconView = nil + if let iconLayer = self.iconLayer { + self.iconLayer = nil if !transition.animation.isImmediate { - iconView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak iconView] _ in - iconView?.removeFromSuperview() + iconLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak iconLayer] _ in + iconLayer?.removeFromSuperlayer() }) - iconView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false) + iconLayer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false) } else { - iconView.removeFromSuperview() + iconLayer.removeFromSuperlayer() } } + self.iconLayerImage = nil } let emojiFileUpdated = component.emojiFileUpdated @@ -607,44 +608,6 @@ public final class EmojiStatusComponent: Component { animationLayer.frame = CGRect(origin: CGPoint(), size: size) animationLayer.isVisibleForAnimations = component.isVisibleForAnimations - /*} else { - if self.emojiFileDataPathDisposable == nil { - let account = component.context.account - self.emojiFileDataPathDisposable = (Signal { subscriber in - let disposable = MetaDisposable() - - let _ = (account.postbox.mediaBox.resourceData(emojiFile.resource) - |> take(1)).start(next: { firstAttemptData in - if firstAttemptData.complete { - subscriber.putNext(AnimationFileProperties.load(from: firstAttemptData.path)) - subscriber.putCompletion() - } else { - let fetchDisposable = freeMediaFileInteractiveFetched(account: account, fileReference: .standalone(media: emojiFile)).start() - let dataDisposable = account.postbox.mediaBox.resourceData(emojiFile.resource).start(next: { data in - if data.complete { - subscriber.putNext(AnimationFileProperties.load(from: data.path)) - subscriber.putCompletion() - } - }) - - disposable.set(ActionDisposable { - fetchDisposable.dispose() - dataDisposable.dispose() - }) - } - }) - - return disposable - } - |> deliverOnMainQueue).start(next: { [weak self] properties in - guard let strongSelf = self else { - return - } - strongSelf.emojiFileDataProperties = properties - strongSelf.state?.updated(transition: transition) - }) - } - }*/ } else { if self.emojiFileDisposable == nil { self.emojiFileDisposable = (component.resolveInlineStickers([emojiFileId]) diff --git a/submodules/TelegramUI/Components/ForumSettingsScreen/Sources/ForumSettingsScreen.swift b/submodules/TelegramUI/Components/ForumSettingsScreen/Sources/ForumSettingsScreen.swift index ce2e1c43ee..aec7e0f1e8 100644 --- a/submodules/TelegramUI/Components/ForumSettingsScreen/Sources/ForumSettingsScreen.swift +++ b/submodules/TelegramUI/Components/ForumSettingsScreen/Sources/ForumSettingsScreen.swift @@ -172,24 +172,27 @@ final class ForumSettingsScreenComponent: Component { if let controller = self.environment?.controller(), let navigationController = controller.navigationController as? NavigationController { var viewControllers = navigationController.viewControllers - if self.isOn && self.mode == .list { - for i in 0 ..< viewControllers.count { - if let chatController = viewControllers[i] as? ChatController, chatController.chatLocation.peerId == component.peerId { - let chatListController = component.context.sharedContext.makeChatListController(context: component.context, location: .forum(peerId: component.peerId), controlsHistoryPreload: false, hideNetworkActivityStatus: false, previewing: false, enableDebugActions: false) - viewControllers[i] = chatListController - } - } - navigationController.setViewControllers(viewControllers, animated: false) + if case .legacyGroup = peer { } else { - for i in (0 ..< viewControllers.count).reversed() { - if let chatListController = viewControllers[i] as? ChatListController, chatListController.location == .forum(peerId: component.peerId) { - viewControllers.remove(at: i) + if self.isOn && self.mode == .list { + for i in 0 ..< viewControllers.count { + if let chatController = viewControllers[i] as? ChatController, chatController.chatLocation.peerId == component.peerId { + let chatListController = component.context.sharedContext.makeChatListController(context: component.context, location: .forum(peerId: component.peerId), controlsHistoryPreload: false, hideNetworkActivityStatus: false, previewing: false, enableDebugActions: false) + viewControllers[i] = chatListController + } + } + navigationController.setViewControllers(viewControllers, animated: false) + } else { + for i in (0 ..< viewControllers.count).reversed() { + if let chatListController = viewControllers[i] as? ChatListController, chatListController.location == .forum(peerId: component.peerId) { + viewControllers.remove(at: i) + } + } + navigationController.setViewControllers(viewControllers, animated: false) + + if let baseController = navigationController as? TelegramRootControllerInterface, let chatListController = baseController.getChatsController() as? ChatListController { + chatListController.resetForumStackIfOpen() } - } - navigationController.setViewControllers(viewControllers, animated: false) - - if let baseController = navigationController as? TelegramRootControllerInterface, let chatListController = baseController.getChatsController() as? ChatListController { - chatListController.resetForumStackIfOpen() } } } @@ -232,6 +235,34 @@ final class ForumSettingsScreenComponent: Component { } if let resultPeerId { self.peerIdPromise.set(resultPeerId) + + let _ = component.context.engine.peers.setChannelForumMode(id: resultPeerId, isForum: true, displayForumAsTabs: self.mode == .tabs).startStandalone() + + if let controller = self.environment?.controller(), let navigationController = controller.navigationController as? NavigationController { + var viewControllers = navigationController.viewControllers + if self.mode == .list { + for i in 0 ..< viewControllers.count { + if let chatController = viewControllers[i] as? ChatController, chatController.chatLocation.peerId == component.peerId { + let chatListController = component.context.sharedContext.makeChatListController(context: component.context, location: .forum(peerId: resultPeerId), controlsHistoryPreload: false, hideNetworkActivityStatus: false, previewing: false, enableDebugActions: false) + viewControllers[i] = chatListController + } + } + navigationController.setViewControllers(viewControllers, animated: false) + } else { + for i in (0 ..< viewControllers.count).reversed() { + if let chatListController = viewControllers[i] as? ChatListController, chatListController.location == .forum(peerId: component.peerId) { + viewControllers.remove(at: i) + } else if let peerInfoScreen = viewControllers[i] as? PeerInfoScreen, peerInfoScreen.peerId == component.peerId { + viewControllers.remove(at: i) + } + } + navigationController.setViewControllers(viewControllers, animated: false) + + if let baseController = navigationController as? TelegramRootControllerInterface, let chatListController = baseController.getChatsController() as? ChatListController { + chatListController.resetForumStackIfOpen() + } + } + } } else { self.isOn = false self.state?.updated(transition: .easeInOut(duration: 0.2)) diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift index d8774b4f45..bdff41b6e8 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift @@ -2290,8 +2290,12 @@ func peerInfoHeaderButtons(peer: Peer?, cachedData: CachedPeerData?, isOpenedFro if hasVoiceChat || canStartVoiceChat { result.append(.voiceChat) } + if case let .broadcast(info) = channel.info, info.flags.contains(.hasMonoforum), !channel.hasPermission(.manageDirect) { + result.append(.message) + } result.append(.mute) - if hasDiscussion { + if case let .broadcast(info) = channel.info, info.flags.contains(.hasMonoforum), !channel.hasPermission(.manageDirect) { + } else if hasDiscussion { result.append(.discussion) } result.append(.search) diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift index 51bfcc130d..2195d81ebc 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift @@ -1959,7 +1959,7 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese })) } - if let personalChannel = data.personalChannel { + if channel.hasPermission(.manageDirect), let personalChannel = data.personalChannel { let peerId = personalChannel.peer.peerId items[.channelMonoforum]?.append(PeerInfoScreenPersonalChannelItem(id: ItemPeerPersonalChannel, context: context, data: personalChannel, controller: { [weak interaction] in guard let interaction else { @@ -5998,18 +5998,32 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro switch key { case .message: if let navigationController = controller.navigationController as? NavigationController, let peer = self.data?.peer { - self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(EnginePeer(peer)), keepStack: self.nearbyPeerDistance != nil ? .always : .default, peerNearbyData: self.nearbyPeerDistance.flatMap({ ChatPeerNearbyData(distance: $0) }), completion: { [weak self] _ in - if let strongSelf = self, strongSelf.nearbyPeerDistance != nil { - var viewControllers = navigationController.viewControllers - viewControllers = viewControllers.filter { controller in - if controller is PeerInfoScreen { - return false - } - return true + if let channel = peer as? TelegramChannel, case let .broadcast(info) = channel.info, info.flags.contains(.hasMonoforum), let linkedMonoforumId = channel.linkedMonoforumId { + Task { @MainActor [weak self] in + guard let self else { + return } - navigationController.setViewControllers(viewControllers, animated: false) + + guard let peer = await self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: linkedMonoforumId)).get() else { + return + } + + self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(peer), keepStack: .default)) } - })) + } else { + self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(EnginePeer(peer)), keepStack: self.nearbyPeerDistance != nil ? .always : .default, peerNearbyData: self.nearbyPeerDistance.flatMap({ ChatPeerNearbyData(distance: $0) }), completion: { [weak self] _ in + if let strongSelf = self, strongSelf.nearbyPeerDistance != nil { + var viewControllers = navigationController.viewControllers + viewControllers = viewControllers.filter { controller in + if controller is PeerInfoScreen { + return false + } + return true + } + navigationController.setViewControllers(viewControllers, animated: false) + } + })) + } } case .discussion: if let cachedData = self.data?.cachedData as? CachedChannelData, case let .known(maybeLinkedDiscussionPeerId) = cachedData.linkedDiscussionPeerId, let linkedDiscussionPeerId = maybeLinkedDiscussionPeerId { diff --git a/submodules/TelegramUI/Components/PeerInfo/PostSuggestionsSettingsScreen/Sources/PostSuggestionsSettingsScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PostSuggestionsSettingsScreen/Sources/PostSuggestionsSettingsScreen.swift index 12568056f3..947cb43f32 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PostSuggestionsSettingsScreen/Sources/PostSuggestionsSettingsScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PostSuggestionsSettingsScreen/Sources/PostSuggestionsSettingsScreen.swift @@ -29,7 +29,7 @@ final class PostSuggestionsSettingsScreenComponent: Component { let context: AccountContext let usdWithdrawRate: Int64 - let paidMessageCommissionPermille: Int + let channelMessageSuggestionCommissionPermille: Int let peer: EnginePeer? let initialPrice: StarsAmount? let completion: () -> Void @@ -37,14 +37,14 @@ final class PostSuggestionsSettingsScreenComponent: Component { init( context: AccountContext, usdWithdrawRate: Int64, - paidMessageCommissionPermille: Int, + channelMessageSuggestionCommissionPermille: Int, peer: EnginePeer?, initialPrice: StarsAmount?, completion: @escaping () -> Void ) { self.context = context self.usdWithdrawRate = usdWithdrawRate - self.paidMessageCommissionPermille = paidMessageCommissionPermille + self.channelMessageSuggestionCommissionPermille = channelMessageSuggestionCommissionPermille self.peer = peer self.initialPrice = initialPrice self.completion = completion @@ -373,7 +373,7 @@ final class PostSuggestionsSettingsScreenComponent: Component { } let currentAmount: StarsAmount = StarsAmount(value: Int64(self.starCount), nanos: 0) - let starsScreen = component.context.sharedContext.makeStarsWithdrawalScreen(context: component.context, subject: .enterAmount(current: currentAmount, minValue: StarsAmount(value: 0, nanos: 0), fractionAfterCommission: component.paidMessageCommissionPermille / 10, kind: .postSuggestion, completion: { [weak self] amount in + let starsScreen = component.context.sharedContext.makeStarsWithdrawalScreen(context: component.context, subject: .enterAmount(current: currentAmount, minValue: StarsAmount(value: 0, nanos: 0), fractionAfterCommission: component.channelMessageSuggestionCommissionPermille / 10, kind: .postSuggestion, completion: { [weak self] amount in guard let self else { return } @@ -404,7 +404,7 @@ final class PostSuggestionsSettingsScreenComponent: Component { )), footer: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString( - string: environment.strings.ChannelMessages_PriceSectionFooterValue("\(component.paidMessageCommissionPermille / 10)").string, + string: environment.strings.ChannelMessages_PriceSectionFooterValue("\(component.channelMessageSuggestionCommissionPermille / 10)").string, font: Font.regular(13.0), textColor: self.starCount == 0 ? .clear : environment.theme.list.freeTextColor )), @@ -503,7 +503,7 @@ public final class PostSuggestionsSettingsScreen: ViewControllerComponentContain super.init(context: context, component: PostSuggestionsSettingsScreenComponent( context: context, usdWithdrawRate: configuration.usdWithdrawRate, - paidMessageCommissionPermille: Int(configuration.paidMessageCommissionPermille), + channelMessageSuggestionCommissionPermille: Int(configuration.channelMessageSuggestionCommissionPermille), peer: peer, initialPrice: initialPrice, completion: completion diff --git a/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsWithdrawalScreen.swift b/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsWithdrawalScreen.swift index 540792b660..b0428d9f05 100644 --- a/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsWithdrawalScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsWithdrawalScreen.swift @@ -24,6 +24,7 @@ import UndoUI import ListActionItemComponent import ChatScheduleTimeController import TabSelectorComponent +import PresentationDataUtils private let amountTag = GenericComponentViewTag() @@ -83,8 +84,15 @@ private final class SheetContent: CombinedComponent { let constrainedTitleWidth = context.availableSize.width - 16.0 * 2.0 if case let .suggestedPost(mode, _, _, _) = component.mode { + var displayBalance = false switch mode { - case .sender: + case let .sender(_, isFromAdmin): + displayBalance = !isFromAdmin + case .admin: + break + } + + if displayBalance { let balance = balance.update( component: BalanceComponent( context: component.context, @@ -102,8 +110,6 @@ private final class SheetContent: CombinedComponent { .anchorPoint(CGPoint(x: 1.0, y: 0.0)) .position(CGPoint(x: balanceFrame.maxX, y: balanceFrame.minY)) ) - case .admin: - break } let closeButton = closeButton.update( @@ -214,14 +220,14 @@ private final class SheetContent: CombinedComponent { switch state.currency { case .stars: amountTitle = "ENTER A PRICE IN STARS" + maxAmount = StarsAmount(value: resaleConfiguration.channelMessageSuggestionMaxStarsAmount, nanos: 0) case .ton: amountTitle = "ENTER A PRICE IN TON" + maxAmount = StarsAmount(value: resaleConfiguration.channelMessageSuggestionMaxTonAmount, nanos: 0) } amountPlaceholder = "Price" minAmount = StarsAmount(value: 0, nanos: 0) - //TODO:release - maxAmount = StarsAmount(value: resaleConfiguration.paidMessageMaxAmount, nanos: 0) } let title = title.update( @@ -289,69 +295,90 @@ private final class SheetContent: CombinedComponent { ) } + var tonBalanceValue: StarsAmount = .zero + if let tonBalance = state.tonBalance { + tonBalanceValue = tonBalance + } + if case let .suggestedPost(mode, _, _, _) = component.mode { - //TODO:localize - let selectedId: AnyHashable = state.currency == .stars ? AnyHashable(0 as Int) : AnyHashable(1 as Int) - let starsTitle: String - let tonTitle: String + var displayCurrencySelector = false switch mode { - case .sender: - starsTitle = "Offer Stars" - tonTitle = "Offer TON" + case let .sender(_, isFromAdmin): + if isFromAdmin { + displayCurrencySelector = true + } else { + if state.currency == .ton || tonBalanceValue > StarsAmount.zero { + displayCurrencySelector = true + } + } case .admin: - starsTitle = "Request Stars" - tonTitle = "Request TON" + displayCurrencySelector = true } - let currencyToggle = currencyToggle.update( - component: TabSelectorComponent( - colors: TabSelectorComponent.Colors( - foreground: theme.list.itemSecondaryTextColor, - selection: theme.list.itemSecondaryTextColor.withMultipliedAlpha(0.15), - simple: true - ), - customLayout: TabSelectorComponent.CustomLayout( - font: Font.medium(14.0), - spacing: 10.0 - ), - items: [ - TabSelectorComponent.Item( - id: AnyHashable(0), - content: .component(AnyComponent(CurrencyTabItemComponent(icon: .stars, title: starsTitle, theme: theme))) + if displayCurrencySelector { + //TODO:localize + let selectedId: AnyHashable = state.currency == .stars ? AnyHashable(0 as Int) : AnyHashable(1 as Int) + let starsTitle: String + let tonTitle: String + switch mode { + case .sender: + starsTitle = "Offer Stars" + tonTitle = "Offer TON" + case .admin: + starsTitle = "Request Stars" + tonTitle = "Request TON" + } + + let currencyToggle = currencyToggle.update( + component: TabSelectorComponent( + colors: TabSelectorComponent.Colors( + foreground: theme.list.itemSecondaryTextColor, + selection: theme.list.itemSecondaryTextColor.withMultipliedAlpha(0.15), + simple: true ), - TabSelectorComponent.Item( - id: AnyHashable(1), - content: .component(AnyComponent(CurrencyTabItemComponent(icon: .ton, title: tonTitle, theme: theme))) - ) - ], - selectedId: selectedId, - setSelectedId: { [weak state] id in - guard let state else { - return + customLayout: TabSelectorComponent.CustomLayout( + font: Font.medium(14.0), + spacing: 10.0 + ), + items: [ + TabSelectorComponent.Item( + id: AnyHashable(0), + content: .component(AnyComponent(CurrencyTabItemComponent(icon: .stars, title: starsTitle, theme: theme))) + ), + TabSelectorComponent.Item( + id: AnyHashable(1), + content: .component(AnyComponent(CurrencyTabItemComponent(icon: .ton, title: tonTitle, theme: theme))) + ) + ], + selectedId: selectedId, + setSelectedId: { [weak state] id in + guard let state else { + return + } + + let currency: CurrencyAmount.Currency + if id == AnyHashable(0) { + currency = .stars + } else { + currency = .ton + } + if state.currency != currency { + state.currency = currency + state.amount = nil + } + state.updated(transition: .spring(duration: 0.4)) } - - let currency: CurrencyAmount.Currency - if id == AnyHashable(0) { - currency = .stars - } else { - currency = .ton - } - if state.currency != currency { - state.currency = currency - state.amount = nil - } - state.updated(transition: .spring(duration: 0.4)) - } - ), - availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: 100.0), - transition: context.transition - ) - contentSize.height -= 17.0 - let currencyToggleFrame = CGRect(origin: CGPoint(x: floor((context.availableSize.width - currencyToggle.size.width) * 0.5), y: contentSize.height), size: currencyToggle.size) - context.add(currencyToggle - .position(currencyToggle.size.centered(in: currencyToggleFrame).center)) - - contentSize.height += currencyToggle.size.height + 29.0 + ), + availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: 100.0), + transition: context.transition + ) + contentSize.height -= 17.0 + let currencyToggleFrame = CGRect(origin: CGPoint(x: floor((context.availableSize.width - currencyToggle.size.width) * 0.5), y: contentSize.height), size: currencyToggle.size) + context.add(currencyToggle + .position(currencyToggle.size.centered(in: currencyToggleFrame).center)) + + contentSize.height += currencyToggle.size.height + 29.0 + } } let amountFont = Font.regular(13.0) @@ -432,14 +459,23 @@ private final class SheetContent: CombinedComponent { )) case let .suggestedPost(mode, _, _, _): switch mode { - case let .sender(channel): + case let .sender(channel, isFromAdmin): //TODO:localize let string: String - switch state.currency { - case .stars: - string = "Choose how many Stars you want to offer \(channel.compactDisplayTitle) to publish this message." - case .ton: - string = "Choose how many TON you want to offer \(channel.compactDisplayTitle) to publish this message." + if isFromAdmin { + switch state.currency { + case .stars: + string = "Choose how many Stars you charge for the message." + case .ton: + string = "Choose how many TON you charge for the message." + } + } else { + switch state.currency { + case .stars: + string = "Choose how many Stars you want to offer \(channel.compactDisplayTitle) to publish this message." + case .ton: + string = "Choose how many TON you want to offer \(channel.compactDisplayTitle) to publish this message." + } } let amountInfoString = NSAttributedString(attributedString: parseMarkdownIntoAttributedString(string, attributes: amountMarkdownAttributes, textAlignment: .natural)) amountFooter = AnyComponent(MultilineTextComponent( @@ -487,7 +523,7 @@ private final class SheetContent: CombinedComponent { accentColor: theme.list.itemAccentColor, value: state.amount?.value, minValue: minAmount?.value, - maxValue: state.currency == .ton ? nil : maxAmount?.value, + maxValue: maxAmount?.value, placeholderText: amountPlaceholder, labelText: amountLabel, currency: state.currency, @@ -595,7 +631,9 @@ private final class SheetContent: CombinedComponent { let component = state.component let theme = environment.theme - let controller = ChatScheduleTimeController(context: state.context, updatedPresentationData: (state.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: theme), state.context.sharedContext.presentationData |> map { $0.withUpdated(theme: theme) }), mode: .suggestPost(needsTime: false), style: .default, currentTime: state.timestamp, minimalTime: nil, dismissByTapOutside: true, completion: { [weak state] time in + + let minimalTime: Int32 = Int32(Date().timeIntervalSince1970) + 5 * 60 + 10 + let controller = ChatScheduleTimeController(context: state.context, updatedPresentationData: (state.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: theme), state.context.sharedContext.presentationData |> map { $0.withUpdated(theme: theme) }), mode: .suggestPost(needsTime: false), style: .default, currentTime: state.timestamp, minimalTime: minimalTime, dismissByTapOutside: true, completion: { [weak state] time in guard let state else { return } @@ -637,7 +675,7 @@ private final class SheetContent: CombinedComponent { //TODO:localize switch mode { case .sender: - if let amount = state.amount { + if let amount = state.amount, amount != .zero { let currencySymbol: String let currencyAmount: String switch state.currency { @@ -729,6 +767,36 @@ private final class SheetContent: CombinedComponent { case let .paidMessages(_, _, _, _, completion): completion(amount.value) case let .suggestedPost(_, _, _, completion): + switch state.currency { + case .stars: + if let balance = state.starsBalance, amount > balance { + guard let starsContext = state.context.starsContext else { + return + } + let _ = (state.context.engine.payments.starsTopUpOptions() + |> take(1) + |> deliverOnMainQueue).startStandalone(next: { [weak controller, weak state] options in + guard let controller, let state else { + return + } + let purchaseController = state.context.sharedContext.makeStarsPurchaseScreen(context: state.context, starsContext: starsContext, options: options, purpose: .generic, completion: { _ in + }) + controller.push(purchaseController) + }) + + return + } + case .ton: + if let balance = state.tonBalance, amount > balance { + //TODO:localize + let presentationData = state.context.sharedContext.currentPresentationData.with { $0 } + controller.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: "Not enough TON", actions: [ + TextAlertAction(type: .defaultAction, title: strings.Common_OK, action: {}) + ]), in: .window(.root)) + return + } + } + completion(CurrencyAmount(amount: amount, currency: state.currency), state.timestamp) } @@ -971,7 +1039,7 @@ private final class StarsWithdrawSheetComponent: CombinedComponent { public final class StarsWithdrawScreen: ViewControllerComponentContainer { public enum Mode { public enum SuggestedPostMode { - case sender(channel: EnginePeer) + case sender(channel: EnginePeer, isFromAdmin: Bool) case admin } @@ -1057,322 +1125,6 @@ public final class StarsWithdrawScreen: ViewControllerComponentContainer { } } -private let invalidAmountCharacters = CharacterSet.decimalDigits.inverted - -private final class AmountFieldTonFormatter: NSObject, UITextFieldDelegate { - private struct Representation { - private let format: CurrencyFormat - private var caretIndex: Int = 0 - private var wholePart: [Int] = [] - private var decimalPart: [Int] = [] - - init(string: String, format: CurrencyFormat) { - self.format = format - - var isDecimalPart = false - for c in string { - if c.isNumber { - if let value = Int(String(c)) { - if isDecimalPart { - self.decimalPart.append(value) - } else { - self.wholePart.append(value) - } - } - } else if String(c) == format.decimalSeparator { - isDecimalPart = true - } - } - - while self.wholePart.count > 1 { - if self.wholePart[0] != 0 { - break - } else { - self.wholePart.removeFirst() - } - } - if self.wholePart.isEmpty { - self.wholePart = [0] - } - - while self.decimalPart.count > 1 { - if self.decimalPart[self.decimalPart.count - 1] != 0 { - break - } else { - self.decimalPart.removeLast() - } - } - while self.decimalPart.count < format.decimalDigits { - self.decimalPart.append(0) - } - - self.caretIndex = self.wholePart.count - } - - var minCaretIndex: Int { - for i in 0 ..< self.wholePart.count { - if self.wholePart[i] != 0 { - return i - } - } - return self.wholePart.count - } - - mutating func moveCaret(offset: Int) { - self.caretIndex = max(self.minCaretIndex, min(self.caretIndex + offset, self.wholePart.count + self.decimalPart.count)) - } - - mutating func normalize() { - while self.wholePart.count > 1 { - if self.wholePart[0] != 0 { - break - } else { - self.wholePart.removeFirst() - self.moveCaret(offset: -1) - } - } - if self.wholePart.isEmpty { - self.wholePart = [0] - } - - while self.decimalPart.count < format.decimalDigits { - self.decimalPart.append(0) - } - while self.decimalPart.count > format.decimalDigits { - self.decimalPart.removeLast() - } - - self.caretIndex = max(self.minCaretIndex, min(self.caretIndex, self.wholePart.count + self.decimalPart.count)) - } - - mutating func backspace() { - if self.caretIndex > self.wholePart.count { - let decimalIndex = self.caretIndex - self.wholePart.count - if decimalIndex > 0 { - self.decimalPart.remove(at: decimalIndex - 1) - - self.moveCaret(offset: -1) - self.normalize() - } - } else { - if self.caretIndex > 0 { - self.wholePart.remove(at: self.caretIndex - 1) - - self.moveCaret(offset: -1) - self.normalize() - } - } - } - - mutating func insert(letter: String) { - if letter == "." || letter == "," { - if self.caretIndex == self.wholePart.count { - return - } else if self.caretIndex < self.wholePart.count { - for i in (self.caretIndex ..< self.wholePart.count).reversed() { - self.decimalPart.insert(self.wholePart[i], at: 0) - self.wholePart.remove(at: i) - } - } - - self.normalize() - } else if letter.count == 1 && letter[letter.startIndex].isNumber { - if let value = Int(letter) { - if self.caretIndex <= self.wholePart.count { - self.wholePart.insert(value, at: self.caretIndex) - } else { - let decimalIndex = self.caretIndex - self.wholePart.count - self.decimalPart.insert(value, at: decimalIndex) - } - self.moveCaret(offset: 1) - self.normalize() - } - } - } - - var string: String { - var result = "" - - for digit in self.wholePart { - result.append("\(digit)") - } - result.append(self.format.decimalSeparator) - for digit in self.decimalPart { - result.append("\(digit)") - } - - return result - } - - var stringCaretIndex: Int { - var logicalIndex = 0 - var resolvedIndex = 0 - - if logicalIndex == self.caretIndex { - return resolvedIndex - } - - for _ in self.wholePart { - logicalIndex += 1 - resolvedIndex += 1 - - if logicalIndex == self.caretIndex { - return resolvedIndex - } - } - - resolvedIndex += 1 - - for _ in self.decimalPart { - logicalIndex += 1 - resolvedIndex += 1 - - if logicalIndex == self.caretIndex { - return resolvedIndex - } - } - - return resolvedIndex - } - - var numericalValue: Int64 { - var result: Int64 = 0 - - for digit in self.wholePart { - result *= 10 - result += Int64(digit) - } - for digit in self.decimalPart { - result *= 10 - result += Int64(digit) - } - - return result - } - } - - private let format: CurrencyFormat - private let currency: String - private let maxNumericalValue: Int64 - private let updated: (Int64) -> Void - private let isEmptyUpdated: (Bool) -> Void - private let focusUpdated: (Bool) -> Void - - private var representation: Representation - - private var previousResolvedCaretIndex: Int = 0 - private var ignoreTextSelection: Bool = false - private var enableTextSelectionProcessing: Bool = false - - init?(textField: UITextField, currency: String, maxNumericalValue: Int64, initialValue: String, updated: @escaping (Int64) -> Void, isEmptyUpdated: @escaping (Bool) -> Void, focusUpdated: @escaping (Bool) -> Void) { - guard let format = CurrencyFormat(currency: currency) else { - return nil - } - self.format = format - self.currency = currency - self.maxNumericalValue = maxNumericalValue - self.updated = updated - self.isEmptyUpdated = isEmptyUpdated - self.focusUpdated = focusUpdated - - self.representation = Representation(string: initialValue, format: format) - - super.init() - - textField.text = self.representation.string - self.previousResolvedCaretIndex = self.representation.stringCaretIndex - - self.isEmptyUpdated(false) - } - - func reset(textField: UITextField, initialValue: String) { - self.representation = Representation(string: initialValue, format: self.format) - self.resetFromRepresentation(textField: textField, notifyUpdated: false) - } - - private func resetFromRepresentation(textField: UITextField, notifyUpdated: Bool) { - self.ignoreTextSelection = true - - if self.representation.numericalValue > self.maxNumericalValue { - self.representation = Representation(string: formatCurrencyAmountCustom(self.maxNumericalValue, currency: self.currency).0, format: self.format) - } - - textField.text = self.representation.string - self.previousResolvedCaretIndex = self.representation.stringCaretIndex - - if self.enableTextSelectionProcessing { - let stringCaretIndex = self.representation.stringCaretIndex - if let caretPosition = textField.position(from: textField.beginningOfDocument, offset: stringCaretIndex) { - textField.selectedTextRange = textField.textRange(from: caretPosition, to: caretPosition) - } - } - self.ignoreTextSelection = false - - if notifyUpdated { - self.updated(self.representation.numericalValue) - } - } - - @objc public func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { - if string.count == 1 { - self.representation.insert(letter: string) - self.resetFromRepresentation(textField: textField, notifyUpdated: true) - } else if string.count == 0 { - self.representation.backspace() - self.resetFromRepresentation(textField: textField, notifyUpdated: true) - } - - return false - } - - @objc public func textFieldShouldReturn(_ textField: UITextField) -> Bool { - return false - } - - @objc public func textFieldDidBeginEditing(_ textField: UITextField) { - self.enableTextSelectionProcessing = true - self.focusUpdated(true) - - let stringCaretIndex = self.representation.stringCaretIndex - self.previousResolvedCaretIndex = stringCaretIndex - if let caretPosition = textField.position(from: textField.beginningOfDocument, offset: stringCaretIndex) { - self.ignoreTextSelection = true - textField.selectedTextRange = textField.textRange(from: caretPosition, to: caretPosition) - DispatchQueue.main.async { - textField.selectedTextRange = textField.textRange(from: caretPosition, to: caretPosition) - self.ignoreTextSelection = false - } - } - } - - @objc public func textFieldDidChangeSelection(_ textField: UITextField) { - if self.ignoreTextSelection { - return - } - if !self.enableTextSelectionProcessing { - return - } - - if let selectedTextRange = textField.selectedTextRange { - let index = textField.offset(from: textField.beginningOfDocument, to: selectedTextRange.end) - if self.previousResolvedCaretIndex != index { - self.representation.moveCaret(offset: self.previousResolvedCaretIndex < index ? 1 : -1) - - let stringCaretIndex = self.representation.stringCaretIndex - self.previousResolvedCaretIndex = stringCaretIndex - if let caretPosition = textField.position(from: textField.beginningOfDocument, offset: stringCaretIndex) { - textField.selectedTextRange = textField.textRange(from: caretPosition, to: caretPosition) - } - } - } - } - - @objc public func textFieldDidEndEditing(_ textField: UITextField) { - self.enableTextSelectionProcessing = false - self.focusUpdated(false) - } -} - private final class AmountFieldStarsFormatter: NSObject, UITextFieldDelegate { private let currency: CurrencyAmount.Currency private let dateTimeFormat: PresentationDateTimeFormat @@ -1429,9 +1181,14 @@ private final class AmountFieldStarsFormatter: NSObject, UITextFieldDelegate { // Convert and combine if let whole = Int64(wholeSlice), let frac = Int64(fractionStr) { + + let whole = min(whole, Int64.max / scale) + amount = whole * scale + frac } } else if let whole = Int64(text) { // string had no dot at all + let whole = min(whole, Int64.max / scale) + amount = whole * scale } } @@ -1479,8 +1236,21 @@ private final class AmountFieldStarsFormatter: NSObject, UITextFieldDelegate { return false } case .ton: + var fixedText = false + if let index = newText.firstIndex(of: ".") { + let fractionalString = newText[newText.index(after: index)...] + if fractionalString.count > 2 { + newText = String(newText[newText.startIndex ..< newText.index(index, offsetBy: 3)]) + fixedText = true + } + } + if (newText == "0" && !acceptZero) || (newText.count > 1 && newText.hasPrefix("0") && !newText.hasPrefix("0.")) { newText.removeFirst() + fixedText = true + } + + if fixedText { textField.text = newText self.onTextChanged(text: newText) return false @@ -1493,7 +1263,7 @@ private final class AmountFieldStarsFormatter: NSObject, UITextFieldDelegate { case .stars: textField.text = "\(self.maxValue)" case .ton: - textField.text = "\(formatTonAmountText(self.maxValue, dateTimeFormat: self.dateTimeFormat))" + textField.text = "\(formatTonAmountText(self.maxValue, dateTimeFormat: PresentationDateTimeFormat(timeFormat: self.dateTimeFormat.timeFormat, dateFormat: self.dateTimeFormat.dateFormat, dateSeparator: "", dateSuffix: "", requiresFullYear: false, decimalSeparator: ".", groupingSeparator: "")))" } self.onTextChanged(text: self.textField.text ?? "") self.animateError() @@ -1648,13 +1418,13 @@ private final class AmountFieldComponent: Component { self.textField.textColor = component.textColor if self.component?.currency != component.currency { - if let value = component.value { + if let value = component.value, value != .zero { var text = "" switch component.currency { case .stars: text = "\(value)" case .ton: - text = "\(formatTonAmountText(value, dateTimeFormat: component.dateTimeFormat))" + text = "\(formatTonAmountText(value, dateTimeFormat: PresentationDateTimeFormat(timeFormat: component.dateTimeFormat.timeFormat, dateFormat: component.dateTimeFormat.dateFormat, dateSeparator: "", dateSuffix: "", requiresFullYear: false, decimalSeparator: ".", groupingSeparator: "")))" } self.textField.text = text } else { @@ -1705,7 +1475,6 @@ private final class AmountFieldComponent: Component { } self.tonFormatter = nil self.textField.delegate = self.starsFormatter - self.textField.text = "" case .ton: self.textField.keyboardType = .numbersAndPunctuation if self.tonFormatter == nil { @@ -1714,7 +1483,7 @@ private final class AmountFieldComponent: Component { currency: component.currency, dateTimeFormat: component.dateTimeFormat, minValue: component.minValue ?? 0, - maxValue: component.maxValue ?? Int64.max, + maxValue: component.maxValue ?? 10000000, updated: { [weak self] value in guard let self, let component = self.component else { return diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/DeletePaid.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/DeletePaid.imageset/Contents.json new file mode 100644 index 0000000000..ee235a4085 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/DeletePaid.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "trash_24.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/DeletePaid.imageset/trash_24.pdf b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/DeletePaid.imageset/trash_24.pdf new file mode 100644 index 0000000000..d6ed06acc0 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/DeletePaid.imageset/trash_24.pdf differ diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerPaste.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerPaste.swift index 76b71ea164..346f2e6b8f 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerPaste.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerPaste.swift @@ -20,6 +20,11 @@ extension ChatControllerImpl { } |> deliverOnMainQueue).startStandalone(next: { [weak self] settings in if let strongSelf = self, let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer { + var enableMultiselection = true + if strongSelf.presentationInterfaceState.interfaceState.postSuggestionState != nil { + enableMultiselection = false + } + strongSelf.chatDisplayNode.dismissInput() let controller = mediaPasteboardScreen( context: strongSelf.context, @@ -28,7 +33,7 @@ extension ChatControllerImpl { subjects: subjects, presentMediaPicker: { [weak self] subject, saveEditedPhotos, bannedSendPhotos, bannedSendVideos, present in if let strongSelf = self { - strongSelf.presentMediaPicker(subject: subject, saveEditedPhotos: saveEditedPhotos, bannedSendPhotos: bannedSendPhotos, bannedSendVideos: bannedSendVideos, present: present, updateMediaPickerContext: { _ in }, completion: { [weak self] fromGallery, signals, silentPosting, scheduleTime, parameters, getAnimatedTransitionSource, completion in + strongSelf.presentMediaPicker(subject: subject, saveEditedPhotos: saveEditedPhotos, bannedSendPhotos: bannedSendPhotos, bannedSendVideos: bannedSendVideos, enableMultiselection: enableMultiselection, present: present, updateMediaPickerContext: { _ in }, completion: { [weak self] fromGallery, signals, silentPosting, scheduleTime, parameters, getAnimatedTransitionSource, completion in self?.enqueueMediaMessages(fromGallery: fromGallery, signals: signals, silentPosting: silentPosting, scheduleTime: scheduleTime, parameters: parameters, getAnimatedTransitionSource: getAnimatedTransitionSource, completion: completion) }) } diff --git a/submodules/TelegramUI/Sources/Chat/ChatMessageActionOptions.swift b/submodules/TelegramUI/Sources/Chat/ChatMessageActionOptions.swift index 6d9a1c7d34..4db78b786f 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatMessageActionOptions.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatMessageActionOptions.swift @@ -1011,8 +1011,15 @@ extension ChatControllerImpl { }) }) } else { + var isFromAdmin = false + if let channel = self.presentationInterfaceState.renderedPeer?.peer as? TelegramChannel, channel.isMonoForum { + if let linkedMonoforumId = channel.linkedMonoforumId, let mainChannel = self.presentationInterfaceState.renderedPeer?.peers[linkedMonoforumId] as? TelegramChannel, mainChannel.hasPermission(.manageDirect) { + isFromAdmin = true + } + } subject = .postSuggestion( channel: .channel(channel), + isFromAdmin: isFromAdmin, current: postSuggestionState.price ?? CurrencyAmount(amount: .zero, currency: .stars), timestamp: postSuggestionState.timestamp, completion: { [weak self] price, timestamp in diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index d9d294768d..6763da6747 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -967,7 +967,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return false } switch action.action { - case .pinnedMessageUpdated, .gameScore, .setSameChatWallpaper, .giveawayResults, .customText, .todoCompletions, .todoAppendTasks: + case .pinnedMessageUpdated, .gameScore, .setSameChatWallpaper, .giveawayResults, .customText, .todoCompletions, .todoAppendTasks, .suggestedPostRefund, .suggestedPostSuccess, .suggestedPostApprovalStatus: for attribute in message.attributes { if let attribute = attribute as? ReplyMessageAttribute { var todoTaskId: Int32? @@ -1173,6 +1173,19 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let controller = self.context.sharedContext.makeStarsGiftScreen(context: self.context, message: EngineMessage(message)) self.push(controller) return true + case let .giftTon(_, _, _, _, transactionId): + Task { @MainActor [weak self] in + guard let self, let transactionId, let peerId = self.chatLocation.peerId else { + return + } + let transactionData = await self.context.engine.payments.getStarsTransaction(reference: StarsTransactionReference(peerId: self.context.account.peerId, ton: true, id: transactionId, isRefund: false)).get() + let peer = await self.context.engine.data.get( + TelegramEngine.EngineData.Item.Peer.Peer(id: peerId) + ).get() + if let transactionData, let peer { + self.push(self.context.sharedContext.makeStarsTransactionScreen(context: self.context, transaction: transactionData, peer: peer)) + } + } case let .giftCode(slug, _, _, _, _, _, _, _, _, _, _): self.openResolved(result: .premiumGiftCode(slug: slug), sourceMessageId: message.id, progress: params.progress) return true @@ -1244,6 +1257,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G case .boostsApplied: self.controllerInteraction?.openGroupBoostInfo(nil, 0) return true + case .paidMessagesPriceEdited: + self.interfaceInteraction?.openMonoforum() + return true default: break } diff --git a/submodules/TelegramUI/Sources/ChatControllerAdminBanUsers.swift b/submodules/TelegramUI/Sources/ChatControllerAdminBanUsers.swift index a7280cfbe2..cf28fe7d9a 100644 --- a/submodules/TelegramUI/Sources/ChatControllerAdminBanUsers.swift +++ b/submodules/TelegramUI/Sources/ChatControllerAdminBanUsers.swift @@ -389,6 +389,47 @@ extension ChatControllerImpl { return } + if let message = messages.values.compactMap({ $0 }).first(where: { message in message.attributes.contains(where: { $0 is PublishedSuggestedPostMessageAttribute }) }), let attribute = message.attributes.first(where: { $0 is PublishedSuggestedPostMessageAttribute }) as? PublishedSuggestedPostMessageAttribute { + let commit = { [weak self] in + guard let self else { + return + } + //TODO:localize + let titleString: String + let textString: String + switch attribute.currency { + case .stars: + titleString = "Stars Will Be Lost" + textString = "You won't receive **Stars** for this post if you delete it now. The post must remain visible for at least **24 hours** after publication." + case .ton: + titleString = "TON Will Be Lost" + textString = "You won't receive **TON** for this post if you delete it now. The post must remain visible for at least **24 hours** after publication." + } + self.present(standardTextAlertController( + theme: AlertControllerTheme(presentationData: self.presentationData), + title: titleString, + text: textString, + actions: [ + TextAlertAction(type: .destructiveAction, title: "Delete Anyway", action: { [weak self] in + guard let self else { + return + } + self.beginDeleteMessagesWithUndo(messageIds: messageIds, type: .forEveryone) + }), + TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_Cancel, action: {}) + ], + actionLayout: .vertical, + parseMarkdown: true + ), in: .window(.root)) + } + if let contextController { + contextController.dismiss(completion: commit) + } else { + commit() + } + return + } + let actionSheet = ActionSheetController(presentationData: self.presentationData) var items: [ActionSheetItem] = [] var personalPeerName: String? diff --git a/submodules/TelegramUI/Sources/ChatControllerContentData.swift b/submodules/TelegramUI/Sources/ChatControllerContentData.swift index c7c8fe80d9..ebf5cdc9d3 100644 --- a/submodules/TelegramUI/Sources/ChatControllerContentData.swift +++ b/submodules/TelegramUI/Sources/ChatControllerContentData.swift @@ -548,12 +548,20 @@ extension ChatControllerImpl { strongSelf.state.chatTitleContent = .custom(strings.Chat_TitlePinnedMessages(Int32(displayedCount ?? 1)), nil, false) } else if let channel = peer as? TelegramChannel, channel.isMonoForum { if let linkedMonoforumId = channel.linkedMonoforumId, let mainPeer = peerView.peers[linkedMonoforumId] { - strongSelf.state.chatTitleContent = .custom(mainPeer.debugDisplayTitle, strings.Chat_Monoforum_Subtitle, true) + strongSelf.state.chatTitleContent = .peer(peerView: ChatTitleContent.PeerData( + peerId: mainPeer.id, + peer: mainPeer, + isContact: false, + isSavedMessages: false, + notificationSettings: nil, + peerPresences: [:], + cachedData: nil + ), customTitle: nil, customSubtitle: strings.Chat_Monoforum_Subtitle, onlineMemberCount: (nil, nil), isScheduledMessages: false, isMuted: nil, customMessageCount: nil, isEnabled: true) } else { strongSelf.state.chatTitleContent = .custom(channel.debugDisplayTitle, nil, true) } } else { - strongSelf.state.chatTitleContent = .peer(peerView: ChatTitleContent.PeerData(peerView: peerView), customTitle: nil, onlineMemberCount: onlineMemberCount, isScheduledMessages: isScheduledMessages, isMuted: nil, customMessageCount: nil, isEnabled: hasPeerInfo) + strongSelf.state.chatTitleContent = .peer(peerView: ChatTitleContent.PeerData(peerView: peerView), customTitle: nil, customSubtitle: nil, onlineMemberCount: onlineMemberCount, isScheduledMessages: isScheduledMessages, isMuted: nil, customMessageCount: nil, isEnabled: hasPeerInfo) let imageOverride: AvatarNodeImageOverride? if context.account.peerId == peer.id { @@ -1447,7 +1455,7 @@ extension ChatControllerImpl { customMessageCount = savedMessagesPeer?.messageCount ?? 0 } - strongSelf.state.chatTitleContent = .peer(peerView: mappedPeerData, customTitle: nil, onlineMemberCount: (nil, nil), isScheduledMessages: false, isMuted: false, customMessageCount: customMessageCount, isEnabled: true) + strongSelf.state.chatTitleContent = .peer(peerView: mappedPeerData, customTitle: nil, customSubtitle: nil, onlineMemberCount: (nil, nil), isScheduledMessages: false, isMuted: false, customMessageCount: customMessageCount, isEnabled: true) strongSelf.state.peerView = peerView @@ -1534,7 +1542,7 @@ extension ChatControllerImpl { } if let threadInfo = messageAndTopic.threadData?.info { - strongSelf.state.chatTitleContent = .peer(peerView: ChatTitleContent.PeerData(peerView: peerView), customTitle: threadInfo.title, onlineMemberCount: onlineMemberCount, isScheduledMessages: false, isMuted: peerIsMuted, customMessageCount: messageAndTopic.messageCount == 0 ? nil : messageAndTopic.messageCount, isEnabled: true) + strongSelf.state.chatTitleContent = .peer(peerView: ChatTitleContent.PeerData(peerView: peerView), customTitle: threadInfo.title, customSubtitle: nil, onlineMemberCount: onlineMemberCount, isScheduledMessages: false, isMuted: peerIsMuted, customMessageCount: messageAndTopic.messageCount == 0 ? nil : messageAndTopic.messageCount, isEnabled: true) let avatarContent: EmojiStatusComponent.Content if chatLocation.threadId == 1 { diff --git a/submodules/TelegramUI/Sources/ChatControllerNode.swift b/submodules/TelegramUI/Sources/ChatControllerNode.swift index 4287a59f0e..62d5b14286 100644 --- a/submodules/TelegramUI/Sources/ChatControllerNode.swift +++ b/submodules/TelegramUI/Sources/ChatControllerNode.swift @@ -2477,11 +2477,11 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { ChatSidePanelEnvironment(insets: UIEdgeInsets( top: 0.0, left: leftPanelLeftInset, - bottom: 0.0, + bottom: containerInsets.bottom + inputPanelsHeight, right: 0.0 )) }, - containerSize: CGSize(width: leftPanelSize.width, height: leftPanelSize.height - sidePanelTopInset - (containerInsets.bottom + inputPanelsHeight)) + containerSize: CGSize(width: leftPanelSize.width, height: leftPanelSize.height - sidePanelTopInset) ) let leftPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: sidePanelTopInset), size: leftPanelSize) @@ -4472,6 +4472,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { } var effectivePresentationInterfaceState = self.chatPresentationInterfaceState + if let textInputPanelNode = self.textInputPanelNode { effectivePresentationInterfaceState = effectivePresentationInterfaceState.updatedInterfaceState { $0.withUpdatedEffectiveInputState(textInputPanelNode.inputTextState) } } diff --git a/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift b/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift index d001fdf81a..7b2614ba8f 100644 --- a/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift +++ b/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift @@ -61,6 +61,11 @@ extension ChatControllerImpl { var bannedSendVideos: (Int32, Bool)? var bannedSendFiles: (Int32, Bool)? + var enableMultiselection = true + if self.presentationInterfaceState.interfaceState.postSuggestionState != nil { + enableMultiselection = false + } + var canSendPolls = true var canSendTodos = true if let peer = self.presentationInterfaceState.renderedPeer?.peer { @@ -336,7 +341,7 @@ extension ChatControllerImpl { controller.prepareForReuse() return } - strongSelf.presentMediaPicker(saveEditedPhotos: dataSettings.storeEditedPhotos, bannedSendPhotos: bannedSendPhotos, bannedSendVideos: bannedSendVideos, present: { controller, mediaPickerContext in + strongSelf.presentMediaPicker(saveEditedPhotos: dataSettings.storeEditedPhotos, bannedSendPhotos: bannedSendPhotos, bannedSendVideos: bannedSendVideos, enableMultiselection: enableMultiselection, present: { controller, mediaPickerContext in let _ = currentMediaController.swap(controller) if !inputText.string.isEmpty { mediaPickerContext?.setCaption(inputText) @@ -1230,7 +1235,7 @@ extension ChatControllerImpl { self.present(actionSheet, in: .window(.root)) } - func presentMediaPicker(subject: MediaPickerScreenImpl.Subject = .assets(nil, .default), saveEditedPhotos: Bool, bannedSendPhotos: (Int32, Bool)?, bannedSendVideos: (Int32, Bool)?, present: @escaping (MediaPickerScreenImpl, AttachmentMediaPickerContext?) -> Void, updateMediaPickerContext: @escaping (AttachmentMediaPickerContext?) -> Void, completion: @escaping (Bool, [Any], Bool, Int32?, ChatSendMessageActionSheetController.SendParameters?, @escaping (String) -> UIView?, @escaping () -> Void) -> Void) { + func presentMediaPicker(subject: MediaPickerScreenImpl.Subject = .assets(nil, .default), saveEditedPhotos: Bool, bannedSendPhotos: (Int32, Bool)?, bannedSendVideos: (Int32, Bool)?, enableMultiselection: Bool, present: @escaping (MediaPickerScreenImpl, AttachmentMediaPickerContext?) -> Void, updateMediaPickerContext: @escaping (AttachmentMediaPickerContext?) -> Void, completion: @escaping (Bool, [Any], Bool, Int32?, ChatSendMessageActionSheetController.SendParameters?, @escaping (String) -> UIView?, @escaping () -> Void) -> Void) { var isScheduledMessages = false if case .scheduledMessages = self.presentationInterfaceState.subject { isScheduledMessages = true @@ -1248,6 +1253,7 @@ extension ChatControllerImpl { isScheduledMessages: isScheduledMessages, bannedSendPhotos: bannedSendPhotos, bannedSendVideos: bannedSendVideos, + enableMultiselection: enableMultiselection, canBoostToUnrestrict: (self.presentationInterfaceState.boostsToUnrestrict ?? 0) > 0 && bannedSendPhotos?.1 != true && bannedSendVideos?.1 != true, paidMediaAllowed: paidMediaAllowed, subject: subject, diff --git a/submodules/TelegramUI/Sources/ChatInterfaceInputContexts.swift b/submodules/TelegramUI/Sources/ChatInterfaceInputContexts.swift index 0569f1ebc1..9567217139 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceInputContexts.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceInputContexts.swift @@ -233,7 +233,7 @@ func inputTextPanelStateForChatPresentationInterfaceState(_ chatPresentationInte } } - if let channel = chatPresentationInterfaceState.renderedPeer?.peer as? TelegramChannel, channel.isMonoForum, let mainChannel = chatPresentationInterfaceState.renderedPeer?.chatOrMonoforumMainPeer as? TelegramChannel, !mainChannel.hasPermission(.manageDirect) { + if let channel = chatPresentationInterfaceState.renderedPeer?.peer as? TelegramChannel, channel.isMonoForum, let mainChannel = chatPresentationInterfaceState.renderedPeer?.chatOrMonoforumMainPeer as? TelegramChannel, (!mainChannel.hasPermission(.manageDirect) || chatPresentationInterfaceState.chatLocation.threadId != nil) { if chatPresentationInterfaceState.interfaceState.postSuggestionState == nil { accessoryItems.append(.suggestPost) } diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift index 03277f6fa0..5423350bac 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift @@ -1924,8 +1924,13 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState } }), false)) } else if !isUnremovableAction { + var iconName: String = isSending ? "Chat/Context Menu/Clear" : "Chat/Context Menu/Delete" + if message.attributes.contains(where: { $0 is PublishedSuggestedPostMessageAttribute }) { + iconName = "Chat/Context Menu/DeletePaid" + } + actions.append(.action(ContextMenuActionItem(text: title, textColor: .destructive, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: isSending ? "Chat/Context Menu/Clear" : "Chat/Context Menu/Delete"), color: theme.actionSheet.destructiveActionTextColor) + return generateTintedImage(image: UIImage(bundleImageName: iconName), color: theme.actionSheet.destructiveActionTextColor) }, action: { controller, f in if isEditing { context.account.pendingUpdateMessageManager.cancel(messageId: message.id) diff --git a/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift b/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift index d2af18fb7d..a336b54b98 100644 --- a/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift @@ -1523,6 +1523,13 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch if case let .media(value) = editMessageState.content { isEditingMedia = !value.isEmpty isMediaEnabled = !value.isEmpty + + if interfaceState.interfaceState.postSuggestionState != nil { + if value.contains(.file) { + isEditingMedia = false + isMediaEnabled = false + } + } } else { isMediaEnabled = true } diff --git a/submodules/TelegramUI/Sources/NavigateToChatController.swift b/submodules/TelegramUI/Sources/NavigateToChatController.swift index 478a10afa0..87cb864f00 100644 --- a/submodules/TelegramUI/Sources/NavigateToChatController.swift +++ b/submodules/TelegramUI/Sources/NavigateToChatController.swift @@ -149,6 +149,13 @@ public func navigateToChatControllerImpl(_ params: NavigateToChatControllerParam canMatchThread = true switchToThread = true } + if case .replyThread = params.chatLocation { + if case let .replyThread(replyThread) = params.chatLocation, (replyThread.isForumPost || replyThread.isMonoforumPost) { + } else { + canMatchThread = false + switchToThread = false + } + } if controller.chatLocation.peerId == params.chatLocation.asChatLocation.peerId && canMatchThread && (controller.subject != .scheduledMessages || controller.subject == params.subject) { if let updateTextInputState = params.updateTextInputState { @@ -191,7 +198,10 @@ public func navigateToChatControllerImpl(_ params: NavigateToChatControllerParam controller.purposefulAction = params.purposefulAction if let activateInput = params.activateInput { - controller.activateInput(type: activateInput) + if case let .replyThread(replyThread) = params.chatLocation, (replyThread.isForumPost || replyThread.isMonoforumPost) { + } else { + controller.activateInput(type: activateInput) + } } if params.changeColors { controller.presentThemeSelection() diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index c39b3ac5d2..91f821e5a3 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -3734,8 +3734,8 @@ public final class SharedAccountContextImpl: SharedAccountContext { mode = .accountWithdraw(completion: completion) case let .enterAmount(current, minValue, fractionAfterCommission, kind, completion): mode = .paidMessages(current: current.value, minValue: minValue.value, fractionAfterCommission: fractionAfterCommission, kind: kind, completion: completion) - case let .postSuggestion(channel, current, timestamp, completion): - mode = .suggestedPost(mode: .sender(channel: channel), price: current, timestamp: timestamp, completion: completion) + case let .postSuggestion(channel, isFromAdmin, current, timestamp, completion): + mode = .suggestedPost(mode: .sender(channel: channel, isFromAdmin: isFromAdmin), price: current, timestamp: timestamp, completion: completion) case let .postSuggestionModification(current, timestamp, completion): mode = .suggestedPost(mode: .admin, price: current, timestamp: timestamp, completion: completion) } diff --git a/third-party/td/TdBinding/Sources/TdBinding.mm b/third-party/td/TdBinding/Sources/TdBinding.mm index d7f5f1f369..edc8270c38 100644 --- a/third-party/td/TdBinding/Sources/TdBinding.mm +++ b/third-party/td/TdBinding/Sources/TdBinding.mm @@ -392,6 +392,26 @@ NSData * _Nullable tdGenerateSelfAddBlock(TdKeyPair *keyPair, int64_t userId, NS std::string mappedPublicKey((uint8_t *)keyPair.publicKey.bytes, ((uint8_t *)keyPair.publicKey.bytes) + keyPair.publicKey.length); std::string mappedPreviousBlock((uint8_t *)previousBlock.bytes, ((uint8_t *)previousBlock.bytes) + previousBlock.length); + + #if DEBUG + auto describeResult = tde2e_api::call_describe_block(mappedPreviousBlock); + if (describeResult.is_ok()) { + NSString *utf8String = [[NSString alloc] initWithBytes:describeResult.value().data() length:describeResult.value().size() encoding:NSUTF8StringEncoding]; + if (utf8String) { + NSLog(@"TdCall.selfAddBlock block: %@", utf8String); + } else { + NSString *lossyString = [[NSString alloc] initWithData:[NSData dataWithBytes:describeResult.value().data() length:describeResult.value().size()] encoding:NSASCIIStringEncoding]; + if (lossyString) { + NSLog(@"TdCall.selfAddBlock block (lossy conversion): %@", lossyString); + } else { + NSLog(@"TdCall.selfAddBlock block: [binary data, length: %lu]", (unsigned long)describeResult.value().size()); + } + } + } else { + NSLog(@"TdCall.selfAddBlock describe block failed"); + } + #endif + auto publicKeyId = tde2e_api::key_from_public_key(mappedPublicKey); if (!publicKeyId.is_ok()) { return nil;