Business intro screen

This commit is contained in:
Ilya Laktyushin 2024-02-20 14:55:40 +00:00
parent 0ba1164a0b
commit 7d1711c25e
11 changed files with 706 additions and 319 deletions

View File

@ -11341,7 +11341,6 @@ to respond to messages faster.";
"Business.Title" = "Telegram Business"; "Business.Title" = "Telegram Business";
"Business.Description" = "Turn your account into a **business page** with these additional features."; "Business.Description" = "Turn your account into a **business page** with these additional features.";
"Business.SubscribedDescription" = "You have now unlocked these additional business features."; "Business.SubscribedDescription" = "You have now unlocked these additional business features.";
"Business.PlusPremiumFeatures" = "+20 MORE TELEGRAM PREMIUM FEATURES";
"Business.Location" = "Location"; "Business.Location" = "Location";
"Business.OpeningHours" = "Opening Hours"; "Business.OpeningHours" = "Opening Hours";
@ -11356,3 +11355,24 @@ to respond to messages faster.";
"Business.GreetingMessagesInfo" = "Create greetings that will be automatically sent to new customers."; "Business.GreetingMessagesInfo" = "Create greetings that will be automatically sent to new customers.";
"Business.AwayMessagesInfo" = "Define messages that are automatically sent when you are off."; "Business.AwayMessagesInfo" = "Define messages that are automatically sent when you are off.";
"Business.ChatbotsInfo" = "Add any third-party chatbots that will process customer interactions."; "Business.ChatbotsInfo" = "Add any third-party chatbots that will process customer interactions.";
"Business.MoreFeaturesTitle" = "MORE BUSINESS FEATURES";
"Business.MoreFeaturesInfo" = "Check this section later for new business features.";
"Business.SetEmojiStatus" = "Set Emoji Status";
"Business.SetEmojiStatusInfo" = "Display the current status of your business next to your name.";
"Business.TagYourChats" = "Tag Your Chats";
"Business.TagYourChatsInfo" = "Add colorful labels to chats for faster access in chat list.";
"Business.AddPost" = "Add a Post";
"Business.AddPostInfo" = "Publish photos and videos of your goods or services on your page.";
"StoryList.SavedEmptyPosts.Title" = "No posts yet...";
"StoryList.SavedEmptyPosts.Text" = "Publish photos and videos to display on your profile page.";
"StoryList.SavedAddAction" = "Add a Post";
"PeerInfo.Channel.Boost" = "Boost Channel";
"ChannelBoost.Title" = "Boost Channel";
"ChannelBoost.Info" = "Subscribers of your channel can **boost** it so that it **levels up** and gets **exclusive features**.";

View File

