Group boosts

This commit is contained in:
Ilya Laktyushin
2024-02-10 16:21:17 +04:00
parent 132efae09b
commit 5e92cc90e4
9 changed files with 126 additions and 67 deletions

View File

@@ -11013,7 +11013,7 @@ Sorry for the inconvenience.";
"GroupBoost.EnableStoriesText" = "Your group needs %1$@ to enable posting stories.";
"GroupBoost.IncreaseLimitText" = "Your group needs %1$@ to post %2$@.";
"BoostGift.Group.Description" = "Get more boosts for your group by gifting\nPremium to your subscribers.";
"BoostGift.Group.Description" = "Get more boosts for your group by gifting\nPremium to your members.";
"BoostGift.Group.AllMembers" = "All members";
"BoostGift.Group.OnlyNewMembers" = "Only new members";

View File

@@ -309,7 +309,11 @@ public func chatListItemStrings(strings: PresentationStrings, nameDisplayOrder:
if let forwardInfo = message.forwardInfo, let author = forwardInfo.author {
messageText = strings.Message_GiveawayStartedOther(EnginePeer(author).compactDisplayTitle).string
} else {
messageText = isPeerGroup ? strings.Message_GiveawayStartedGroup : strings.Message_GiveawayStarted
if let author = message.author, case let .channel(channel) = author, case .group = channel.info {
messageText = strings.Message_GiveawayStartedGroup
} else {
messageText = strings.Message_GiveawayStarted
}
}
case let results as TelegramMediaGiveawayResults:
if results.winnersCount == 0 {

View File

@@ -1210,7 +1210,7 @@ public func createGiveawayController(context: AccountContext, updatedPresentatio
navigationController.setViewControllers(controllers, animated: true)
let title = presentationData.strings.BoostGift_GiveawayCreated_Title
let text = presentationData.strings.BoostGift_GiveawayCreated_Text
let text = isGroup ? presentationData.strings.BoostGift_Group_GiveawayCreated_Text : presentationData.strings.BoostGift_GiveawayCreated_Text
let tooltipController = UndoOverlayController(presentationData: presentationData, content: .premiumPaywall(title: title, text: text, customUndoText: nil, timeout: nil, linkAction: { [weak navigationController] _ in
let statsController = context.sharedContext.makeChannelStatsController(context: context, updatedPresentationData: updatedPresentationData, peerId: peerId, boosts: true, boostStatus: nil)

View File

@@ -252,14 +252,17 @@ public class PremiumLimitDisplayComponent: Component {
rotationAngle = 0.26
}
let to: CGFloat = self.badgeView.center.x
let positionAnimation = CABasicAnimation(keyPath: "position.x")
positionAnimation.fromValue = NSValue(cgPoint: CGPoint(x: from ?? 0.0, y: 0.0))
positionAnimation.toValue = NSValue(cgPoint: self.badgeView.center)
positionAnimation.toValue = NSValue(cgPoint: CGPoint(x: to, y: 0.0))
positionAnimation.duration = 0.5
positionAnimation.fillMode = .forwards
positionAnimation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
self.badgeView.layer.add(positionAnimation, forKey: "appearance1")
if from != to {
let rotateAnimation = CABasicAnimation(keyPath: "transform.rotation.z")
rotateAnimation.fromValue = 0.0 as NSNumber
rotateAnimation.toValue = -rotationAngle as NSNumber
@@ -295,6 +298,7 @@ public class PremiumLimitDisplayComponent: Component {
self.badgeView.layer.removeAnimation(forKey: "appearance3")
}
})
}
if from == nil {
self.badgeView.alpha = 1.0

View File

@@ -19,7 +19,7 @@ final class BoostHeaderItem: ItemListControllerHeaderItem {
let context: AccountContext
let theme: PresentationTheme
let strings: PresentationStrings
let status: ChannelBoostStatus
let status: ChannelBoostStatus?
let title: String
let text: String
let openBoost: () -> Void
@@ -28,7 +28,7 @@ final class BoostHeaderItem: ItemListControllerHeaderItem {
let back: () -> Void
let updateStatusBar: (StatusBarStyle) -> Void
init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, status: ChannelBoostStatus, title: String, text: String, openBoost: @escaping () -> Void, createGiveaway: @escaping () -> Void, openFeatures: @escaping () -> Void, back: @escaping () -> Void, updateStatusBar: @escaping (StatusBarStyle) -> Void) {
init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, status: ChannelBoostStatus?, title: String, text: String, openBoost: @escaping () -> Void, createGiveaway: @escaping () -> Void, openFeatures: @escaping () -> Void, back: @escaping () -> Void, updateStatusBar: @escaping (StatusBarStyle) -> Void) {
self.context = context
self.theme = theme
self.strings = strings
@@ -44,7 +44,7 @@ final class BoostHeaderItem: ItemListControllerHeaderItem {
func isEqual(to: ItemListControllerHeaderItem) -> Bool {
if let item = to as? BoostHeaderItem {
return self.theme === item.theme && self.title == item.title && self.text == item.text
return self.theme === item.theme && self.title == item.title && self.text == item.text && self.status == item.status
} else {
return false
}
@@ -78,7 +78,7 @@ final class BoostHeaderItemNode: ItemListControllerHeaderItemNode {
didSet {
self.updateItem()
if let layout = self.validLayout {
let _ = self.updateLayout(layout: layout, transition: .immediate)
let _ = self.updateLayout(layout: layout, transition: .animated(duration: 0.2, curve: .easeInOut))
}
}
}
@@ -197,6 +197,7 @@ final class BoostHeaderItemNode: ItemListControllerHeaderItemNode {
strings: self.item.strings,
text: self.item.text,
status: self.item.status,
insets: layout.safeInsets,
openBoost: self.item.openBoost,
createGiveaway: self.item.createGiveaway,
openFeatures: self.item.openFeatures
@@ -205,7 +206,7 @@ final class BoostHeaderItemNode: ItemListControllerHeaderItemNode {
if let hostView = self.hostView {
let size = hostView.update(
transition: .immediate,
transition: Transition(transition),
component: component,
environment: {},
containerSize: containerSize
@@ -220,7 +221,7 @@ final class BoostHeaderItemNode: ItemListControllerHeaderItemNode {
self.whiteTitleNode.position = self.titleNode.position
let backSize = self.backButton.update(key: .back, presentationData: self.item.context.sharedContext.currentPresentationData.with { $0 }, height: 44.0)
self.backButton.frame = CGRect(origin: CGPoint(x: 16.0, y: 54.0), size: backSize)
self.backButton.frame = CGRect(origin: CGPoint(x: layout.safeInsets.left + 16.0, y: statusBarHeight), size: backSize)
self.component = component
self.validLayout = layout
@@ -244,7 +245,8 @@ final class BoostHeaderItemNode: ItemListControllerHeaderItemNode {
private final class BoostHeaderComponent: CombinedComponent {
let strings: PresentationStrings
let text: String
let status: ChannelBoostStatus
let status: ChannelBoostStatus?
let insets: UIEdgeInsets
let openBoost: () -> Void
let createGiveaway: () -> Void
let openFeatures: () -> Void
@@ -252,7 +254,8 @@ private final class BoostHeaderComponent: CombinedComponent {
public init(
strings: PresentationStrings,
text: String,
status: ChannelBoostStatus,
status: ChannelBoostStatus?,
insets: UIEdgeInsets,
openBoost: @escaping () -> Void,
createGiveaway: @escaping () -> Void,
openFeatures: @escaping () -> Void
@@ -260,6 +263,7 @@ private final class BoostHeaderComponent: CombinedComponent {
self.strings = strings
self.text = text
self.status = status
self.insets = insets
self.openBoost = openBoost
self.createGiveaway = createGiveaway
self.openFeatures = openFeatures
@@ -275,6 +279,9 @@ private final class BoostHeaderComponent: CombinedComponent {
if lhs.status != rhs.status {
return false
}
if lhs.insets != rhs.insets {
return false
}
return true
}
@@ -290,8 +297,9 @@ private final class BoostHeaderComponent: CombinedComponent {
return { context in
let size = context.availableSize
let sideInset: CGFloat = 16.0
let component = context.component
let sideInset: CGFloat = 16.0 + component.insets.left
let background = background.update(
component: PremiumGradientBackgroundComponent(
@@ -321,13 +329,20 @@ private final class BoostHeaderComponent: CombinedComponent {
.position(CGPoint(x: size.width / 2.0, y: size.height / 2.0 + 10.0))
)
let level = component.status.level
let boosts: Int
let level = component.status?.level ?? 0
let position: CGFloat
if let nextLevelBoosts = component.status.nextLevelBoosts {
position = CGFloat(component.status.boosts - component.status.currentLevelBoosts) / CGFloat(nextLevelBoosts - component.status.currentLevelBoosts)
if let status = component.status {
if let nextLevelBoosts = status.nextLevelBoosts {
position = CGFloat(status.boosts - status.currentLevelBoosts) / CGFloat(nextLevelBoosts - status.currentLevelBoosts)
} else {
position = 1.0
}
boosts = status.boosts
} else {
boosts = 0
position = 0.0
}
let inactiveText = component.strings.ChannelBoost_Level("\(level)").string
let activeText = component.strings.ChannelBoost_Level("\(level + 1)").string
@@ -343,7 +358,7 @@ private final class BoostHeaderComponent: CombinedComponent {
activeValue: activeText,
activeTitleColor: UIColor(rgb: 0x6f8fff),
badgeIconName: "Premium/Boost",
badgeText: "\(component.status.boosts)",
badgeText: "\(boosts)",
badgePosition: position,
badgeGraphPosition: position,
invertProgress: true,

View File

@@ -1307,9 +1307,9 @@ public func channelStatsController(context: AccountContext, updatedPresentationD
var headerItem: BoostHeaderItem?
var leftNavigationButton: ItemListNavigationButton?
var boostsOnly = false
if isGroup, section == .boosts, let boostStatus {
if isGroup, section == .boosts {
title = .text("")
headerItem = BoostHeaderItem(context: context, theme: presentationData.theme, strings: presentationData.strings, status: boostStatus, title: presentationData.strings.GroupBoost_Title, text: presentationData.strings.GroupBoost_Info, openBoost: {
headerItem = BoostHeaderItem(context: context, theme: presentationData.theme, strings: presentationData.strings, status: boostData, title: presentationData.strings.GroupBoost_Title, text: presentationData.strings.GroupBoost_Info, openBoost: {
openBoostImpl?(false)
}, createGiveaway: {
arguments.openGifts()

View File

@@ -208,6 +208,7 @@ final class PeerInfoScreenData {
let isPowerSavingEnabled: Bool?
let accountIsPremium: Bool
let hasSavedMessageTags: Bool
let isPremiumRequiredForStoryPosting: Bool
let _isContact: Bool
var forceIsContact: Bool = false
@@ -244,7 +245,8 @@ final class PeerInfoScreenData {
appConfiguration: AppConfiguration?,
isPowerSavingEnabled: Bool?,
accountIsPremium: Bool,
hasSavedMessageTags: Bool
hasSavedMessageTags: Bool,
isPremiumRequiredForStoryPosting: Bool
) {
self.peer = peer
self.chatPeer = chatPeer
@@ -270,6 +272,7 @@ final class PeerInfoScreenData {
self.isPowerSavingEnabled = isPowerSavingEnabled
self.accountIsPremium = accountIsPremium
self.hasSavedMessageTags = hasSavedMessageTags
self.isPremiumRequiredForStoryPosting = isPremiumRequiredForStoryPosting
}
}
@@ -666,7 +669,8 @@ func peerInfoScreenSettingsData(context: AccountContext, peerId: EnginePeer.Id,
appConfiguration: appConfiguration,
isPowerSavingEnabled: isPowerSavingEnabled,
accountIsPremium: peer?.isPremium ?? false,
hasSavedMessageTags: false
hasSavedMessageTags: false,
isPremiumRequiredForStoryPosting: true
)
}
}
@@ -702,7 +706,8 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
appConfiguration: nil,
isPowerSavingEnabled: nil,
accountIsPremium: false,
hasSavedMessageTags: false
hasSavedMessageTags: false,
isPremiumRequiredForStoryPosting: true
))
case let .user(userPeerId, secretChatId, kind):
let groupsInCommon: GroupsInCommonContext?
@@ -973,7 +978,8 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
appConfiguration: nil,
isPowerSavingEnabled: nil,
accountIsPremium: accountIsPremium,
hasSavedMessageTags: hasSavedMessageTags
hasSavedMessageTags: hasSavedMessageTags,
isPremiumRequiredForStoryPosting: false
)
}
case .channel:
@@ -1047,6 +1053,8 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
hasSavedMessageTags = .single(false)
}
let isPremiumRequiredForStoryPosting: Signal<Bool, NoError> = isPremiumRequiredForStoryPosting(context: context)
return combineLatest(
context.account.viewTracker.peerView(peerId, updateData: true),
peerInfoAvailableMediaPanes(context: context, peerId: peerId, chatLocation: chatLocation, chatLocationContextHolder: chatLocationContextHolder),
@@ -1061,9 +1069,10 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
context.engine.peers.recommendedChannels(peerId: peerId),
hasSavedMessages,
hasSavedMessagesChats,
hasSavedMessageTags
hasSavedMessageTags,
isPremiumRequiredForStoryPosting
)
|> map { peerView, availablePanes, globalNotificationSettings, status, currentInvitationsContext, invitations, currentRequestsContext, requests, hasStories, accountIsPremium, recommendedChannels, hasSavedMessages, hasSavedMessagesChats, hasSavedMessageTags -> PeerInfoScreenData in
|> map { peerView, availablePanes, globalNotificationSettings, status, currentInvitationsContext, invitations, currentRequestsContext, requests, hasStories, accountIsPremium, recommendedChannels, hasSavedMessages, hasSavedMessagesChats, hasSavedMessageTags, isPremiumRequiredForStoryPosting -> PeerInfoScreenData in
var availablePanes = availablePanes
if let hasStories {
if hasStories {
@@ -1138,7 +1147,8 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
appConfiguration: nil,
isPowerSavingEnabled: nil,
accountIsPremium: accountIsPremium,
hasSavedMessageTags: hasSavedMessageTags
hasSavedMessageTags: hasSavedMessageTags,
isPremiumRequiredForStoryPosting: isPremiumRequiredForStoryPosting
)
}
case let .group(groupId):
@@ -1315,6 +1325,8 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
hasSavedMessageTags = .single(false)
}
let isPremiumRequiredForStoryPosting: Signal<Bool, NoError> = isPremiumRequiredForStoryPosting(context: context)
return combineLatest(queue: .mainQueue(),
context.account.viewTracker.peerView(groupId, updateData: true),
peerInfoAvailableMediaPanes(context: context, peerId: groupId, chatLocation: chatLocation, chatLocationContextHolder: chatLocationContextHolder),
@@ -1331,9 +1343,10 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
accountIsPremium,
hasSavedMessages,
hasSavedMessagesChats,
hasSavedMessageTags
hasSavedMessageTags,
isPremiumRequiredForStoryPosting
)
|> mapToSignal { peerView, availablePanes, globalNotificationSettings, status, membersData, currentInvitationsContext, invitations, currentRequestsContext, requests, hasStories, threadData, preferencesView, accountIsPremium, hasSavedMessages, hasSavedMessagesChats, hasSavedMessageTags -> Signal<PeerInfoScreenData, NoError> in
|> mapToSignal { peerView, availablePanes, globalNotificationSettings, status, membersData, currentInvitationsContext, invitations, currentRequestsContext, requests, hasStories, threadData, preferencesView, accountIsPremium, hasSavedMessages, hasSavedMessagesChats, hasSavedMessageTags, isPremiumRequiredForStoryPosting -> Signal<PeerInfoScreenData, NoError> in
var discussionPeer: Peer?
if case let .known(maybeLinkedDiscussionPeerId) = (peerView.cachedData as? CachedChannelData)?.linkedDiscussionPeerId, let linkedDiscussionPeerId = maybeLinkedDiscussionPeerId, let peer = peerView.peers[linkedDiscussionPeerId] {
discussionPeer = peer
@@ -1426,7 +1439,8 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
appConfiguration: appConfiguration,
isPowerSavingEnabled: nil,
accountIsPremium: accountIsPremium,
hasSavedMessageTags: hasSavedMessageTags
hasSavedMessageTags: hasSavedMessageTags,
isPremiumRequiredForStoryPosting: isPremiumRequiredForStoryPosting
))
}
}
@@ -1812,3 +1826,25 @@ func peerInfoIsChatMuted(peer: Peer?, peerNotificationSettings: TelegramPeerNoti
}
return chatIsMuted
}
private var isPremiumRequired: Bool?
private func isPremiumRequiredForStoryPosting(context: AccountContext) -> Signal<Bool, NoError> {
if let isPremiumRequired {
return .single(isPremiumRequired)
}
return .single(true)
|> then(
context.engine.messages.checkStoriesUploadAvailability(target: .myStories)
|> deliverOnMainQueue
|> map { status -> Bool in
if case .premiumRequired = status {
return true
} else {
return false
}
} |> afterNext { value in
isPremiumRequired = value
}
)
}

View File

@@ -5732,7 +5732,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
})))
}
if case .broadcast = channel.info, channel.hasPermission(.editStories) {
if channel.hasPermission(.editStories) {
items.append(.action(ContextMenuActionItem(text: presentationData.strings.PeerInfo_Channel_ArchivedStories, icon: { theme in
generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Archive"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] _, f in
@@ -10353,7 +10353,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
} else if peerInfoCanEdit(peer: self.data?.peer, chatLocation: self.chatLocation, threadData: self.data?.threadData, cachedData: self.data?.cachedData, isContact: self.data?.isContact) {
rightNavigationButtons.append(PeerInfoHeaderNavigationButtonSpec(key: .edit, isForExpandedView: false))
}
if let data = self.data, data.accountIsPremium, let channel = data.peer as? TelegramChannel, channel.hasPermission(.postStories) {
if let data = self.data, !data.isPremiumRequiredForStoryPosting || data.accountIsPremium, let channel = data.peer as? TelegramChannel, channel.hasPermission(.postStories) {
rightNavigationButtons.insert(PeerInfoHeaderNavigationButtonSpec(key: .postStory, isForExpandedView: false), at: 0)
}

View File

@@ -1083,7 +1083,7 @@ final class ChannelAppearanceScreenComponent: Component {
}
self.backButton.updateContentsColor(backgroundColor: scrolledUp ? UIColor(white: 0.0, alpha: 0.1) : .clear, contentsColor: scrolledUp ? .white : environment.theme.rootController.navigationBar.accentTextColor, canBeExpanded: !scrolledUp, transition: .animated(duration: 0.2, curve: .easeInOut))
self.backButton.frame = CGRect(origin: CGPoint(x: 16.0, y: 54.0), size: backSize)
self.backButton.frame = CGRect(origin: CGPoint(x: environment.safeInsets.left + 16.0, y: environment.navigationHeight - 44.0), size: backSize)
if self.backButton.view.superview == nil {
if let controller = self.environment?.controller(), let navigationBar = controller.navigationBar {
navigationBar.view.addSubview(self.backButton.view)