From 42cc67ade7dda41468950133df384c27cfe4c0ee Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Mon, 12 Feb 2024 18:48:48 -0400 Subject: [PATCH] Group stories --- .../Telegram-iOS/en.lproj/Localizable.strings | 3 + .../TelegramEngine/Data/PeersData.swift | 56 +++++++++ .../Data/TelegramEngineData.swift | 113 ++++++++++++++++++ .../Sources/StoryChatContent.swift | 55 ++++++--- .../Sources/StoryContent.swift | 14 ++- .../StoryItemSetContainerComponent.swift | 99 ++++++++++++--- 6 files changed, 305 insertions(+), 35 deletions(-) diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 3799e62f52..90e52d4d2c 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -11304,3 +11304,6 @@ Sorry for the inconvenience."; "Channel.AdminLog.MessageRemovedGroupEmojiPack" = "%@ removed group emoji pack"; "Attachment.BoostToUnlock" = "Boost to Unlock"; + +"Story.GroupCommentingRestrictedPlaceholder" = "Comments restricted"; +"Story.GroupCommentingRestrictedPlaceholderAction" = "Boost to unlock"; diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift b/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift index da715c2d31..08f63d5fab 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift @@ -879,6 +879,62 @@ public extension TelegramEngine.EngineData.Item { } } + public struct BoostsToUnrestrict: TelegramEngineDataItem, TelegramEngineMapKeyDataItem, PostboxViewDataItem { + public typealias Result = Int32? + + fileprivate var id: EnginePeer.Id + public var mapKey: EnginePeer.Id { + return self.id + } + + public init(id: EnginePeer.Id) { + self.id = id + } + + var key: PostboxViewKey { + return .cachedPeerData(peerId: self.id) + } + + func extract(view: PostboxView) -> Result { + guard let view = view as? CachedPeerDataView else { + preconditionFailure() + } + if let cachedData = view.cachedPeerData as? CachedChannelData { + return cachedData.boostsToUnrestrict + } else { + return nil + } + } + } + + public struct AppliedBoosts: TelegramEngineDataItem, TelegramEngineMapKeyDataItem, PostboxViewDataItem { + public typealias Result = Int32? + + fileprivate var id: EnginePeer.Id + public var mapKey: EnginePeer.Id { + return self.id + } + + public init(id: EnginePeer.Id) { + self.id = id + } + + var key: PostboxViewKey { + return .cachedPeerData(peerId: self.id) + } + + func extract(view: PostboxView) -> Result { + guard let view = view as? CachedPeerDataView else { + preconditionFailure() + } + if let cachedData = view.cachedPeerData as? CachedChannelData { + return cachedData.appliedBoosts + } else { + return nil + } + } + } + public struct MessageReadStatsAreHidden: TelegramEngineDataItem, TelegramEngineMapKeyDataItem, PostboxViewDataItem { public typealias Result = Bool? diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Data/TelegramEngineData.swift b/submodules/TelegramCore/Sources/TelegramEngine/Data/TelegramEngineData.swift index de74d882fb..b72ee5fa4b 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Data/TelegramEngineData.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Data/TelegramEngineData.swift @@ -391,6 +391,119 @@ public extension TelegramEngine { } } + public func subscribe< + T0: TelegramEngineDataItem, + T1: TelegramEngineDataItem, + T2: TelegramEngineDataItem, + T3: TelegramEngineDataItem, + T4: TelegramEngineDataItem, + T5: TelegramEngineDataItem, + T6: TelegramEngineDataItem, + T7: TelegramEngineDataItem + >( + _ t0: T0, + _ t1: T1, + _ t2: T2, + _ t3: T3, + _ t4: T4, + _ t5: T5, + _ t6: T6, + _ t7: T7 + ) -> Signal< + ( + T0.Result, + T1.Result, + T2.Result, + T3.Result, + T4.Result, + T5.Result, + T6.Result, + T7.Result + ), + NoError> { + return self._subscribe(items: [ + t0 as! AnyPostboxViewDataItem, + t1 as! AnyPostboxViewDataItem, + t2 as! AnyPostboxViewDataItem, + t3 as! AnyPostboxViewDataItem, + t4 as! AnyPostboxViewDataItem, + t5 as! AnyPostboxViewDataItem, + t6 as! AnyPostboxViewDataItem, + t7 as! AnyPostboxViewDataItem + ]) + |> map { results -> (T0.Result, T1.Result, T2.Result, T3.Result, T4.Result, T5.Result, T6.Result, T7.Result) in + return ( + results[0] as! T0.Result, + results[1] as! T1.Result, + results[2] as! T2.Result, + results[3] as! T3.Result, + results[4] as! T4.Result, + results[5] as! T5.Result, + results[6] as! T6.Result, + results[7] as! T7.Result + ) + } + } + + public func subscribe< + T0: TelegramEngineDataItem, + T1: TelegramEngineDataItem, + T2: TelegramEngineDataItem, + T3: TelegramEngineDataItem, + T4: TelegramEngineDataItem, + T5: TelegramEngineDataItem, + T6: TelegramEngineDataItem, + T7: TelegramEngineDataItem, + T8: TelegramEngineDataItem + >( + _ t0: T0, + _ t1: T1, + _ t2: T2, + _ t3: T3, + _ t4: T4, + _ t5: T5, + _ t6: T6, + _ t7: T7, + _ t8: T8 + ) -> Signal< + ( + T0.Result, + T1.Result, + T2.Result, + T3.Result, + T4.Result, + T5.Result, + T6.Result, + T7.Result, + T8.Result + ), + NoError> { + return self._subscribe(items: [ + t0 as! AnyPostboxViewDataItem, + t1 as! AnyPostboxViewDataItem, + t2 as! AnyPostboxViewDataItem, + t3 as! AnyPostboxViewDataItem, + t4 as! AnyPostboxViewDataItem, + t5 as! AnyPostboxViewDataItem, + t6 as! AnyPostboxViewDataItem, + t7 as! AnyPostboxViewDataItem, + t8 as! AnyPostboxViewDataItem + ]) + |> map { results -> (T0.Result, T1.Result, T2.Result, T3.Result, T4.Result, T5.Result, T6.Result, T7.Result, T8.Result) in + return ( + results[0] as! T0.Result, + results[1] as! T1.Result, + results[2] as! T2.Result, + results[3] as! T3.Result, + results[4] as! T4.Result, + results[5] as! T5.Result, + results[6] as! T6.Result, + results[7] as! T7.Result, + results[8] as! T8.Result + ) + } + } + public func get< T0: TelegramEngineDataItem, T1: TelegramEngineDataItem diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryChatContent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryChatContent.swift index d76bb2a168..1146bf3101 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryChatContent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryChatContent.swift @@ -216,7 +216,9 @@ public final class StoryContentContextImpl: StoryContentContext { presence: peerPresence.flatMap { EnginePeer.Presence($0) }, canViewStats: false, isPremiumRequiredForMessaging: isPremiumRequiredForMessaging, - preferHighQualityStories: preferHighQualityStories + preferHighQualityStories: preferHighQualityStories, + boostsToUnrestrict: nil, + appliedBoosts: nil ) } else if let cachedChannelData = cachedPeerDataView.cachedPeerData as? CachedChannelData { additionalPeerData = StoryContentContextState.AdditionalPeerData( @@ -225,7 +227,9 @@ public final class StoryContentContextImpl: StoryContentContext { presence: peerPresence.flatMap { EnginePeer.Presence($0) }, canViewStats: cachedChannelData.flags.contains(.canViewStats), isPremiumRequiredForMessaging: isPremiumRequiredForMessaging, - preferHighQualityStories: preferHighQualityStories + preferHighQualityStories: preferHighQualityStories, + boostsToUnrestrict: cachedChannelData.boostsToUnrestrict, + appliedBoosts: cachedChannelData.appliedBoosts ) } else { additionalPeerData = StoryContentContextState.AdditionalPeerData( @@ -234,18 +238,21 @@ public final class StoryContentContextImpl: StoryContentContext { presence: peerPresence.flatMap { EnginePeer.Presence($0) }, canViewStats: false, isPremiumRequiredForMessaging: isPremiumRequiredForMessaging, - preferHighQualityStories: preferHighQualityStories + preferHighQualityStories: preferHighQualityStories, + boostsToUnrestrict: nil, + appliedBoosts: nil ) } - } - else { + } else { additionalPeerData = StoryContentContextState.AdditionalPeerData( isMuted: true, areVoiceMessagesAvailable: true, presence: peerPresence.flatMap { EnginePeer.Presence($0) }, canViewStats: false, isPremiumRequiredForMessaging: isPremiumRequiredForMessaging, - preferHighQualityStories: preferHighQualityStories + preferHighQualityStories: preferHighQualityStories, + boostsToUnrestrict: nil, + appliedBoosts: nil ) } let state = stateView.value?.get(Stories.PeerState.self) @@ -1165,7 +1172,9 @@ public final class SingleStoryContentContextImpl: StoryContentContext { TelegramEngine.EngineData.Item.Peer.CanViewStats(id: storyId.peerId), TelegramEngine.EngineData.Item.Peer.NotificationSettings(id: storyId.peerId), TelegramEngine.EngineData.Item.NotificationSettings.Global(), - TelegramEngine.EngineData.Item.Peer.IsPremiumRequiredForMessaging(id: storyId.peerId) + TelegramEngine.EngineData.Item.Peer.IsPremiumRequiredForMessaging(id: storyId.peerId), + TelegramEngine.EngineData.Item.Peer.BoostsToUnrestrict(id: storyId.peerId), + TelegramEngine.EngineData.Item.Peer.AppliedBoosts(id: storyId.peerId) ), item |> mapToSignal { item -> Signal<(Stories.StoredItem?, [PeerId: Peer], [MediaId: TelegramMediaFile], [StoryId: EngineStoryItem?]), NoError> in return context.account.postbox.transaction { transaction -> (Stories.StoredItem?, [PeerId: Peer], [MediaId: TelegramMediaFile], [StoryId: EngineStoryItem?]) in @@ -1236,7 +1245,7 @@ public final class SingleStoryContentContextImpl: StoryContentContext { return } - let (peer, presence, areVoiceMessagesAvailable, canViewStats, notificationSettings, globalNotificationSettings, isPremiumRequiredForMessaging) = data + let (peer, presence, areVoiceMessagesAvailable, canViewStats, notificationSettings, globalNotificationSettings, isPremiumRequiredForMessaging, boostsToUnrestrict, appliedBoosts) = data let (item, peers, allEntityFiles, forwardInfoStories) = itemAndPeers guard let peer else { @@ -1251,7 +1260,9 @@ public final class SingleStoryContentContextImpl: StoryContentContext { presence: presence, canViewStats: canViewStats, isPremiumRequiredForMessaging: isPremiumRequiredForMessaging, - preferHighQualityStories: preferHighQualityStories + preferHighQualityStories: preferHighQualityStories, + boostsToUnrestrict: boostsToUnrestrict, + appliedBoosts: appliedBoosts ) for (storyId, story) in forwardInfoStories { @@ -1447,7 +1458,9 @@ public final class PeerStoryListContentContextImpl: StoryContentContext { TelegramEngine.EngineData.Item.Peer.CanViewStats(id: peerId), TelegramEngine.EngineData.Item.Peer.NotificationSettings(id: peerId), TelegramEngine.EngineData.Item.NotificationSettings.Global(), - TelegramEngine.EngineData.Item.Peer.IsPremiumRequiredForMessaging(id: peerId) + TelegramEngine.EngineData.Item.Peer.IsPremiumRequiredForMessaging(id: peerId), + TelegramEngine.EngineData.Item.Peer.BoostsToUnrestrict(id: peerId), + TelegramEngine.EngineData.Item.Peer.AppliedBoosts(id: peerId) ), listContext.state, self.focusedIdUpdated.get(), @@ -1458,7 +1471,7 @@ public final class PeerStoryListContentContextImpl: StoryContentContext { return } - let (peer, presence, areVoiceMessagesAvailable, canViewStats, notificationSettings, globalNotificationSettings, isPremiumRequiredForMessaging) = data + let (peer, presence, areVoiceMessagesAvailable, canViewStats, notificationSettings, globalNotificationSettings, isPremiumRequiredForMessaging, boostsToUnrestrict, appliedBoosts) = data guard let peer else { return @@ -1472,7 +1485,9 @@ public final class PeerStoryListContentContextImpl: StoryContentContext { presence: presence, canViewStats: canViewStats, isPremiumRequiredForMessaging: isPremiumRequiredForMessaging, - preferHighQualityStories: preferHighQualityStories + preferHighQualityStories: preferHighQualityStories, + boostsToUnrestrict: boostsToUnrestrict, + appliedBoosts: appliedBoosts ) self.listState = state @@ -2389,7 +2404,9 @@ public final class RepostStoriesContentContextImpl: StoryContentContext { presence: peerPresence.flatMap { EnginePeer.Presence($0) }, canViewStats: false, isPremiumRequiredForMessaging: isPremiumRequiredForMessaging, - preferHighQualityStories: preferHighQualityStories + preferHighQualityStories: preferHighQualityStories, + boostsToUnrestrict: nil, + appliedBoosts: nil ) } else if let cachedChannelData = cachedPeerDataView.cachedPeerData as? CachedChannelData { additionalPeerData = StoryContentContextState.AdditionalPeerData( @@ -2398,7 +2415,9 @@ public final class RepostStoriesContentContextImpl: StoryContentContext { presence: peerPresence.flatMap { EnginePeer.Presence($0) }, canViewStats: cachedChannelData.flags.contains(.canViewStats), isPremiumRequiredForMessaging: isPremiumRequiredForMessaging, - preferHighQualityStories: preferHighQualityStories + preferHighQualityStories: preferHighQualityStories, + boostsToUnrestrict: cachedChannelData.boostsToUnrestrict, + appliedBoosts: cachedChannelData.appliedBoosts ) } else { additionalPeerData = StoryContentContextState.AdditionalPeerData( @@ -2407,7 +2426,9 @@ public final class RepostStoriesContentContextImpl: StoryContentContext { presence: peerPresence.flatMap { EnginePeer.Presence($0) }, canViewStats: false, isPremiumRequiredForMessaging: isPremiumRequiredForMessaging, - preferHighQualityStories: preferHighQualityStories + preferHighQualityStories: preferHighQualityStories, + boostsToUnrestrict: nil, + appliedBoosts: nil ) } } @@ -2418,7 +2439,9 @@ public final class RepostStoriesContentContextImpl: StoryContentContext { presence: peerPresence.flatMap { EnginePeer.Presence($0) }, canViewStats: false, isPremiumRequiredForMessaging: isPremiumRequiredForMessaging, - preferHighQualityStories: preferHighQualityStories + preferHighQualityStories: preferHighQualityStories, + boostsToUnrestrict: nil, + appliedBoosts: nil ) } diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContent.swift index 4afd4c85dc..3b4f6bbf43 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContent.swift @@ -147,6 +147,8 @@ public final class StoryContentContextState { public let canViewStats: Bool public let isPremiumRequiredForMessaging: Bool public let preferHighQualityStories: Bool + public let boostsToUnrestrict: Int32? + public let appliedBoosts: Int32? public init( isMuted: Bool, @@ -154,7 +156,9 @@ public final class StoryContentContextState { presence: EnginePeer.Presence?, canViewStats: Bool, isPremiumRequiredForMessaging: Bool, - preferHighQualityStories: Bool + preferHighQualityStories: Bool, + boostsToUnrestrict: Int32?, + appliedBoosts: Int32? ) { self.isMuted = isMuted self.areVoiceMessagesAvailable = areVoiceMessagesAvailable @@ -162,6 +166,8 @@ public final class StoryContentContextState { self.canViewStats = canViewStats self.isPremiumRequiredForMessaging = isPremiumRequiredForMessaging self.preferHighQualityStories = preferHighQualityStories + self.boostsToUnrestrict = boostsToUnrestrict + self.appliedBoosts = appliedBoosts } public static func == (lhs: StoryContentContextState.AdditionalPeerData, rhs: StoryContentContextState.AdditionalPeerData) -> Bool { @@ -183,6 +189,12 @@ public final class StoryContentContextState { if lhs.preferHighQualityStories != rhs.preferHighQualityStories { return false } + if lhs.boostsToUnrestrict != rhs.boostsToUnrestrict { + return false + } + if lhs.appliedBoosts != rhs.appliedBoosts { + return false + } return true } } diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift index ced2a7cf69..14aea4a93b 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift @@ -1668,7 +1668,13 @@ public final class StoryItemSetContainerComponent: Component { case .broadcast: displayFooter = true case .group: - displayFooter = false + var canBypassRestrictions = false + if let appliedBoosts = component.slice.additionalPeerData.appliedBoosts, let boostsToUnrestrict = component.slice.additionalPeerData.boostsToUnrestrict { + canBypassRestrictions = appliedBoosts >= boostsToUnrestrict + } + if let bannedSendText = channel.hasBannedPermission(.banSendText, ignoreDefault: canBypassRestrictions), bannedSendText.1 || component.slice.additionalPeerData.boostsToUnrestrict == nil { + displayFooter = true + } } } else if component.slice.peer.id == component.context.account.peerId { displayFooter = true @@ -2744,9 +2750,42 @@ public final class StoryItemSetContainerComponent: Component { inputPanelTransition = .immediate } + var canBypassRestrictions = false + if let appliedBoosts = component.slice.additionalPeerData.appliedBoosts, let boostsToUnrestrict = component.slice.additionalPeerData.boostsToUnrestrict { + canBypassRestrictions = appliedBoosts >= boostsToUnrestrict + } + + var isChannel = false + var isGroup = false + var showMessageInputPanel = true + var isGroupCommentRestricted = false + if case let .channel(channel) = component.slice.peer { + switch channel.info { + case .broadcast: + isChannel = true + showMessageInputPanel = false + case .group: + if let bannedSendText = channel.hasBannedPermission(.banSendText, ignoreDefault: canBypassRestrictions) { + if bannedSendText.1 || component.slice.additionalPeerData.boostsToUnrestrict == nil { + showMessageInputPanel = false + } else { + isGroupCommentRestricted = true + } + } + isGroup = true + } + } else { + showMessageInputPanel = component.slice.peer.id != component.context.account.peerId + } + var isUnsupported = false var disabledPlaceholder: MessageInputPanelComponent.DisabledPlaceholder? - if component.slice.additionalPeerData.isPremiumRequiredForMessaging { + + if isGroupCommentRestricted { + disabledPlaceholder = .boostRequired(title: component.strings.Story_GroupCommentingRestrictedPlaceholder, subtitle: component.strings.Story_GroupCommentingRestrictedPlaceholderAction, action: { [weak self] in + self?.presentBoostToUnrestrict() + }) + } else if component.slice.additionalPeerData.isPremiumRequiredForMessaging { disabledPlaceholder = .premiumRequired(title: component.strings.Story_MessagingRestrictedPlaceholder(component.slice.peer.compactDisplayTitle).string, subtitle: component.strings.Story_MessagingRestrictedPlaceholderAction, action: { [weak self] in self?.presentPremiumRequiredForMessaging() }) @@ -2757,21 +2796,6 @@ public final class StoryItemSetContainerComponent: Component { disabledPlaceholder = .text(component.strings.Story_FooterReplyUnavailable) } - var isChannel = false - var isGroup = false - var showMessageInputPanel = true - if case let .channel(channel) = component.slice.peer { - switch channel.info { - case .broadcast: - isChannel = true - showMessageInputPanel = false - case .group: - isGroup = true - } - } else { - showMessageInputPanel = component.slice.peer.id != component.context.account.peerId - } - let inputPlaceholder: MessageInputPanelComponent.Placeholder if let stealthModeTimeout = component.stealthModeTimeout { let minutes = Int(stealthModeTimeout / 60) @@ -5673,7 +5697,7 @@ public final class StoryItemSetContainerComponent: Component { let controller = PremiumIntroScreen(context: component.context, source: .settings, forceDark: true) self.sendMessageContext.actionSheet = controller - controller.wasDismissed = { [weak self, weak controller]in + controller.wasDismissed = { [weak self, weak controller] in guard let self else { return } @@ -5688,6 +5712,45 @@ public final class StoryItemSetContainerComponent: Component { component.controller()?.push(controller) } + private func presentBoostToUnrestrict() { + guard let component = self.component, let boostsToUnrestrict = component.slice.additionalPeerData.boostsToUnrestrict else { + return + } + + HapticFeedback().impact() + + let _ = combineLatest(queue: Queue.mainQueue(), + component.context.engine.peers.getChannelBoostStatus(peerId: component.slice.peer.id), + component.context.engine.peers.getMyBoostStatus() + ).startStandalone(next: { [weak self] boostStatus, myBoostStatus in + guard let self, let component = self.component, let boostStatus, let myBoostStatus else { + return + } + let boostController = PremiumBoostLevelsScreen( + context: component.context, + peerId: component.slice.peer.id, + mode: .user(mode: .unrestrict(Int(boostsToUnrestrict))), + status: boostStatus, + myBoostStatus: myBoostStatus, + forceDark: true + ) + boostController.disposed = { [weak self, weak boostController] in + guard let self else { + return + } + + if self.sendMessageContext.actionSheet === boostController { + self.sendMessageContext.actionSheet = nil + } + self.updateIsProgressPaused() + } + self.sendMessageContext.actionSheet = boostController + + self.updateIsProgressPaused() + component.controller()?.push(boostController) + }) + } + private func presentStoriesUpgradeScreen(source: PremiumSource) { guard let component = self.component else { return