Various improvements

This commit is contained in:
Ilya Laktyushin 2022-05-28 14:58:27 +04:00
parent bdd7d0d9a2
commit 9740eff548
42 changed files with 3160 additions and 476 deletions

View File

@ -7564,6 +7564,7 @@ Sorry for the inconvenience.";
"Premium.MaxPinsText" = "Sorry, you can't pin more than **%1$@** chats to the top. Unpin some of the currently pinned ones or subscribe to **Telegram Premium** to double the limit to **%2$@** chats.";
"Premium.MaxFavedStickersTitle" = "The Limit of %@ Stickers Reached";
"Premium.MaxFavedStickersText" = "An older sticker was replaced with this one. You can [increase the limit]() to %@ stickers.";
"Premium.MaxAccountsText" = "You have reached the limit of **%@** connected accounts. You can free one place by subscribing to **Telegram Premium**.";
"Premium.Free" = "Free";
"Premium.Premium" = "Premium";
@ -7571,7 +7572,7 @@ Sorry for the inconvenience.";
"Premium.Title" = "Telegram Premium";
"Premium.Description" = "Go **beyond the limits**, get **exclusive features** and support us by subscribing to **Telegram Premium**.";
"Premium.PersonalTitle" = "%@ is a subscriber of Telegram Premium";
"Premium.PersonalTitle" = "[%@]() is a subscriber\nof Telegram Premium";
"Premium.PersonalDescription" = "Owners of **Telegram Premium** accounts have exclusive access to multiple additional features.";
"Premium.SubscribedTitle" = "You are all set!";
@ -7595,6 +7596,9 @@ Sorry for the inconvenience.";
"Premium.Reactions" = "Unique Reactions";
"Premium.ReactionsInfo" = "Additional animated reactions on messages, available only to the Premium subscribers.";
"Premium.ReactionsStandalone" = "Additional Reactions";
"Premium.ReactionsStandaloneInfo" = "Unlock a wider range of reactions by subscribing to **Telegram Premium**.";
"Premium.Stickers" = "Premium Stickers";
"Premium.StickersInfo" = "Exclusive enlarged stickers featuring additional effects, updated monthly.";
@ -7614,6 +7618,8 @@ Sorry for the inconvenience.";
"Premium.Terms" = "By purchasing a Premium subscription, you agree to our [Terms of Service](terms) and [Privacy Policy](privacy).";
"Premium.ChargeInfo" = "Next charge: %1$@ on %2$@. [Cancel](cancel).";
"Premium.MoreAboutPremium" = "More About Premium";
"Conversation.CopyProtectionSavingDisabledSecret" = "Saving is restricted";

View File