@ -932,6 +932,7 @@ public protocol SharedAccountContext: AnyObject {
func makeHashtagSearchController(context: AccountContext, peer: EnginePeer?, query: String, all: Bool) -> ViewController func makeHashtagSearchController(context: AccountContext, peer: EnginePeer?, query: String, all: Bool) -> ViewController
func makeMyStoriesController(context: AccountContext, isArchive: Bool) -> ViewController func makeMyStoriesController(context: AccountContext, isArchive: Bool) -> ViewController
func makeArchiveSettingsController(context: AccountContext) -> ViewController func makeArchiveSettingsController(context: AccountContext) -> ViewController
func makeFilterSettingsController(context: AccountContext, modal: Bool, dismissed: (() -> Void)?) -> ViewController
func makeBusinessSetupScreen(context: AccountContext) -> ViewController func makeBusinessSetupScreen(context: AccountContext) -> ViewController
func makeChatbotSetupScreen(context: AccountContext) -> ViewController func makeChatbotSetupScreen(context: AccountContext) -> ViewController
func makeBusinessLocationSetupScreen(context: AccountContext) -> ViewController func makeBusinessLocationSetupScreen(context: AccountContext) -> ViewController

View File

@ -5617,9 +5617,10 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
private func openFilterSettings() { private func openFilterSettings() {
self.chatListDisplayNode.mainContainerNode.updateEnableAdjacentFilterLoading(false) self.chatListDisplayNode.mainContainerNode.updateEnableAdjacentFilterLoading(false)
if let navigationController = self.context.sharedContext.mainWindow?.viewController as? NavigationController { if let navigationController = self.context.sharedContext.mainWindow?.viewController as? NavigationController {
navigationController.pushViewController(chatListFilterPresetListController(context: self.context, mode: .modal, dismissed: { [weak self] in let controller = self.context.sharedContext.makeFilterSettingsController(context: self.context, modal: true, dismissed: { [weak self] in
self?.chatListDisplayNode.mainContainerNode.updateEnableAdjacentFilterLoading(true) self?.chatListDisplayNode.mainContainerNode.updateEnableAdjacentFilterLoading(true)
})) })
navigationController.pushViewController(controller)
} }
} }

View File

@ -616,12 +616,12 @@ private func chatListFilterPresetControllerEntries(context: AccountContext, pres
entries.append(.excludePeerInfo(presentationData.strings.ChatListFolder_ExcludeSectionInfo)) entries.append(.excludePeerInfo(presentationData.strings.ChatListFolder_ExcludeSectionInfo))
} }
/*let tagColor = state.color ?? .blue let tagColor = state.color ?? .blue
let resolvedColor = context.peerNameColors.getProfile(tagColor, dark: presentationData.theme.overallDarkAppearance, subject: .palette) let resolvedColor = context.peerNameColors.getProfile(tagColor, dark: presentationData.theme.overallDarkAppearance, subject: .palette)
entries.append(.tagColorHeader(name: state.name, color: resolvedColor)) entries.append(.tagColorHeader(name: state.name, color: resolvedColor))
entries.append(.tagColor(colors: context.peerNameColors, currentColor: tagColor)) entries.append(.tagColor(colors: context.peerNameColors, currentColor: tagColor))
entries.append(.tagColorFooter)*/ entries.append(.tagColorFooter)
var hasLinks = false var hasLinks = false
if let inviteLinks, !inviteLinks.isEmpty { if let inviteLinks, !inviteLinks.isEmpty {

View File

@ -274,8 +274,8 @@ private func chatListFilterPresetListControllerEntries(presentationData: Present
} }
} }
/*entries.append(.displayTags(displayTags)) entries.append(.displayTags(displayTags))
entries.append(.displayTagsFooter)*/ entries.append(.displayTagsFooter)
return entries return entries
} }

View File