@ -1323,7 +1323,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
})
})))
if let filter = filters.first(where: { $0.id == id }), case let .filter(_, _, _, data) = filter, data.includePeers.peers.count < premiumLimits.maxFolderChatsCount {
if let _ = filters.first(where: { $0.id == id }) {
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.ChatList_AddChatsToFolder, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Add"), color: theme.contextMenu.primaryColor)
}, action: { c, f in
@ -1348,27 +1348,28 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
for filter in presetList {
if filter.id == id, case let .filter(_, _, _, data) = filter {
let (accountPeer, limits, premiumLimits) = result
let isPremium = accountPeer?.isPremium ?? false
let limit = limits.maxFolderChatsCount
let premiumLimit = premiumLimits.maxFolderChatsCount
if let accountPeer = accountPeer, accountPeer.isPremium {
if data.includePeers.peers.count >= premiumLimit {
return
}
} else {
if data.includePeers.peers.count >= limit {
var replaceImpl: ((ViewController) -> Void)?
let controller = PremiumLimitScreen(context: context, subject: .chatsInFolder, count: Int32(data.includePeers.peers.count), action: {
let controller = PremiumIntroScreen(context: context, source: .chatsPerFolder)
replaceImpl?(controller)
})
replaceImpl = { [weak controller] c in
controller?.replace(with: c)
}
strongSelf.push(controller)
f(.dismissWithoutContent)
return
if data.includePeers.peers.count >= premiumLimit {
let controller = PremiumLimitScreen(context: context, subject: .chatsPerFolder, count: Int32(data.includePeers.peers.count), action: {})
strongSelf.push(controller)
f(.dismissWithoutContent)
return
} else if data.includePeers.peers.count >= limit && !isPremium {
var replaceImpl: ((ViewController) -> Void)?
let controller = PremiumLimitScreen(context: context, subject: .chatsPerFolder, count: Int32(data.includePeers.peers.count), action: {
let controller = PremiumIntroScreen(context: context, source: .chatsPerFolder)
replaceImpl?(controller)
})
replaceImpl = { [weak controller] c in
controller?.replace(with: c)
}
strongSelf.push(controller)
f(.dismissWithoutContent)
return
}
let _ = (strongSelf.context.engine.peers.currentChatListFilters()

View File

@ -605,6 +605,7 @@ private func internalChatListFilterAddChatsController(context: AccountContext, f
queue: Queue.mainQueue(),
controller.result |> take(1),
context.engine.data.get(
TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId),
TelegramEngine.EngineData.Item.Configuration.UserLimits(isPremium: false),
TelegramEngine.EngineData.Item.Configuration.UserLimits(isPremium: true)
)
@ -615,8 +616,9 @@ private func internalChatListFilterAddChatsController(context: AccountContext, f
return
}
let (limits, premiumLimits) = data
let (accountPeer, limits, premiumLimits) = data
let isPremium = accountPeer?.isPremium ?? false
var includePeers: [PeerId] = []
for peerId in peerIds {
switch peerId {
@ -628,15 +630,13 @@ private func internalChatListFilterAddChatsController(context: AccountContext, f
}
includePeers.sort()
if includePeers.count > limits.maxFolderChatsCount {
if includePeers.count > premiumLimits.maxFolderChatsCount {
let alertController = textAlertController(context: context, title: nil, text: presentationData.strings.ChatListFolder_MaxChatsInFolder(Int(premiumLimits.maxFolderChatsCount)).string, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})])
controller?.present(alertController, in: .window(.root))
return
}
if includePeers.count > premiumLimits.maxFolderChatsCount {
let limitController = PremiumLimitScreen(context: context, subject: .chatsPerFolder, count: Int32(includePeers.count), action: {})
controller?.push(limitController)
return
} else if includePeers.count > limits.maxFolderChatsCount && !isPremium {
var replaceImpl: ((ViewController) -> Void)?
let limitController = PremiumLimitScreen(context: context, subject: .chatsInFolder, count: Int32(includePeers.count), action: {
let limitController = PremiumLimitScreen(context: context, subject: .chatsPerFolder, count: Int32(includePeers.count), action: {
let introController = PremiumIntroScreen(context: context, source: .chatsPerFolder)
replaceImpl?(introController)
})
@ -980,28 +980,28 @@ func chatListFilterPresetController(context: AccountContext, currentPreset: Chat
stateWithPeers |> take(1)
).start(next: { result, state in
let (accountPeer, limits, premiumLimits) = result
let isPremium = accountPeer?.isPremium ?? false
let (_, currentIncludePeers, _) = state
let limit = limits.maxFolderChatsCount
let premiumLimit = premiumLimits.maxFolderChatsCount
if let accountPeer = accountPeer, accountPeer.isPremium {
if currentIncludePeers.count >= premiumLimit {
return
}
} else {
if currentIncludePeers.count >= limit {
var replaceImpl: ((ViewController) -> Void)?
let controller = PremiumLimitScreen(context: context, subject: .chatsInFolder, count: Int32(currentIncludePeers.count), action: {
let controller = PremiumIntroScreen(context: context, source: .chatsPerFolder)
replaceImpl?(controller)
})
replaceImpl = { [weak controller] c in
controller?.replace(with: c)
}
pushControllerImpl?(controller)
return
if currentIncludePeers.count >= premiumLimit {
let controller = PremiumLimitScreen(context: context, subject: .chatsPerFolder, count: Int32(currentIncludePeers.count), action: {})
pushControllerImpl?(controller)
return
} else if currentIncludePeers.count >= limit && !isPremium {
var replaceImpl: ((ViewController) -> Void)?
let controller = PremiumLimitScreen(context: context, subject: .chatsPerFolder, count: Int32(currentIncludePeers.count), action: {
let controller = PremiumIntroScreen(context: context, source: .chatsPerFolder)
replaceImpl?(controller)
})
replaceImpl = { [weak controller] c in
controller?.replace(with: c)
}
pushControllerImpl?(controller)
return
}
let state = stateValue.with { $0 }

View File

@ -154,7 +154,11 @@ private enum ChatListFilterPresetListEntry: ItemListNodeEntry {
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, multiline: true, sectionId: self.section)
case let .preset(_, title, label, preset, canBeReordered, canBeDeleted, isEditing, isAllChats, isDisabled):
return ChatListFilterPresetListItem(presentationData: presentationData, preset: preset, title: title, label: label, editing: ChatListFilterPresetListItemEditing(editable: true, editing: isEditing, revealed: false), canBeReordered: canBeReordered, canBeDeleted: canBeDeleted, isAllChats: isAllChats, isDisabled: isDisabled, sectionId: self.section, action: {
arguments.openPreset(preset)
if isDisabled {
arguments.addNew()
} else {
arguments.openPreset(preset)
}
}, setItemWithRevealedOptions: { lhs, rhs in
arguments.setItemWithRevealedOptions(lhs, rhs)
}, remove: {
@ -191,7 +195,7 @@ private func filtersWithAppliedOrder(filters: [(ChatListFilter, Int)], order: [I
return sortedFilters
}
private func chatListFilterPresetListControllerEntries(presentationData: PresentationData, state: ChatListFilterPresetListControllerState, filters: [(ChatListFilter, Int)], updatedFilterOrder: [Int32]?, suggestedFilters: [ChatListFeaturedFilter], settings: ChatListFilterSettings, isPremium: Bool, limits: EngineConfiguration.UserLimits) -> [ChatListFilterPresetListEntry] {
private func chatListFilterPresetListControllerEntries(presentationData: PresentationData, state: ChatListFilterPresetListControllerState, filters: [(ChatListFilter, Int)], updatedFilterOrder: [Int32]?, suggestedFilters: [ChatListFeaturedFilter], settings: ChatListFilterSettings, isPremium: Bool, limits: EngineConfiguration.UserLimits, premiumLimits: EngineConfiguration.UserLimits) -> [ChatListFilterPresetListEntry] {
var entries: [ChatListFilterPresetListEntry] = []
entries.append(.screenHeader(presentationData.strings.ChatListFolderSettings_Info))
@ -217,17 +221,19 @@ private func chatListFilterPresetListControllerEntries(presentationData: Present
if !filters.isEmpty || suggestedFilters.isEmpty {
entries.append(.listHeader(presentationData.strings.ChatListFolderSettings_FoldersSection))
var folderCount = 0
for (filter, chatCount) in filtersWithAppliedOrder(filters: filters, order: updatedFilterOrder) {
if isPremium, case .allChats = filter {
entries.append(.preset(index: PresetIndex(value: entries.count), title: "", label: "", preset: filter, canBeReordered: filters.count > 1, canBeDeleted: false, isEditing: state.isEditing, isAllChats: true, isDisabled: false))
}
if case let .filter(_, title, _, _) = filter {
entries.append(.preset(index: PresetIndex(value: entries.count), title: title, label: chatCount == 0 ? "" : "\(chatCount)", preset: filter, canBeReordered: filters.count > 1, canBeDeleted: true, isEditing: state.isEditing, isAllChats: false, isDisabled: false))
folderCount += 1
entries.append(.preset(index: PresetIndex(value: entries.count), title: title, label: chatCount == 0 ? "" : "\(chatCount)", preset: filter, canBeReordered: filters.count > 1, canBeDeleted: true, isEditing: state.isEditing, isAllChats: false, isDisabled: !isPremium && folderCount > limits.maxFoldersCount))
}
}
if actualFilters.count < limits.maxFoldersCount {
entries.append(.addItem(text: presentationData.strings.ChatListFolderSettings_NewFolder, isEditing: state.isEditing))
}
entries.append(.addItem(text: presentationData.strings.ChatListFolderSettings_NewFolder, isEditing: state.isEditing))
entries.append(.listFooter(presentationData.strings.ChatListFolderSettings_EditFoldersInfo))
}
@ -284,6 +290,7 @@ public func chatListFilterPresetListController(context: AccountContext, mode: Ch
filtersWithCounts.get() |> take(1)
).start(next: { result, filters in
let (accountPeer, limits, premiumLimits) = result
let isPremium = accountPeer?.isPremium ?? false
let filters = filters.filter { filter in
if case .allChats = filter.0 {
@ -291,25 +298,24 @@ public func chatListFilterPresetListController(context: AccountContext, mode: Ch
}
return true
}
let limit = limits.maxFoldersCount
let premiumLimit = premiumLimits.maxFoldersCount
if let accountPeer = accountPeer, accountPeer.isPremium {
if filters.count >= premiumLimit {
return
}
} else {
if filters.count >= limit {
var replaceImpl: ((ViewController) -> Void)?
let controller = PremiumLimitScreen(context: context, subject: .folders, count: Int32(filters.count), action: {
let controller = PremiumIntroScreen(context: context, source: .folders)
replaceImpl?(controller)
})
replaceImpl = { [weak controller] c in
controller?.replace(with: c)
}
pushControllerImpl?(controller)
return
if filters.count >= premiumLimit {
let controller = PremiumLimitScreen(context: context, subject: .folders, count: Int32(filters.count), action: {})
pushControllerImpl?(controller)
return
} else if filters.count >= limit && !isPremium {
var replaceImpl: ((ViewController) -> Void)?
let controller = PremiumLimitScreen(context: context, subject: .folders, count: Int32(filters.count), action: {
let controller = PremiumIntroScreen(context: context, source: .folders)
replaceImpl?(controller)
})
replaceImpl = { [weak controller] c in
controller?.replace(with: c)
}
pushControllerImpl?(controller)
return
}
let _ = (context.engine.peers.updateChatListFiltersInteractively { filters in
var filters = filters
@ -333,6 +339,7 @@ public func chatListFilterPresetListController(context: AccountContext, mode: Ch
filtersWithCounts.get() |> take(1)
).start(next: { result, filters in
let (accountPeer, limits, premiumLimits) = result
let isPremium = accountPeer?.isPremium ?? false
let filters = filters.filter { filter in
if case .allChats = filter.0 {
@ -340,25 +347,24 @@ public func chatListFilterPresetListController(context: AccountContext, mode: Ch
}
return true
}
let limit = limits.maxFoldersCount
let premiumLimit = premiumLimits.maxFoldersCount
if let accountPeer = accountPeer, accountPeer.isPremium {
if filters.count >= premiumLimit {
return
}
} else {
if filters.count >= limit {
var replaceImpl: ((ViewController) -> Void)?
let controller = PremiumLimitScreen(context: context, subject: .folders, count: Int32(filters.count), action: {
let controller = PremiumIntroScreen(context: context, source: .folders)
replaceImpl?(controller)
})
replaceImpl = { [weak controller] c in
controller?.replace(with: c)
}
pushControllerImpl?(controller)
return
if filters.count >= premiumLimit {
let controller = PremiumLimitScreen(context: context, subject: .folders, count: Int32(filters.count), action: {})
pushControllerImpl?(controller)
return
} else if filters.count >= limit && !isPremium {
var replaceImpl: ((ViewController) -> Void)?
let controller = PremiumLimitScreen(context: context, subject: .folders, count: Int32(filters.count), action: {
let controller = PremiumIntroScreen(context: context, source: .folders)
replaceImpl?(controller)
})
replaceImpl = { [weak controller] c in
controller?.replace(with: c)
}
pushControllerImpl?(controller)
return
}
pushControllerImpl?(chatListFilterPresetController(context: context, currentPreset: nil, updated: { _ in }))
})
@ -426,9 +432,10 @@ public func chatListFilterPresetListController(context: AccountContext, mode: Ch
context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)),
limits
)
|> map { presentationData, state, filtersWithCountsValue, preferences, updatedFilterOrderValue, suggestedFilters, peer, limits -> (ItemListControllerState, (ItemListNodeState, Any)) in
|> map { presentationData, state, filtersWithCountsValue, preferences, updatedFilterOrderValue, suggestedFilters, peer, allLimits -> (ItemListControllerState, (ItemListNodeState, Any)) in
let isPremium = peer?.isPremium ?? false
let effectiveLimits = limits.1
let limits = allLimits.0
let premiumLimits = allLimits.1
let filterSettings = preferences.values[ApplicationSpecificPreferencesKeys.chatListFilterSettings]?.get(ChatListFilterSettings.self) ?? ChatListFilterSettings.default
let leftNavigationButton: ItemListNavigationButton?
@ -498,7 +505,7 @@ public func chatListFilterPresetListController(context: AccountContext, mode: Ch
}
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.ChatListFolderSettings_Title), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false)
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: chatListFilterPresetListControllerEntries(presentationData: presentationData, state: state, filters: filtersWithCountsValue, updatedFilterOrder: updatedFilterOrderValue, suggestedFilters: suggestedFilters, settings: filterSettings, isPremium: isPremium, limits: effectiveLimits), style: .blocks, animateChanges: true)
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: chatListFilterPresetListControllerEntries(presentationData: presentationData, state: state, filters: filtersWithCountsValue, updatedFilterOrder: updatedFilterOrderValue, suggestedFilters: suggestedFilters, settings: filterSettings, isPremium: isPremium, limits: limits, premiumLimits: premiumLimits), style: .blocks, animateChanges: true)
return (controllerState, (listState, arguments))
}

View File

@ -200,9 +200,13 @@ private final class ChatListFilterPresetListItemNode: ItemListRevealOptionsItemN
var updatedTheme: PresentationTheme?
var updateArrowImage: UIImage?
if currentItem?.presentationData.theme !== item.presentationData.theme {
if currentItem?.presentationData.theme !== item.presentationData.theme || currentItem?.isDisabled != item.isDisabled {
updatedTheme = item.presentationData.theme
updateArrowImage = PresentationResourcesItemList.disclosureArrowImage(item.presentationData.theme)
if item.isDisabled {
updateArrowImage = PresentationResourcesItemList.disclosureLockedImage(item.presentationData.theme)
} else {
updateArrowImage = PresentationResourcesItemList.disclosureArrowImage(item.presentationData.theme)
}
}
let peerRevealOptions: [ItemListRevealOption]
@ -381,9 +385,13 @@ private final class ChatListFilterPresetListItemNode: ItemListRevealOptionsItemN
}
if let arrowImage = strongSelf.arrowNode.image {
strongSelf.arrowNode.frame = CGRect(origin: CGPoint(x: params.width - params.rightInset - 7.0 - arrowImage.size.width + revealOffset, y: floorToScreenPixels((layout.contentSize.height - arrowImage.size.height) / 2.0)), size: arrowImage.size)
var rightArrowInset = 0.0
if item.isDisabled == true {
rightArrowInset -= 3.0
}
strongSelf.arrowNode.frame = CGRect(origin: CGPoint(x: params.width - params.rightInset - 7.0 - arrowImage.size.width + rightArrowInset + revealOffset, y: floorToScreenPixels((layout.contentSize.height - arrowImage.size.height) / 2.0)), size: arrowImage.size)
}
strongSelf.arrowNode.isHidden = item.isAllChats || item.isDisabled
strongSelf.arrowNode.isHidden = item.isAllChats
strongSelf.activateArea.frame = CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: 0.0), size: CGSize(width: params.width - params.rightInset - 56.0 - (leftInset + revealOffset + editingOffset), height: layout.contentSize.height))
@ -452,8 +460,10 @@ private final class ChatListFilterPresetListItemNode: ItemListRevealOptionsItemN
}
let leftInset: CGFloat = 16.0 + params.leftInset
let rightArrowInset: CGFloat = 34.0 + params.rightInset
var rightArrowInset: CGFloat = 34.0 + params.rightInset
if self.item?.isDisabled == true {
rightArrowInset -= 3.0
}
let editingOffset: CGFloat
if let editableControlNode = self.editableControlNode {
editingOffset = editableControlNode.bounds.size.width

View File

@ -845,15 +845,10 @@ public final class ChatListNode: ListView {
switch result {
case .done:
break
case let .limitExceeded(count, limit):
case let .limitExceeded(count, _):
if isPremium {
let text: String
if chatListFilter != nil {
text = strongSelf.currentState.presentationData.strings.DialogList_UnknownPinLimitError
} else {
text = strongSelf.currentState.presentationData.strings.DialogList_PinLimitError("\(limit)").string
}
strongSelf.presentAlert?(text)
let controller = PremiumLimitScreen(context: context, subject: .pins, count: Int32(count), action: {})
strongSelf.push?(controller)
} else {
var replaceImpl: ((ViewController) -> Void)?
let controller = PremiumLimitScreen(context: context, subject: .pins, count: Int32(count), action: {

View File

@ -99,7 +99,7 @@ public final class _ConcreteChildComponent<ComponentType: Component>: _AnyChildC
view = current.view as! ComponentType.View
} else {
view = component.makeView()
transition = .immediate
transition = transition.withAnimation(.none)
}
let context = view.context(component: component)
@ -342,7 +342,7 @@ public final class _EnvironmentChildComponentFromMap<EnvironmentType>: _AnyChild
view = current.view
} else {
view = component._makeView()
transition = .immediate
transition = transition.withAnimation(.none)
}
let viewContext = view.context(component: component)

View File

@ -115,6 +115,7 @@ public final class SheetComponent<ChildEnvironmentType: Equatable>: Component {
}
private func animateOut(completion: @escaping () -> Void) {
self.isUserInteractionEnabled = false
self.dimView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false)
self.scrollView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: self.bounds.height - self.scrollView.contentInset.top), duration: 0.25, timingFunction: CAMediaTimingFunctionName.easeIn.rawValue, removeOnCompletion: false, additive: true, completion: { _ in
completion()

View File

@ -104,9 +104,7 @@ public final class SolidRoundedButtonComponent: Component {
cornerRadius: component.cornerRadius,
gloss: component.gloss
)
button.iconPosition = component.iconPosition
button.progressType = .embedded
button.icon = component.iconName.flatMap { UIImage(bundleImageName: $0) }
self.button = button
self.addSubview(button)
@ -117,6 +115,10 @@ public final class SolidRoundedButtonComponent: Component {
if let button = self.button {
button.title = component.title
button.iconPosition = component.iconPosition
button.icon = component.iconName.flatMap { UIImage(bundleImageName: $0) }
button.gloss = component.gloss
button.updateTheme(component.theme)
let height = button.updateLayout(width: availableSize.width, transition: .immediate)
transition.setFrame(view: button, frame: CGRect(origin: CGPoint(), size: CGSize(width: availableSize.width, height: height)), completion: nil)

View File

@ -382,7 +382,7 @@ public func generateScaledImage(image: UIImage?, size: CGSize, opaque: Bool = tr
}, opaque: opaque, scale: scale)
}
private func generateSingleColorImage(size: CGSize, color: UIColor) -> UIImage? {
public func generateSingleColorImage(size: CGSize, color: UIColor) -> UIImage? {
return generateImage(size, contextGenerator: { size, context in
context.setFillColor(color.cgColor)
context.fill(CGRect(origin: CGPoint(), size: size))

View File

@ -45,6 +45,8 @@ swift_library(
"//submodules/TextFormat:TextFormat",
"//submodules/GZip:GZip",
"//submodules/InstantPageCache:InstantPageCache",
"//submodules/MediaPlayer:UniversalMediaPlayer",
"//submodules/TelegramUniversalVideoContent:TelegramUniversalVideoContent",
],
visibility = [
"//visibility:public",

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1,44 +0,0 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import ComponentFlow
import AccountContext
final class DemoComponent: Component {
public typealias EnvironmentType = DemoPageEnvironment
let context: AccountContext
public init(
context: AccountContext
) {
self.context = context
}
public static func ==(lhs: DemoComponent, rhs: DemoComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
return true
}
public final class View: UIView {
private var component: DemoComponent?
public func update(component: DemoComponent, availableSize: CGSize, transition: Transition) -> CGSize {
self.component = component
return availableSize
}
}
public func makeView() -> View {
return View()
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<DemoPageEnvironment>, transition: Transition) -> CGSize {
return view.update(component: self, availableSize: availableSize, transition: transition)
}
}

View File

@ -0,0 +1,405 @@
import Foundation
import UIKit
import ComponentFlow
public final class PageIndicatorComponent: Component {
private let pageCount: Int
private let position: CGFloat
private let inactiveColor: UIColor
private let activeColor: UIColor
public init(
pageCount: Int,
position: CGFloat,
inactiveColor: UIColor,
activeColor: UIColor
) {
self.pageCount = pageCount
self.position = position
self.inactiveColor = inactiveColor
self.activeColor = activeColor
}
public static func ==(lhs: PageIndicatorComponent, rhs: PageIndicatorComponent) -> Bool {
if lhs.pageCount != rhs.pageCount {
return false
}
if lhs.position != rhs.position {
return false
}
if !lhs.inactiveColor.isEqual(rhs.inactiveColor) {
return false
}
if !lhs.activeColor.isEqual(rhs.activeColor) {
return false
}
return true
}
public final class View: UIView {
private var component: PageIndicatorComponent?
private let indicatorView: PageIndicatorView
public override init(frame: CGRect) {
self.indicatorView = PageIndicatorView(frame: frame)
super.init(frame: frame)
self.addSubview(self.indicatorView)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public func update(component: PageIndicatorComponent, availableSize: CGSize, transition: Transition) -> CGSize {
self.component = component
self.indicatorView.pageCount = component.pageCount
self.indicatorView.setProgress(progress: component.position)
self.indicatorView.activeColor = component.activeColor
self.indicatorView.inactiveColor = component.inactiveColor
let size = self.indicatorView.intrinsicContentSize
self.indicatorView.frame = CGRect(origin: .zero, size: size)
return size
}
}
public func makeView() -> View {
return View()
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
return view.update(component: self, availableSize: availableSize, transition: transition)
}
}
private final class PageIndicatorView: UIView {
var displayCount: Int {
return min(9, self.pageCount)
}
var dotSize: CGFloat = 8.0
var dotSpace: CGFloat = 10.0
var smallDotSizeRatio: CGFloat = 0.5
var mediumDotSizeRatio: CGFloat = 0.75
public func setCurrentPage(at currentPage: Int, animated: Bool = false) {
guard (currentPage < self.pageCount && currentPage >= 0) else { return }
guard currentPage != self.currentPage else { return }
self.scrollView.layer.removeAllAnimations()
self.updateDot(at: currentPage, animated: animated)
self.currentPage = currentPage
}
public private(set) var currentPage: Int = 0
public var pageCount: Int = 0 {
didSet {
guard self.pageCount != oldValue else {
return
}
self.update(currentPage: self.currentPage)
}
}
public var inactiveColor: UIColor = .gray {
didSet {
guard !self.inactiveColor.isEqual(oldValue) else {
return
}
self.updateDotColor(currentPage: self.currentPage)
}
}
public var activeColor: UIColor = .blue {
didSet {
guard !self.activeColor.isEqual(oldValue) else {
return
}
self.updateDotColor(currentPage: self.currentPage)
}
}
public var animationDuration: Double = 0.3
public init() {
super.init(frame: .zero)
self.setup()
self.updateViewSize()
}
public override init(frame: CGRect) {
super.init(frame: frame)
self.setup()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public override func layoutSubviews() {
super.layoutSubviews()
self.scrollView.center = CGPoint(x: bounds.width / 2, y: bounds.height / 2)
}
public override var intrinsicContentSize: CGSize {
return CGSize(width: self.itemSize * CGFloat(self.displayCount), height: self.itemSize)
}
public func setProgress(progress: CGFloat) {
let currentPage = Int(round(progress * CGFloat(self.pageCount - 1)))
self.setCurrentPage(at: currentPage, animated: true)
}
public func updateViewSize() {
self.bounds.size = intrinsicContentSize
}
private let scrollView = UIScrollView()
private var itemSize: CGFloat {
return self.dotSize + self.dotSpace
}
private var items: [ItemView] = []
private func setup() {
self.backgroundColor = .clear
self.scrollView.backgroundColor = .clear
self.scrollView.isUserInteractionEnabled = false
self.scrollView.showsHorizontalScrollIndicator = false
self.addSubview(self.scrollView)
}
private func update(currentPage: Int) {
if currentPage < self.displayCount {
self.items = (-2..<(self.displayCount + 2))
.map { ItemView(itemSize: self.itemSize, dotSize: self.dotSize, smallDotSizeRatio: self.smallDotSizeRatio, mediumDotSizeRatio: self.mediumDotSizeRatio, index: $0) }
}
else {
guard let firstItem = self.items.first else { return }
guard let lastItem = self.items.last else { return }
self.items = (firstItem.index...lastItem.index)
.map { ItemView(itemSize: self.itemSize, dotSize: self.dotSize, smallDotSizeRatio: self.smallDotSizeRatio, mediumDotSizeRatio: self.mediumDotSizeRatio, index: $0) }
}
self.scrollView.contentSize = .init(width: self.itemSize * CGFloat(self.pageCount), height: self.itemSize)
self.scrollView.subviews.forEach { $0.removeFromSuperview() }
self.items.forEach { self.scrollView.addSubview($0) }
let size: CGSize = .init(width: self.itemSize * CGFloat(self.displayCount), height: self.itemSize)
self.scrollView.bounds.size = size
if self.displayCount < self.pageCount {
self.scrollView.contentInset = .init(top: 0.0, left: self.itemSize * 2.0, bottom: 0, right: self.itemSize * 2.0)
} else {
self.scrollView.contentInset = .zero
}
self.updateDot(at: currentPage, animated: false)
}
private func updateDot(at currentPage: Int, animated: Bool) {
self.updateDotColor(currentPage: currentPage)
if self.pageCount > self.displayCount {
self.updateDotPosition(currentPage: currentPage, animated: animated)
self.updateDotSize(currentPage: currentPage, animated: animated)
}
}
private func updateDotColor(currentPage: Int) {
self.items.forEach {
$0.dotColor = ($0.index == currentPage) ?
self.activeColor : self.inactiveColor
}
}
private func updateDotPosition(currentPage: Int, animated: Bool) {
let duration = animated ? self.animationDuration : 0
if currentPage == 0 {
let x = -self.scrollView.contentInset.left
self.moveScrollView(x: x, duration: duration)
}
else if currentPage == self.pageCount - 1 {
let x = self.scrollView.contentSize.width - self.scrollView.bounds.width + self.scrollView.contentInset.right
self.moveScrollView(x: x, duration: duration)
}
else if CGFloat(currentPage) * self.itemSize <= self.scrollView.contentOffset.x + self.itemSize {
let x = self.scrollView.contentOffset.x - self.itemSize
self.moveScrollView(x: x, duration: duration)
}
else if CGFloat(currentPage) * self.itemSize + self.itemSize >= self.scrollView.contentOffset.x + self.scrollView.bounds.width - self.itemSize {
let x = self.scrollView.contentOffset.x + self.itemSize
self.moveScrollView(x: x, duration: duration)
}
}
private func updateDotSize(currentPage: Int, animated: Bool) {
let duration = animated ? self.animationDuration : 0
self.items.forEach { item in
item.animateDuration = duration
if item.index == currentPage {
item.state = .normal
} else if item.index < 0 {
item.state = .none
} else if item.index > self.pageCount - 1 {
item.state = .none
} else if item.frame.minX <= self.scrollView.contentOffset.x {
item.state = .small
} else if item.frame.maxX >= self.scrollView.contentOffset.x + self.scrollView.bounds.width {
item.state = .small
} else if item.frame.minX <= self.scrollView.contentOffset.x + self.itemSize {
item.state = .medium
} else if item.frame.maxX >= self.scrollView.contentOffset.x + self.scrollView.bounds.width - self.itemSize {
item.state = .medium
} else {
item.state = .normal
}
}
}
private func moveScrollView(x: CGFloat, duration: TimeInterval) {
let direction = self.behaviorDirection(x: x)
self.reusedView(direction: direction)
UIView.animate(withDuration: duration, animations: { [unowned self] in
self.scrollView.contentOffset.x = x
})
}
private enum Direction {
case left
case right
case stay
}
private func behaviorDirection(x: CGFloat) -> Direction {
switch x {
case let x where x > self.scrollView.contentOffset.x:
return .right
case let x where x < self.scrollView.contentOffset.x:
return .left
default:
return .stay
}
}
private func reusedView(direction: Direction) {
guard let firstItem = self.items.first else { return }
guard let lastItem = self.items.last else { return }
switch direction {
case .left:
lastItem.index = firstItem.index - 1
lastItem.frame = CGRect(origin: CGPoint(x: CGFloat(lastItem.index) * self.itemSize, y: 0.0), size: CGSize(width: self.itemSize, height: self.itemSize))
self.items.insert(lastItem, at: 0)
self.items.removeLast()
case .right:
firstItem.index = lastItem.index + 1
firstItem.frame = CGRect(origin: CGPoint(x: CGFloat(firstItem.index) * self.itemSize, y: 0.0), size: CGSize(width: self.itemSize, height: self.itemSize))
self.items.insert(firstItem, at: self.items.count)
self.items.removeFirst()
case .stay:
break
}
}
}
private class ItemView: UIView {
enum State {
case none
case small
case medium
case normal
}
private let dotView = UIView()
var index: Int
var dotColor = UIColor.lightGray {
didSet {
self.dotView.backgroundColor = dotColor
}
}
var state: State = .normal {
didSet {
self.updateDotSize(state: state)
}
}
private let itemSize: CGFloat
private let dotSize: CGFloat
private let smallSizeRatio: CGFloat
private let mediumSizeRatio: CGFloat
var animateDuration: Double = 0.3
init(itemSize: CGFloat, dotSize: CGFloat, smallDotSizeRatio: CGFloat, mediumDotSizeRatio: CGFloat, index: Int) {
self.itemSize = itemSize
self.dotSize = dotSize
self.mediumSizeRatio = mediumDotSizeRatio
self.smallSizeRatio = smallDotSizeRatio
self.index = index
let x = itemSize * CGFloat(index)
let frame = CGRect(x: x, y: 0, width: itemSize, height: itemSize)
super.init(frame: frame)
self.backgroundColor = UIColor.clear
self.dotView.frame.size = CGSize(width: dotSize, height: dotSize)
self.dotView.center = CGPoint(x: itemSize / 2.0, y: itemSize / 2.0)
self.dotView.backgroundColor = self.dotColor
self.dotView.layer.cornerRadius = dotSize / 2.0
self.dotView.layer.masksToBounds = true
addSubview(dotView)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func updateDotSize(state: State) {
var size: CGSize
switch state {
case .normal:
size = CGSize(width: self.dotSize, height: self.dotSize)
case .medium:
size = CGSize(width: self.dotSize * self.mediumSizeRatio, height: self.dotSize * self.mediumSizeRatio)
case .small:
size = CGSize( width: self.dotSize * self.smallSizeRatio, height: self.dotSize * self.smallSizeRatio
)
case .none:
size = CGSize.zero
}
UIView.animate(withDuration: self.animateDuration, animations: { [unowned self] in
self.dotView.layer.cornerRadius = size.height / 2.0
self.dotView.layer.bounds.size = size
})
}
}

View File

@ -0,0 +1,392 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import Postbox
import TelegramCore
import ComponentFlow
import AccountContext
import AppBundle
import UniversalMediaPlayer
import TelegramUniversalVideoContent
private let phoneSize = CGSize(width: 262.0, height: 539.0)
private var phoneBorderImage = {
return generateImage(phoneSize, rotatedContext: { size, context in
context.clear(CGRect(origin: .zero, size: size))
context.setFillColor(UIColor(rgb: 0x000000, alpha: 0.5).cgColor)
try? drawSvgPath(context, path: "M203.506,7.0 C211.281,0.0 217.844,0.0 223.221,0.439253 C228.851,0.899605 234.245,1.90219 239.377,4.51905 C247.173,8.49411 253.512,14.8369 257.484,22.6384 C260.099,27.7743 261.101,33.1718 261.561,38.8062 C262.0,44.1865 262.0,50.754 262.0,58.5351 V480.465 C262.0,488.246 262.0,494.813 261.561,500.194 C261.101,505.828 260.099,511.226 257.484,516.362 C253.512,524.163 247.173,530.506 239.377,534.481 C234.245,537.098 228.851,538.1 223.221,538.561 C217.844,539.0 211.281,539.0 203.506,539.0 H58.4942 C50.7185,539 44.1556,539.0 38.7791,538.561 C33.1486,538.1 27.7549,537.098 22.6226,534.481 C14.8265,530.506 8.48817,524.163 4.51589,516.362 C1.90086,511.226 0.898976,505.828 0.438946,500.194 C0.0,494.813 0.0,488.246 7.0,480.465 V58.5354 C0.0,50.7541 0.0,44.1866 0.438946,38.8062 C0.898976,33.1718 1.90086,27.7743 4.51589,22.6384 C8.48817,14.8369 14.8265,8.49411 22.6226,4.51905 C27.7549,1.90219 33.1486,0.899605 38.7791,0.439253 C44.1557,-0.0 50.7187,0.0 58.4945,7.0 H203.506 Z ")
context.setBlendMode(.copy)
context.fill(CGRect(origin: CGPoint(x: 43.0, y: UIScreenPixel), size: CGSize(width: 175.0, height: 8.0)))
context.fill(CGRect(origin: CGPoint(x: UIScreenPixel, y: 43.0), size: CGSize(width: 8.0, height: 452.0)))
context.setBlendMode(.clear)
try? drawSvgPath(context, path: "M15.3737,28.1746 C12.1861,34.4352 12.1861,42.6307 12.1861,59.0217 V479.978 C12.1861,496.369 12.1861,504.565 15.3737,510.825 C18.1777,516.332 22.6518,520.81 28.1549,523.615 C34.4111,526.805 42.6009,526.805 58.9805,526.805 H203.02 C219.399,526.805 227.589,526.805 233.845,523.615 C239.348,520.81 243.822,516.332 246.626,510.825 C249.814,504.565 249.814,496.369 249.814,479.978 V59.0217 C249.814,42.6307 249.814,34.4352 246.626,28.1746 C243.822,22.6677 239.348,18.1904 233.845,15.3845 C227.589,12.1946 219.399,12.1946 203.02,12.1946 H58.9805 C42.6009,12.1946 34.4111,12.1946 28.1549,15.3845 C22.6518,18.1904 18.1777,22.6677 15.3737,28.1746 Z ")
context.setBlendMode(.copy)
context.setFillColor(UIColor.black.cgColor)
try? drawSvgPath(context, path: "M222.923,4.08542 C217.697,3.65815 211.263,3.65823 203.378,3.65833 H58.6219 C50.7366,3.65823 44.3026,3.65815 39.0768,4.08542 C33.6724,4.52729 28.8133,5.46834 24.2823,7.77863 C17.1741,11.4029 11.395,17.1861 7.77325,24.2992 C5.46457,28.8334 4.52418,33.6959 4.08262,39.1041 C3.65565,44.3336 3.65573,50.7721 3.65583,58.6628 V480.337 C3.65573,488.228 3.65565,494.666 4.08262,499.896 C4.52418,505.304 5.46457,510.167 7.77325,514.701 C11.395,521.814 17.1741,527.597 24.2823,531.221 C28.8133,533.532 33.6724,534.473 39.0768,534.915 C44.3028,535.342 50.737,535.342 58.6226,535.342 H203.377 C211.263,535.342 217.697,535.342 222.923,534.915 C228.328,534.473 233.187,533.532 237.718,531.221 C244.826,527.597 250.605,521.814 254.227,514.701 C256.535,510.167 257.476,505.304 257.917,499.896 C258.344,494.667 258.344,488.228 258.344,480.338 V58.6617 C258.344,50.7714 258.344,44.3333 257.917,39.1041 C257.476,33.6959 256.535,28.8334 254.227,24.2992 C250.605,17.1861 244.826,11.4029 237.718,7.77863 C233.187,5.46834 228.328,4.52729 222.923,4.08542 Z ")
context.setBlendMode(.clear)
try? drawSvgPath(context, path: "M12.1861,59.0217 C12.1861,42.6306 12.1861,34.4351 15.3737,28.1746 C18.1777,22.6676 22.6519,18.1904 28.1549,15.3844 C34.4111,12.1945 42.6009,12.1945 58.9805,12.1945 H76.6868 L76.8652,12.1966 C78.1834,12.2201 79.0316,12.4428 79.7804,12.8418 C80.5733,13.2644 81.1963,13.8848 81.6226,14.6761 C81.9735,15.3276 82.1908,16.0553 82.2606,17.1064 C82.3128,22.5093 82.9306,24.5829 84.0474,26.6727 C85.2157,28.8587 86.9301,30.5743 89.1145,31.7434 C91.299,32.9124 93.4658,33.535 99.441,33.535 H162.561 C168.537,33.535 170.703,32.9124 172.888,31.7434 C175.072,30.5743 176.787,28.8587 177.955,26.6727 C179.072,24.5829 179.69,22.5093 179.742,17.1051 C179.812,16.0553 180.029,15.3276 180.38,14.6761 C180.806,13.8848 181.429,13.2644 182.222,12.8418 C182.971,12.4428 183.819,12.2201 185.137,12.1966 L185.316,12.1945 H203.02 C219.399,12.1945 227.589,12.1945 233.845,15.3844 C239.348,18.1904 243.822,22.6676 246.626,28.1746 C249.814,34.4351 249.814,42.6306 249.814,59.0217 V479.978 C249.814,496.369 249.814,504.565 246.626,510.825 C243.822,516.332 239.348,520.81 233.845,523.615 C227.589,526.805 219.399,526.805 203.02,526.805 H58.9805 C42.6009,526.805 34.4111,526.805 28.1549,523.615 C22.6519,520.81 18.1777,516.332 15.3737,510.825 C12.1861,504.565 12.1861,496.369 12.1861,479.978 V59.0217 Z")
})
}()
private final class PhoneView: UIView {
let contentContainerView: UIView
let overlayView: UIView
let borderView: UIImageView
fileprivate var videoNode: UniversalVideoNode?
var screenRotation: CGFloat = 0.0 {
didSet {
if self.screenRotation > 0.0 {
self.overlayView.backgroundColor = .white
} else {
self.overlayView.backgroundColor = .black
}
self.contentContainerView.alpha = self.screenRotation > 0.0 ? 1.0 - self.screenRotation : 1.0
self.overlayView.alpha = self.screenRotation > 0.0 ? self.screenRotation * 0.5 : self.screenRotation * -1.0
}
}
override init(frame: CGRect) {
self.contentContainerView = UIView()
self.contentContainerView.clipsToBounds = true
self.contentContainerView.backgroundColor = .darkGray
self.contentContainerView.layer.cornerRadius = 10.0
self.overlayView = UIView()
self.overlayView.backgroundColor = .black
self.borderView = UIImageView(image: phoneBorderImage)
super.init(frame: frame)
self.addSubview(self.contentContainerView)
self.contentContainerView.addSubview(self.overlayView)
self.addSubview(self.borderView)
}
private var position: PhoneDemoComponent.Position = .top
func setup(context: AccountContext, videoName: String?, position: PhoneDemoComponent.Position) {
self.position = position
guard self.videoNode == nil, let videoName = videoName, let path = getAppBundle().path(forResource: videoName, ofType: "mp4"), let size = fileSize(path) else {
return
}
self.contentContainerView.backgroundColor = .clear
let dimensions = PixelDimensions(width: 1170, height: 1754)
let id = Int64.random(in: 0..<Int64.max)
let dummyFile = TelegramMediaFile(fileId: MediaId(namespace: 0, id: id), partialReference: nil, resource: LocalFileReferenceMediaResource(localFilePath: path, randomId: id), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "video/mp4", size: size, attributes: [.Video(duration: 3, size: dimensions, flags: [])])
let videoContent = NativeVideoContent(id: .message(1, MediaId(namespace: 0, id: id)), fileReference: .standalone(media: dummyFile), streamVideo: .none, loopVideo: true, enableSound: false, fetchAutomatically: true, onlyFullSizeThumbnail: false, continuePlayingWithoutSoundOnLostAudioSession: false, placeholderColor: .clear)
let videoNode = UniversalVideoNode(postbox: context.account.postbox, audioSession: context.sharedContext.mediaManager.audioSession, manager: context.sharedContext.mediaManager.universalVideoManager, decoration: VideoDecoration(), content: videoContent, priority: .embedded)
videoNode.canAttachContent = true
self.videoNode = videoNode
self.contentContainerView.insertSubview(videoNode.view, at: 0)
videoNode.pause()
self.setNeedsLayout()
}
private var isPlaying = false
func play() {
if let videoNode = self.videoNode, !self.isPlaying {
self.isPlaying = true
videoNode.play()
}
}
func reset() {
if let videoNode = self.videoNode {
self.isPlaying = false
videoNode.pause()
videoNode.seek(0.0)
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
if let phoneImage = self.borderView.image {
self.borderView.frame = CGRect(origin: .zero, size: phoneImage.size)
self.contentContainerView.frame = CGRect(origin: CGPoint(x: 12.0, y: 12.0), size: CGSize(width: phoneImage.size.width - 24.0, height: phoneImage.size.height - 24.0))
self.overlayView.frame = self.contentContainerView.bounds
if let videoNode = self.videoNode {
let videoSize = CGSize(width: self.contentContainerView.frame.width, height: 353.0)
videoNode.view.frame = CGRect(origin: CGPoint(x: 0.0, y: self.position == .top ? 0.0 : self.contentContainerView.frame.height - videoSize.height), size: videoSize)
videoNode.updateLayout(size: videoSize, transition: .immediate)
}
}
}
}
final class PhoneDemoComponent: Component {
typealias EnvironmentType = DemoPageEnvironment
enum Position {
case top
case bottom
}
let context: AccountContext
let position: Position
let videoName: String?
public init(
context: AccountContext,
position: PhoneDemoComponent.Position,
videoName: String?
) {
self.context = context
self.position = position
self.videoName = videoName
}
public static func ==(lhs: PhoneDemoComponent, rhs: PhoneDemoComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.position != rhs.position {
return false
}
if lhs.videoName != rhs.videoName {
return false
}
return true
}
final class View: UIView, ComponentTaggedView {
final class Tag {
}
func matches(tag: Any) -> Bool {
if let _ = tag as? Tag, self.isCentral {
return true
}
return false
}
private var isCentral = false
private var component: PhoneDemoComponent?
private let containerView: UIView
private let phoneView: PhoneView
public var ready: Signal<Bool, NoError> {
if let videoNode = self.phoneView.videoNode {
return videoNode.ready
|> map { _ in
return true
}
} else {
return .single(true)
}
}
public override init(frame: CGRect) {
self.containerView = UIView(frame: frame)
self.containerView.clipsToBounds = true
self.phoneView = PhoneView(frame: CGRect(origin: .zero, size: phoneSize))
super.init(frame: frame)
self.addSubview(self.containerView)
self.containerView.addSubview(self.phoneView)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public func update(component: PhoneDemoComponent, availableSize: CGSize, environment: Environment<DemoPageEnvironment>, transition: Transition) -> CGSize {
self.component = component
self.phoneView.setup(context: component.context, videoName: component.videoName, position: component.position)
self.containerView.frame = CGRect(origin: .zero, size: availableSize)
self.phoneView.bounds = CGRect(origin: .zero, size: phoneSize)
var mappedPosition = environment[DemoPageEnvironment.self].position
mappedPosition *= abs(mappedPosition)
let phoneY: CGFloat
switch component.position {
case .top:
phoneY = phoneSize.height / 2.0 + 24.0 + abs(mappedPosition) * 24.0
case .bottom:
phoneY = availableSize.height - phoneSize.height / 2.0 - 24.0 - abs(mappedPosition) * 24.0
}
let isVisible = environment[DemoPageEnvironment.self].isDisplaying
let isCentral = environment[DemoPageEnvironment.self].isCentral
self.isCentral = isCentral
self.phoneView.center = CGPoint(x: availableSize.width / 2.0 + mappedPosition * 50.0, y: phoneY)
self.phoneView.screenRotation = mappedPosition * -0.7
var perspective = CATransform3DIdentity
perspective.m34 = mappedPosition / 50.0
self.phoneView.layer.transform = CATransform3DRotate(perspective, 0.1, 0, 1, 0)
if abs(mappedPosition) < .ulpOfOne {
self.phoneView.play()
} else if !isVisible {
self.phoneView.reset()
}
if let _ = transition.userData(DemoAnimateInTransition.self), abs(mappedPosition) < .ulpOfOne {
let from: CGFloat
switch component.position {
case .top:
from = -200.0
case .bottom:
from = 200.0
}
self.containerView.layer.animateBoundsOriginYAdditive(from: from, to: 0.0, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring)
}
return availableSize
}
}
public func makeView() -> View {
return View()
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<DemoPageEnvironment>, transition: Transition) -> CGSize {
return view.update(component: self, availableSize: availableSize, environment: environment, transition: transition)
}
}
private final class VideoDecoration: UniversalVideoDecoration {
public let backgroundNode: ASDisplayNode? = nil
public let contentContainerNode: ASDisplayNode
public let foregroundNode: ASDisplayNode? = nil
private var contentNode: (ASDisplayNode & UniversalVideoContentNode)?
private var validLayoutSize: CGSize?
public init() {
self.contentContainerNode = ASDisplayNode()
}
public func updateContentNode(_ contentNode: (UniversalVideoContentNode & ASDisplayNode)?) {
if self.contentNode !== contentNode {
let previous = self.contentNode
self.contentNode = contentNode
if let previous = previous {
if previous.supernode === self.contentContainerNode {
previous.removeFromSupernode()
}
}
if let contentNode = contentNode {
if contentNode.supernode !== self.contentContainerNode {
self.contentContainerNode.addSubnode(contentNode)
if let validLayoutSize = self.validLayoutSize {
contentNode.frame = CGRect(origin: CGPoint(), size: validLayoutSize)
contentNode.updateLayout(size: validLayoutSize, transition: .immediate)
}
}
}
}
}
public func updateCorners(_ corners: ImageCorners) {
self.contentContainerNode.clipsToBounds = true
if isRoundEqualCorners(corners) {
self.contentContainerNode.cornerRadius = corners.topLeft.radius
} else {
let boundingSize: CGSize = CGSize(width: max(corners.topLeft.radius, corners.bottomLeft.radius) + max(corners.topRight.radius, corners.bottomRight.radius), height: max(corners.topLeft.radius, corners.topRight.radius) + max(corners.bottomLeft.radius, corners.bottomRight.radius))
let size: CGSize = CGSize(width: boundingSize.width + corners.extendedEdges.left + corners.extendedEdges.right, height: boundingSize.height + corners.extendedEdges.top + corners.extendedEdges.bottom)
let arguments = TransformImageArguments(corners: corners, imageSize: size, boundingSize: boundingSize, intrinsicInsets: UIEdgeInsets())
let context = DrawingContext(size: size, clear: true)
context.withContext { ctx in
ctx.setFillColor(UIColor.black.cgColor)
ctx.fill(arguments.drawingRect)
}
addCorners(context, arguments: arguments)
if let maskImage = context.generateImage() {
let mask = CALayer()
mask.contents = maskImage.cgImage
mask.contentsScale = maskImage.scale
mask.contentsCenter = CGRect(x: max(corners.topLeft.radius, corners.bottomLeft.radius) / maskImage.size.width, y: max(corners.topLeft.radius, corners.topRight.radius) / maskImage.size.height, width: (maskImage.size.width - max(corners.topLeft.radius, corners.bottomLeft.radius) - max(corners.topRight.radius, corners.bottomRight.radius)) / maskImage.size.width, height: (maskImage.size.height - max(corners.topLeft.radius, corners.topRight.radius) - max(corners.bottomLeft.radius, corners.bottomRight.radius)) / maskImage.size.height)
self.contentContainerNode.layer.mask = mask
self.contentContainerNode.layer.mask?.frame = self.contentContainerNode.bounds
}
}
}
public func updateClippingFrame(_ frame: CGRect, completion: (() -> Void)?) {
self.contentContainerNode.layer.animate(from: NSValue(cgRect: self.contentContainerNode.bounds), to: NSValue(cgRect: frame), keyPath: "bounds", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25, removeOnCompletion: false, completion: { _ in
})
if let maskLayer = self.contentContainerNode.layer.mask {
maskLayer.animate(from: NSValue(cgRect: self.contentContainerNode.bounds), to: NSValue(cgRect: frame), keyPath: "bounds", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25, removeOnCompletion: false, completion: { _ in
})
maskLayer.animate(from: NSValue(cgPoint: maskLayer.position), to: NSValue(cgPoint: CGPoint(x: frame.midX, y: frame.midY)), keyPath: "position", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25, removeOnCompletion: false, completion: { _ in
})
}
if let contentNode = self.contentNode {
contentNode.layer.animate(from: NSValue(cgPoint: contentNode.layer.position), to: NSValue(cgPoint: CGPoint(x: frame.midX, y: frame.midY)), keyPath: "position", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25, removeOnCompletion: false, completion: { _ in
completion?()
})
}
}
public func updateContentNodeSnapshot(_ snapshot: UIView?) {
}
public func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) {
self.validLayoutSize = size
let bounds = CGRect(origin: CGPoint(), size: size)
if let backgroundNode = self.backgroundNode {
transition.updateFrame(node: backgroundNode, frame: bounds)
}
if let foregroundNode = self.foregroundNode {
transition.updateFrame(node: foregroundNode, frame: bounds)
}
transition.updateFrame(node: self.contentContainerNode, frame: bounds)
if let maskLayer = self.contentContainerNode.layer.mask {
transition.updateFrame(layer: maskLayer, frame: bounds)
}
if let contentNode = self.contentNode {
transition.updateFrame(node: contentNode, frame: CGRect(origin: CGPoint(), size: size))
contentNode.updateLayout(size: size, transition: transition)
}
}
public func setStatus(_ status: Signal<MediaPlayerStatus?, NoError>) {
}
public func tap() {
}
}

View File

@ -130,10 +130,12 @@ private final class GradientBackgroundComponent: Component {
final class DemoPageEnvironment: Equatable {
public let isDisplaying: Bool
public let isCentral: Bool
public let position: CGFloat
public init(isDisplaying: Bool, isCentral: Bool) {
public init(isDisplaying: Bool, isCentral: Bool, position: CGFloat) {
self.isDisplaying = isDisplaying
self.isCentral = isCentral
self.position = position
}
public static func ==(lhs: DemoPageEnvironment, rhs: DemoPageEnvironment) -> Bool {
@ -143,6 +145,9 @@ final class DemoPageEnvironment: Equatable {
if lhs.isCentral != rhs.isCentral {
return false
}
if lhs.position != rhs.position {
return false
}
return true
}
}
@ -192,8 +197,8 @@ private final class PageComponent<ChildEnvironment: Equatable>: CombinedComponen
let availableSize = context.availableSize
let component = context.component
let sideInset: CGFloat = 16.0 //+ environment.safeInsets.left
let textSideInset: CGFloat = 24.0 //+ environment.safeInsets.left
let sideInset: CGFloat = 16.0
let textSideInset: CGFloat = 24.0
let textColor = component.textColor
let textFont = Font.regular(17.0)
@ -223,9 +228,14 @@ private final class PageComponent<ChildEnvironment: Equatable>: CombinedComponen
transition: .immediate
)
let markdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor), link: MarkdownAttributeSet(font: textFont, textColor: textColor), linkAttribute: { _ in
return nil
})
let markdownAttributes = MarkdownAttributes(
body: MarkdownAttributeSet(font: textFont, textColor: textColor),
bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor),
link: MarkdownAttributeSet(font: textFont, textColor: textColor),
linkAttribute: { _ in
return nil
}
)
let text = text.update(
component: MultilineTextComponent(
text: .markdown(text: component.text, attributes: markdownAttributes),
@ -269,28 +279,42 @@ private final class DemoPagerComponent: Component {
}
}
public let items: [Item]
public let index: Int
let items: [Item]
let index: Int
let activeColor: UIColor
let inactiveColor: UIColor
public init(
items: [Item],
index: Int = 0
index: Int = 0,
activeColor: UIColor,
inactiveColor: UIColor
) {
self.items = items
self.index = index
self.activeColor = activeColor
self.inactiveColor = inactiveColor
}
public static func ==(lhs: DemoPagerComponent, rhs: DemoPagerComponent) -> Bool {
if lhs.items != rhs.items {
return false
}
if !lhs.activeColor.isEqual(rhs.activeColor) {
return false
}
if !lhs.inactiveColor.isEqual(rhs.inactiveColor) {
return false
}
return true
}
public final class View: UIView, UIScrollViewDelegate {
fileprivate final class View: UIView, UIScrollViewDelegate {
private let scrollView: UIScrollView
private var itemViews: [AnyHashable: ComponentHostView<DemoPageEnvironment>] = [:]
private let pageIndicatorView: ComponentHostView<Empty>
private var component: DemoPagerComponent?
override init(frame: CGRect) {
@ -302,11 +326,15 @@ private final class DemoPagerComponent: Component {
self.scrollView.bounces = false
self.scrollView.layer.cornerRadius = 10.0
self.pageIndicatorView = ComponentHostView<Empty>()
self.pageIndicatorView.isUserInteractionEnabled = false
super.init(frame: frame)
self.scrollView.delegate = self
self.addSubview(self.scrollView)
self.addSubview(self.pageIndicatorView)
}
required init?(coder: NSCoder) {
@ -318,22 +346,10 @@ private final class DemoPagerComponent: Component {
return
}
for item in component.items {
if let itemView = self.itemViews[item.content.id] {
let isDisplaying = itemView.frame.intersects(self.scrollView.bounds)
let environment = DemoPageEnvironment(isDisplaying: isDisplaying, isCentral: isDisplaying)
let _ = itemView.update(
transition: .immediate,
component: item.content.component,
environment: { environment },
containerSize: self.bounds.size
)
}
}
let _ = self.update(component: component, availableSize: self.bounds.size, transition: .immediate)
}
func update(component: DemoPagerComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
func update(component: DemoPagerComponent, availableSize: CGSize, transition: Transition) -> CGSize {
var validIds: [AnyHashable] = []
let firstTime = self.itemViews.isEmpty
@ -342,14 +358,30 @@ private final class DemoPagerComponent: Component {
if self.scrollView.contentSize != contentSize {
self.scrollView.contentSize = contentSize
}
self.scrollView.frame = CGRect(origin: .zero, size: availableSize)
let scrollFrame = CGRect(origin: .zero, size: availableSize)
if self.scrollView.frame != scrollFrame {
self.scrollView.frame = scrollFrame
}
if firstTime {
self.scrollView.contentOffset = CGPoint(x: CGFloat(component.index) * availableSize.width, y: 0.0)
}
let viewportCenter = self.scrollView.contentOffset.x + availableSize.width * 0.5
var i = 0
for item in component.items {
let itemFrame = CGRect(origin: CGPoint(x: availableSize.width * CGFloat(i), y: 0.0), size: availableSize)
let isDisplaying = itemFrame.intersects(self.scrollView.bounds)
let centerDelta = itemFrame.midX - viewportCenter
let position = centerDelta / (availableSize.width * 0.75)
i += 1
if abs(position) > 1.5 {
continue
}
validIds.append(item.content.id)
let itemView: ComponentHostView<DemoPageEnvironment>
@ -363,11 +395,8 @@ private final class DemoPagerComponent: Component {
self.itemViews[item.content.id] = itemView
self.scrollView.addSubview(itemView)
}
let itemFrame = CGRect(origin: CGPoint(x: availableSize.width * CGFloat(i), y: 0.0), size: availableSize)
let isDisplaying = itemFrame.intersects(self.scrollView.bounds)
let environment = DemoPageEnvironment(isDisplaying: isDisplaying, isCentral: isDisplaying)
let environment = DemoPageEnvironment(isDisplaying: isDisplaying, isCentral: abs(centerDelta) < CGFloat.ulpOfOne, position: position)
let _ = itemView.update(
transition: itemTransition,
component: item.content.component,
@ -376,8 +405,6 @@ private final class DemoPagerComponent: Component {
)
itemView.frame = itemFrame
i += 1
}
var removeIds: [AnyHashable] = []
@ -393,6 +420,24 @@ private final class DemoPagerComponent: Component {
self.component = component
if component.items.count > 1 {
let pageIndicatorComponent = PageIndicatorComponent(
pageCount: component.items.count,
position: self.scrollView.contentOffset.x / (self.scrollView.contentSize.width - availableSize.width),
inactiveColor: component.inactiveColor,
activeColor: component.activeColor
)
let indicatorSize = self.pageIndicatorView.update(
transition: .immediate,
component: AnyComponent(
pageIndicatorComponent
),
environment: {},
containerSize: availableSize
)
self.pageIndicatorView.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - indicatorSize.width) / 2.0), y: availableSize.height - indicatorSize.height - 11.0), size: indicatorSize)
}
return availableSize
}
}
@ -402,10 +447,13 @@ private final class DemoPagerComponent: Component {
}
public 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)
return view.update(component: self, availableSize: availableSize, transition: transition)
}
}
public final class DemoAnimateInTransition {
}
private final class DemoSheetContent: CombinedComponent {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
@ -440,56 +488,63 @@ private final class DemoSheetContent: CombinedComponent {
private let context: AccountContext
var cachedCloseImage: UIImage?
var isPremium: Bool?
var reactions: [AvailableReactions.Reaction]?
var stickers: [TelegramMediaFile]?
var reactionsDisposable: Disposable?
var stickersDisposable: Disposable?
var disposable: Disposable?
init(context: AccountContext) {
self.context = context
super.init()
self.reactionsDisposable = (self.context.engine.stickers.availableReactions()
|> map { reactions -> [AvailableReactions.Reaction] in
if let reactions = reactions {
return reactions.reactions.filter { $0.isPremium }
} else {
return []
let stickersKey: PostboxViewKey = .orderedItemList(id: Namespaces.OrderedItemList.CloudPremiumStickers)
self.disposable = (combineLatest(
queue: Queue.mainQueue(),
self.context.engine.stickers.availableReactions(),
self.context.account.postbox.combinedView(keys: [stickersKey])
|> map { views -> [OrderedItemListEntry]? in
if let view = views.views[stickersKey] as? OrderedItemListView, !view.items.isEmpty {
return view.items
} else {
return nil
}
}
}
|> deliverOnMainQueue).start(next: { [weak self] reactions in
|> filter { items in
return items != nil
}
|> take(1),
self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId))
)
|> map { reactions, items, accountPeer -> ([AvailableReactions.Reaction], [TelegramMediaFile], Bool?) in
if let reactions = reactions {
var result: [TelegramMediaFile] = []
if let items = items {
for item in items {
if let mediaItem = item.contents.get(RecentMediaItem.self) {
result.append(mediaItem.media)
}
}
}
return (reactions.reactions.filter({ $0.isPremium }), result, accountPeer?.isPremium ?? false)
} else {
return ([], [], nil)
}
}).start(next: { [weak self] reactions, stickers, isPremium in
guard let strongSelf = self else {
return
}
strongSelf.reactions = reactions
strongSelf.updated(transition: .immediate)
})
self.stickersDisposable = (self.context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: [Namespaces.OrderedItemList.PremiumStickers], namespaces: [Namespaces.ItemCollection.CloudDice], aroundIndex: nil, count: 100)
|> map { view -> [TelegramMediaFile] in
var result: [TelegramMediaFile] = []
if let premiumStickers = view.orderedItemListsViews.first {
for i in 0 ..< premiumStickers.items.count {
if let item = premiumStickers.items[i].contents.get(RecentMediaItem.self) {
result.append(item.media)
}
}
}
return result
}
|> deliverOnMainQueue).start(next: { [weak self] stickers in
guard let strongSelf = self else {
return
}
strongSelf.stickers = stickers
strongSelf.updated(transition: .immediate)
strongSelf.isPremium = isPremium
if !reactions.isEmpty && !stickers.isEmpty {
strongSelf.updated(transition: Transition(.immediate).withUserData(DemoAnimateInTransition()))
}
})
}
deinit {
self.reactionsDisposable?.dispose()
self.stickersDisposable?.dispose()
self.disposable?.dispose()
}
}
@ -502,7 +557,6 @@ private final class DemoSheetContent: CombinedComponent {
let background = Child(GradientBackgroundComponent.self)
let pager = Child(DemoPagerComponent.self)
let button = Child(SolidRoundedButtonComponent.self)
let dots = Child(BundleIconComponent.self)
return { context in
let environment = context.environment[ViewControllerComponentContainer.Environment.self].value
@ -532,21 +586,28 @@ private final class DemoSheetContent: CombinedComponent {
if let image = state.cachedCloseImage {
closeImage = image
} else {
closeImage = generateCloseButtonImage(backgroundColor: UIColor(rgb: 0xffffff, alpha: 0.1), foregroundColor: UIColor(rgb: 0xffffff))!
closeImage = generateCloseButtonImage(backgroundColor: .clear, foregroundColor: UIColor(rgb: 0xffffff))!
state.cachedCloseImage = closeImage
}
var isStandalone = false
if case .other = component.source {
isStandalone = true
}
if let reactions = state.reactions, let stickers = state.stickers {
let textColor = theme.actionSheet.primaryTextColor
let items: [DemoPagerComponent.Item] = [
var items: [DemoPagerComponent.Item] = [
DemoPagerComponent.Item(
AnyComponentWithIdentity(
id: PremiumDemoScreen.Subject.moreUpload,
component: AnyComponent(
PageComponent(
content: AnyComponent(DemoComponent(
context: component.context
content: AnyComponent(PhoneDemoComponent(
context: component.context,
position: .bottom,
videoName: "4gb"
)),
title: strings.Premium_UploadSize,
text: strings.Premium_UploadSizeInfo,
@ -560,8 +621,10 @@ private final class DemoSheetContent: CombinedComponent {
id: PremiumDemoScreen.Subject.fasterDownload,
component: AnyComponent(
PageComponent(
content: AnyComponent(DemoComponent(
context: component.context
content: AnyComponent(PhoneDemoComponent(
context: component.context,
position: .top,
videoName: "fastdownload"
)),
title: strings.Premium_FasterSpeed,
text: strings.Premium_FasterSpeedInfo,
@ -575,8 +638,10 @@ private final class DemoSheetContent: CombinedComponent {
id: PremiumDemoScreen.Subject.voiceToText,
component: AnyComponent(
PageComponent(
content: AnyComponent(DemoComponent(
context: component.context
content: AnyComponent(PhoneDemoComponent(
context: component.context,
position: .top,
videoName: "voice"
)),
title: strings.Premium_VoiceToText,
text: strings.Premium_VoiceToTextInfo,
@ -590,8 +655,10 @@ private final class DemoSheetContent: CombinedComponent {
id: PremiumDemoScreen.Subject.noAds,
component: AnyComponent(
PageComponent(
content: AnyComponent(DemoComponent(
context: component.context
content: AnyComponent(PhoneDemoComponent(
context: component.context,
position: .bottom,
videoName: "noads"
)),
title: strings.Premium_NoAds,
text: strings.Premium_NoAdsInfo,
@ -612,8 +679,8 @@ private final class DemoSheetContent: CombinedComponent {
reactions: reactions
)
),
title: strings.Premium_Reactions,
text: strings.Premium_ReactionsInfo,
title: isStandalone ? strings.Premium_ReactionsStandalone : strings.Premium_Reactions,
text: isStandalone ? strings.Premium_ReactionsStandaloneInfo : strings.Premium_ReactionsInfo,
textColor: textColor
)
)
@ -642,8 +709,10 @@ private final class DemoSheetContent: CombinedComponent {
id: PremiumDemoScreen.Subject.advancedChatManagement,
component: AnyComponent(
PageComponent(
content: AnyComponent(DemoComponent(
context: component.context
content: AnyComponent(PhoneDemoComponent(
context: component.context,
position: .top,
videoName: "fastdownload"
)),
title: strings.Premium_ChatManagement,
text: strings.Premium_ChatManagementInfo,
@ -657,8 +726,10 @@ private final class DemoSheetContent: CombinedComponent {
id: PremiumDemoScreen.Subject.profileBadge,
component: AnyComponent(
PageComponent(
content: AnyComponent(DemoComponent(
context: component.context
content: AnyComponent(PhoneDemoComponent(
context: component.context,
position: .top,
videoName: "badge"
)),
title: strings.Premium_Badge,
text: strings.Premium_BadgeInfo,
@ -672,8 +743,10 @@ private final class DemoSheetContent: CombinedComponent {
id: PremiumDemoScreen.Subject.animatedUserpics,
component: AnyComponent(
PageComponent(
content: AnyComponent(DemoComponent(
context: component.context
content: AnyComponent(PhoneDemoComponent(
context: component.context,
position: .top,
videoName: "badge"
)),
title: strings.Premium_Avatar,
text: strings.Premium_AvatarInfo,
@ -683,15 +756,26 @@ private final class DemoSheetContent: CombinedComponent {
)
)
]
let index = items.firstIndex(where: { (component.subject as AnyHashable) == $0.content.id }) ?? 0
let index: Int
switch component.source {
case .intro:
index = items.firstIndex(where: { (component.subject as AnyHashable) == $0.content.id }) ?? 0
case .other:
items = items.filter { item in
return item.content.id == (component.subject as AnyHashable)
}
index = 0
}
let pager = pager.update(
component: DemoPagerComponent(
items: items,
index: index
index: index,
activeColor: UIColor(rgb: 0x7169ff),
inactiveColor: theme.list.disclosureArrowColor
),
availableSize: CGSize(width: context.availableSize.width, height: context.availableSize.width + 154.0),
transition: .immediate
transition: context.transition
)
context.add(pager
.position(CGPoint(x: context.availableSize.width / 2.0, y: pager.size.height / 2.0))
@ -700,7 +784,23 @@ private final class DemoSheetContent: CombinedComponent {
let closeButton = closeButton.update(
component: Button(
content: AnyComponent(Image(image: closeImage)),
content: AnyComponent(ZStack([
AnyComponentWithIdentity(
id: "background",
component: AnyComponent(
BlurredRectangle(
color: UIColor(rgb: 0x888888, alpha: 0.1),
radius: 15.0
)
)
),
AnyComponentWithIdentity(
id: "icon",
component: AnyComponent(
Image(image: closeImage)
)
),
])),
action: { [weak component] in
component?.dismiss()
}
@ -713,11 +813,15 @@ private final class DemoSheetContent: CombinedComponent {
)
let buttonText: String
switch component.source {
case let .intro(price):
buttonText = strings.Premium_SubscribeFor(price ?? "").string
case .other:
buttonText = strings.Premium_MoreAboutPremium
if state.isPremium == true {
buttonText = strings.Common_OK
} else {
switch component.source {
case let .intro(price):
buttonText = strings.Premium_SubscribeFor(price ?? "").string
case .other:
buttonText = strings.Premium_MoreAboutPremium
}
}
let button = button.update(
@ -737,7 +841,7 @@ private final class DemoSheetContent: CombinedComponent {
fontSize: 17.0,
height: 50.0,
cornerRadius: 10.0,
gloss: true,
gloss: state.isPremium != true,
iconPosition: .right,
action: { [weak component] in
guard let component = component else {
@ -750,21 +854,17 @@ private final class DemoSheetContent: CombinedComponent {
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: 50.0),
transition: context.transition
)
var contentHeight: CGFloat = context.availableSize.width + 154.0
if case .other = component.source {
contentHeight -= 40.0
}
let buttonFrame = CGRect(origin: CGPoint(x: sideInset, y: context.availableSize.width + 154.0 + 20.0), size: button.size)
let buttonFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight + 20.0), size: button.size)
context.add(button
.position(CGPoint(x: buttonFrame.midX, y: buttonFrame.midY))
)
let dots = dots.update(
component: BundleIconComponent(name: "Components/Dots", tintColor: nil),
availableSize: CGSize(width: 110.0, height: 20.0),
transition: .immediate
)
context.add(dots
.position(CGPoint(x: context.availableSize.width / 2.0, y: buttonFrame.minY - dots.size.height - 18.0))
)
let contentSize = CGSize(width: context.availableSize.width, height: buttonFrame.maxY + 5.0 + environment.safeInsets.bottom)
return contentSize
@ -875,6 +975,12 @@ public class PremiumDemoScreen: ViewControllerComponentContainer {
var disposed: () -> Void = {}
private var didSetReady = false
private let _ready = Promise<Bool>()
public override var ready: Promise<Bool> {
return self._ready
}
public init(context: AccountContext, subject: PremiumDemoScreen.Subject, source: PremiumDemoScreen.Source = .other, action: @escaping () -> Void) {
super.init(context: context, component: DemoSheetComponent(context: context, subject: subject, source: source, action: action), navigationBarAppearance: .none)
@ -896,5 +1002,18 @@ public class PremiumDemoScreen: ViewControllerComponentContainer {
self.view.disablesInteractiveModalDismiss = true
}
public override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
if !self.didSetReady {
self.didSetReady = true
if let view = self.node.hostView.findTaggedView(tag: PhoneDemoComponent.View.Tag()) as? PhoneDemoComponent.View {
self._ready.set(view.ready)
} else {
self._ready.set(.single(true) |> delay(0.1, queue: Queue.mainQueue()))
}
}
}
}

View File

@ -3,6 +3,7 @@ import UIKit
import Display
import ComponentFlow
import SwiftSignalKit
import Postbox
import TelegramCore
import TelegramPresentationData
import PresentationDataUtils
@ -34,6 +35,7 @@ public enum PremiumSource: Equatable {
case chatsPerFolder
case accounts
case deeplink(String?)
case profile(PeerId)
var identifier: String {
switch self {
@ -63,6 +65,8 @@ public enum PremiumSource: Equatable {
return "double_limits__dialog_filters_chats"
case .accounts:
return "double_limits__accounts"
case .profile:
return "profile"
case let .deeplink(reference):
if let reference = reference {
return "deeplink_\(reference)"
@ -727,15 +731,17 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
let context: AccountContext
let source: PremiumSource
let isPremium: Bool?
let otherPeerName: String?
let price: String?
let present: (ViewController) -> Void
let buy: () -> Void
let updateIsFocused: (Bool) -> Void
init(context: AccountContext, source: PremiumSource, isPremium: Bool?, price: String?, present: @escaping (ViewController) -> Void, buy: @escaping () -> Void, updateIsFocused: @escaping (Bool) -> Void) {
init(context: AccountContext, source: PremiumSource, isPremium: Bool?, otherPeerName: String?, price: String?, present: @escaping (ViewController) -> Void, buy: @escaping () -> Void, updateIsFocused: @escaping (Bool) -> Void) {
self.context = context
self.source = source
self.isPremium = isPremium
self.otherPeerName = otherPeerName
self.price = price
self.present = present
self.buy = buy
@ -752,6 +758,9 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
if lhs.isPremium != rhs.isPremium {
return false
}
if lhs.otherPeerName != rhs.otherPeerName {
return false
}
if lhs.price != rhs.price {
return false
}
@ -874,13 +883,22 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
let textFont = Font.regular(15.0)
let boldTextFont = Font.semibold(15.0)
let textString: String
if let _ = context.component.otherPeerName {
textString = strings.Premium_PersonalDescription
} else if context.component.isPremium == true {
textString = strings.Premium_SubscribedDescription
} else {
textString = strings.Premium_Description
}
let markdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor), link: MarkdownAttributeSet(font: textFont, textColor: textColor), linkAttribute: { _ in
return nil
})
let text = text.update(
component: MultilineTextComponent(
text: .markdown(
text: context.component.isPremium == true ? strings.Premium_SubscribedDescription : strings.Premium_Description,
text: textString,
attributes: markdownAttributes
),
horizontalAlignment: .center,
@ -917,6 +935,8 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
let buy = context.component.buy
let updateIsFocused = context.component.updateIsFocused
let isPremium = context.component.isPremium ?? false
var i = 0
for perk in state.configuration.perks {
let iconBackgroundColors = gradientColors[i]
@ -942,6 +962,8 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
var demoSubject: PremiumDemoScreen.Subject
switch perk {
case .doubleLimits:
// let controller = PremimLimitsListScreen(context: accountContext)
// present(controller)
return
case .moreUpload:
demoSubject = .moreUpload
@ -970,7 +992,9 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
source: .intro(state?.price),
action: {
dismissImpl?()
buy()
if !isPremium {
buy()
}
}
)
controller.disposed = {
@ -1071,7 +1095,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
let termsText = termsText.update(
component: MultilineTextComponent(
text: .markdown(
text: strings.Premium_Terms,
text: context.component.isPremium == true ? strings.Premium_ChargeInfo("$4.99", "").string : strings.Premium_Terms,
attributes: termsMarkdownAttributes
),
horizontalAlignment: .natural,
@ -1088,24 +1112,28 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
tapAction: { attributes, _ in
if let url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String,
let controller = environment.controller() as? PremiumIntroScreen, let navigationController = controller.navigationController as? NavigationController {
let context = controller.context
let signal: Signal<ResolvedUrl, NoError>?
switch url {
case "terms":
signal = cachedTermsPage(context: context)
case "privacy":
signal = cachedPrivacyPage(context: context)
default:
signal = nil
}
if let signal = signal {
let _ = (signal
|> deliverOnMainQueue).start(next: { resolvedUrl in
context.sharedContext.openResolvedUrl(resolvedUrl, context: context, urlContext: .generic, navigationController: navigationController, forceExternal: false, openPeer: { peer, navigation in
}, sendFile: nil, sendSticker: nil, requestMessageActionUrlAuth: nil, joinVoiceChat: nil, present: { [weak controller] c, arguments in
controller?.push(c)
}, dismissInput: {}, contentContext: nil)
})
if url == "cancel" {
} else {
let context = controller.context
let signal: Signal<ResolvedUrl, NoError>?
switch url {
case "terms":
signal = cachedTermsPage(context: context)
case "privacy":
signal = cachedPrivacyPage(context: context)
default:
signal = nil
}
if let signal = signal {
let _ = (signal
|> deliverOnMainQueue).start(next: { resolvedUrl in
context.sharedContext.openResolvedUrl(resolvedUrl, context: context, urlContext: .generic, navigationController: navigationController, forceExternal: false, openPeer: { peer, navigation in
}, sendFile: nil, sendSticker: nil, requestMessageActionUrlAuth: nil, joinVoiceChat: nil, present: { [weak controller] c, arguments in
controller?.push(c)
}, dismissInput: {}, contentContext: nil)
})
}
}
}
}
@ -1126,17 +1154,22 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
}
}
private class BlurredRectangle: Component {
class BlurredRectangle: Component {
let color: UIColor
let radius: CGFloat
init(color: UIColor) {
init(color: UIColor, radius: CGFloat = 0.0) {
self.color = color
self.radius = radius
}
static func ==(lhs: BlurredRectangle, rhs: BlurredRectangle) -> Bool {
if !lhs.color.isEqual(rhs.color) {
return false
}
if lhs.radius != rhs.radius {
return false
}
return true
}
@ -1158,7 +1191,7 @@ private class BlurredRectangle: Component {
func update(component: BlurredRectangle, availableSize: CGSize, transition: Transition) -> CGSize {
transition.setFrame(view: self.background.view, frame: CGRect(origin: CGPoint(), size: availableSize))
self.background.updateColor(color: component.color, transition: .immediate)
self.background.update(size: availableSize, cornerRadius: 0.0, transition: .immediate)
self.background.update(size: availableSize, cornerRadius: component.radius, transition: .immediate)
return availableSize
}
@ -1213,12 +1246,13 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
var inProgress = false
var premiumProduct: InAppPurchaseManager.Product?
var isPremium: Bool?
var otherPeerName: String?
private var disposable: Disposable?
private var paymentDisposable = MetaDisposable()
private var activationDisposable = MetaDisposable()
init(context: AccountContext, updateInProgress: @escaping (Bool) -> Void, completion: @escaping () -> Void) {
init(context: AccountContext, source: PremiumSource, updateInProgress: @escaping (Bool) -> Void, completion: @escaping () -> Void) {
self.context = context
self.updateInProgress = updateInProgress
self.completion = completion
@ -1226,17 +1260,30 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
super.init()
if let inAppPurchaseManager = context.sharedContext.inAppPurchaseManager {
let otherPeerName: Signal<String?, NoError>
if case let .profile(peerId) = source {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
otherPeerName = context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))
|> map { peer -> String? in
return peer?.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)
}
} else {
otherPeerName = .single(nil)
}
self.disposable = combineLatest(
queue: Queue.mainQueue(),
inAppPurchaseManager.availableProducts,
context.account.postbox.peerView(id: context.account.peerId)
|> map { view -> Bool in
return view.peers[view.peerId]?.isPremium ?? false
}
).start(next: { [weak self] products, isPremium in
},
otherPeerName
).start(next: { [weak self] products, isPremium, otherPeerName in
if let strongSelf = self {
strongSelf.premiumProduct = products.first
strongSelf.isPremium = isPremium
strongSelf.otherPeerName = otherPeerName
strongSelf.updated(transition: .immediate)
}
})
@ -1296,7 +1343,7 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
}
func makeState() -> State {
return State(context: self.context, updateInProgress: self.updateInProgress, completion: self.completion)
return State(context: self.context, source: self.source, updateInProgress: self.updateInProgress, completion: self.completion)
}
static var body: Body {
@ -1306,6 +1353,7 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
let topPanel = Child(BlurredRectangle.self)
let topSeparator = Child(Rectangle.self)
let title = Child(Text.self)
let secondaryTitle = Child(MultilineTextComponent.self)
let bottomPanel = Child(BlurredRectangle.self)
let bottomSeparator = Child(Rectangle.self)
let button = Child(SolidRoundedButtonComponent.self)
@ -1343,9 +1391,16 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
transition: context.transition
)
let titleString: String
if state.isPremium == true {
titleString = environment.strings.Premium_SubscribedTitle
} else {
titleString = environment.strings.Premium_Title
}
let title = title.update(
component: Text(
text: state.isPremium == true ? environment.strings.Premium_SubscribedTitle : environment.strings.Premium_Title,
text: titleString,
font: Font.bold(28.0),
color: environment.theme.rootController.navigationBar.primaryTextColor
),
@ -1353,55 +1408,38 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
transition: context.transition
)
let sideInset: CGFloat = 16.0
let button = button.update(
component: SolidRoundedButtonComponent(
title: environment.strings.Premium_SubscribeFor(state.premiumProduct?.price ?? "").string,
theme: SolidRoundedButtonComponent.Theme(
backgroundColor: UIColor(rgb: 0x8878ff),
backgroundColors: [
UIColor(rgb: 0x0077ff),
UIColor(rgb: 0x6b93ff),
UIColor(rgb: 0x8878ff),
UIColor(rgb: 0xe46ace)
],
foregroundColor: .white
),
height: 50.0,
cornerRadius: 10.0,
gloss: true,
isLoading: state.inProgress,
action: {
state.buy()
}
let textColor = environment.theme.list.itemPrimaryTextColor
let accentColor = UIColor(rgb: 0x597cf5)
let textFont = Font.bold(18.0)
let boldTextFont = Font.bold(18.0)
let markdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor), link: MarkdownAttributeSet(font: textFont, textColor: accentColor), linkAttribute: { _ in
return nil
})
let secondaryTitle = secondaryTitle.update(
component: MultilineTextComponent(
text: .markdown(text: state.otherPeerName.flatMap({ environment.strings.Premium_PersonalTitle($0).string }) ?? "", attributes: markdownAttributes),
horizontalAlignment: .center,
truncationType: .end,
maximumNumberOfLines: 2,
lineSpacing: 0.0
),
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - environment.safeInsets.left - environment.safeInsets.right, height: 50.0),
transition: context.transition)
availableSize: context.availableSize,
transition: context.transition
)
let bottomPanelPadding: CGFloat = 12.0
let bottomInset: CGFloat = environment.safeInsets.bottom > 0.0 ? environment.safeInsets.bottom + 5.0 : bottomPanelPadding
let bottomPanel = bottomPanel.update(
component: BlurredRectangle(
color: environment.theme.rootController.tabBar.backgroundColor
),
availableSize: CGSize(width: context.availableSize.width, height: bottomPanelPadding + button.size.height + bottomInset),
transition: context.transition
)
let bottomSeparator = bottomSeparator.update(
component: Rectangle(
color: environment.theme.rootController.tabBar.separatorColor
),
availableSize: CGSize(width: context.availableSize.width, height: UIScreenPixel),
transition: context.transition
)
let bottomPanelHeight: CGFloat = state.isPremium == true ? bottomInset : bottomPanelPadding + 50.0 + bottomInset
let scrollContent = scrollContent.update(
component: ScrollComponent<EnvironmentType>(
content: AnyComponent(PremiumIntroScreenContentComponent(
context: context.component.context,
source: context.component.source,
isPremium: state.isPremium,
otherPeerName: state.otherPeerName,
price: state.premiumProduct?.price,
present: context.component.present,
buy: { [weak state] in
@ -1410,7 +1448,7 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
state?.updateIsFocused(isFocused)
}
)),
contentInsets: UIEdgeInsets(top: environment.navigationHeight, left: 0.0, bottom: bottomPanel.size.height, right: 0.0),
contentInsets: UIEdgeInsets(top: environment.navigationHeight, left: 0.0, bottom: bottomPanelHeight, right: 0.0),
contentOffsetUpdated: { [weak state] topContentOffset, bottomContentOffset in
state?.topContentOffset = topContentOffset
state?.bottomContentOffset = bottomContentOffset
@ -1445,17 +1483,25 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
let titleOffset: CGFloat
let titleScale: CGFloat
let titleOffsetDelta = (topInset + 160.0) - (environment.statusBarHeight + (environment.navigationHeight - environment.statusBarHeight) / 2.0)
let titleAlpha: CGFloat
if let topContentOffset = state.topContentOffset {
topPanelAlpha = min(20.0, max(0.0, topContentOffset - 95.0)) / 20.0
let topContentOffset = topContentOffset + max(0.0, min(1.0, topContentOffset / titleOffsetDelta)) * 10.0
titleOffset = topContentOffset
let fraction = max(0.0, min(1.0, titleOffset / titleOffsetDelta))
titleScale = 1.0 - fraction * 0.36
if state.otherPeerName != nil {
titleAlpha = min(1.0, fraction * 1.5)
} else {
titleAlpha = 1.0
}
} else {
topPanelAlpha = 0.0
titleScale = 1.0
titleOffset = 0.0
titleAlpha = state.otherPeerName != nil ? 0.0 : 1.0
}
context.add(star
@ -1475,27 +1521,79 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
context.add(title
.position(CGPoint(x: context.availableSize.width / 2.0, y: max(topInset + 160.0 - titleOffset, environment.statusBarHeight + (environment.navigationHeight - environment.statusBarHeight) / 2.0)))
.scale(titleScale)
.opacity(titleAlpha)
)
let bottomPanelAlpha: CGFloat
if let bottomContentOffset = state.bottomContentOffset {
bottomPanelAlpha = min(16.0, bottomContentOffset) / 16.0
context.add(secondaryTitle
.position(CGPoint(x: context.availableSize.width / 2.0, y: max(topInset + 160.0 - titleOffset, environment.statusBarHeight + (environment.navigationHeight - environment.statusBarHeight) / 2.0)))
.scale(titleScale)
.opacity(1.0 - titleAlpha)
)
if state.isPremium == true {
} else {
bottomPanelAlpha = 1.0
let sideInset: CGFloat = 16.0
let button = button.update(
component: SolidRoundedButtonComponent(
title: environment.strings.Premium_SubscribeFor(state.premiumProduct?.price ?? "").string,
theme: SolidRoundedButtonComponent.Theme(
backgroundColor: UIColor(rgb: 0x8878ff),
backgroundColors: [
UIColor(rgb: 0x0077ff),
UIColor(rgb: 0x6b93ff),
UIColor(rgb: 0x8878ff),
UIColor(rgb: 0xe46ace)
],
foregroundColor: .white
),
height: 50.0,
cornerRadius: 10.0,
gloss: true,
isLoading: state.inProgress,
action: {
state.buy()
}
),
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - environment.safeInsets.left - environment.safeInsets.right, height: 50.0),
transition: context.transition)
let bottomPanel = bottomPanel.update(
component: BlurredRectangle(
color: environment.theme.rootController.tabBar.backgroundColor
),
availableSize: CGSize(width: context.availableSize.width, height: bottomPanelPadding + button.size.height + bottomInset),
transition: context.transition
)
let bottomSeparator = bottomSeparator.update(
component: Rectangle(
color: environment.theme.rootController.tabBar.separatorColor
),
availableSize: CGSize(width: context.availableSize.width, height: UIScreenPixel),
transition: context.transition
)
let bottomPanelAlpha: CGFloat
if let bottomContentOffset = state.bottomContentOffset {
bottomPanelAlpha = min(16.0, bottomContentOffset) / 16.0
} else {
bottomPanelAlpha = 1.0
}
context.add(bottomPanel
.position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height - bottomPanel.size.height / 2.0))
.opacity(bottomPanelAlpha)
)
context.add(bottomSeparator
.position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height - bottomPanel.size.height))
.opacity(bottomPanelAlpha)
)
context.add(button
.position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height - bottomPanel.size.height + bottomPanelPadding + button.size.height / 2.0))
)
}
context.add(bottomPanel
.position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height - bottomPanel.size.height / 2.0))
.opacity(bottomPanelAlpha)
)
context.add(bottomSeparator
.position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height - bottomPanel.size.height))
.opacity(bottomPanelAlpha)
)
context.add(button
.position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height - bottomPanel.size.height + bottomPanelPadding + button.size.height / 2.0))
)
return context.availableSize
}
}
@ -1510,6 +1608,10 @@ public final class PremiumIntroScreen: ViewControllerComponentContainer {
return self._ready
}
public weak var sourceView: UIView?
public weak var containerView: UIView?
public var animationColor: UIColor?
public init(context: AccountContext, modal: Bool = true, source: PremiumSource) {
self.context = context
@ -1577,6 +1679,16 @@ public final class PremiumIntroScreen: ViewControllerComponentContainer {
if let view = self.node.hostView.findTaggedView(tag: PremiumStarComponent.View.Tag()) as? PremiumStarComponent.View {
self.didSetReady = true
self._ready.set(view.ready)
if let sourceView = self.sourceView {
view.animateFrom = sourceView
view.containerView = self.containerView
view.animationColor = self.animationColor
self.sourceView = nil
self.containerView = nil
self.animationColor = nil
}
}
}
}

View File

@ -94,6 +94,7 @@ private class PremiumLimitAnimationComponent: Component {
private let badgeMaskView: UIView
private let badgeMaskBackgroundView: UIView
private let badgeMaskArrowView: UIImageView
private let badgeMaskTailView: UIImageView
private let badgeForeground: SimpleLayer
private let badgeIcon: UIImageView
private let badgeCountLabel: RollingLabel
@ -114,7 +115,6 @@ private class PremiumLimitAnimationComponent: Component {
self.badgeView = UIView()
self.badgeView.alpha = 0.0
self.badgeView.layer.anchorPoint = CGPoint(x: 0.5, y: 1.0)
self.badgeMaskBackgroundView = UIView()
self.badgeMaskBackgroundView.backgroundColor = .white
@ -129,9 +129,23 @@ private class PremiumLimitAnimationComponent: Component {
try? drawSvgPath(context, path: "M6.4,0.0 C2.9,0.0 0.0,2.84 0.0,6.35 C0.0,9.86 2.9,12.7 6.4,12.7 H9.302 H11.3 C11.7,12.7 12.1,12.87 12.4,13.17 L14.4,15.13 C14.8,15.54 15.5,15.54 15.9,15.13 L17.8,13.17 C18.1,12.87 18.5,12.7 18.9,12.7 H20.9 H23.6 C27.1,12.7 29.9,9.86 29.9,6.35 C29.9,2.84 27.1,0.0 23.6,0.0 Z ")
})
self.badgeMaskTailView = UIImageView()
self.badgeMaskTailView.isHidden = true
let img = generateImage(CGSize(width: 44.0, height: 36.0), rotatedContext: { size, context in
context.clear(CGRect(origin: .zero, size: size))
context.setFillColor(UIColor.white.cgColor)
context.fill(CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: 44.0, height: 24.0)))
context.translateBy(x: 22.0, y: 24.0)
try? drawSvgPath(context, path: "M0.0,0.0 H22.0 V4.75736 C22.0,7.43007 18.7686,8.76857 16.8787,6.87868 L11.7574,1.75736 C10.6321,0.632141 9.10602,0.0 7.51472,0.0 H0.0 Z ")
})
self.badgeMaskTailView.image = img
self.badgeMaskView = UIView()
self.badgeMaskView.addSubview(self.badgeMaskBackgroundView)
self.badgeMaskView.addSubview(self.badgeMaskArrowView)
self.badgeMaskView.addSubview(self.badgeMaskTailView)
self.badgeMaskView.layer.rasterizationScale = UIScreenScale
self.badgeMaskView.layer.shouldRasterize = true
self.badgeView.mask = self.badgeMaskView
@ -254,12 +268,22 @@ private class PremiumLimitAnimationComponent: Component {
self.badgeMaskBackgroundView.frame = CGRect(origin: .zero, size: CGSize(width: badgeSize.width, height: 48.0))
self.badgeMaskArrowView.frame = CGRect(origin: CGPoint(x: (badgeSize.width - 44.0) / 2.0, y: badgeSize.height - 12.0), size: CGSize(width: 44.0, height: 12.0))
self.badgeMaskTailView.frame = CGRect(origin: CGPoint(x: badgeSize.width - 44.0, y: badgeSize.height - 36.0), size: CGSize(width: 44.0, height: 36.0))
self.badgeView.bounds = CGRect(origin: .zero, size: badgeSize)
if component.badgePosition > 1.0 - .ulpOfOne {
let offset = badgeWidth / 2.0 - 16.0
self.badgeView.center = CGPoint(x: 3.0 + (availableSize.width - 6.0) * component.badgePosition - offset, y: 82.0)
self.badgeMaskArrowView.frame = self.badgeMaskArrowView.frame.offsetBy(dx: offset - 18.0, dy: 0.0)
self.badgeView.layer.anchorPoint = CGPoint(x: 1.0, y: 1.0)
self.badgeMaskTailView.isHidden = false
self.badgeMaskArrowView.isHidden = true
self.badgeView.center = CGPoint(x: 3.0 + (availableSize.width - 6.0) * component.badgePosition + 3.0, y: 82.0)
} else {
self.badgeView.layer.anchorPoint = CGPoint(x: 0.5, y: 1.0)
self.badgeMaskTailView.isHidden = true
self.badgeMaskArrowView.isHidden = false
self.badgeView.center = CGPoint(x: 3.0 + (availableSize.width - 6.0) * component.badgePosition, y: 82.0)
if self.badgeView.frame.maxX > availableSize.width {
@ -575,6 +599,7 @@ private final class LimitSheetContent: CombinedComponent {
var initialized = false
var limits: EngineConfiguration.UserLimits
var premiumLimits: EngineConfiguration.UserLimits
var isPremium = false
var cachedCloseImage: (UIImage, PresentationTheme)?
@ -587,13 +612,15 @@ private final class LimitSheetContent: CombinedComponent {
self.disposable = (context.engine.data.get(
TelegramEngine.EngineData.Item.Configuration.UserLimits(isPremium: false),
TelegramEngine.EngineData.Item.Configuration.UserLimits(isPremium: true)
TelegramEngine.EngineData.Item.Configuration.UserLimits(isPremium: true),
TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)
) |> deliverOnMainQueue).start(next: { [weak self] result in
if let strongSelf = self {
let (limits, premiumLimits) = result
let (limits, premiumLimits, accountPeer) = result
strongSelf.initialized = true
strongSelf.limits = limits
strongSelf.premiumLimits = premiumLimits
strongSelf.isPremium = accountPeer?.isPremium ?? false
strongSelf.updated(transition: .immediate)
}
})
@ -648,7 +675,7 @@ private final class LimitSheetContent: CombinedComponent {
context.add(closeButton
.position(CGPoint(x: context.availableSize.width - environment.safeInsets.left - closeButton.size.width, y: 28.0))
)
var titleText = strings.Premium_LimitReached
let iconName: String
let badgeText: String
@ -664,16 +691,16 @@ private final class LimitSheetContent: CombinedComponent {
badgeText = "\(component.count)"
string = strings.Premium_MaxFoldersCountText("\(limit)", "\(premiumLimit)").string
defaultValue = component.count > limit ? "\(limit)" : ""
premiumValue = "\(premiumLimit)"
premiumValue = component.count >= premiumLimit ? "" : "\(premiumLimit)"
badgePosition = CGFloat(component.count) / CGFloat(premiumLimit)
case .chatsInFolder:
case .chatsPerFolder:
let limit = state.limits.maxFolderChatsCount
let premiumLimit = state.premiumLimits.maxFolderChatsCount
iconName = "Premium/Chat"
badgeText = "\(component.count)"
string = strings.Premium_MaxChatsInFolderCountText("\(limit)", "\(premiumLimit)").string
defaultValue = component.count > limit ? "\(limit)" : ""
premiumValue = "\(premiumLimit)"
premiumValue = component.count >= premiumLimit ? "" : "\(premiumLimit)"
badgePosition = CGFloat(component.count) / CGFloat(premiumLimit)
case .pins:
let limit = state.limits.maxPinnedChatCount
@ -682,7 +709,7 @@ private final class LimitSheetContent: CombinedComponent {
badgeText = "\(component.count)"
string = strings.Premium_MaxPinsText("\(limit)", "\(premiumLimit)").string
defaultValue = component.count > limit ? "\(limit)" : ""
premiumValue = "\(premiumLimit)"
premiumValue = component.count >= premiumLimit ? "" : "\(premiumLimit)"
badgePosition = CGFloat(component.count) / CGFloat(premiumLimit)
case .files:
let limit = Int64(state.limits.maxUploadFileParts) * 512 * 1024 + 1024 * 1024 * 100
@ -690,10 +717,27 @@ private final class LimitSheetContent: CombinedComponent {
iconName = "Premium/File"
badgeText = dataSizeString(limit, formatting: DataSizeStringFormatting(strings: environment.strings, decimalSeparator: environment.dateTimeFormat.decimalSeparator))
string = strings.Premium_MaxFileSizeText(dataSizeString(premiumLimit, formatting: DataSizeStringFormatting(strings: environment.strings, decimalSeparator: environment.dateTimeFormat.decimalSeparator))).string
defaultValue = ""
premiumValue = dataSizeString(premiumLimit, formatting: DataSizeStringFormatting(strings: environment.strings, decimalSeparator: environment.dateTimeFormat.decimalSeparator))
badgePosition = 0.5
defaultValue = component.count == 4 ? dataSizeString(limit, formatting: DataSizeStringFormatting(strings: environment.strings, decimalSeparator: environment.dateTimeFormat.decimalSeparator)) : ""
premiumValue = component.count != 4 ? dataSizeString(premiumLimit, formatting: DataSizeStringFormatting(strings: environment.strings, decimalSeparator: environment.dateTimeFormat.decimalSeparator)) : ""
badgePosition = component.count == 4 ? 1.0 : 0.5
titleText = strings.Premium_FileTooLarge
case .accounts:
let limit = 3
let premiumLimit = component.count + 1
iconName = "Premium/Account"
badgeText = "\(component.count)"
string = strings.Premium_MaxAccountsText("\(component.count)").string
defaultValue = component.count > limit ? "\(limit)" : ""
premiumValue = component.count >= premiumLimit ? "" : "\(premiumLimit)"
if component.count == limit {
badgePosition = 0.5
} else {
badgePosition = CGFloat(component.count) / CGFloat(premiumLimit)
}
}
var reachedMaximumLimit = badgePosition >= 1.0
if case .folders = subject, !state.isPremium {
reachedMaximumLimit = false
}
let title = title.update(
@ -759,7 +803,7 @@ private final class LimitSheetContent: CombinedComponent {
let button = button.update(
component: SolidRoundedButtonComponent(
title: strings.Premium_IncreaseLimit,
title: !reachedMaximumLimit ? strings.Premium_IncreaseLimit : strings.Common_OK,
theme: SolidRoundedButtonComponent.Theme(
backgroundColor: .black,
backgroundColors: [
@ -774,8 +818,8 @@ private final class LimitSheetContent: CombinedComponent {
fontSize: 17.0,
height: 50.0,
cornerRadius: 10.0,
gloss: true,
iconName: "Premium/X2",
gloss: !reachedMaximumLimit,
iconName: !reachedMaximumLimit ? "Premium/X2" : nil,
iconPosition: .right,
action: { [weak component] in
guard let component = component else {
@ -890,9 +934,10 @@ private final class LimitSheetComponent: CombinedComponent {
public class PremiumLimitScreen: ViewControllerComponentContainer {
public enum Subject {
case folders
case chatsInFolder
case chatsPerFolder
case pins
case files
case accounts
}
public init(context: AccountContext, subject: PremiumLimitScreen.Subject, count: Int32, action: @escaping () -> Void) {

File diff suppressed because it is too large Load Diff

View File

@ -47,7 +47,7 @@ private func generateDiffuseTexture() -> UIImage {
class PremiumStarComponent: Component {
let isVisible: Bool
let hasIdleAnimations: Bool
init(isVisible: Bool, hasIdleAnimations: Bool) {
self.isVisible = isVisible
self.hasIdleAnimations = hasIdleAnimations
@ -73,6 +73,10 @@ class PremiumStarComponent: Component {
return self._ready.get()
}
weak var animateFrom: UIView?
weak var containerView: UIView?
var animationColor: UIColor?
private let sceneView: SCNView
private var previousInteractionTimestamp: Double = 0.0
@ -80,7 +84,7 @@ class PremiumStarComponent: Component {
private var hasIdleAnimations = false
override init(frame: CGRect) {
self.sceneView = SCNView(frame: frame)
self.sceneView = SCNView(frame: CGRect(origin: .zero, size: CGSize(width: 64.0, height: 64.0)))
self.sceneView.backgroundColor = .clear
self.sceneView.transform = CGAffineTransform(scaleX: 0.5, y: 0.5)
self.sceneView.isUserInteractionEnabled = false
@ -194,7 +198,19 @@ class PremiumStarComponent: Component {
case .changed:
let translation = gesture.translation(in: gesture.view)
let yawPan = deg2rad(Float(translation.x))
let pitchPan = deg2rad(Float(translation.y))
func rubberBandingOffset(offset: CGFloat, bandingStart: CGFloat) -> CGFloat {
let bandedOffset = offset - bandingStart
let range: CGFloat = 60.0
let coefficient: CGFloat = 0.4
return bandingStart + (1.0 - (1.0 / ((bandedOffset * coefficient / range) + 1.0))) * range
}
var pitchTranslation = rubberBandingOffset(offset: abs(translation.y), bandingStart: 0.0)
if translation.y < 0.0 {
pitchTranslation *= -1.0
}
let pitchPan = deg2rad(Float(pitchTranslation))
self.previousYaw = yawPan
node.eulerAngles = SCNVector3(pitchPan, yawPan, 0.0)
@ -246,15 +262,68 @@ class PremiumStarComponent: Component {
if !self.didSetReady {
self.didSetReady = true
self._ready.set(.single(true))
self.onReady()
Queue.mainQueue().justDispatch {
self._ready.set(.single(true))
self.onReady()
}
}
}
private func maybeAnimateIn() {
guard let scene = self.sceneView.scene, let node = scene.rootNode.childNode(withName: "star", recursively: false), let animateFrom = self.animateFrom, let containerView = self.containerView else {
return
}
if let animationColor = self.animationColor {
let newNode = node.clone()
newNode.geometry = node.geometry?.copy() as? SCNGeometry
let colorMaterial = SCNMaterial()
colorMaterial.diffuse.contents = animationColor
colorMaterial.lightingModel = SCNMaterial.LightingModel.blinn
newNode.geometry?.materials = [colorMaterial]
node.addChildNode(newNode)
newNode.scale = SCNVector3(1.03, 1.03, 1.03)
newNode.geometry?.materials.first?.diffuse.contents = animationColor
let animation = CABasicAnimation(keyPath: "opacity")
animation.beginTime = CACurrentMediaTime() + 0.1
animation.duration = 0.7
animation.fromValue = 1.0
animation.toValue = 0.0
animation.fillMode = .forwards
animation.isRemovedOnCompletion = false
animation.completion = { [weak newNode] _ in
newNode?.removeFromParentNode()
}
newNode.addAnimation(animation, forKey: "opacity")
}
let initialPosition = self.sceneView.center
let targetPosition = self.sceneView.superview!.convert(self.sceneView.center, to: containerView)
let sourcePosition = animateFrom.superview!.convert(animateFrom.center, to: containerView).offsetBy(dx: 0.0, dy: -20.0)
containerView.addSubview(self.sceneView)
self.sceneView.center = targetPosition
animateFrom.alpha = 0.0
self.sceneView.layer.animateScale(from: 0.05, to: 0.5, duration: 1.0, timingFunction: kCAMediaTimingFunctionSpring)
self.sceneView.layer.animatePosition(from: sourcePosition, to: targetPosition, duration: 1.0, timingFunction: kCAMediaTimingFunctionSpring, completion: { _ in
self.addSubview(self.sceneView)
self.sceneView.center = initialPosition
animateFrom.alpha = 1.0
})
self.animateFrom = nil
self.containerView = nil
}
private func onReady() {
self.setupGradientAnimation()
self.setupShineAnimation()
self.maybeAnimateIn()
self.playAppearanceAnimation(explode: true)
self.previousInteractionTimestamp = CACurrentMediaTime()
@ -367,7 +436,9 @@ class PremiumStarComponent: Component {
func update(component: PremiumStarComponent, availableSize: CGSize, transition: Transition) -> CGSize {
self.sceneView.bounds = CGRect(origin: .zero, size: CGSize(width: availableSize.width * 2.0, height: availableSize.height * 2.0))
self.sceneView.center = CGPoint(x: availableSize.width / 2.0, y: availableSize.height / 2.0)
if self.sceneView.superview == self {
self.sceneView.center = CGPoint(x: availableSize.width / 2.0, y: availableSize.height / 2.0)
}
self.hasIdleAnimations = component.hasIdleAnimations

View File

@ -67,7 +67,11 @@ final class ReactionsCarouselComponent: Component {
}
if isDisplaying && !self.isVisible {
self.node?.setVisible(true)
var fast = false
if let _ = transition.userData(DemoAnimateInTransition.self) {
fast = true
}
self.node?.setVisible(true, fast: fast)
} else if !isDisplaying && self.isVisible {
self.node?.setVisible(false)
}
@ -89,7 +93,7 @@ final class ReactionsCarouselComponent: Component {
private let itemSize = CGSize(width: 110.0, height: 110.0)
//private let order = ["👌","😍","🤡","🕊","🥱","🥴"]
private let order = ["😍","👌","🥴","🥱","🕊","🤡"]
private let order = ["😍","👌","🥴","🐳","🥱","🕊","🤡"]
private class ReactionCarouselNode: ASDisplayNode, UIScrollViewDelegate {
private let context: AccountContext
@ -113,7 +117,6 @@ private class ReactionCarouselNode: ASDisplayNode, UIScrollViewDelegate {
private var previousInteractionTimestamp: Double = 0.0
private var timer: SwiftSignalKit.Timer?
private var hasIdleAnimations = false
init(context: AccountContext, theme: PresentationTheme, reactions: [AvailableReactions.Reaction]) {
self.context = context
@ -163,7 +166,12 @@ private class ReactionCarouselNode: ASDisplayNode, UIScrollViewDelegate {
@objc private func reactionTapped(_ gestureRecognizer: UITapGestureRecognizer) {
self.previousInteractionTimestamp = CACurrentMediaTime()
guard self.animator == nil, self.scrollStartPosition == nil else {
if let animator = self.animator {
animator.invalidate()
self.animator = nil
}
guard self.scrollStartPosition == nil else {
return
}
@ -172,13 +180,17 @@ private class ReactionCarouselNode: ASDisplayNode, UIScrollViewDelegate {
return
}
self.scrollTo(index, playReaction: true, immediately: true, duration: 0.4)
self.scrollTo(index, playReaction: true, immediately: true, duration: 0.85)
self.hapticFeedback.impact(.light)
}
func setVisible(_ visible: Bool) {
func setVisible(_ visible: Bool, fast: Bool = false) {
if visible {
self.animateIn()
self.animateIn(fast: fast)
} else {
self.animator?.invalidate()
self.animator = nil
self.scrollTo(0, playReaction: false, immediately: false, duration: 0.0, clockwise: false)
self.timer?.invalidate()
self.timer = nil
@ -188,20 +200,25 @@ private class ReactionCarouselNode: ASDisplayNode, UIScrollViewDelegate {
}
}
func animateIn() {
self.scrollTo(1, playReaction: true, immediately: false, duration: 0.5, clockwise: true)
func animateIn(fast: Bool) {
let duration: Double = fast ? 1.4 : 2.2
let delay: Double = fast ? 0.5 : 0.8
self.scrollTo(1, playReaction: false, immediately: false, duration: duration, damping: 0.75, clockwise: true)
Queue.mainQueue().after(delay, {
self.playReaction(index: 1)
})
if self.timer == nil {
self.previousInteractionTimestamp = CACurrentMediaTime()
self.timer = SwiftSignalKit.Timer(timeout: 1.0, repeat: true, completion: { [weak self] in
if let strongSelf = self {
let currentTimestamp = CACurrentMediaTime()
if currentTimestamp > strongSelf.previousInteractionTimestamp + 5.0 {
if currentTimestamp > strongSelf.previousInteractionTimestamp + 4.0 {
var nextIndex = strongSelf.currentIndex - 1
if nextIndex < 0 {
nextIndex = strongSelf.reactions.count + nextIndex
}
strongSelf.scrollTo(nextIndex, playReaction: true, immediately: true, duration: 0.3, clockwise: true)
strongSelf.scrollTo(nextIndex, playReaction: true, immediately: true, duration: 0.85, clockwise: true)
strongSelf.previousInteractionTimestamp = currentTimestamp
}
}
@ -216,7 +233,34 @@ private class ReactionCarouselNode: ASDisplayNode, UIScrollViewDelegate {
}
}
func scrollTo(_ index: Int, playReaction: Bool, immediately: Bool, duration: Double, clockwise: Bool? = nil) {
func springCurveFunc(_ t: Double, zeta: Double) -> Double {
let v0 = 0.0
let omega = 20.285
let y: Double
if abs(zeta - 1.0) < 1e-8 {
let c1 = -1.0
let c2 = v0 - omega
y = (c1 + c2 * t) * exp(-omega * t)
} else if zeta > 1 {
let s1 = omega * (-zeta + sqrt(zeta * zeta - 1))
let s2 = omega * (-zeta - sqrt(zeta * zeta - 1))
let c1 = (-s2 - v0) / (s2 - s1)
let c2 = (s1 + v0) / (s2 - s1)
y = c1 * exp(s1 * t) + c2 * exp(s2 * t)
} else {
let a = -omega * zeta
let b = omega * sqrt(1 - zeta * zeta)
let c2 = (v0 + a) / b
let theta = atan(c2)
// Alternatively y = (-cos(b * t) + c2 * sin(b * t)) * exp(a * t)
y = sqrt(1 + c2 * c2) * exp(a * t) * cos(b * t + theta + Double.pi)
}
return y + 1
}
func scrollTo(_ index: Int, playReaction: Bool, immediately: Bool, duration: Double, damping: Double = 0.6, clockwise: Bool? = nil) {
guard index >= 0 && index < self.itemNodes.count else {
return
}
@ -255,7 +299,12 @@ private class ReactionCarouselNode: ASDisplayNode, UIScrollViewDelegate {
}
} else {
self.animator = DisplayLinkAnimator(duration: duration * UIView.animationDurationFactor(), from: 0.0, to: 1.0, update: { [weak self] t in
let t = listViewAnimationCurveSystem(t)
var t = t
if duration <= 0.2 {
t = listViewAnimationCurveSystem(t)
} else {
t = self?.springCurveFunc(t, zeta: damping) ?? 0.0
}
var updatedPosition = startPosition + change * t
while updatedPosition >= 1.0 {
updatedPosition -= 1.0
@ -459,7 +508,7 @@ private class ReactionCarouselNode: ASDisplayNode, UIScrollViewDelegate {
let delta = self.positionDelta
let areaSize = CGSize(width: floor(size.width * 0.7), height: size.height * 0.45)
let areaSize = CGSize(width: floor(size.width * 0.7), height: size.height * 0.44)
for i in 0 ..< self.itemNodes.count {
let itemNode = self.itemNodes[i]
@ -492,7 +541,7 @@ private class ReactionCarouselNode: ASDisplayNode, UIScrollViewDelegate {
let itemFrame = CGRect(origin: CGPoint(x: size.width * 0.5 + point.x * areaSize.width * 0.5 - itemSize.width * 0.5, y: size.height * 0.5 + point.y * areaSize.height * 0.5 - itemSize.height * 0.5), size: itemSize)
containerNode.bounds = CGRect(origin: CGPoint(), size: itemFrame.size)
containerNode.position = CGPoint(x: itemFrame.midX, y: itemFrame.midY)
transition.updateTransformScale(node: containerNode, scale: 1.0 - distance * 0.55)
transition.updateTransformScale(node: containerNode, scale: 1.0 - distance * 0.65)
itemNode.frame = CGRect(origin: CGPoint(), size: itemFrame.size)
itemNode.updateLayout(size: itemFrame.size, isExpanded: false, largeExpanded: false, isPreviewing: false, transition: transition)

View File

@ -218,6 +218,9 @@ private class StickersCarouselNode: ASDisplayNode, UIScrollViewDelegate {
private let positionDelta: Double
private var previousInteractionTimestamp: Double = 0.0
private var timer: SwiftSignalKit.Timer?
init(context: AccountContext, stickers: [TelegramMediaFile]) {
self.context = context
self.stickers = Array(stickers.shuffled().prefix(14))
@ -249,6 +252,8 @@ private class StickersCarouselNode: ASDisplayNode, UIScrollViewDelegate {
}
@objc private func stickerTapped(_ gestureRecognizer: UITapGestureRecognizer) {
self.previousInteractionTimestamp = CACurrentMediaTime()
guard self.animator == nil, self.scrollStartPosition == nil else {
return
}
@ -263,6 +268,24 @@ private class StickersCarouselNode: ASDisplayNode, UIScrollViewDelegate {
func animateIn() {
self.scrollTo(1, playAnimation: true, duration: 0.5, clockwise: true)
if self.timer == nil {
self.previousInteractionTimestamp = CACurrentMediaTime()
self.timer = SwiftSignalKit.Timer(timeout: 1.0, repeat: true, completion: { [weak self] in
if let strongSelf = self {
let currentTimestamp = CACurrentMediaTime()
if currentTimestamp > strongSelf.previousInteractionTimestamp + 4.0 {
var nextIndex = strongSelf.currentIndex - 1
if nextIndex < 0 {
nextIndex = strongSelf.stickers.count + nextIndex
}
strongSelf.scrollTo(nextIndex, playAnimation: true, duration: 0.85, clockwise: true)
strongSelf.previousInteractionTimestamp = currentTimestamp
}
}
}, queue: Queue.mainQueue())
self.timer?.start()
}
}
func scrollTo(_ index: Int, playAnimation: Bool, duration: Double, clockwise: Bool? = nil) {
@ -360,6 +383,9 @@ private class StickersCarouselNode: ASDisplayNode, UIScrollViewDelegate {
let containerNode = self.itemContainerNodes[i]
let isCentral = i == index
itemNode.setCentral(isCentral)
if !isCentral {
itemNode.setVisible(false)
}
if isCentral {
containerNode.view.superview?.bringSubviewToFront(containerNode.view)
@ -372,10 +398,18 @@ private class StickersCarouselNode: ASDisplayNode, UIScrollViewDelegate {
if self.scrollStartPosition == nil {
self.scrollStartPosition = (scrollView.contentOffset.y, self.currentPosition)
}
for itemNode in self.itemNodes {
itemNode.setCentral(false)
}
}
private let hapticFeedback = HapticFeedback()
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if scrollView.isTracking {
self.previousInteractionTimestamp = CACurrentMediaTime()
}
guard !self.ignoreContentOffsetChange, let (startContentOffset, startPosition) = self.scrollStartPosition else {
return
}
@ -422,6 +456,8 @@ private class StickersCarouselNode: ASDisplayNode, UIScrollViewDelegate {
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
if !decelerate {
self.previousInteractionTimestamp = CACurrentMediaTime()
self.resetScrollPosition()
let delta = self.positionDelta
@ -431,6 +467,8 @@ private class StickersCarouselNode: ASDisplayNode, UIScrollViewDelegate {
}
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
self.previousInteractionTimestamp = CACurrentMediaTime()
self.resetScrollPosition()
self.playSelectedSticker()
}
@ -482,8 +520,8 @@ private class StickersCarouselNode: ASDisplayNode, UIScrollViewDelegate {
let itemFrame = CGRect(origin: CGPoint(x: -size.width - 0.5 * itemSize.width - 30.0 + point.x * areaSize.width * 0.5 - itemSize.width * 0.5, y: size.height * 0.5 + point.y * areaSize.height * 0.5 - itemSize.height * 0.5), size: itemSize)
containerNode.bounds = CGRect(origin: CGPoint(), size: itemFrame.size)
containerNode.position = CGPoint(x: itemFrame.midX, y: itemFrame.midY)
transition.updateTransformScale(node: containerNode, scale: 1.0 - distance * 0.65)
transition.updateAlpha(node: containerNode, alpha: 1.0 - distance * 0.5)
transition.updateTransformScale(node: containerNode, scale: 1.0 - distance * 0.75)
transition.updateAlpha(node: containerNode, alpha: 1.0 - distance * 0.6)
let isVisible = self.visibility && itemFrame.intersects(bounds)
itemNode.setVisible(isVisible)

View File

@ -523,6 +523,14 @@ public final class SolidRoundedButtonView: UIView {
}
}
public var gloss: Bool {
didSet {
if self.gloss != oldValue {
self.setupGloss()
}
}
}
public var progressType: SolidRoundedButtonProgressType = .fullSize
public init(title: String? = nil, icon: UIImage? = nil, theme: SolidRoundedButtonTheme, font: SolidRoundedButtonFont = .bold, fontSize: CGFloat = 17.0, height: CGFloat = 48.0, cornerRadius: CGFloat = 24.0, gloss: Bool = false) {
@ -532,6 +540,7 @@ public final class SolidRoundedButtonView: UIView {
self.buttonHeight = height
self.buttonCornerRadius = cornerRadius
self.title = title
self.gloss = gloss
self.buttonBackgroundNode = UIImageView()
self.buttonBackgroundNode.clipsToBounds = true
@ -604,33 +613,55 @@ public final class SolidRoundedButtonView: UIView {
}
if gloss {
let shimmerView = ShimmerEffectForegroundView()
self.shimmerView = shimmerView
if #available(iOS 13.0, *) {
shimmerView.layer.cornerCurve = .continuous
shimmerView.layer.cornerRadius = self.buttonCornerRadius
self.setupGloss()
}
}
private func setupGloss() {
if self.gloss {
if self.shimmerView == nil {
let shimmerView = ShimmerEffectForegroundView()
self.shimmerView = shimmerView
if #available(iOS 13.0, *) {
shimmerView.layer.cornerCurve = .continuous
shimmerView.layer.cornerRadius = self.buttonCornerRadius
}
let borderView = UIView()
borderView.isUserInteractionEnabled = false
self.borderView = borderView
let borderMaskView = UIView()
borderMaskView.layer.borderWidth = 1.0 + UIScreenPixel
borderMaskView.layer.borderColor = UIColor.white.cgColor
borderMaskView.layer.cornerRadius = self.buttonCornerRadius
borderView.mask = borderMaskView
self.borderMaskView = borderMaskView
let borderShimmerView = ShimmerEffectForegroundView()
self.borderShimmerView = borderShimmerView
borderView.addSubview(borderShimmerView)
self.insertSubview(shimmerView, belowSubview: self.buttonNode)
self.insertSubview(borderView, belowSubview: self.buttonNode)
self.updateShimmerParameters()
if let width = self.validLayout {
_ = self.updateLayout(width: width, transition: .immediate)
}
}
} else if self.shimmerView != nil {
self.shimmerView?.removeFromSuperview()
self.borderView?.removeFromSuperview()
self.borderMaskView?.removeFromSuperview()
self.borderShimmerView?.removeFromSuperview()
let borderView = UIView()
borderView.isUserInteractionEnabled = false
self.borderView = borderView
let borderMaskView = UIView()
borderMaskView.layer.borderWidth = 1.0 + UIScreenPixel
borderMaskView.layer.borderColor = UIColor.white.cgColor
borderMaskView.layer.cornerRadius = self.buttonCornerRadius
borderView.mask = borderMaskView
self.borderMaskView = borderMaskView
let borderShimmerView = ShimmerEffectForegroundView()
self.borderShimmerView = borderShimmerView
borderView.addSubview(borderShimmerView)
self.insertSubview(shimmerView, belowSubview: self.buttonNode)
self.insertSubview(borderView, belowSubview: self.buttonNode)
self.updateShimmerParameters()
self.shimmerView = nil
self.borderView = nil
self.borderMaskView = nil
self.borderShimmerView = nil
}
}

View File

@ -67,7 +67,9 @@ final class StickerPackPreviewGridItemNode: GridItemNode {
private var animationNode: AnimatedStickerNode?
private var placeholderNode: StickerShimmerEffectNode
private var lockBackground: UIImageView?
private var lockBackground: UIVisualEffectView?
private var lockTintView: UIView?
private var lockIconNode: ASImageNode?
private var theme: PresentationTheme?
@ -167,21 +169,43 @@ final class StickerPackPreviewGridItemNode: GridItemNode {
self.isLocked = isLocked
if isLocked {
let lockBackground: UIImageView
if let currentBackground = self.lockBackground {
let lockBackground: UIVisualEffectView
let lockIconNode: ASImageNode
if let currentBackground = self.lockBackground, let currentIcon = self.lockIconNode {
lockBackground = currentBackground
lockIconNode = currentIcon
} else {
lockBackground = UIImageView()
let effect: UIBlurEffect
if #available(iOS 10.0, *) {
effect = UIBlurEffect(style: .regular)
} else {
effect = UIBlurEffect(style: .light)
}
lockBackground = UIVisualEffectView(effect: effect)
lockBackground.clipsToBounds = true
lockBackground.isUserInteractionEnabled = false
lockBackground.image = PresentationResourcesChat.chatInputMediaStickerGridPremiumIcon(theme)
lockIconNode = ASImageNode()
lockIconNode.displaysAsynchronously = false
lockIconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Stickers/Lock"), color: .white)
let lockTintView = UIView()
lockTintView.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.15)
lockBackground.contentView.addSubview(lockTintView)
self.lockBackground = lockBackground
self.lockTintView = lockTintView
self.lockIconNode = lockIconNode
self.view.addSubview(lockBackground)
self.addSubnode(lockIconNode)
}
} else if let lockBackground = self.lockBackground {
} else if let lockBackground = self.lockBackground, let lockTintView = self.lockTintView, let lockIconNode = self.lockIconNode {
self.lockBackground = nil
self.lockTintView = nil
self.lockIconNode = nil
lockBackground.removeFromSuperview()
lockTintView.removeFromSuperview()
lockIconNode.removeFromSupernode()
}
if let stickerItem = stickerItem {
@ -290,10 +314,18 @@ final class StickerPackPreviewGridItemNode: GridItemNode {
self.placeholderNode.update(backgroundColor: theme.list.itemBlocksBackgroundColor, foregroundColor: theme.list.mediaPlaceholderColor, shimmeringColor: theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.4), data: item.file.immediateThumbnailData, size: placeholderFrame.size)
}
if let lockBackground = self.lockBackground {
let lockSize = CGSize(width: 32.0, height: 32.0)
if let lockBackground = self.lockBackground, let lockTintView = self.lockTintView, let lockIconNode = self.lockIconNode {
let lockSize = CGSize(width: 30.0, height: 30.0)
let lockBackgroundFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((bounds.width - lockSize.width) / 2.0), y: bounds.height - lockSize.height - 6.0), size: lockSize)
lockBackground.frame = lockBackgroundFrame
lockBackground.layer.cornerRadius = lockSize.width / 2.0
if #available(iOS 13.0, *) {
lockBackground.layer.cornerCurve = .circular
}
lockTintView.frame = CGRect(origin: CGPoint(), size: lockBackgroundFrame.size)
if let icon = lockIconNode.image {
lockIconNode.frame = CGRect(origin: CGPoint(x: lockBackgroundFrame.minX + floorToScreenPixels((lockBackgroundFrame.width - icon.size.width) / 2.0), y: lockBackgroundFrame.minY + floorToScreenPixels((lockBackgroundFrame.height - icon.size.height) / 2.0)), size: icon.size)
}
}
}

View File

@ -181,7 +181,7 @@ public final class StickerPreviewPeekContentNode: ASDisplayNode, PeekControllerC
}
if let dimensitons = self.item.file.dimensions {
let textSpacing: CGFloat = 10.0
let textSpacing: CGFloat = 50.0
let textSize = self.textNode.measure(CGSize(width: 100.0, height: 100.0))
let imageSize = dimensitons.cgSize.aspectFitted(boundingSize)

View File

@ -1169,6 +1169,7 @@ public class Account {
self.managedOperationsDisposable.add(managedAnimatedEmojiAnimationsUpdates(postbox: self.postbox, network: self.network).start())
}
self.managedOperationsDisposable.add(managedGreetingStickers(postbox: self.postbox, network: self.network).start())
self.managedOperationsDisposable.add(managedPremiumStickers(postbox: self.postbox, network: self.network).start())
if !supplementary {
let mediaBox = postbox.mediaBox

View File

@ -154,3 +154,27 @@ func managedGreetingStickers(postbox: Postbox, network: Network) -> Signal<Void,
})
return (poll |> then(.complete() |> suspendAwareDelay(3.0 * 60.0 * 60.0, queue: Queue.concurrentDefaultQueue()))) |> restart
}
func managedPremiumStickers(postbox: Postbox, network: Network) -> Signal<Void, NoError> {
let poll = managedRecentMedia(postbox: postbox, network: network, collectionId: Namespaces.OrderedItemList.CloudPremiumStickers, reverseHashOrder: false, forceFetch: false, fetch: { hash in
return network.request(Api.functions.messages.getStickers(emoticon: "⭐️⭐️", hash: 0))
|> retryRequest
|> mapToSignal { result -> Signal<[OrderedItemListEntry]?, NoError> in
switch result {
case .stickersNotModified:
return .single(nil)
case let .stickers(_, stickers):
var items: [OrderedItemListEntry] = []
for sticker in stickers {
if let file = telegramMediaFileFromApiDocument(sticker), let id = file.id {
if let entry = CodableEntry(RecentMediaItem(file)) {
items.append(OrderedItemListEntry(id: RecentMediaItemId(id).rawValue, contents: entry))
}
}
}
return .single(items)
}
}
})
return (poll |> then(.complete() |> suspendAwareDelay(3.0 * 60.0 * 60.0, queue: Queue.concurrentDefaultQueue()))) |> restart
}

View File

@ -62,6 +62,7 @@ public struct Namespaces {
public static let CloudGreetingStickers: Int32 = 10
public static let RecentDownloads: Int32 = 11
public static let PremiumStickers: Int32 = 12
public static let CloudPremiumStickers: Int32 = 13
}
public struct CachedItemCollection {

View File

@ -38,6 +38,7 @@ public enum PresentationResourceKey: Int32 {
case itemListDownArrow
case itemListDisclosureArrow
case itemListDisclosureLocked
case itemListCheckIcon
case itemListSecondaryCheckIcon
case itemListPlusIcon

View File

@ -33,6 +33,12 @@ public struct PresentationResourcesItemList {
})
}
public static func disclosureLockedImage(_ theme: PresentationTheme) -> UIImage? {
return theme.image(PresentationResourceKey.itemListDisclosureLocked.rawValue, { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Stickers/SmallLock"), color: theme.list.disclosureArrowColor)
})
}
public static func checkIconImage(_ theme: PresentationTheme) -> UIImage? {
return theme.image(PresentationResourceKey.itemListCheckIcon.rawValue, { theme in
return generateItemListCheckIcon(color: theme.list.itemAccentColor)

View File

@ -1,21 +0,0 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "dots@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "phone.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,176 @@
%PDF-1.7
1 0 obj
<< /ExtGState << /E1 << /ca 0.500000 >> >> >>
endobj
2 0 obj
<< /Length 3 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
/E1 gs
1.000000 0.000000 -0.000000 1.000000 0.000000 -0.003418 cm
0.000000 0.000000 0.000000 scn
1001.992859 2652.003418 m
1040.278442 2652.004395 1072.592285 2652.005127 1099.064819 2649.842285 c
1126.787598 2647.577148 1153.344360 2642.644287 1178.614136 2629.768555 c
1216.999146 2610.210449 1248.207153 2579.002441 1267.765259 2540.617432 c
1280.640869 2515.347656 1285.573730 2488.791016 1287.838745 2461.068115 c
1290.001709 2434.595947 1290.000854 2402.282471 1290.000000 2363.997559 c
1290.000000 288.009521 l
1290.000854 249.724609 1290.001709 217.410889 1287.838745 190.938965 c
1285.573730 163.216064 1280.640869 136.659180 1267.765259 111.389648 c
1248.207153 73.004395 1216.999146 41.796631 1178.614136 22.238281 c
1153.344360 9.362793 1126.787598 4.429932 1099.064819 2.164795 c
1072.592529 0.001709 1040.279175 0.002686 1001.994263 0.003662 c
288.005798 0.003662 l
249.720932 0.002686 217.407471 0.001709 190.935211 2.164795 c
163.212448 4.429932 136.655716 9.362793 111.386002 22.238281 c
73.000938 41.796631 41.792908 73.004395 22.234743 111.389648 c
9.359177 136.659180 4.426258 163.216064 2.161221 190.938965 c
-0.001672 217.411621 -0.000894 249.725342 0.000039 288.010742 c
0.000039 2363.996338 l
-0.000894 2402.281738 -0.001672 2434.595703 2.161221 2461.068115 c
4.426258 2488.791016 9.359177 2515.347656 22.234743 2540.617432 c
41.792908 2579.002441 73.000938 2610.210449 111.386002 2629.768555 c
136.655716 2642.644287 163.212463 2647.577148 190.935226 2649.842285 c
217.407806 2652.005127 249.721725 2652.004395 288.007172 2652.003418 c
1001.992859 2652.003418 l
h
75.695122 2513.377930 m
60.000065 2482.574707 60.000069 2442.250977 60.000069 2361.603516 c
60.000069 290.403564 l
60.000069 209.756104 60.000065 169.432129 75.695122 138.628906 c
89.500893 111.533691 111.530090 89.504395 138.625427 75.698730 c
169.428711 60.003662 209.752472 60.003662 290.399994 60.003662 c
999.600037 60.003662 l
1080.247559 60.003662 1120.571289 60.003662 1151.374634 75.698730 c
1178.469971 89.504395 1200.499146 111.533691 1214.304932 138.628906 c
1230.000000 169.432129 1230.000000 209.756104 1230.000000 290.403564 c
1230.000000 2361.603271 l
1230.000000 2442.250977 1230.000000 2482.574707 1214.304932 2513.377930 c
1200.499146 2540.473389 1178.469971 2562.502441 1151.374634 2576.308350 c
1120.571289 2592.003418 1080.247559 2592.003418 999.600037 2592.003418 c
290.400055 2592.003418 l
209.752487 2592.003418 169.428711 2592.003418 138.625427 2576.308350 c
111.530090 2562.502441 89.500893 2540.473389 75.695122 2513.377930 c
h
f*
n
Q
q
1.000000 0.000000 -0.000000 1.000000 18.000000 17.998047 cm
0.000000 0.000000 0.000000 scn
1079.599121 2613.900635 m
1053.868774 2616.002686 1022.190063 2616.002441 983.365845 2616.001953 c
270.634308 2616.001953 l
231.810059 2616.002441 200.131302 2616.002686 174.400970 2613.900635 c
147.791351 2611.726562 123.867096 2607.096191 101.557800 2595.729248 c
66.559647 2577.896729 38.105267 2549.442383 20.272820 2514.444092 c
8.905663 2492.134766 4.275493 2468.210693 2.101401 2441.601074 c
-0.000850 2415.870605 -0.000461 2384.191895 0.000014 2345.367676 c
0.000014 270.636230 l
-0.000461 231.812012 -0.000850 200.133301 2.101401 174.402832 c
4.275493 147.793457 8.905663 123.868896 20.272820 101.559814 c
38.105267 66.561523 66.559647 38.107178 101.557800 20.274658 c
123.867096 8.907715 147.791351 4.277344 174.400955 2.103271 c
200.132080 0.000977 231.812027 0.001465 270.637878 0.001953 c
983.362183 0.001953 l
1022.187927 0.001465 1053.867920 0.000977 1079.599121 2.103271 c
1106.208740 4.277344 1130.132935 8.907715 1152.442139 20.274658 c
1187.440308 38.107178 1215.894775 66.561523 1233.727173 101.559814 c
1245.094360 123.868896 1249.724609 147.793457 1251.898560 174.402832 c
1254.000854 200.131836 1254.000488 231.808838 1254.000000 270.630859 c
1254.000000 2345.373291 l
1254.000488 2384.194824 1254.000854 2415.871826 1251.898560 2441.601074 c
1249.724609 2468.210693 1245.094360 2492.134766 1233.727173 2514.444092 c
1215.894775 2549.442383 1187.440308 2577.896729 1152.442139 2595.729248 c
1130.132935 2607.096191 1106.208740 2611.726562 1079.599121 2613.900635 c
h
42.000034 2343.602051 m
42.000034 2424.249512 42.000031 2464.573242 57.695091 2495.376465 c
71.500854 2522.471924 93.530060 2544.500977 120.625397 2558.306885 c
151.428680 2574.001953 191.752472 2574.001953 272.400024 2574.001953 c
359.579956 2574.001953 l
360.458221 2573.991699 l
366.948761 2573.875977 371.125000 2572.780518 374.811859 2570.816895 c
378.715668 2568.737793 381.783417 2565.685303 383.882019 2561.791992 c
385.609863 2558.586670 386.679810 2555.006104 387.023712 2549.834473 c
387.280701 2523.251221 390.322266 2513.048340 395.821350 2502.766113 c
401.573456 2492.010498 410.014465 2483.569336 420.770050 2477.817383 c
431.525635 2472.065186 442.194092 2469.001953 471.614197 2469.001953 c
782.397766 2469.001953 l
811.817871 2469.001953 822.486328 2472.065186 833.241821 2477.817383 c
843.997375 2483.569336 852.438477 2492.010498 858.190613 2502.766113 c
863.689697 2513.048340 866.731323 2523.251221 866.988037 2549.840820 c
867.332153 2555.006104 868.402039 2558.586670 870.129883 2561.791992 c
872.228577 2565.685303 875.296265 2568.737793 879.200073 2570.816895 c
882.886963 2572.780518 887.063110 2573.875977 893.553711 2573.991699 c
894.431946 2574.001953 l
981.600037 2574.001953 l
1062.247559 2574.001953 1102.571289 2574.001953 1133.374634 2558.306885 c
1160.469971 2544.500977 1182.499146 2522.471924 1196.304932 2495.376465 c
1212.000000 2464.573242 1212.000000 2424.249512 1212.000000 2343.602051 c
1212.000000 272.401855 l
1212.000000 191.754395 1212.000000 151.430664 1196.304932 120.627197 c
1182.499146 93.531982 1160.469971 71.502930 1133.374634 57.697021 c
1102.571289 42.001953 1062.247559 42.001953 981.600037 42.001953 c
272.399963 42.001953 l
191.752441 42.001953 151.428680 42.001953 120.625397 57.697021 c
93.530060 71.502930 71.500854 93.531982 57.695091 120.627197 c
42.000031 151.430664 42.000034 191.754395 42.000034 272.401855 c
42.000034 2343.602051 l
h
f*
n
Q
endstream
endobj
3 0 obj
6076
endobj
4 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 1290.000000 2652.000000 ]
/Resources 1 0 R
/Contents 2 0 R
/Parent 5 0 R
>>
endobj
5 0 obj
<< /Kids [ 4 0 R ]
/Count 1
/Type /Pages
>>
endobj
6 0 obj
<< /Pages 5 0 R
/Type /Catalog
>>
endobj
xref
0 7
0000000000 65535 f
0000000010 00000 n
0000000074 00000 n
0000006206 00000 n
0000006229 00000 n
0000006406 00000 n
0000006480 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 6 0 R
/Size 7
>>
startxref
6539
%%EOF

View File

@ -4695,6 +4695,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
themeEmoticon = nil
}
}
if strongSelf.chatLocation.peerId == strongSelf.context.account.peerId {
themeEmoticon = nil
}
var presentationData = presentationData
var useDarkAppearance = presentationData.theme.overallDarkAppearance
@ -11535,12 +11538,14 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
for item in results {
if let item = item {
if item.fileSize > Int64(premiumLimits.maxUploadFileParts) * 512 * 1024 {
strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: strongSelf.presentationData.strings.Premium_FileTooLarge, text: strongSelf.presentationData.strings.Conversation_PremiumUploadFileTooLarge, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
let controller = PremiumLimitScreen(context: strongSelf.context, subject: .files, count: 4, action: {
})
strongSelf.push(controller)
return
} else if item.fileSize > Int64(limits.maxUploadFileParts) * 512 * 1024 && !isPremium {
let context = strongSelf.context
var replaceImpl: ((ViewController) -> Void)?
let controller = PremiumLimitScreen(context: context, subject: .files, count: 0, action: {
let controller = PremiumLimitScreen(context: context, subject: .files, count: 2, action: {
replaceImpl?(PremiumIntroScreen(context: context, source: .upload))
})
replaceImpl = { [weak controller] c in

View File

@ -178,7 +178,9 @@ final class ChatMediaInputStickerGridItemNode: GridItemNode {
private(set) var animationNode: AnimatedStickerNode?
private(set) var placeholderNode: StickerShimmerEffectNode?
private var lockBackground: UIImageView?
private var lockBackground: UIVisualEffectView?
private var lockTintView: UIView?
private var lockIconNode: ASImageNode?
var isLocked: Bool?
private var didSetUpAnimationNode = false
@ -313,21 +315,43 @@ final class ChatMediaInputStickerGridItemNode: GridItemNode {
self.isLocked = item.isLocked
if item.isLocked {
let lockBackground: UIImageView
if let currentBackground = self.lockBackground {
let lockBackground: UIVisualEffectView
let lockIconNode: ASImageNode
if let currentBackground = self.lockBackground, let currentIcon = self.lockIconNode {
lockBackground = currentBackground
lockIconNode = currentIcon
} else {
lockBackground = UIImageView()
let effect: UIBlurEffect
if #available(iOS 10.0, *) {
effect = UIBlurEffect(style: .regular)
} else {
effect = UIBlurEffect(style: .light)
}
lockBackground = UIVisualEffectView(effect: effect)
lockBackground.clipsToBounds = true
lockBackground.isUserInteractionEnabled = false
lockBackground.image = PresentationResourcesChat.chatInputMediaStickerGridPremiumIcon(item.theme)
lockIconNode = ASImageNode()
lockIconNode.displaysAsynchronously = false
lockIconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Stickers/SmallLock"), color: .white)
let lockTintView = UIView()
lockTintView.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.15)
lockBackground.contentView.addSubview(lockTintView)
self.lockBackground = lockBackground
self.lockTintView = lockTintView
self.lockIconNode = lockIconNode
self.view.addSubview(lockBackground)
self.addSubnode(lockIconNode)
}
} else if let lockBackground = self.lockBackground {
} else if let lockBackground = self.lockBackground, let lockTintView = self.lockTintView, let lockIconNode = self.lockIconNode {
self.lockBackground = nil
self.lockTintView = nil
self.lockIconNode = nil
lockBackground.removeFromSuperview()
lockTintView.removeFromSuperview()
lockIconNode.removeFromSupernode()
}
}
@ -360,10 +384,18 @@ final class ChatMediaInputStickerGridItemNode: GridItemNode {
placeholderNode.update(backgroundColor: theme.chat.inputMediaPanel.stickersBackgroundColor.withAlphaComponent(1.0), foregroundColor: theme.chat.inputMediaPanel.stickersSectionTextColor.blitOver(theme.chat.inputMediaPanel.stickersBackgroundColor, alpha: 0.15), shimmeringColor: theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.3), data: item.stickerItem.file.immediateThumbnailData, size: placeholderFrame.size)
}
if let lockBackground = self.lockBackground {
let lockSize = CGSize(width: 26.0, height: 26.0)
if let lockBackground = self.lockBackground, let lockTintView = self.lockTintView, let lockIconNode = self.lockIconNode {
let lockSize = CGSize(width: 24.0, height: 24.0)
let lockBackgroundFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - lockSize.width) / 2.0), y: size.height - lockSize.height - 2.0), size: lockSize)
lockBackground.frame = lockBackgroundFrame
lockBackground.layer.cornerRadius = lockSize.width / 2.0
if #available(iOS 13.0, *) {
lockBackground.layer.cornerCurve = .circular
}
lockTintView.frame = CGRect(origin: CGPoint(), size: lockBackgroundFrame.size)
if let icon = lockIconNode.image {
lockIconNode.frame = CGRect(origin: CGPoint(x: lockBackgroundFrame.minX + floorToScreenPixels((lockBackgroundFrame.width - icon.size.width) / 2.0), y: lockBackgroundFrame.minY + floorToScreenPixels((lockBackgroundFrame.height - icon.size.height) / 2.0)), size: icon.size)
}
}
}

View File

@ -2016,6 +2016,8 @@ final class PeerInfoHeaderNode: ASDisplayNode {
var displayAvatarContextMenu: ((ASDisplayNode, ContextGesture?) -> Void)?
var displayCopyContextMenu: ((ASDisplayNode, Bool, Bool) -> Void)?
var displayPremiumIntro: ((UIView, Bool) -> Void)?
var navigationTransition: PeerInfoHeaderNavigationTransition?
var backgroundAlpha: CGFloat = 1.0
@ -2180,6 +2182,12 @@ final class PeerInfoHeaderNode: ASDisplayNode {
let phoneGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(self.handlePhoneLongPress(_:)))
self.subtitleNodeRawContainer.view.addGestureRecognizer(phoneGestureRecognizer)
let premiumGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.handleStarTap(_:)))
self.titleCredibilityIconNode.view.addGestureRecognizer(premiumGestureRecognizer)
let expandedPremiumGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.handleStarTap(_:)))
self.titleExpandedCredibilityIconNode.view.addGestureRecognizer(expandedPremiumGestureRecognizer)
}
@objc private func handleUsernameLongPress(_ gestureRecognizer: UILongPressGestureRecognizer) {
@ -2194,6 +2202,13 @@ final class PeerInfoHeaderNode: ASDisplayNode {
}
}
@objc private func handleStarTap(_ gestureRecognizer: UITapGestureRecognizer) {
guard let view = gestureRecognizer.view, self.currentCredibilityIcon == .premium else {
return
}
self.displayPremiumIntro?(view, view == self.titleExpandedCredibilityIconNode.view)
}
func initiateAvatarExpansion(gallery: Bool, first: Bool) {
if let peer = self.peer, peer.profileImageRepresentations.isEmpty && gallery {
self.requestOpenAvatarForEditing?(false)
@ -3073,6 +3088,16 @@ final class PeerInfoHeaderNode: ASDisplayNode {
if !self.backgroundNode.frame.contains(point) {
return nil
}
if self.currentCredibilityIcon == .premium {
let iconFrame = self.titleCredibilityIconNode.view.convert(self.titleCredibilityIconNode.bounds, to: self.view)
let expandedIconFrame = self.titleExpandedCredibilityIconNode.view.convert(self.titleExpandedCredibilityIconNode.bounds, to: self.view)
if expandedIconFrame.contains(point) && self.isAvatarExpanded {
return self.titleExpandedCredibilityIconNode.view
} else if iconFrame.contains(point) {
return self.titleCredibilityIconNode.view
}
}
if result == self.view || result == self.regularContentNode.view || result == self.editingContentNode.view {
return nil
}

View File

@ -651,11 +651,10 @@ private func settingsItems(data: PeerInfoScreenData?, context: AccountContext, p
interaction.accountContextMenu(peerAccountContext.account.id, node, gesture)
}))
}
if settings.accountsAndPeers.count + 1 < maximumNumberOfAccounts {
items[.accounts]!.append(PeerInfoScreenActionItem(id: 100, text: presentationData.strings.Settings_AddAccount, icon: PresentationResourcesItemList.plusIconImage(presentationData.theme), action: {
interaction.openSettings(.addAccount)
}))
}
items[.accounts]!.append(PeerInfoScreenActionItem(id: 100, text: presentationData.strings.Settings_AddAccount, icon: PresentationResourcesItemList.plusIconImage(presentationData.theme), action: {
interaction.openSettings(.addAccount)
}))
}
if !settings.proxySettings.servers.isEmpty {
@ -824,12 +823,10 @@ private func settingsEditingItems(data: PeerInfoScreenData?, state: PeerInfoStat
interaction.openSettings(.username)
}))
if let settings = data.globalSettings, settings.accountsAndPeers.count + 1 < maximumNumberOfAccounts {
items[.account]!.append(PeerInfoScreenActionItem(id: ItemAddAccount, text: presentationData.strings.Settings_AddAnotherAccount, alignment: .center, action: {
interaction.openSettings(.addAccount)
}))
items[.account]!.append(PeerInfoScreenCommentItem(id: ItemAddAccountHelp, text: presentationData.strings.Settings_AddAnotherAccount_Help))
}
items[.account]!.append(PeerInfoScreenActionItem(id: ItemAddAccount, text: presentationData.strings.Settings_AddAnotherAccount, alignment: .center, action: {
interaction.openSettings(.addAccount)
}))
items[.account]!.append(PeerInfoScreenCommentItem(id: ItemAddAccountHelp, text: presentationData.strings.Settings_AddAnotherAccount_Help))
items[.logout]!.append(PeerInfoScreenActionItem(id: ItemLogout, text: presentationData.strings.Settings_Logout, color: .destructive, alignment: .center, action: {
interaction.openSettings(.logout)
@ -2936,6 +2933,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
screenData = peerInfoScreenSettingsData(context: context, peerId: peerId, accountsAndPeers: self.accountsAndPeers.get(), activeSessionsContextAndCount: self.activeSessionsContextAndCount.get(), notificationExceptions: self.notificationExceptions.get(), privacySettings: self.privacySettings.get(), archivedStickerPacks: self.archivedPacks.get(), hasPassport: self.hasPassport.get())
self.headerNode.displayCopyContextMenu = { [weak self] node, copyPhone, copyUsername in
guard let strongSelf = self, let data = strongSelf.data, let user = data.peer as? TelegramUser else {
return
@ -2974,7 +2972,19 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
}
} else {
screenData = peerInfoScreenData(context: context, peerId: peerId, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, isSettings: self.isSettings, hintGroupInCommon: hintGroupInCommon, existingRequestsContext: requestsContext)
self.headerNode.displayPremiumIntro = { [weak self] sourceView, white in
guard let strongSelf = self else {
return
}
let controller = PremiumIntroScreen(context: strongSelf.context, source: .profile(strongSelf.peerId))
controller.sourceView = sourceView
controller.containerView = strongSelf.controller?.navigationController?.view
controller.animationColor = white ? .white : strongSelf.presentationData.theme.list.itemAccentColor
strongSelf.controller?.push(controller)
}
self.headerNode.displayAvatarContextMenu = { [weak self] node, gesture in
guard let strongSelf = self, let peer = strongSelf.data?.peer else {
return
@ -6206,7 +6216,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
case .language:
push(LocalizationListController(context: self.context))
case .premium:
self.controller?.push(PremiumIntroScreen(context: self.context, modal: false, source: .settings))
self.controller?.push(PremiumIntroScreen(context: self.context, modal: false, source: .settings))
case .stickers:
if let settings = self.data?.globalSettings {
push(installedStickerPacksController(context: self.context, mode: .general, archivedPacks: settings.archivedStickerPacks, updatedPacks: { [weak self] packs in
@ -6247,11 +6257,37 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
case .username:
push(usernameSetupController(context: self.context))
case .addAccount:
self.context.sharedContext.beginNewAuth(testingEnvironment: self.context.account.testingEnvironment)
var maximumAvailableAccounts: Int = 3
if self.data?.peer?.isPremium == true {
maximumAvailableAccounts = 4
}
var count: Int = 1
if let settings = self.data?.globalSettings {
for (_, peer, _) in settings.accountsAndPeers {
if peer.isPremium {
maximumAvailableAccounts = 4
}
}
count += settings.accountsAndPeers.count
}
if count >= maximumAvailableAccounts {
let context = self.context
var replaceImpl: ((ViewController) -> Void)?
let controller = PremiumLimitScreen(context: context, subject: .accounts, count: Int32(count), action: {
let controller = PremiumIntroScreen(context: context, source: .accounts)
replaceImpl?(controller)
})
replaceImpl = { [weak controller] c in
controller?.replace(with: c)
}
self.controller?.push(controller)
} else {
self.context.sharedContext.beginNewAuth(testingEnvironment: self.context.account.testingEnvironment)
}
case .logout:
if let user = self.data?.peer as? TelegramUser, let phoneNumber = user.phone, let accounts = self.data?.globalSettings?.accountsAndPeers {
if let user = self.data?.peer as? TelegramUser, let phoneNumber = user.phone {
if let controller = self.controller, let navigationController = controller.navigationController as? NavigationController {
self.controller?.push(logoutOptionsController(context: self.context, navigationController: navigationController, canAddAccounts: accounts.count + 1 < maximumNumberOfAccounts, phoneNumber: phoneNumber))
self.controller?.push(logoutOptionsController(context: self.context, navigationController: navigationController, canAddAccounts: true, phoneNumber: phoneNumber))
}
}
case .rememberPassword:
@ -7969,7 +8005,7 @@ public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen {
let presentationDataSignal: Signal<PresentationData, NoError>
if let updatedPresentationData = updatedPresentationData {
presentationDataSignal = updatedPresentationData.signal
} else {
} else if self.peerId != self.context.account.peerId {
let themeEmoticon: Signal<String?, NoError> = self.cachedDataPromise.get()
|> map { cachedData -> String? in
if let cachedData = cachedData as? CachedUserData {
@ -7995,6 +8031,8 @@ public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen {
}
return presentationData
}
} else {
presentationDataSignal = context.sharedContext.presentationData
}
self.presentationDataDisposable = (presentationDataSignal
@ -8151,17 +8189,16 @@ public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen {
let strings = self.presentationData.strings
var items: [ContextMenuItem] = []
if other.count + 1 < maximumNumberOfAccounts {
items.append(.action(ContextMenuActionItem(text: strings.Settings_AddAccount, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Add"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] _, f in
guard let strongSelf = self else {
return
}
strongSelf.controllerNode.openSettings(section: .addAccount)
f(.dismissWithoutContent)
})))
}
items.append(.action(ContextMenuActionItem(text: strings.Settings_AddAccount, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Add"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] _, f in
guard let strongSelf = self else {
return
}
strongSelf.controllerNode.openSettings(section: .addAccount)
f(.dismissWithoutContent)
})))
let avatarSize = CGSize(width: 28.0, height: 28.0)