@ -112,6 +112,10 @@ swift_library(
"//submodules/AlertUI", "//submodules/AlertUI",
"//submodules/TelegramUI/Components/Chat/MergedAvatarsNode", "//submodules/TelegramUI/Components/Chat/MergedAvatarsNode",
"//submodules/TelegramUI/Components/LottieComponent", "//submodules/TelegramUI/Components/LottieComponent",
"//submodules/TelegramUI/Components/ListSectionComponent",
"//submodules/TelegramUI/Components/ListActionItemComponent",
"//submodules/TelegramUI/Components/EmojiStatusSelectionComponent",
"//submodules/TelegramUI/Components/EntityKeyboard",
], ],
visibility = [ visibility = [
"//visibility:public", "//visibility:public",

View File

@ -27,6 +27,11 @@ import MultiAnimationRenderer
import TelegramNotices import TelegramNotices
import UndoUI import UndoUI
import TelegramStringFormatting import TelegramStringFormatting
import ListSectionComponent
import ListActionItemComponent
import EmojiStatusSelectionComponent
import EmojiStatusComponent
import EntityKeyboard
public enum PremiumSource: Equatable { public enum PremiumSource: Equatable {
public static func == (lhs: PremiumSource, rhs: PremiumSource) -> Bool { public static func == (lhs: PremiumSource, rhs: PremiumSource) -> Bool {
@ -839,6 +844,73 @@ private struct PremiumProduct: Equatable {
} }
} }
final class PerkIconComponent: CombinedComponent {
let backgroundColor: UIColor
let foregroundColor: UIColor
let iconName: String
init(
backgroundColor: UIColor,
foregroundColor: UIColor,
iconName: String
) {
self.backgroundColor = backgroundColor
self.foregroundColor = foregroundColor
self.iconName = iconName
}
static func ==(lhs: PerkIconComponent, rhs: PerkIconComponent) -> Bool {
if lhs.backgroundColor != rhs.backgroundColor {
return false
}
if lhs.foregroundColor != rhs.foregroundColor {
return false
}
if lhs.iconName != rhs.iconName {
return false
}
return true
}
static var body: Body {
let background = Child(RoundedRectangle.self)
let icon = Child(BundleIconComponent.self)
return { context in
let component = context.component
let iconSize = CGSize(width: 30.0, height: 30.0)
let background = background.update(
component: RoundedRectangle(
color: component.backgroundColor,
cornerRadius: 7.0
),
availableSize: iconSize,
transition: context.transition
)
let icon = icon.update(
component: BundleIconComponent(
name: component.iconName,
tintColor: .white
),
availableSize: iconSize,
transition: context.transition
)
let iconPosition = CGPoint(x: background.size.width / 2.0, y: background.size.height / 2.0)
context.add(background
.position(iconPosition)
)
context.add(icon
.position(iconPosition)
)
return iconSize
}
}
}
final class SectionGroupComponent: Component { final class SectionGroupComponent: Component {
public final class Item: Equatable { public final class Item: Equatable {
public let content: AnyComponentWithIdentity<Empty> public let content: AnyComponentWithIdentity<Empty>
@ -1332,6 +1404,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
final class State: ComponentState { final class State: ComponentState {
private let context: AccountContext private let context: AccountContext
private let present: (ViewController) -> Void
var products: [PremiumProduct]? var products: [PremiumProduct]?
var selectedProductId: String? var selectedProductId: String?
@ -1340,6 +1413,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
var newPerks: [String] = [] var newPerks: [String] = []
var isPremium: Bool? var isPremium: Bool?
var peer: EnginePeer?
private var disposable: Disposable? private var disposable: Disposable?
private(set) var configuration = PremiumIntroConfiguration.defaultValue private(set) var configuration = PremiumIntroConfiguration.defaultValue
@ -1368,20 +1442,29 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
} }
} }
init(context: AccountContext, source: PremiumSource) { init(
context: AccountContext,
source: PremiumSource,
present: @escaping (ViewController) -> Void
) {
self.context = context self.context = context
self.present = present
super.init() super.init()
self.disposable = (context.engine.data.subscribe( self.disposable = (context.engine.data.subscribe(
TelegramEngine.EngineData.Item.Configuration.App() TelegramEngine.EngineData.Item.Configuration.App(),
TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)
) )
|> deliverOnMainQueue).start(next: { [weak self] appConfiguration in |> deliverOnMainQueue).start(next: { [weak self] appConfiguration, accountPeer in
if let strongSelf = self { if let strongSelf = self {
let isFirstTime = strongSelf.peer == nil
strongSelf.configuration = PremiumIntroConfiguration.with(appConfiguration: appConfiguration) strongSelf.configuration = PremiumIntroConfiguration.with(appConfiguration: appConfiguration)
strongSelf.peer = accountPeer
strongSelf.updated(transition: .immediate) strongSelf.updated(transition: .immediate)
if let identifier = source.identifier { if let identifier = source.identifier, isFirstTime {
var jsonString: String = "{" var jsonString: String = "{"
jsonString += "\"source\": \"\(identifier)\"," jsonString += "\"source\": \"\(identifier)\","
@ -1465,10 +1548,59 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
self.stickersDisposable?.dispose() self.stickersDisposable?.dispose()
self.newPerksDisposable?.dispose() self.newPerksDisposable?.dispose()
} }
private var updatedPeerStatus: PeerEmojiStatus?
private weak var emojiStatusSelectionController: ViewController?
private var previousEmojiSetupTimestamp: Double?
func openEmojiSetup(sourceView: UIView, currentFileId: Int64?, color: UIColor?) {
let currentTimestamp = CACurrentMediaTime()
if let previousTimestamp = self.previousEmojiSetupTimestamp, currentTimestamp < previousTimestamp + 1.0 {
return
}
self.previousEmojiSetupTimestamp = currentTimestamp
self.emojiStatusSelectionController?.dismiss()
var selectedItems = Set<MediaId>()
if let currentFileId {
selectedItems.insert(MediaId(namespace: Namespaces.Media.CloudFile, id: currentFileId))
}
let controller = EmojiStatusSelectionController(
context: self.context,
mode: .statusSelection,
sourceView: sourceView,
emojiContent: EmojiPagerContentComponent.emojiInputData(
context: self.context,
animationCache: self.context.animationCache,
animationRenderer: self.context.animationRenderer,
isStandalone: false,
subject: .status,
hasTrending: false,
topReactionItems: [],
areUnicodeEmojiEnabled: false,
areCustomEmojiEnabled: true,
chatPeerId: self.context.account.peerId,
selectedItems: selectedItems,
topStatusTitle: nil,
backgroundIconColor: color
),
currentSelection: currentFileId,
color: color,
destinationItemView: { [weak sourceView] in
guard let sourceView else {
return nil
}
return sourceView
}
)
self.emojiStatusSelectionController = controller
self.present(controller)
}
} }
func makeState() -> State { func makeState() -> State {
return State(context: self.context, source: self.source) return State(context: self.context, source: self.source, present: self.present)
} }
static var body: Body { static var body: Body {
@ -1478,9 +1610,9 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
let completedText = Child(MultilineTextComponent.self) let completedText = Child(MultilineTextComponent.self)
let linkButton = Child(Button.self) let linkButton = Child(Button.self)
let optionsSection = Child(SectionGroupComponent.self) let optionsSection = Child(SectionGroupComponent.self)
let businessSection = Child(SectionGroupComponent.self) let businessSection = Child(ListSectionComponent.self)
let perksTitle = Child(MultilineTextComponent.self) let moreBusinessSection = Child(ListSectionComponent.self)
let perksSection = Child(SectionGroupComponent.self) let perksSection = Child(ListSectionComponent.self)
let infoBackground = Child(RoundedRectangle.self) let infoBackground = Child(RoundedRectangle.self)
let infoTitle = Child(MultilineTextComponent.self) let infoTitle = Child(MultilineTextComponent.self)
let infoText = Child(MultilineTextComponent.self) let infoText = Child(MultilineTextComponent.self)
@ -1499,6 +1631,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
let theme = environment.theme let theme = environment.theme
let strings = environment.strings let strings = environment.strings
let presentationData = context.component.context.sharedContext.currentPresentationData.with { $0 }
let availableWidth = context.availableSize.width let availableWidth = context.availableSize.width
let sideInsets = sideInset * 2.0 + environment.safeInsets.left + environment.safeInsets.right let sideInsets = sideInset * 2.0 + environment.safeInsets.left + environment.safeInsets.right
@ -1539,9 +1672,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
let textColor = theme.list.itemPrimaryTextColor let textColor = theme.list.itemPrimaryTextColor
let accentColor = theme.list.itemAccentColor let accentColor = theme.list.itemAccentColor
let titleColor = theme.list.itemPrimaryTextColor
let subtitleColor = theme.list.itemSecondaryTextColor let subtitleColor = theme.list.itemSecondaryTextColor
let arrowColor = theme.list.disclosureArrowColor
let textFont = Font.regular(15.0) let textFont = Font.regular(15.0)
let boldTextFont = Font.semibold(15.0) let boldTextFont = Font.semibold(15.0)
@ -1783,60 +1914,39 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
let layoutPerks = { let layoutPerks = {
size.height += 8.0 size.height += 8.0
let title: String
if case .business = context.component.mode {
title = strings.Business_PlusPremiumFeatures
} else {
title = strings.Premium_WhatsIncluded
}
let perksTitle = perksTitle.update(
component: MultilineTextComponent(
text: .plain(
NSAttributedString(string: title.uppercased(), font: Font.regular(14.0), textColor: environment.theme.list.freeTextColor)
),
horizontalAlignment: .natural,
maximumNumberOfLines: 0,
lineSpacing: 0.2
),
environment: {},
availableSize: CGSize(width: availableWidth - sideInsets, height: .greatestFiniteMagnitude),
transition: context.transition
)
context.add(perksTitle
.position(CGPoint(x: sideInset + environment.safeInsets.left + textSideInset + perksTitle.size.width / 2.0, y: size.height + perksTitle.size.height / 2.0))
.disappear(.default(alpha: true))
)
size.height += perksTitle.size.height
size.height += 3.0
var i = 0 var i = 0
var perksItems: [SectionGroupComponent.Item] = [] var perksItems: [AnyComponentWithIdentity<Empty>] = []
for perk in state.configuration.perks { for perk in state.configuration.perks {
if case .business = context.component.mode, case .business = perk { if case .business = context.component.mode, case .business = perk {
continue continue
} }
let iconBackgroundColors = gradientColors[i] perksItems.append(AnyComponentWithIdentity(id: perksItems.count, component: AnyComponent(ListActionItemComponent(
perksItems.append(SectionGroupComponent.Item( theme: environment.theme,
AnyComponentWithIdentity( title: AnyComponent(VStack([
id: perk.identifier, AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(
component: AnyComponent( text: .plain(NSAttributedString(
PerkComponent( string: perk.title(strings: strings),
iconName: perk.iconName, font: Font.regular(presentationData.listsFontSize.baseDisplaySize),
iconBackgroundColors: [ textColor: environment.theme.list.itemPrimaryTextColor
iconBackgroundColors )),
], maximumNumberOfLines: 0
title: perk.title(strings: strings), ))),
titleColor: titleColor, AnyComponentWithIdentity(id: AnyHashable(1), component: AnyComponent(MultilineTextComponent(
subtitle: perk.subtitle(strings: strings), text: .plain(NSAttributedString(
subtitleColor: subtitleColor, string: perk.subtitle(strings: strings),
arrowColor: arrowColor, font: Font.regular(floor(presentationData.listsFontSize.baseDisplaySize * 13.0 / 17.0)),
accentColor: accentColor, textColor: environment.theme.list.itemSecondaryTextColor
badge: state.newPerks.contains(perk.identifier) ? strings.Premium_New : nil )),
) maximumNumberOfLines: 0,
) lineSpacing: 0.18
), )))
accessibilityLabel: "\(perk.title(strings: strings)). \(perk.subtitle(strings: strings))", ], alignment: .left, spacing: 2.0)),
action: { [weak state] in leftIcon: AnyComponentWithIdentity(id: 0, component: AnyComponent(PerkIconComponent(
backgroundColor: gradientColors[i],
foregroundColor: .white,
iconName: perk.iconName
))),
action: { [weak state] _ in
var demoSubject: PremiumDemoScreen.Subject var demoSubject: PremiumDemoScreen.Subject
switch perk { switch perk {
case .doubleLimits: case .doubleLimits:
@ -1908,16 +2018,23 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
addAppLogEvent(postbox: accountContext.account.postbox, type: "premium.promo_screen_tap", data: ["item": perk.identifier]) addAppLogEvent(postbox: accountContext.account.postbox, type: "premium.promo_screen_tap", data: ["item": perk.identifier])
} }
)) ))))
i += 1 i += 1
} }
let perksSection = perksSection.update( let perksSection = perksSection.update(
component: SectionGroupComponent( component: ListSectionComponent(
items: perksItems, theme: environment.theme,
backgroundColor: environment.theme.list.itemBlocksBackgroundColor, header: AnyComponent(MultilineTextComponent(
selectionColor: environment.theme.list.itemHighlightedBackgroundColor, text: .plain(NSAttributedString(
separatorColor: environment.theme.list.itemBlocksSeparatorColor string: strings.Premium_WhatsIncluded.uppercased(),
font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize),
textColor: environment.theme.list.freeTextColor
)),
maximumNumberOfLines: 0
)),
footer: nil,
items: perksItems
), ),
environment: {}, environment: {},
availableSize: CGSize(width: availableWidth - sideInsets, height: .greatestFiniteMagnitude), availableSize: CGSize(width: availableWidth - sideInsets, height: .greatestFiniteMagnitude),
@ -1946,39 +2063,44 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
size.height += 8.0 size.height += 8.0
let gradientColors: [UIColor] = [ let gradientColors: [UIColor] = [
UIColor(rgb: 0x007aff),
UIColor(rgb: 0xac64f3),
UIColor(rgb: 0xef6922), UIColor(rgb: 0xef6922),
UIColor(rgb: 0xe95d44), UIColor(rgb: 0xe54937),
UIColor(rgb: 0xf2822a), UIColor(rgb: 0xdb374b),
UIColor(rgb: 0xe79519) UIColor(rgb: 0xbc4395),
UIColor(rgb: 0x9b4fed),
UIColor(rgb: 0x8958ff)
] ]
var i = 0 var i = 0
var perksItems: [SectionGroupComponent.Item] = [] var perksItems: [AnyComponentWithIdentity<Empty>] = []
for perk in BusinessPerk.allCases { for perk in BusinessPerk.allCases {
let iconBackgroundColors = gradientColors[i] perksItems.append(AnyComponentWithIdentity(id: perksItems.count, component: AnyComponent(ListActionItemComponent(
perksItems.append(SectionGroupComponent.Item( theme: environment.theme,
AnyComponentWithIdentity( title: AnyComponent(VStack([
id: perk.identifier, AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(
component: AnyComponent( text: .plain(NSAttributedString(
PerkComponent( string: perk.title(strings: strings),
iconName: perk.iconName, font: Font.regular(presentationData.listsFontSize.baseDisplaySize),
iconBackgroundColors: [ textColor: environment.theme.list.itemPrimaryTextColor
iconBackgroundColors )),
], maximumNumberOfLines: 0
title: perk.title(strings: strings), ))),
titleColor: titleColor, AnyComponentWithIdentity(id: AnyHashable(1), component: AnyComponent(MultilineTextComponent(
subtitle: perk.subtitle(strings: strings), text: .plain(NSAttributedString(
subtitleColor: subtitleColor, string: perk.subtitle(strings: strings),
arrowColor: arrowColor, font: Font.regular(floor(presentationData.listsFontSize.baseDisplaySize * 13.0 / 17.0)),
accentColor: accentColor, textColor: environment.theme.list.itemSecondaryTextColor
badge: nil )),
) maximumNumberOfLines: 0,
) lineSpacing: 0.18
), )))
accessibilityLabel: "\(perk.title(strings: strings)). \(perk.subtitle(strings: strings))", ], alignment: .left, spacing: 2.0)),
action: { leftIcon: AnyComponentWithIdentity(id: 0, component: AnyComponent(PerkIconComponent(
backgroundColor: gradientColors[i],
foregroundColor: .white,
iconName: perk.iconName
))),
action: { _ in
switch perk { switch perk {
case .location: case .location:
push(accountContext.sharedContext.makeBusinessLocationSetupScreen(context: accountContext)) push(accountContext.sharedContext.makeBusinessLocationSetupScreen(context: accountContext))
@ -1994,16 +2116,16 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
push(accountContext.sharedContext.makeChatbotSetupScreen(context: accountContext)) push(accountContext.sharedContext.makeChatbotSetupScreen(context: accountContext))
} }
} }
)) ))))
i += 1 i += 1
} }
let businessSection = businessSection.update( let businessSection = businessSection.update(
component: SectionGroupComponent( component: ListSectionComponent(
items: perksItems, theme: environment.theme,
backgroundColor: environment.theme.list.itemBlocksBackgroundColor, header: nil,
selectionColor: environment.theme.list.itemHighlightedBackgroundColor, footer: nil,
separatorColor: environment.theme.list.itemBlocksSeparatorColor items: perksItems
), ),
environment: {}, environment: {},
availableSize: CGSize(width: availableWidth - sideInsets, height: .greatestFiniteMagnitude), availableSize: CGSize(width: availableWidth - sideInsets, height: .greatestFiniteMagnitude),
@ -2018,6 +2140,151 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
size.height += 23.0 size.height += 23.0
} }
let layoutMoreBusinessPerks = {
size.height += 8.0
let status = state.peer?.emojiStatus
// let statusColor = state.peer?.nameColor.flatMap { context.component.context.peerNameColors.get($0, dark: environment.theme.overallDarkAppearance).main } ?? .blue
let accentColor = environment.theme.list.itemAccentColor
var perksItems: [AnyComponentWithIdentity<Empty>] = []
perksItems.append(AnyComponentWithIdentity(id: perksItems.count, component: AnyComponent(ListActionItemComponent(
theme: environment.theme,
title: AnyComponent(VStack([
AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: strings.Business_SetEmojiStatus,
font: Font.regular(presentationData.listsFontSize.baseDisplaySize),
textColor: environment.theme.list.itemPrimaryTextColor
)),
maximumNumberOfLines: 0
))),
AnyComponentWithIdentity(id: AnyHashable(1), component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: strings.Business_SetEmojiStatusInfo,
font: Font.regular(floor(presentationData.listsFontSize.baseDisplaySize * 13.0 / 17.0)),
textColor: environment.theme.list.itemSecondaryTextColor
)),
maximumNumberOfLines: 0,
lineSpacing: 0.18
)))
], alignment: .left, spacing: 2.0)),
leftIcon: AnyComponentWithIdentity(id: 0, component: AnyComponent(PerkIconComponent(
backgroundColor: UIColor(rgb: 0x676bff),
foregroundColor: .white,
iconName: "Premium/BusinessPerk/Status"
))),
icon: ListActionItemComponent.Icon(component: AnyComponentWithIdentity(id: 0, component: AnyComponent(EmojiActionIconComponent(
context: context.component.context,
color: accentColor,
fileId: status?.fileId,
file: nil
)))),
accessory: nil,
action: { [weak state] view in
guard let view = view as? ListActionItemComponent.View, let iconView = view.iconView else {
return
}
state?.openEmojiSetup(sourceView: iconView, currentFileId: nil, color: accentColor)
}
))))
perksItems.append(AnyComponentWithIdentity(id: perksItems.count, component: AnyComponent(ListActionItemComponent(
theme: environment.theme,
title: AnyComponent(VStack([
AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: strings.Business_TagYourChats,
font: Font.regular(presentationData.listsFontSize.baseDisplaySize),
textColor: environment.theme.list.itemPrimaryTextColor
)),
maximumNumberOfLines: 0
))),
AnyComponentWithIdentity(id: AnyHashable(1), component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: strings.Business_TagYourChatsInfo,
font: Font.regular(floor(presentationData.listsFontSize.baseDisplaySize * 13.0 / 17.0)),
textColor: environment.theme.list.itemSecondaryTextColor
)),
maximumNumberOfLines: 0,
lineSpacing: 0.18
)))
], alignment: .left, spacing: 2.0)),
leftIcon: AnyComponentWithIdentity(id: 0, component: AnyComponent(PerkIconComponent(
backgroundColor: UIColor(rgb: 0x4492ff),
foregroundColor: .white,
iconName: "Premium/BusinessPerk/Tag"
))),
action: { _ in
push(accountContext.sharedContext.makeFilterSettingsController(context: accountContext, modal: false, dismissed: nil))
}
))))
perksItems.append(AnyComponentWithIdentity(id: perksItems.count, component: AnyComponent(ListActionItemComponent(
theme: environment.theme,
title: AnyComponent(VStack([
AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: strings.Business_AddPost,
font: Font.regular(presentationData.listsFontSize.baseDisplaySize),
textColor: environment.theme.list.itemPrimaryTextColor
)),
maximumNumberOfLines: 0
))),
AnyComponentWithIdentity(id: AnyHashable(1), component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: strings.Business_AddPostInfo,
font: Font.regular(floor(presentationData.listsFontSize.baseDisplaySize * 13.0 / 17.0)),
textColor: environment.theme.list.itemSecondaryTextColor
)),
maximumNumberOfLines: 0,
lineSpacing: 0.18
)))
], alignment: .left, spacing: 2.0)),
leftIcon: AnyComponentWithIdentity(id: 0, component: AnyComponent(PerkIconComponent(
backgroundColor: UIColor(rgb: 0x41a6a5),
foregroundColor: .white,
iconName: "Premium/Perk/Stories"
))),
action: { _ in
push(accountContext.sharedContext.makeMyStoriesController(context: accountContext, isArchive: false))
}
))))
let moreBusinessSection = moreBusinessSection.update(
component: ListSectionComponent(
theme: environment.theme,
header: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: strings.Business_MoreFeaturesTitle.uppercased(),
font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize),
textColor: environment.theme.list.freeTextColor
)),
maximumNumberOfLines: 0
)),
footer: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: environment.strings.Business_MoreFeaturesInfo,
font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize),
textColor: environment.theme.list.freeTextColor
)),
maximumNumberOfLines: 0
)),
items: perksItems
),
environment: {},
availableSize: CGSize(width: availableWidth - sideInsets, height: .greatestFiniteMagnitude),
transition: context.transition
)
context.add(moreBusinessSection
.position(CGPoint(x: availableWidth / 2.0, y: size.height + moreBusinessSection.size.height / 2.0))
.clipsToBounds(true)
.cornerRadius(10.0)
)
size.height += moreBusinessSection.size.height
size.height += 23.0
}
let copyLink = context.component.copyLink let copyLink = context.component.copyLink
if case .emojiStatus = context.component.source { if case .emojiStatus = context.component.source {
layoutPerks() layoutPerks()
@ -2051,12 +2318,12 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
if case .business = context.component.mode { if case .business = context.component.mode {
layoutBusinessPerks() layoutBusinessPerks()
if context.component.isPremium == false { if context.component.isPremium == true {
layoutPerks() layoutMoreBusinessPerks()
} }
} else { } else {
layoutPerks() layoutPerks()
}
let textPadding: CGFloat = 13.0 let textPadding: CGFloat = 13.0
@ -2202,6 +2469,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
size.height += termsText.size.height size.height += termsText.size.height
size.height += 10.0 size.height += 10.0
} }
}
size.height += scrollEnvironment.insets.bottom size.height += scrollEnvironment.insets.bottom
@ -3271,3 +3539,86 @@ public final class PremiumIntroScreen: ViewControllerComponentContainer {
} }
} }
} }
private final class EmojiActionIconComponent: Component {
let context: AccountContext
let color: UIColor
let fileId: Int64?
let file: TelegramMediaFile?
init(
context: AccountContext,
color: UIColor,
fileId: Int64?,
file: TelegramMediaFile?
) {
self.context = context
self.color = color
self.fileId = fileId
self.file = file
}
static func ==(lhs: EmojiActionIconComponent, rhs: EmojiActionIconComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.color != rhs.color {
return false
}
if lhs.fileId != rhs.fileId {
return false
}
if lhs.file != rhs.file {
return false
}
return true
}
final class View: UIView {
private let icon = ComponentView<Empty>()
func update(component: EmojiActionIconComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
let size = CGSize(width: 24.0, height: 24.0)
let _ = self.icon.update(
transition: .immediate,
component: AnyComponent(EmojiStatusComponent(
context: component.context,
animationCache: component.context.animationCache,
animationRenderer: component.context.animationRenderer,
content: component.fileId.flatMap { .animation(
content: .customEmoji(fileId: $0),
size: size,
placeholderColor: .lightGray,
themeColor: component.color,
loopMode: .forever
) } ?? .premium(color: component.color),
isVisibleForAnimations: false,
action: nil
)),
environment: {},
containerSize: size
)
let iconFrame = CGRect(origin: CGPoint(), size: size)
if let iconView = self.icon.view {
if iconView.superview == nil {
self.addSubview(iconView)
}
iconView.frame = iconFrame
}
return size
}
}
func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}

View File

@ -28,7 +28,6 @@ swift_library(
"//submodules/ShimmerEffect:ShimmerEffect", "//submodules/ShimmerEffect:ShimmerEffect",
"//submodules/ContextUI:ContextUI", "//submodules/ContextUI:ContextUI",
"//submodules/SolidRoundedButtonNode:SolidRoundedButtonNode", "//submodules/SolidRoundedButtonNode:SolidRoundedButtonNode",
"//submodules/PremiumUI:PremiumUI",
], ],
visibility = [ visibility = [
"//visibility:public", "//visibility:public",

View File

@ -236,7 +236,13 @@ public final class ListActionItemComponent: Component {
environment: {}, environment: {},
containerSize: CGSize(width: availableSize.width, height: availableSize.height) containerSize: CGSize(width: availableSize.width, height: availableSize.height)
) )
let iconFrame = CGRect(origin: CGPoint(x: availableSize.width - contentRightInset - iconSize.width, y: floor((contentHeight - iconSize.height) * 0.5)), size: iconSize)
var iconOffset: CGFloat = 0.0
if case .none = component.accessory {
iconOffset = 6.0
}
let iconFrame = CGRect(origin: CGPoint(x: availableSize.width - contentRightInset - iconSize.width + iconOffset, y: floor((contentHeight - iconSize.height) * 0.5)), size: iconSize)
if let iconView = icon.view { if let iconView = icon.view {
if iconView.superview == nil { if iconView.superview == nil {
self.addSubview(iconView) self.addSubview(iconView)

View File

@ -8751,7 +8751,8 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
} }
}) })
case .chatFolders: case .chatFolders:
push(chatListFilterPresetListController(context: self.context, mode: .default)) let controller = self.context.sharedContext.makeFilterSettingsController(context: self.context, modal: false, dismissed: nil)
push(controller)
case .notificationsAndSounds: case .notificationsAndSounds:
if let settings = self.data?.globalSettings { if let settings = self.data?.globalSettings {
push(notificationsAndSoundsController(context: self.context, exceptionsList: settings.notificationExceptions)) push(notificationsAndSoundsController(context: self.context, exceptionsList: settings.notificationExceptions))

View File

@ -1883,6 +1883,10 @@ public final class SharedAccountContextImpl: SharedAccountContext {
return archiveSettingsController(context: context) return archiveSettingsController(context: context)
} }
public func makeFilterSettingsController(context: AccountContext, modal: Bool, dismissed: (() -> Void)?) -> ViewController {
return chatListFilterPresetListController(context: context, mode: modal ? .modal : .default, dismissed: dismissed)
}
public func makeBusinessSetupScreen(context: AccountContext) -> ViewController { public func makeBusinessSetupScreen(context: AccountContext) -> ViewController {
return PremiumIntroScreen(context: context, mode: .business, source: .settings, modal: false, forceDark: false) return PremiumIntroScreen(context: context, mode: .business, source: .settings, modal: false, forceDark: false)
} }