Various improvements
@ -298,6 +298,9 @@ alternate_icon_folders = [
|
|||||||
"WhiteFilledIcon",
|
"WhiteFilledIcon",
|
||||||
"New1",
|
"New1",
|
||||||
"New2",
|
"New2",
|
||||||
|
"PremiumCosmic",
|
||||||
|
"PremiumCherry",
|
||||||
|
"PremiumDuck",
|
||||||
]
|
]
|
||||||
|
|
||||||
[
|
[
|
||||||
|
BIN
Telegram/Telegram-iOS/PremiumCherry.alticon/PremiumCherry@2x.png
Normal file
After Width: | Height: | Size: 20 KiB |
BIN
Telegram/Telegram-iOS/PremiumCherry.alticon/PremiumCherry@3x.png
Normal file
After Width: | Height: | Size: 34 KiB |
BIN
Telegram/Telegram-iOS/PremiumCosmic.alticon/PremiumCosmic@2x.png
Normal file
After Width: | Height: | Size: 24 KiB |
BIN
Telegram/Telegram-iOS/PremiumCosmic.alticon/PremiumCosmic@3x.png
Normal file
After Width: | Height: | Size: 48 KiB |
BIN
Telegram/Telegram-iOS/PremiumDuck.alticon/PremiumDuck@2x.png
Normal file
After Width: | Height: | Size: 20 KiB |
BIN
Telegram/Telegram-iOS/PremiumDuck.alticon/PremiumDuck@3x.png
Normal file
After Width: | Height: | Size: 33 KiB |
@ -7541,6 +7541,8 @@ Sorry for the inconvenience.";
|
|||||||
|
|
||||||
"Premium.Reactions.Proceed" = "Unlock Premium Reactions";
|
"Premium.Reactions.Proceed" = "Unlock Premium Reactions";
|
||||||
|
|
||||||
|
"Premium.AppIcons.Proceed" = "Unlock Premium Icons";
|
||||||
|
|
||||||
"AccessDenied.LocationPreciseDenied" = "To share your specific location in this chat, please go to Settings > Privacy > Location Services > Telegram and set Precise Location to On.";
|
"AccessDenied.LocationPreciseDenied" = "To share your specific location in this chat, please go to Settings > Privacy > Location Services > Telegram and set Precise Location to On.";
|
||||||
|
|
||||||
"Chat.MultipleTypingPair" = "%@ and %@";
|
"Chat.MultipleTypingPair" = "%@ and %@";
|
||||||
@ -7619,6 +7621,12 @@ Sorry for the inconvenience.";
|
|||||||
"Premium.Avatar" = "Animated Profile Pictures";
|
"Premium.Avatar" = "Animated Profile Pictures";
|
||||||
"Premium.AvatarInfo" = "Video avatars animated in chat lists and chats to allow for additional self-expression.";
|
"Premium.AvatarInfo" = "Video avatars animated in chat lists and chats to allow for additional self-expression.";
|
||||||
|
|
||||||
|
"Premium.AppIcon" = "App Icons";
|
||||||
|
"Premium.AppIconInfo" = "Additional app icons description goes here.";
|
||||||
|
|
||||||
|
"Premium.AppIconStandalone" = "Additional App Icons";
|
||||||
|
"Premium.AppIconStandaloneInfo" = "Unlock a wider range of app icons by subscribing to **Telegram Premium**.";
|
||||||
|
|
||||||
"Premium.SubscribeFor" = "Subscribe for %@ / month";
|
"Premium.SubscribeFor" = "Subscribe for %@ / month";
|
||||||
|
|
||||||
"Premium.AboutTitle" = "ABOUT TELEGRAM PREMIUM";
|
"Premium.AboutTitle" = "ABOUT TELEGRAM PREMIUM";
|
||||||
@ -7667,3 +7675,5 @@ Sorry for the inconvenience.";
|
|||||||
"Premium.Limits.FoldersInfo" = "Organize your chats into 20 folders";
|
"Premium.Limits.FoldersInfo" = "Organize your chats into 20 folders";
|
||||||
"Premium.Limits.ChatsPerFolderInfo" = "Add up to 200 chats into each of your folders";
|
"Premium.Limits.ChatsPerFolderInfo" = "Add up to 200 chats into each of your folders";
|
||||||
"Premium.Limits.AccountsInfo" = "Connect 4 accounts with different mobile numbers";
|
"Premium.Limits.AccountsInfo" = "Connect 4 accounts with different mobile numbers";
|
||||||
|
|
||||||
|
"WebApp.Settings" = "Settings";
|
||||||
|
@ -746,6 +746,7 @@ public enum PremiumIntroSource {
|
|||||||
case folders
|
case folders
|
||||||
case chatsPerFolder
|
case chatsPerFolder
|
||||||
case accounts
|
case accounts
|
||||||
|
case appIcons
|
||||||
case about
|
case about
|
||||||
case deeplink(String?)
|
case deeplink(String?)
|
||||||
case profile(PeerId)
|
case profile(PeerId)
|
||||||
|
@ -652,6 +652,7 @@ final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDelegate {
|
|||||||
|
|
||||||
@objc private func panGesture(_ recognizer: UIPanGestureRecognizer) {
|
@objc private func panGesture(_ recognizer: UIPanGestureRecognizer) {
|
||||||
let filtersLimit = self.filtersLimit.flatMap({ $0 + 1 }) ?? Int32(self.availableFilters.count)
|
let filtersLimit = self.filtersLimit.flatMap({ $0 + 1 }) ?? Int32(self.availableFilters.count)
|
||||||
|
let maxFilterIndex = min(Int(filtersLimit), self.availableFilters.count) - 1
|
||||||
|
|
||||||
switch recognizer.state {
|
switch recognizer.state {
|
||||||
case .began:
|
case .began:
|
||||||
@ -693,11 +694,11 @@ final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDelegate {
|
|||||||
transitionFraction = rubberBandingOffset(offset: overscroll, bandingStart: 0.0) / layout.size.width
|
transitionFraction = rubberBandingOffset(offset: overscroll, bandingStart: 0.0) / layout.size.width
|
||||||
}
|
}
|
||||||
|
|
||||||
if selectedIndex >= filtersLimit - 1 && translation.x < 0.0 {
|
if selectedIndex >= maxFilterIndex && translation.x < 0.0 {
|
||||||
let overscroll = -translation.x
|
let overscroll = -translation.x
|
||||||
transitionFraction = -rubberBandingOffset(offset: overscroll, bandingStart: 0.0) / layout.size.width
|
transitionFraction = -rubberBandingOffset(offset: overscroll, bandingStart: 0.0) / layout.size.width
|
||||||
|
|
||||||
if self.filtersLimit != nil {
|
if let filtersLimit = self.filtersLimit, selectedIndex >= filtersLimit - 1 {
|
||||||
transitionFraction = 0.0
|
transitionFraction = 0.0
|
||||||
recognizer.isEnabled = false
|
recognizer.isEnabled = false
|
||||||
recognizer.isEnabled = true
|
recognizer.isEnabled = true
|
||||||
@ -744,7 +745,7 @@ final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDelegate {
|
|||||||
if let directionIsToRight = directionIsToRight {
|
if let directionIsToRight = directionIsToRight {
|
||||||
var updatedIndex = selectedIndex
|
var updatedIndex = selectedIndex
|
||||||
if directionIsToRight {
|
if directionIsToRight {
|
||||||
updatedIndex = min(updatedIndex + 1, Int(filtersLimit) - 1)
|
updatedIndex = min(updatedIndex + 1, maxFilterIndex)
|
||||||
} else {
|
} else {
|
||||||
updatedIndex = max(updatedIndex - 1, 0)
|
updatedIndex = max(updatedIndex - 1, 0)
|
||||||
}
|
}
|
||||||
|
@ -781,7 +781,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
|||||||
}
|
}
|
||||||
let cachedPeerData = peerView.cachedData
|
let cachedPeerData = peerView.cachedData
|
||||||
if let cachedPeerData = cachedPeerData as? CachedUserData {
|
if let cachedPeerData = cachedPeerData as? CachedUserData {
|
||||||
if let photo = cachedPeerData.photo, let video = photo.videoRepresentations.last, let peerReference = PeerReference(peer._asPeer()) {
|
if let photo = cachedPeerData.photo, let video = smallestVideoRepresentation(photo.videoRepresentations), let peerReference = PeerReference(peer._asPeer()) {
|
||||||
let videoId = photo.id?.id ?? peer.id.id._internalGetInt64Value()
|
let videoId = photo.id?.id ?? peer.id.id._internalGetInt64Value()
|
||||||
let videoFileReference = FileMediaReference.avatarList(peer: peerReference, media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: video.resource, previewRepresentations: photo.representations, videoThumbnails: [], immediateThumbnailData: photo.immediateThumbnailData, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: video.dimensions, flags: [])]))
|
let videoFileReference = FileMediaReference.avatarList(peer: peerReference, media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: video.resource, previewRepresentations: photo.representations, videoThumbnails: [], immediateThumbnailData: photo.immediateThumbnailData, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: video.dimensions, flags: [])]))
|
||||||
let videoContent = NativeVideoContent(id: .profileVideo(videoId, nil), fileReference: videoFileReference, streamVideo: isMediaStreamable(resource: video.resource) ? .conservative : .none, loopVideo: true, enableSound: false, fetchAutomatically: true, onlyFullSizeThumbnail: false, useLargeThumbnail: true, autoFetchFullSizeThumbnail: true, startTimestamp: video.startTimestamp, continuePlayingWithoutSoundOnLostAudioSession: false, placeholderColor: .clear, captureProtected: false)
|
let videoContent = NativeVideoContent(id: .profileVideo(videoId, nil), fileReference: videoFileReference, streamVideo: isMediaStreamable(resource: video.resource) ? .conservative : .none, loopVideo: true, enableSound: false, fetchAutomatically: true, onlyFullSizeThumbnail: false, useLargeThumbnail: true, autoFetchFullSizeThumbnail: true, startTimestamp: video.startTimestamp, continuePlayingWithoutSoundOnLostAudioSession: false, placeholderColor: .clear, captureProtected: false)
|
||||||
|
@ -121,8 +121,12 @@ public final class SheetComponent<ChildEnvironmentType: Equatable>: Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var ignoreScrolling: Bool = false
|
||||||
public func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
public func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||||
|
guard !self.ignoreScrolling else {
|
||||||
|
return
|
||||||
|
}
|
||||||
let contentOffset = (scrollView.contentOffset.y + scrollView.contentInset.top - scrollView.contentSize.height) * -1.0
|
let contentOffset = (scrollView.contentOffset.y + scrollView.contentInset.top - scrollView.contentSize.height) * -1.0
|
||||||
if contentOffset >= scrollView.contentSize.height {
|
if contentOffset >= scrollView.contentSize.height {
|
||||||
self.dismiss?(false)
|
self.dismiss?(false)
|
||||||
@ -194,11 +198,13 @@ public final class SheetComponent<ChildEnvironmentType: Equatable>: Component {
|
|||||||
containerSize: CGSize(width: availableSize.width, height: .greatestFiniteMagnitude)
|
containerSize: CGSize(width: availableSize.width, height: .greatestFiniteMagnitude)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
self.ignoreScrolling = true
|
||||||
transition.setFrame(view: self.contentView, frame: CGRect(origin: .zero, size: contentSize), completion: nil)
|
transition.setFrame(view: self.contentView, frame: CGRect(origin: .zero, size: contentSize), completion: nil)
|
||||||
transition.setFrame(view: self.backgroundView, frame: CGRect(origin: .zero, size: CGSize(width: contentSize.width, height: contentSize.height + 1000.0)), completion: nil)
|
transition.setFrame(view: self.backgroundView, frame: CGRect(origin: .zero, size: CGSize(width: contentSize.width, height: contentSize.height + 1000.0)), completion: nil)
|
||||||
transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(), size: availableSize), completion: nil)
|
transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(), size: availableSize), completion: nil)
|
||||||
self.scrollView.contentSize = contentSize
|
self.scrollView.contentSize = contentSize
|
||||||
self.scrollView.contentInset = UIEdgeInsets(top: max(0.0, availableSize.height - contentSize.height) + contentSize.height, left: 0.0, bottom: 0.0, right: 0.0)
|
self.scrollView.contentInset = UIEdgeInsets(top: max(0.0, availableSize.height - contentSize.height) + contentSize.height, left: 0.0, bottom: 0.0, right: 0.0)
|
||||||
|
self.ignoreScrolling = false
|
||||||
|
|
||||||
if environment[SheetComponentEnvironment.self].value.isDisplaying, !self.previousIsDisplaying, let _ = transition.userData(ViewControllerComponentContainer.AnimateInTransition.self) {
|
if environment[SheetComponentEnvironment.self].value.isDisplaying, !self.previousIsDisplaying, let _ = transition.userData(ViewControllerComponentContainer.AnimateInTransition.self) {
|
||||||
self.animateIn()
|
self.animateIn()
|
||||||
|
@ -749,6 +749,37 @@ public extension ContainedViewLayoutTransition {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func updateBackgroundColor(layer: CALayer, color: UIColor, completion: ((Bool) -> Void)? = nil) {
|
||||||
|
if let nodeColor = layer.backgroundColor, nodeColor == color.cgColor {
|
||||||
|
if let completion = completion {
|
||||||
|
completion(true)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch self {
|
||||||
|
case .immediate:
|
||||||
|
layer.backgroundColor = color.cgColor
|
||||||
|
if let completion = completion {
|
||||||
|
completion(true)
|
||||||
|
}
|
||||||
|
case let .animated(duration, curve):
|
||||||
|
if let nodeColor = layer.backgroundColor {
|
||||||
|
layer.backgroundColor = color.cgColor
|
||||||
|
layer.animate(from: nodeColor, to: color.cgColor, keyPath: "backgroundColor", timingFunction: curve.timingFunction, duration: duration, mediaTimingFunction: curve.mediaTimingFunction, completion: { result in
|
||||||
|
if let completion = completion {
|
||||||
|
completion(result)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
layer.backgroundColor = color.cgColor
|
||||||
|
if let completion = completion {
|
||||||
|
completion(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func updateCornerRadius(node: ASDisplayNode, cornerRadius: CGFloat, completion: ((Bool) -> Void)? = nil) {
|
func updateCornerRadius(node: ASDisplayNode, cornerRadius: CGFloat, completion: ((Bool) -> Void)? = nil) {
|
||||||
if node.cornerRadius.isEqual(to: cornerRadius) {
|
if node.cornerRadius.isEqual(to: cornerRadius) {
|
||||||
if let completion = completion {
|
if let completion = completion {
|
||||||
|
@ -622,7 +622,7 @@ private struct ChannelVisibilityControllerState: Equatable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func channelVisibilityControllerEntries(presentationData: PresentationData, mode: ChannelVisibilityControllerMode, view: PeerView, publicChannelsToRevoke: [Peer]?, importers: PeerInvitationImportersState?, state: ChannelVisibilityControllerState) -> [ChannelVisibilityEntry] {
|
private func channelVisibilityControllerEntries(presentationData: PresentationData, mode: ChannelVisibilityControllerMode, view: PeerView, publicChannelsToRevoke: [Peer]?, importers: PeerInvitationImportersState?, state: ChannelVisibilityControllerState, limits: EngineConfiguration.UserLimits, premiumLimits: EngineConfiguration.UserLimits, isPremium: Bool) -> [ChannelVisibilityEntry] {
|
||||||
var entries: [ChannelVisibilityEntry] = []
|
var entries: [ChannelVisibilityEntry] = []
|
||||||
|
|
||||||
if let peer = view.peers[view.peerId] as? TelegramChannel {
|
if let peer = view.peers[view.peerId] as? TelegramChannel {
|
||||||
@ -730,7 +730,7 @@ private func channelVisibilityControllerEntries(presentationData: PresentationDa
|
|||||||
|
|
||||||
if displayAvailability {
|
if displayAvailability {
|
||||||
if let publicChannelsToRevoke = publicChannelsToRevoke {
|
if let publicChannelsToRevoke = publicChannelsToRevoke {
|
||||||
entries.append(.linksLimitInfo(presentationData.theme, presentationData.strings.Group_Username_RemoveExistingUsernamesOrExtendInfo("\(20)").string, 10, 20))
|
entries.append(.linksLimitInfo(presentationData.theme, presentationData.strings.Group_Username_RemoveExistingUsernamesOrExtendInfo("\(20)").string, limits.maxPublicLinksCount, premiumLimits.maxPublicLinksCount))
|
||||||
|
|
||||||
var index: Int32 = 0
|
var index: Int32 = 0
|
||||||
for peer in publicChannelsToRevoke.sorted(by: { lhs, rhs in
|
for peer in publicChannelsToRevoke.sorted(by: { lhs, rhs in
|
||||||
@ -1349,11 +1349,25 @@ public func channelVisibilityController(context: AccountContext, updatedPresenta
|
|||||||
}
|
}
|
||||||
|
|
||||||
let presentationData = updatedPresentationData?.signal ?? context.sharedContext.presentationData
|
let presentationData = updatedPresentationData?.signal ?? context.sharedContext.presentationData
|
||||||
let signal = combineLatest(presentationData, statePromise.get() |> deliverOnMainQueue, peerView, peersDisablingAddressNameAssignment.get() |> deliverOnMainQueue, importersContext, importersState.get())
|
let signal = combineLatest(
|
||||||
|
presentationData,
|
||||||
|
statePromise.get() |> deliverOnMainQueue,
|
||||||
|
peerView, peersDisablingAddressNameAssignment.get() |> deliverOnMainQueue,
|
||||||
|
importersContext,
|
||||||
|
importersState.get(),
|
||||||
|
context.engine.data.get(
|
||||||
|
TelegramEngine.EngineData.Item.Configuration.UserLimits(isPremium: false),
|
||||||
|
TelegramEngine.EngineData.Item.Configuration.UserLimits(isPremium: true),
|
||||||
|
TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)
|
||||||
|
)
|
||||||
|
)
|
||||||
|> deliverOnMainQueue
|
|> deliverOnMainQueue
|
||||||
|> map { presentationData, state, view, publicChannelsToRevoke, importersContext, importers -> (ItemListControllerState, (ItemListNodeState, Any)) in
|
|> map { presentationData, state, view, publicChannelsToRevoke, importersContext, importers, data -> (ItemListControllerState, (ItemListNodeState, Any)) in
|
||||||
let peer = peerViewMainPeer(view)
|
let peer = peerViewMainPeer(view)
|
||||||
|
|
||||||
|
let (limits, premiumLimits, accountPeer) = data
|
||||||
|
let isPremium = accountPeer?.isPremium ?? false
|
||||||
|
|
||||||
var footerItem: ItemListControllerFooterItem?
|
var footerItem: ItemListControllerFooterItem?
|
||||||
|
|
||||||
var rightNavigationButton: ItemListNavigationButton?
|
var rightNavigationButton: ItemListNavigationButton?
|
||||||
@ -1630,7 +1644,7 @@ public func channelVisibilityController(context: AccountContext, updatedPresenta
|
|||||||
title = presentationData.strings.Premium_LimitReached
|
title = presentationData.strings.Premium_LimitReached
|
||||||
}
|
}
|
||||||
|
|
||||||
let entries = channelVisibilityControllerEntries(presentationData: presentationData, mode: mode, view: view, publicChannelsToRevoke: publicChannelsToRevoke, importers: importers, state: state)
|
let entries = channelVisibilityControllerEntries(presentationData: presentationData, mode: mode, view: view, publicChannelsToRevoke: publicChannelsToRevoke, importers: importers, state: state, limits: limits, premiumLimits: premiumLimits, isPremium: isPremium)
|
||||||
|
|
||||||
var focusItemTag: ItemListItemTag?
|
var focusItemTag: ItemListItemTag?
|
||||||
if entries.count > 1, let cachedChannelData = view.cachedData as? CachedChannelData, cachedChannelData.peerGeoLocation != nil {
|
if entries.count > 1, let cachedChannelData = view.cachedData as? CachedChannelData, cachedChannelData.peerGeoLocation != nil {
|
||||||
|
@ -47,6 +47,7 @@ swift_library(
|
|||||||
"//submodules/InstantPageCache:InstantPageCache",
|
"//submodules/InstantPageCache:InstantPageCache",
|
||||||
"//submodules/MediaPlayer:UniversalMediaPlayer",
|
"//submodules/MediaPlayer:UniversalMediaPlayer",
|
||||||
"//submodules/TelegramUniversalVideoContent:TelegramUniversalVideoContent",
|
"//submodules/TelegramUniversalVideoContent:TelegramUniversalVideoContent",
|
||||||
|
"//submodules/RadialStatusNode:RadialStatusNode",
|
||||||
],
|
],
|
||||||
visibility = [
|
visibility = [
|
||||||
"//visibility:public",
|
"//visibility:public",
|
||||||
|
BIN
submodules/PremiumUI/Resources/lightspeed.scn
Normal file
BIN
submodules/PremiumUI/Resources/lightstreak.png
Normal file
After Width: | Height: | Size: 1.7 KiB |
@ -1 +1 @@
|
|||||||
{"v":"5.8.1","fr":60,"ip":0,"op":120,"w":30,"h":30,"nm":"unlock","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"lock2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[0]},{"i":{"x":[0.667],"y":[0.805]},"o":{"x":[0.333],"y":[0]},"t":10,"s":[15]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[-0.195]},"t":20,"s":[7.407]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":30,"s":[15]},{"t":40,"s":[0]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[19,14,0],"to":[0,-0.139,0],"ti":[0,0,0]},{"i":{"x":0.903,"y":0},"o":{"x":0.333,"y":0},"t":10,"s":[19,13.167,0],"to":[0,0,0],"ti":[0,-0.124,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.152,"y":1},"t":20,"s":[19,13.383,0],"to":[0,0.216,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":30,"s":[19,13.167,0],"to":[0,0,0],"ti":[0,-0.088,0]},{"t":40,"s":[19,14,0]}],"ix":2,"l":2},"a":{"a":0,"k":[24,0,0],"ix":1,"l":2},"s":{"a":0,"k":[16.667,16.667,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-4,-1.5],[-4,-8],[4,-8],[4,8]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"rd","nm":"Round Corners 1","r":{"a":0,"k":4,"ix":1},"ix":2,"mn":"ADBE Vector Filter - RC","hd":false},{"ty":"st","c":{"a":0,"k":[0,0,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[600,600],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"lock2","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":120,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"lock1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[0]},{"i":{"x":[0.667],"y":[0.805]},"o":{"x":[0.333],"y":[0]},"t":10,"s":[-15]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[-0.195]},"t":20,"s":[-7.407]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":30,"s":[-15]},{"t":40,"s":[0]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[19,16,0],"to":[0,0.139,0],"ti":[0,0,0]},{"i":{"x":0.903,"y":0},"o":{"x":0.333,"y":0},"t":10,"s":[19,16.833,0],"to":[0,0,0],"ti":[0,0.124,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.152,"y":1},"t":20,"s":[19,16.617,0],"to":[0,-0.216,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":30,"s":[19,16.833,0],"to":[0,0,0],"ti":[0,0.088,0]},{"t":40,"s":[19,16,0]}],"ix":2,"l":2},"a":{"a":0,"k":[24,-12,0],"ix":1,"l":2},"s":{"a":0,"k":[16.667,16.667,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[14,12],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":4,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[600,600],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"lock1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":120,"st":0,"bm":0}],"markers":[]}
|
{"v":"5.8.1","fr":60,"ip":0,"op":120,"w":30,"h":30,"nm":"unlock","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"lock2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[0]},{"i":{"x":[0.667],"y":[0.805]},"o":{"x":[0.333],"y":[0]},"t":10,"s":[15]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[-0.195]},"t":20,"s":[7.407]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":30,"s":[15]},{"t":40,"s":[0]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[19,14,0],"to":[0,-0.139,0],"ti":[0,0,0]},{"i":{"x":0.903,"y":0},"o":{"x":0.333,"y":0},"t":10,"s":[19,13.167,0],"to":[0,0,0],"ti":[0,-0.124,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.152,"y":1},"t":20,"s":[19,13.383,0],"to":[0,0.216,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":30,"s":[19,13.167,0],"to":[0,0,0],"ti":[0,-0.088,0]},{"t":40,"s":[19,14,0]}],"ix":2,"l":2},"a":{"a":0,"k":[4,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[-2.209,0],[0,-2.209],[0,0]],"o":[[0,0],[0,-2.209],[2.209,0],[0,0],[0,0]],"v":[[-4,-1],[-4,-4],[0,-8],[4,-4],[4,8]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"rd","nm":"Round Corners 1","r":{"a":0,"k":4,"ix":1},"ix":2,"mn":"ADBE Vector Filter - RC","hd":false},{"ty":"st","c":{"a":0,"k":[0,0,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"lock2","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":120,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"lock1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[0]},{"i":{"x":[0.667],"y":[0.805]},"o":{"x":[0.333],"y":[0]},"t":10,"s":[-15]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[-0.195]},"t":20,"s":[-7.407]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":30,"s":[-15]},{"t":40,"s":[0]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[19,16,0],"to":[0,0.139,0],"ti":[0,0,0]},{"i":{"x":0.903,"y":0},"o":{"x":0.333,"y":0},"t":10,"s":[19,16.833,0],"to":[0,0,0],"ti":[0,0.124,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.152,"y":1},"t":20,"s":[19,16.617,0],"to":[0,-0.216,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":30,"s":[19,16.833,0],"to":[0,0,0],"ti":[0,0.088,0]},{"t":40,"s":[19,16,0]}],"ix":2,"l":2},"a":{"a":0,"k":[24,-12,0],"ix":1,"l":2},"s":{"a":0,"k":[16.667,16.667,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[14,12],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":4,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[600,600],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"lock1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":120,"st":0,"bm":0}],"markers":[]}
|
136
submodules/PremiumUI/Sources/AppIconsDemoComponent.swift
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
import Display
|
||||||
|
import AsyncDisplayKit
|
||||||
|
import SwiftSignalKit
|
||||||
|
import ComponentFlow
|
||||||
|
import TelegramCore
|
||||||
|
import AccountContext
|
||||||
|
import TelegramPresentationData
|
||||||
|
import AccountContext
|
||||||
|
import AppBundle
|
||||||
|
|
||||||
|
final class AppIconsDemoComponent: Component {
|
||||||
|
public typealias EnvironmentType = DemoPageEnvironment
|
||||||
|
|
||||||
|
let context: AccountContext
|
||||||
|
let appIcons: [PresentationAppIcon]
|
||||||
|
|
||||||
|
public init(
|
||||||
|
context: AccountContext,
|
||||||
|
appIcons: [PresentationAppIcon]
|
||||||
|
) {
|
||||||
|
self.context = context
|
||||||
|
self.appIcons = appIcons
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func ==(lhs: AppIconsDemoComponent, rhs: AppIconsDemoComponent) -> Bool {
|
||||||
|
if lhs.context !== rhs.context {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.appIcons != rhs.appIcons {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
public final class View: UIView {
|
||||||
|
private var component: AppIconsDemoComponent?
|
||||||
|
|
||||||
|
private var imageViews: [UIImageView] = []
|
||||||
|
|
||||||
|
public func update(component: AppIconsDemoComponent, availableSize: CGSize, environment: Environment<DemoPageEnvironment>, transition: Transition) -> CGSize {
|
||||||
|
// let isDisplaying = environment[DemoPageEnvironment.self].isDisplaying
|
||||||
|
|
||||||
|
// if self.node == nil {
|
||||||
|
// let node = StickersCarouselNode(
|
||||||
|
// context: component.context,
|
||||||
|
// stickers: component.stickers
|
||||||
|
// )
|
||||||
|
// self.node = node
|
||||||
|
// self.addSubnode(node)
|
||||||
|
// }
|
||||||
|
|
||||||
|
// let isFirstTime = self.component == nil
|
||||||
|
self.component = component
|
||||||
|
|
||||||
|
if self.imageViews.isEmpty {
|
||||||
|
for icon in component.appIcons {
|
||||||
|
if let image = UIImage(named: icon.imageName, in: getAppBundle(), compatibleWith: nil) {
|
||||||
|
let imageView = UIImageView(frame: CGRect(origin: .zero, size: CGSize(width: 90.0, height: 90.0)))
|
||||||
|
imageView.clipsToBounds = true
|
||||||
|
imageView.layer.cornerRadius = 24.0
|
||||||
|
imageView.image = image
|
||||||
|
self.addSubview(imageView)
|
||||||
|
|
||||||
|
self.imageViews.append(imageView)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var i = 0
|
||||||
|
for view in self.imageViews {
|
||||||
|
let position: CGPoint
|
||||||
|
switch i {
|
||||||
|
case 0:
|
||||||
|
position = CGPoint(x: availableSize.width * 0.5, y: availableSize.height * 0.333)
|
||||||
|
case 1:
|
||||||
|
position = CGPoint(x: availableSize.width * 0.333, y: availableSize.height * 0.667)
|
||||||
|
case 2:
|
||||||
|
position = CGPoint(x: availableSize.width * 0.667, y: availableSize.height * 0.667)
|
||||||
|
default:
|
||||||
|
position = CGPoint(x: availableSize.width * 0.5, y: availableSize.height * 0.5)
|
||||||
|
}
|
||||||
|
|
||||||
|
view.center = position
|
||||||
|
|
||||||
|
i += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
var mappedPosition = environment[DemoPageEnvironment.self].position
|
||||||
|
mappedPosition *= abs(mappedPosition)
|
||||||
|
|
||||||
|
if let _ = transition.userData(DemoAnimateInTransition.self), abs(mappedPosition) < .ulpOfOne {
|
||||||
|
Queue.mainQueue().after(0.1) {
|
||||||
|
var i = 0
|
||||||
|
for view in self.imageViews {
|
||||||
|
let from: CGPoint
|
||||||
|
let delay: Double
|
||||||
|
switch i {
|
||||||
|
case 0:
|
||||||
|
from = CGPoint(x: -availableSize.width * 0.333, y: -availableSize.height * 0.8)
|
||||||
|
delay = 0.1
|
||||||
|
case 1:
|
||||||
|
from = CGPoint(x: -availableSize.width * 0.75, y: availableSize.height * 0.75)
|
||||||
|
delay = 0.15
|
||||||
|
case 2:
|
||||||
|
from = CGPoint(x: availableSize.width * 0.9, y: availableSize.height * 0.0)
|
||||||
|
delay = 0.0
|
||||||
|
default:
|
||||||
|
from = CGPoint(x: availableSize.width * 0.5, y: availableSize.height * 0.5)
|
||||||
|
delay = 0.0
|
||||||
|
}
|
||||||
|
view.layer.animateScale(from: 3.0, to: 1.0, duration: 0.5, delay: delay, timingFunction: kCAMediaTimingFunctionSpring)
|
||||||
|
view.layer.animatePosition(from: from, to: CGPoint(), duration: 0.5, delay: delay, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
|
||||||
|
|
||||||
|
i += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return availableSize
|
||||||
|
}
|
||||||
|
|
||||||
|
func animateIn() {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,6 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import UIKit
|
import UIKit
|
||||||
|
import Display
|
||||||
import ComponentFlow
|
import ComponentFlow
|
||||||
|
|
||||||
public final class PageIndicatorComponent: Component {
|
public final class PageIndicatorComponent: Component {
|
||||||
@ -336,7 +337,8 @@ private class ItemView: UIView {
|
|||||||
|
|
||||||
var dotColor = UIColor.lightGray {
|
var dotColor = UIColor.lightGray {
|
||||||
didSet {
|
didSet {
|
||||||
self.dotView.backgroundColor = dotColor
|
let transition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .linear)
|
||||||
|
transition.updateBackgroundColor(layer: self.dotView.layer, color: dotColor)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import UIKit
|
import UIKit
|
||||||
|
import SceneKit
|
||||||
import Display
|
import Display
|
||||||
import AsyncDisplayKit
|
import AsyncDisplayKit
|
||||||
import SwiftSignalKit
|
import SwiftSignalKit
|
||||||
@ -7,10 +8,10 @@ import Postbox
|
|||||||
import TelegramCore
|
import TelegramCore
|
||||||
import ComponentFlow
|
import ComponentFlow
|
||||||
import AccountContext
|
import AccountContext
|
||||||
|
import RadialStatusNode
|
||||||
import AppBundle
|
|
||||||
import UniversalMediaPlayer
|
import UniversalMediaPlayer
|
||||||
import TelegramUniversalVideoContent
|
import TelegramUniversalVideoContent
|
||||||
|
import AppBundle
|
||||||
|
|
||||||
private let phoneSize = CGSize(width: 262.0, height: 539.0)
|
private let phoneSize = CGSize(width: 262.0, height: 539.0)
|
||||||
private var phoneBorderImage = {
|
private var phoneBorderImage = {
|
||||||
@ -42,6 +43,14 @@ private final class PhoneView: UIView {
|
|||||||
let borderView: UIImageView
|
let borderView: UIImageView
|
||||||
|
|
||||||
fileprivate var videoNode: UniversalVideoNode?
|
fileprivate var videoNode: UniversalVideoNode?
|
||||||
|
private let statusNode: RadialStatusNode
|
||||||
|
|
||||||
|
var playbackStatus: Signal<MediaPlayerStatus?, NoError> {
|
||||||
|
return self.playbackStatusPromise.get()
|
||||||
|
}
|
||||||
|
private var playbackStatusPromise = ValuePromise<MediaPlayerStatus?>(nil)
|
||||||
|
private var playbackStatusValue: MediaPlayerStatus?
|
||||||
|
private var statusDisposable = MetaDisposable()
|
||||||
|
|
||||||
var screenRotation: CGFloat = 0.0 {
|
var screenRotation: CGFloat = 0.0 {
|
||||||
didSet {
|
didSet {
|
||||||
@ -50,7 +59,6 @@ private final class PhoneView: UIView {
|
|||||||
} else {
|
} else {
|
||||||
self.overlayView.backgroundColor = .black
|
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
|
self.overlayView.alpha = self.screenRotation > 0.0 ? self.screenRotation * 0.5 : self.screenRotation * -1.0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -67,36 +75,66 @@ private final class PhoneView: UIView {
|
|||||||
|
|
||||||
self.borderView = UIImageView(image: phoneBorderImage)
|
self.borderView = UIImageView(image: phoneBorderImage)
|
||||||
|
|
||||||
|
self.statusNode = RadialStatusNode(backgroundNodeColor: UIColor(white: 0.0, alpha: 0.6), enableBlur: false)
|
||||||
|
self.statusNode.transitionToState(.progress(color: .white, lineWidth: nil, value: nil, cancelEnabled: false, animateRotation: true))
|
||||||
|
self.statusNode.isUserInteractionEnabled = false
|
||||||
|
|
||||||
super.init(frame: frame)
|
super.init(frame: frame)
|
||||||
|
|
||||||
self.addSubview(self.contentContainerView)
|
self.addSubview(self.contentContainerView)
|
||||||
|
self.contentContainerView.addSubview(self.statusNode.view)
|
||||||
self.contentContainerView.addSubview(self.overlayView)
|
self.contentContainerView.addSubview(self.overlayView)
|
||||||
self.addSubview(self.borderView)
|
self.addSubview(self.borderView)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
self.statusDisposable.dispose()
|
||||||
|
}
|
||||||
|
|
||||||
private var position: PhoneDemoComponent.Position = .top
|
private var position: PhoneDemoComponent.Position = .top
|
||||||
|
|
||||||
func setup(context: AccountContext, videoName: String?, position: PhoneDemoComponent.Position) {
|
func setup(context: AccountContext, videoFile: TelegramMediaFile?, position: PhoneDemoComponent.Position) {
|
||||||
self.position = position
|
self.position = position
|
||||||
|
|
||||||
guard self.videoNode == nil, let videoName = videoName, let path = getAppBundle().path(forResource: videoName, ofType: "mp4"), let size = fileSize(path) else {
|
guard self.videoNode == nil, let file = videoFile else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
self.contentContainerView.backgroundColor = .clear
|
self.contentContainerView.backgroundColor = .clear
|
||||||
|
|
||||||
let dimensions = PixelDimensions(width: 1170, height: 1754)
|
let videoContent = NativeVideoContent(
|
||||||
|
id: .message(1, MediaId(namespace: 0, id: Int64.random(in: 0..<Int64.max))),
|
||||||
let id = Int64.random(in: 0..<Int64.max)
|
fileReference: .standalone(media: file),
|
||||||
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: [])])
|
streamVideo: .conservative,
|
||||||
|
loopVideo: true,
|
||||||
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)
|
enableSound: false,
|
||||||
|
fetchAutomatically: true,
|
||||||
|
onlyFullSizeThumbnail: false,
|
||||||
|
continuePlayingWithoutSoundOnLostAudioSession: false,
|
||||||
|
placeholderColor: .darkGray,
|
||||||
|
hintDimensions: CGSize(width: 1170, height: 1754)
|
||||||
|
)
|
||||||
let videoNode = UniversalVideoNode(postbox: context.account.postbox, audioSession: context.sharedContext.mediaManager.audioSession, manager: context.sharedContext.mediaManager.universalVideoManager, decoration: VideoDecoration(), content: videoContent, priority: .embedded)
|
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
|
videoNode.canAttachContent = true
|
||||||
self.videoNode = videoNode
|
self.videoNode = videoNode
|
||||||
|
|
||||||
|
let status = videoNode.status
|
||||||
|
|> mapToSignal { status -> Signal<MediaPlayerStatus?, NoError> in
|
||||||
|
if let status = status, case .buffering = status.status {
|
||||||
|
return .single(status) |> delay(1.0, queue: Queue.mainQueue())
|
||||||
|
} else {
|
||||||
|
return .single(status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.statusDisposable.set((status |> deliverOnMainQueue).start(next: { [weak self] status in
|
||||||
|
if let strongSelf = self {
|
||||||
|
strongSelf.playbackStatusValue = status
|
||||||
|
strongSelf.playbackStatusPromise.set(status)
|
||||||
|
strongSelf.updatePlaybackStatus()
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
self.contentContainerView.insertSubview(videoNode.view, at: 0)
|
self.contentContainerView.insertSubview(videoNode.view, at: 0)
|
||||||
|
|
||||||
videoNode.pause()
|
videoNode.pause()
|
||||||
@ -104,6 +142,26 @@ private final class PhoneView: UIView {
|
|||||||
self.setNeedsLayout()
|
self.setNeedsLayout()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func updatePlaybackStatus() {
|
||||||
|
var state: RadialStatusNodeState?
|
||||||
|
if let playbackStatus = self.playbackStatusValue {
|
||||||
|
if case let .buffering(initial, _, progress, _) = playbackStatus.status, initial || !progress.isZero {
|
||||||
|
let adjustedProgress = max(progress, 0.027)
|
||||||
|
state = .progress(color: .white, lineWidth: nil, value: CGFloat(adjustedProgress), cancelEnabled: false, animateRotation: true)
|
||||||
|
} else if playbackStatus.status == .playing {
|
||||||
|
state = RadialStatusNodeState.none
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let state = state {
|
||||||
|
self.statusNode.transitionToState(state, completion: { [weak self] in
|
||||||
|
if case .none = state {
|
||||||
|
self?.statusNode.removeFromSupernode()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private var isPlaying = false
|
private var isPlaying = false
|
||||||
func play() {
|
func play() {
|
||||||
if let videoNode = self.videoNode, !self.isPlaying {
|
if let videoNode = self.videoNode, !self.isPlaying {
|
||||||
@ -133,14 +191,104 @@ private final class PhoneView: UIView {
|
|||||||
self.overlayView.frame = self.contentContainerView.bounds
|
self.overlayView.frame = self.contentContainerView.bounds
|
||||||
|
|
||||||
if let videoNode = self.videoNode {
|
if let videoNode = self.videoNode {
|
||||||
let videoSize = CGSize(width: self.contentContainerView.frame.width, height: 353.0)
|
let videoSize = CGSize(width: self.contentContainerView.frame.width, height: 354.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.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)
|
videoNode.updateLayout(size: videoSize, transition: .immediate)
|
||||||
|
|
||||||
|
let notchHeight: CGFloat = 20.0
|
||||||
|
let radialStatusSize: CGFloat = 40.0
|
||||||
|
self.statusNode.frame = CGRect(x: floor((videoSize.width - radialStatusSize) / 2.0), y: self.position == .top ? notchHeight + floor((videoSize.height - notchHeight - radialStatusSize) / 2.0) : self.contentContainerView.frame.height - videoSize.height + floor((videoSize.height - radialStatusSize) / 2.0), width: radialStatusSize, height: radialStatusSize)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private final class StarsView: UIView {
|
||||||
|
private let sceneView: SCNView
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
self.sceneView = SCNView(frame: CGRect(origin: .zero, size: frame.size))
|
||||||
|
self.sceneView.backgroundColor = .clear
|
||||||
|
if let url = getAppBundle().url(forResource: "lightspeed", withExtension: "scn") {
|
||||||
|
self.sceneView.scene = try? SCNScene(url: url, options: nil)
|
||||||
|
}
|
||||||
|
self.sceneView.isUserInteractionEnabled = false
|
||||||
|
self.sceneView.preferredFramesPerSecond = 60
|
||||||
|
|
||||||
|
super.init(frame: frame)
|
||||||
|
|
||||||
|
self.alpha = 0.0
|
||||||
|
|
||||||
|
self.addSubview(self.sceneView)
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
func setVisible(_ visible: Bool) {
|
||||||
|
let transition = ContainedViewLayoutTransition.animated(duration: 0.3, curve: .linear)
|
||||||
|
transition.updateAlpha(layer: self.layer, alpha: visible ? 1.0 : 0.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var playing = false
|
||||||
|
func startAnimation() {
|
||||||
|
guard !self.playing, let scene = self.sceneView.scene, let node = scene.rootNode.childNode(withName: "particles", recursively: false), let particles = node.particleSystems?.first else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.playing = true
|
||||||
|
|
||||||
|
let speedAnimation = CABasicAnimation(keyPath: "speedFactor")
|
||||||
|
speedAnimation.fromValue = 1.0
|
||||||
|
speedAnimation.toValue = 1.8
|
||||||
|
speedAnimation.duration = 0.8
|
||||||
|
speedAnimation.fillMode = .forwards
|
||||||
|
particles.addAnimation(speedAnimation, forKey: "speedFactor")
|
||||||
|
|
||||||
|
particles.speedFactor = 3.0
|
||||||
|
|
||||||
|
let stretchAnimation = CABasicAnimation(keyPath: "stretchFactor")
|
||||||
|
stretchAnimation.fromValue = 0.05
|
||||||
|
stretchAnimation.toValue = 0.3
|
||||||
|
stretchAnimation.duration = 0.8
|
||||||
|
stretchAnimation.fillMode = .forwards
|
||||||
|
particles.addAnimation(stretchAnimation, forKey: "stretchFactor")
|
||||||
|
|
||||||
|
particles.stretchFactor = 0.3
|
||||||
|
}
|
||||||
|
|
||||||
|
func stopAnimation() {
|
||||||
|
guard self.playing, let scene = self.sceneView.scene, let node = scene.rootNode.childNode(withName: "particles", recursively: false), let particles = node.particleSystems?.first else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.playing = false
|
||||||
|
|
||||||
|
let speedAnimation = CABasicAnimation(keyPath: "speedFactor")
|
||||||
|
speedAnimation.fromValue = 3.0
|
||||||
|
speedAnimation.toValue = 1.0
|
||||||
|
speedAnimation.duration = 0.35
|
||||||
|
speedAnimation.fillMode = .forwards
|
||||||
|
particles.addAnimation(speedAnimation, forKey: "speedFactor")
|
||||||
|
|
||||||
|
particles.speedFactor = 1.0
|
||||||
|
|
||||||
|
let stretchAnimation = CABasicAnimation(keyPath: "stretchFactor")
|
||||||
|
stretchAnimation.fromValue = 0.3
|
||||||
|
stretchAnimation.toValue = 0.05
|
||||||
|
stretchAnimation.duration = 0.35
|
||||||
|
stretchAnimation.fillMode = .forwards
|
||||||
|
particles.addAnimation(stretchAnimation, forKey: "stretchFactor")
|
||||||
|
|
||||||
|
particles.stretchFactor = 0.05
|
||||||
|
}
|
||||||
|
|
||||||
|
override func layoutSubviews() {
|
||||||
|
super.layoutSubviews()
|
||||||
|
|
||||||
|
self.sceneView.frame = CGRect(origin: .zero, size: frame.size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
final class PhoneDemoComponent: Component {
|
final class PhoneDemoComponent: Component {
|
||||||
typealias EnvironmentType = DemoPageEnvironment
|
typealias EnvironmentType = DemoPageEnvironment
|
||||||
|
|
||||||
@ -151,16 +299,19 @@ final class PhoneDemoComponent: Component {
|
|||||||
|
|
||||||
let context: AccountContext
|
let context: AccountContext
|
||||||
let position: Position
|
let position: Position
|
||||||
let videoName: String?
|
let videoFile: TelegramMediaFile?
|
||||||
|
let hasStars: Bool
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
context: AccountContext,
|
context: AccountContext,
|
||||||
position: PhoneDemoComponent.Position,
|
position: PhoneDemoComponent.Position,
|
||||||
videoName: String?
|
videoFile: TelegramMediaFile?,
|
||||||
|
hasStars: Bool = false
|
||||||
) {
|
) {
|
||||||
self.context = context
|
self.context = context
|
||||||
self.position = position
|
self.position = position
|
||||||
self.videoName = videoName
|
self.videoFile = videoFile
|
||||||
|
self.hasStars = hasStars
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func ==(lhs: PhoneDemoComponent, rhs: PhoneDemoComponent) -> Bool {
|
public static func ==(lhs: PhoneDemoComponent, rhs: PhoneDemoComponent) -> Bool {
|
||||||
@ -170,7 +321,10 @@ final class PhoneDemoComponent: Component {
|
|||||||
if lhs.position != rhs.position {
|
if lhs.position != rhs.position {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if lhs.videoName != rhs.videoName {
|
if lhs.videoFile != rhs.videoFile {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.hasStars != rhs.hasStars {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
@ -190,9 +344,13 @@ final class PhoneDemoComponent: Component {
|
|||||||
private var isCentral = false
|
private var isCentral = false
|
||||||
private var component: PhoneDemoComponent?
|
private var component: PhoneDemoComponent?
|
||||||
|
|
||||||
|
private let starsContainerView: UIView
|
||||||
private let containerView: UIView
|
private let containerView: UIView
|
||||||
|
private var starsView: StarsView?
|
||||||
private let phoneView: PhoneView
|
private let phoneView: PhoneView
|
||||||
|
|
||||||
|
private var starsDisposable: Disposable?
|
||||||
|
|
||||||
public var ready: Signal<Bool, NoError> {
|
public var ready: Signal<Bool, NoError> {
|
||||||
if let videoNode = self.phoneView.videoNode {
|
if let videoNode = self.phoneView.videoNode {
|
||||||
return videoNode.ready
|
return videoNode.ready
|
||||||
@ -205,12 +363,17 @@ final class PhoneDemoComponent: Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public override init(frame: CGRect) {
|
public override init(frame: CGRect) {
|
||||||
|
self.starsContainerView = UIView(frame: frame)
|
||||||
|
self.starsContainerView.clipsToBounds = true
|
||||||
|
|
||||||
self.containerView = UIView(frame: frame)
|
self.containerView = UIView(frame: frame)
|
||||||
self.containerView.clipsToBounds = true
|
self.containerView.clipsToBounds = true
|
||||||
|
|
||||||
self.phoneView = PhoneView(frame: CGRect(origin: .zero, size: phoneSize))
|
self.phoneView = PhoneView(frame: CGRect(origin: .zero, size: phoneSize))
|
||||||
|
|
||||||
super.init(frame: frame)
|
super.init(frame: frame)
|
||||||
|
|
||||||
|
self.addSubview(self.starsContainerView)
|
||||||
self.addSubview(self.containerView)
|
self.addSubview(self.containerView)
|
||||||
self.containerView.addSubview(self.phoneView)
|
self.containerView.addSubview(self.phoneView)
|
||||||
}
|
}
|
||||||
@ -219,15 +382,41 @@ final class PhoneDemoComponent: Component {
|
|||||||
fatalError("init(coder:) has not been implemented")
|
fatalError("init(coder:) has not been implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
self.starsDisposable?.dispose()
|
||||||
|
}
|
||||||
|
|
||||||
public func update(component: PhoneDemoComponent, availableSize: CGSize, environment: Environment<DemoPageEnvironment>, transition: Transition) -> CGSize {
|
public func update(component: PhoneDemoComponent, availableSize: CGSize, environment: Environment<DemoPageEnvironment>, transition: Transition) -> CGSize {
|
||||||
self.component = component
|
self.component = component
|
||||||
|
|
||||||
|
|
||||||
self.phoneView.setup(context: component.context, videoName: component.videoName, position: component.position)
|
|
||||||
|
|
||||||
self.containerView.frame = CGRect(origin: .zero, size: availableSize)
|
self.containerView.frame = CGRect(origin: .zero, size: availableSize)
|
||||||
|
self.starsContainerView.frame = CGRect(origin: CGPoint(x: -availableSize.width * 0.5, y: 0.0), size: CGSize(width: availableSize.width * 2.0, height: availableSize.height))
|
||||||
self.phoneView.bounds = CGRect(origin: .zero, size: phoneSize)
|
self.phoneView.bounds = CGRect(origin: .zero, size: phoneSize)
|
||||||
|
|
||||||
|
if component.hasStars {
|
||||||
|
if self.starsView == nil {
|
||||||
|
let starsView = StarsView(frame: self.starsContainerView.bounds)
|
||||||
|
self.starsView = starsView
|
||||||
|
self.starsContainerView.addSubview(starsView)
|
||||||
|
|
||||||
|
self.starsDisposable = (self.phoneView.playbackStatus
|
||||||
|
|> deliverOnMainQueue).start(next: { [weak self] status in
|
||||||
|
if let strongSelf = self, let status = status {
|
||||||
|
if status.timestamp > 8.0 {
|
||||||
|
strongSelf.starsView?.stopAnimation()
|
||||||
|
} else if status.timestamp > 0.85 {
|
||||||
|
strongSelf.starsView?.startAnimation()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else if let starsView = self.starsView {
|
||||||
|
self.starsView = nil
|
||||||
|
starsView.removeFromSuperview()
|
||||||
|
}
|
||||||
|
|
||||||
|
self.phoneView.setup(context: component.context, videoFile: component.videoFile, position: component.position)
|
||||||
|
|
||||||
var mappedPosition = environment[DemoPageEnvironment.self].position
|
var mappedPosition = environment[DemoPageEnvironment.self].position
|
||||||
mappedPosition *= abs(mappedPosition)
|
mappedPosition *= abs(mappedPosition)
|
||||||
|
|
||||||
@ -242,11 +431,12 @@ final class PhoneDemoComponent: Component {
|
|||||||
phoneY = (-149.0 + phoneSize.height / 2.0 - 24.0 - abs(mappedPosition) * 24.0) * scale
|
phoneY = (-149.0 + phoneSize.height / 2.0 - 24.0 - abs(mappedPosition) * 24.0) * scale
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
let isVisible = environment[DemoPageEnvironment.self].isDisplaying
|
let isVisible = environment[DemoPageEnvironment.self].isDisplaying
|
||||||
let isCentral = environment[DemoPageEnvironment.self].isCentral
|
let isCentral = environment[DemoPageEnvironment.self].isCentral
|
||||||
self.isCentral = isCentral
|
self.isCentral = isCentral
|
||||||
|
|
||||||
|
self.starsView?.setVisible(isVisible && abs(mappedPosition) < 0.4)
|
||||||
|
|
||||||
self.phoneView.center = CGPoint(x: availableSize.width / 2.0 + phoneX, y: phoneY)
|
self.phoneView.center = CGPoint(x: availableSize.width / 2.0 + phoneX, y: phoneY)
|
||||||
self.phoneView.screenRotation = mappedPosition * -0.7
|
self.phoneView.screenRotation = mappedPosition * -0.7
|
||||||
|
|
||||||
@ -258,6 +448,7 @@ final class PhoneDemoComponent: Component {
|
|||||||
self.phoneView.play()
|
self.phoneView.play()
|
||||||
} else if !isVisible {
|
} else if !isVisible {
|
||||||
self.phoneView.reset()
|
self.phoneView.reset()
|
||||||
|
self.starsView?.stopAnimation()
|
||||||
}
|
}
|
||||||
|
|
||||||
if let _ = transition.userData(DemoAnimateInTransition.self), abs(mappedPosition) < .ulpOfOne {
|
if let _ = transition.userData(DemoAnimateInTransition.self), abs(mappedPosition) < .ulpOfOne {
|
||||||
|
@ -393,7 +393,13 @@ private final class DemoPagerComponent: Component {
|
|||||||
itemTransition = transition.withAnimation(.none)
|
itemTransition = transition.withAnimation(.none)
|
||||||
itemView = ComponentHostView<DemoPageEnvironment>()
|
itemView = ComponentHostView<DemoPageEnvironment>()
|
||||||
self.itemViews[item.content.id] = itemView
|
self.itemViews[item.content.id] = itemView
|
||||||
self.scrollView.addSubview(itemView)
|
|
||||||
|
|
||||||
|
if item.content.id == (PremiumDemoScreen.Subject.fasterDownload as AnyHashable) {
|
||||||
|
self.scrollView.insertSubview(itemView, at: 0)
|
||||||
|
} else {
|
||||||
|
self.scrollView.addSubview(itemView)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let environment = DemoPageEnvironment(isDisplaying: isDisplaying, isCentral: abs(centerDelta) < CGFloat.ulpOfOne, position: position)
|
let environment = DemoPageEnvironment(isDisplaying: isDisplaying, isCentral: abs(centerDelta) < CGFloat.ulpOfOne, position: position)
|
||||||
@ -464,11 +470,18 @@ private final class DemoSheetContent: CombinedComponent {
|
|||||||
let action: () -> Void
|
let action: () -> Void
|
||||||
let dismiss: () -> Void
|
let dismiss: () -> Void
|
||||||
|
|
||||||
init(context: AccountContext, subject: PremiumDemoScreen.Subject, source: PremiumDemoScreen.Source, order: [PremiumPerk]?, action: @escaping () -> Void, dismiss: @escaping () -> Void) {
|
init(
|
||||||
|
context: AccountContext,
|
||||||
|
subject: PremiumDemoScreen.Subject,
|
||||||
|
source: PremiumDemoScreen.Source,
|
||||||
|
order: [PremiumPerk]?,
|
||||||
|
action: @escaping () -> Void,
|
||||||
|
dismiss: @escaping () -> Void
|
||||||
|
) {
|
||||||
self.context = context
|
self.context = context
|
||||||
self.subject = subject
|
self.subject = subject
|
||||||
self.source = source
|
self.source = source
|
||||||
self.order = order ?? [.moreUpload, .fasterDownload, .voiceToText, .noAds, .uniqueReactions, .premiumStickers, .advancedChatManagement, .profileBadge, .animatedUserpics]
|
self.order = order ?? [.moreUpload, .fasterDownload, .voiceToText, .noAds, .uniqueReactions, .premiumStickers, .advancedChatManagement, .profileBadge, .animatedUserpics, .appIcons]
|
||||||
self.action = action
|
self.action = action
|
||||||
self.dismiss = dismiss
|
self.dismiss = dismiss
|
||||||
}
|
}
|
||||||
@ -496,10 +509,14 @@ private final class DemoSheetContent: CombinedComponent {
|
|||||||
var isPremium: Bool?
|
var isPremium: Bool?
|
||||||
var reactions: [AvailableReactions.Reaction]?
|
var reactions: [AvailableReactions.Reaction]?
|
||||||
var stickers: [TelegramMediaFile]?
|
var stickers: [TelegramMediaFile]?
|
||||||
|
var appIcons: [PresentationAppIcon]?
|
||||||
var disposable: Disposable?
|
var disposable: Disposable?
|
||||||
|
|
||||||
|
var promoConfiguration: PremiumPromoConfiguration?
|
||||||
|
|
||||||
init(context: AccountContext) {
|
init(context: AccountContext) {
|
||||||
self.context = context
|
self.context = context
|
||||||
|
self.appIcons = context.sharedContext.applicationBindings.getAvailableAlternateIcons().filter { $0.isPremium }
|
||||||
|
|
||||||
super.init()
|
super.init()
|
||||||
|
|
||||||
@ -519,9 +536,12 @@ private final class DemoSheetContent: CombinedComponent {
|
|||||||
return items != nil
|
return items != nil
|
||||||
}
|
}
|
||||||
|> take(1),
|
|> take(1),
|
||||||
self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId))
|
self.context.engine.data.get(
|
||||||
|
TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId),
|
||||||
|
TelegramEngine.EngineData.Item.Configuration.PremiumPromo()
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|> map { reactions, items, accountPeer -> ([AvailableReactions.Reaction], [TelegramMediaFile], Bool?) in
|
|> map { reactions, items, data -> ([AvailableReactions.Reaction], [TelegramMediaFile], Bool?, PremiumPromoConfiguration?) in
|
||||||
if let reactions = reactions {
|
if let reactions = reactions {
|
||||||
var result: [TelegramMediaFile] = []
|
var result: [TelegramMediaFile] = []
|
||||||
if let items = items {
|
if let items = items {
|
||||||
@ -531,17 +551,18 @@ private final class DemoSheetContent: CombinedComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return (reactions.reactions.filter({ $0.isPremium }), result, accountPeer?.isPremium ?? false)
|
return (reactions.reactions.filter({ $0.isPremium }), result, data.0?.isPremium ?? false, data.1)
|
||||||
} else {
|
} else {
|
||||||
return ([], [], nil)
|
return ([], [], nil, nil)
|
||||||
}
|
}
|
||||||
}).start(next: { [weak self] reactions, stickers, isPremium in
|
}).start(next: { [weak self] reactions, stickers, isPremium, promoConfiguration in
|
||||||
guard let strongSelf = self else {
|
guard let strongSelf = self else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
strongSelf.reactions = reactions
|
strongSelf.reactions = reactions
|
||||||
strongSelf.stickers = stickers
|
strongSelf.stickers = stickers
|
||||||
strongSelf.isPremium = isPremium
|
strongSelf.isPremium = isPremium
|
||||||
|
strongSelf.promoConfiguration = promoConfiguration
|
||||||
if !reactions.isEmpty && !stickers.isEmpty {
|
if !reactions.isEmpty && !stickers.isEmpty {
|
||||||
strongSelf.updated(transition: Transition(.immediate).withUserData(DemoAnimateInTransition()))
|
strongSelf.updated(transition: Transition(.immediate).withUserData(DemoAnimateInTransition()))
|
||||||
}
|
}
|
||||||
@ -600,7 +621,7 @@ private final class DemoSheetContent: CombinedComponent {
|
|||||||
isStandalone = true
|
isStandalone = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if let reactions = state.reactions, let stickers = state.stickers {
|
if let reactions = state.reactions, let stickers = state.stickers, let appIcons = state.appIcons, let configuration = state.promoConfiguration {
|
||||||
let textColor = theme.actionSheet.primaryTextColor
|
let textColor = theme.actionSheet.primaryTextColor
|
||||||
|
|
||||||
var availableItems: [PremiumPerk: DemoPagerComponent.Item] = [:]
|
var availableItems: [PremiumPerk: DemoPagerComponent.Item] = [:]
|
||||||
@ -613,7 +634,7 @@ private final class DemoSheetContent: CombinedComponent {
|
|||||||
content: AnyComponent(PhoneDemoComponent(
|
content: AnyComponent(PhoneDemoComponent(
|
||||||
context: component.context,
|
context: component.context,
|
||||||
position: .bottom,
|
position: .bottom,
|
||||||
videoName: "4gb"
|
videoFile: configuration.videos["double_limits"]
|
||||||
)),
|
)),
|
||||||
title: strings.Premium_UploadSize,
|
title: strings.Premium_UploadSize,
|
||||||
text: strings.Premium_UploadSizeInfo,
|
text: strings.Premium_UploadSizeInfo,
|
||||||
@ -630,7 +651,8 @@ private final class DemoSheetContent: CombinedComponent {
|
|||||||
content: AnyComponent(PhoneDemoComponent(
|
content: AnyComponent(PhoneDemoComponent(
|
||||||
context: component.context,
|
context: component.context,
|
||||||
position: .top,
|
position: .top,
|
||||||
videoName: "fastdownload"
|
videoFile: configuration.videos["faster_download"],
|
||||||
|
hasStars: true
|
||||||
)),
|
)),
|
||||||
title: strings.Premium_FasterSpeed,
|
title: strings.Premium_FasterSpeed,
|
||||||
text: strings.Premium_FasterSpeedInfo,
|
text: strings.Premium_FasterSpeedInfo,
|
||||||
@ -647,7 +669,7 @@ private final class DemoSheetContent: CombinedComponent {
|
|||||||
content: AnyComponent(PhoneDemoComponent(
|
content: AnyComponent(PhoneDemoComponent(
|
||||||
context: component.context,
|
context: component.context,
|
||||||
position: .top,
|
position: .top,
|
||||||
videoName: "voice"
|
videoFile: configuration.videos["voice_to_text"]
|
||||||
)),
|
)),
|
||||||
title: strings.Premium_VoiceToText,
|
title: strings.Premium_VoiceToText,
|
||||||
text: strings.Premium_VoiceToTextInfo,
|
text: strings.Premium_VoiceToTextInfo,
|
||||||
@ -664,7 +686,7 @@ private final class DemoSheetContent: CombinedComponent {
|
|||||||
content: AnyComponent(PhoneDemoComponent(
|
content: AnyComponent(PhoneDemoComponent(
|
||||||
context: component.context,
|
context: component.context,
|
||||||
position: .bottom,
|
position: .bottom,
|
||||||
videoName: "noads"
|
videoFile: configuration.videos["no_ads"]
|
||||||
)),
|
)),
|
||||||
title: strings.Premium_NoAds,
|
title: strings.Premium_NoAds,
|
||||||
text: strings.Premium_NoAdsInfo,
|
text: strings.Premium_NoAdsInfo,
|
||||||
@ -718,7 +740,7 @@ private final class DemoSheetContent: CombinedComponent {
|
|||||||
content: AnyComponent(PhoneDemoComponent(
|
content: AnyComponent(PhoneDemoComponent(
|
||||||
context: component.context,
|
context: component.context,
|
||||||
position: .top,
|
position: .top,
|
||||||
videoName: "fastdownload"
|
videoFile: configuration.videos["chat_management"]
|
||||||
)),
|
)),
|
||||||
title: strings.Premium_ChatManagement,
|
title: strings.Premium_ChatManagement,
|
||||||
text: strings.Premium_ChatManagementInfo,
|
text: strings.Premium_ChatManagementInfo,
|
||||||
@ -735,7 +757,7 @@ private final class DemoSheetContent: CombinedComponent {
|
|||||||
content: AnyComponent(PhoneDemoComponent(
|
content: AnyComponent(PhoneDemoComponent(
|
||||||
context: component.context,
|
context: component.context,
|
||||||
position: .top,
|
position: .top,
|
||||||
videoName: "badge"
|
videoFile: configuration.videos["profile_badge"]
|
||||||
)),
|
)),
|
||||||
title: strings.Premium_Badge,
|
title: strings.Premium_Badge,
|
||||||
text: strings.Premium_BadgeInfo,
|
text: strings.Premium_BadgeInfo,
|
||||||
@ -752,7 +774,7 @@ private final class DemoSheetContent: CombinedComponent {
|
|||||||
content: AnyComponent(PhoneDemoComponent(
|
content: AnyComponent(PhoneDemoComponent(
|
||||||
context: component.context,
|
context: component.context,
|
||||||
position: .top,
|
position: .top,
|
||||||
videoName: "badge"
|
videoFile: configuration.videos["userpics"]
|
||||||
)),
|
)),
|
||||||
title: strings.Premium_Avatar,
|
title: strings.Premium_Avatar,
|
||||||
text: strings.Premium_AvatarInfo,
|
text: strings.Premium_AvatarInfo,
|
||||||
@ -761,6 +783,22 @@ private final class DemoSheetContent: CombinedComponent {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
availableItems[.appIcons] = DemoPagerComponent.Item(
|
||||||
|
AnyComponentWithIdentity(
|
||||||
|
id: PremiumDemoScreen.Subject.appIcons,
|
||||||
|
component: AnyComponent(
|
||||||
|
PageComponent(
|
||||||
|
content: AnyComponent(AppIconsDemoComponent(
|
||||||
|
context: component.context,
|
||||||
|
appIcons: appIcons
|
||||||
|
)),
|
||||||
|
title: isStandalone ? strings.Premium_AppIconStandalone : strings.Premium_AppIcon,
|
||||||
|
text: isStandalone ? strings.Premium_AppIconStandaloneInfo :strings.Premium_AppIconInfo,
|
||||||
|
textColor: textColor
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
var items: [DemoPagerComponent.Item] = component.order.compactMap { availableItems[$0] }
|
var items: [DemoPagerComponent.Item] = component.order.compactMap { availableItems[$0] }
|
||||||
let index: Int
|
let index: Int
|
||||||
@ -827,7 +865,16 @@ private final class DemoSheetContent: CombinedComponent {
|
|||||||
case let .intro(price):
|
case let .intro(price):
|
||||||
buttonText = strings.Premium_SubscribeFor(price ?? "–").string
|
buttonText = strings.Premium_SubscribeFor(price ?? "–").string
|
||||||
case .other:
|
case .other:
|
||||||
buttonText = strings.Premium_Reactions_Proceed
|
switch component.subject {
|
||||||
|
case .uniqueReactions:
|
||||||
|
buttonText = strings.Premium_Reactions_Proceed
|
||||||
|
case .premiumStickers:
|
||||||
|
buttonText = strings.Premium_Stickers_Proceed
|
||||||
|
case .appIcons:
|
||||||
|
buttonText = strings.Premium_AppIcons_Proceed
|
||||||
|
default:
|
||||||
|
buttonText = strings.Common_OK
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -851,7 +898,7 @@ private final class DemoSheetContent: CombinedComponent {
|
|||||||
gloss: state.isPremium != true,
|
gloss: state.isPremium != true,
|
||||||
animationName: isStandalone && component.subject == .uniqueReactions ? "premium_unlock" : nil,
|
animationName: isStandalone && component.subject == .uniqueReactions ? "premium_unlock" : nil,
|
||||||
iconPosition: .right,
|
iconPosition: .right,
|
||||||
iconSpacing: 6.0,
|
iconSpacing: 4.0,
|
||||||
action: { [weak component] in
|
action: { [weak component] in
|
||||||
guard let component = component else {
|
guard let component = component else {
|
||||||
return
|
return
|
||||||
@ -989,6 +1036,7 @@ public class PremiumDemoScreen: ViewControllerComponentContainer {
|
|||||||
case advancedChatManagement
|
case advancedChatManagement
|
||||||
case profileBadge
|
case profileBadge
|
||||||
case animatedUserpics
|
case animatedUserpics
|
||||||
|
case appIcons
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum Source: Equatable {
|
public enum Source: Equatable {
|
||||||
|
@ -19,6 +19,7 @@ import InAppPurchaseManager
|
|||||||
import ConfettiEffect
|
import ConfettiEffect
|
||||||
import TextFormat
|
import TextFormat
|
||||||
import InstantPageCache
|
import InstantPageCache
|
||||||
|
import UniversalMediaPlayer
|
||||||
|
|
||||||
public enum PremiumSource: Equatable {
|
public enum PremiumSource: Equatable {
|
||||||
case settings
|
case settings
|
||||||
@ -35,6 +36,7 @@ public enum PremiumSource: Equatable {
|
|||||||
case chatsPerFolder
|
case chatsPerFolder
|
||||||
case accounts
|
case accounts
|
||||||
case about
|
case about
|
||||||
|
case appIcons
|
||||||
case deeplink(String?)
|
case deeplink(String?)
|
||||||
case profile(PeerId)
|
case profile(PeerId)
|
||||||
|
|
||||||
@ -50,6 +52,8 @@ public enum PremiumSource: Equatable {
|
|||||||
return "no_ads"
|
return "no_ads"
|
||||||
case .upload:
|
case .upload:
|
||||||
return "more_upload"
|
return "more_upload"
|
||||||
|
case .appIcons:
|
||||||
|
return "app_icons"
|
||||||
case .groupsAndChannels:
|
case .groupsAndChannels:
|
||||||
return "double_limits__channels"
|
return "double_limits__channels"
|
||||||
case .pinnedChats:
|
case .pinnedChats:
|
||||||
@ -91,6 +95,7 @@ enum PremiumPerk: CaseIterable {
|
|||||||
case advancedChatManagement
|
case advancedChatManagement
|
||||||
case profileBadge
|
case profileBadge
|
||||||
case animatedUserpics
|
case animatedUserpics
|
||||||
|
case appIcons
|
||||||
|
|
||||||
static var allCases: [PremiumPerk] {
|
static var allCases: [PremiumPerk] {
|
||||||
return [
|
return [
|
||||||
@ -103,7 +108,8 @@ enum PremiumPerk: CaseIterable {
|
|||||||
.premiumStickers,
|
.premiumStickers,
|
||||||
.advancedChatManagement,
|
.advancedChatManagement,
|
||||||
.profileBadge,
|
.profileBadge,
|
||||||
.animatedUserpics
|
.animatedUserpics,
|
||||||
|
.appIcons
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -139,6 +145,8 @@ enum PremiumPerk: CaseIterable {
|
|||||||
return "profile_badge"
|
return "profile_badge"
|
||||||
case .animatedUserpics:
|
case .animatedUserpics:
|
||||||
return "animated_userpics"
|
return "animated_userpics"
|
||||||
|
case .appIcons:
|
||||||
|
return "app_icon"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -164,6 +172,8 @@ enum PremiumPerk: CaseIterable {
|
|||||||
return strings.Premium_Badge
|
return strings.Premium_Badge
|
||||||
case .animatedUserpics:
|
case .animatedUserpics:
|
||||||
return strings.Premium_Avatar
|
return strings.Premium_Avatar
|
||||||
|
case .appIcons:
|
||||||
|
return strings.Premium_AppIcon
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -189,6 +199,8 @@ enum PremiumPerk: CaseIterable {
|
|||||||
return strings.Premium_BadgeInfo
|
return strings.Premium_BadgeInfo
|
||||||
case .animatedUserpics:
|
case .animatedUserpics:
|
||||||
return strings.Premium_AvatarInfo
|
return strings.Premium_AvatarInfo
|
||||||
|
case .appIcons:
|
||||||
|
return strings.Premium_AppIconInfo
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -214,6 +226,8 @@ enum PremiumPerk: CaseIterable {
|
|||||||
return "Premium/Perk/Badge"
|
return "Premium/Perk/Badge"
|
||||||
case .animatedUserpics:
|
case .animatedUserpics:
|
||||||
return "Premium/Perk/Avatar"
|
return "Premium/Perk/Avatar"
|
||||||
|
case .appIcons:
|
||||||
|
return "Premium/Perk/AppIcon"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -230,7 +244,8 @@ private struct PremiumIntroConfiguration {
|
|||||||
.premiumStickers,
|
.premiumStickers,
|
||||||
.advancedChatManagement,
|
.advancedChatManagement,
|
||||||
.profileBadge,
|
.profileBadge,
|
||||||
.animatedUserpics
|
.animatedUserpics,
|
||||||
|
.appIcons
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -259,6 +274,9 @@ private struct PremiumIntroConfiguration {
|
|||||||
if perks.count < 4 {
|
if perks.count < 4 {
|
||||||
perks = PremiumIntroConfiguration.defaultValue.perks
|
perks = PremiumIntroConfiguration.defaultValue.perks
|
||||||
}
|
}
|
||||||
|
if !perks.contains(.appIcons) {
|
||||||
|
perks.append(.appIcons)
|
||||||
|
}
|
||||||
return PremiumIntroConfiguration(perks: perks)
|
return PremiumIntroConfiguration(perks: perks)
|
||||||
} else {
|
} else {
|
||||||
return .defaultValue
|
return .defaultValue
|
||||||
@ -778,21 +796,23 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
|
|||||||
|
|
||||||
private var disposable: Disposable?
|
private var disposable: Disposable?
|
||||||
private(set) var configuration = PremiumIntroConfiguration.defaultValue
|
private(set) var configuration = PremiumIntroConfiguration.defaultValue
|
||||||
|
private(set) var promoConfiguration: PremiumPromoConfiguration?
|
||||||
|
|
||||||
|
private var preloadDisposableSet = DisposableSet()
|
||||||
|
|
||||||
init(context: AccountContext, source: PremiumSource) {
|
init(context: AccountContext, source: PremiumSource) {
|
||||||
self.context = context
|
self.context = context
|
||||||
|
|
||||||
super.init()
|
super.init()
|
||||||
|
|
||||||
self.disposable = (context.account.postbox.preferencesView(keys: [PreferencesKeys.appConfiguration])
|
self.disposable = (context.engine.data.get(
|
||||||
|> map { view -> AppConfiguration in
|
TelegramEngine.EngineData.Item.Configuration.App(),
|
||||||
let appConfiguration: AppConfiguration = view.values[PreferencesKeys.appConfiguration]?.get(AppConfiguration.self) ?? AppConfiguration.defaultValue
|
TelegramEngine.EngineData.Item.Configuration.PremiumPromo()
|
||||||
return appConfiguration
|
)
|
||||||
}
|
|> deliverOnMainQueue).start(next: { [weak self] appConfiguration, promoConfiguration in
|
||||||
|> take(1)
|
|
||||||
|> deliverOnMainQueue).start(next: { [weak self] appConfiguration in
|
|
||||||
if let strongSelf = self {
|
if let strongSelf = self {
|
||||||
strongSelf.configuration = PremiumIntroConfiguration.with(appConfiguration: appConfiguration)
|
strongSelf.configuration = PremiumIntroConfiguration.with(appConfiguration: appConfiguration)
|
||||||
|
strongSelf.promoConfiguration = promoConfiguration
|
||||||
strongSelf.updated(transition: .immediate)
|
strongSelf.updated(transition: .immediate)
|
||||||
|
|
||||||
var jsonString: String = "{"
|
var jsonString: String = "{"
|
||||||
@ -809,16 +829,20 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
|
|||||||
}
|
}
|
||||||
jsonString += "]}}"
|
jsonString += "]}}"
|
||||||
|
|
||||||
|
|
||||||
if let data = jsonString.data(using: .utf8), let json = JSON(data: data) {
|
if let data = jsonString.data(using: .utf8), let json = JSON(data: data) {
|
||||||
addAppLogEvent(postbox: strongSelf.context.account.postbox, type: "premium.promo_screen_show", data: json)
|
addAppLogEvent(postbox: strongSelf.context.account.postbox, type: "premium.promo_screen_show", data: json)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (_, video) in promoConfiguration.videos {
|
||||||
|
strongSelf.preloadDisposableSet.add(preloadVideoResource(postbox: context.account.postbox, resourceReference: .standalone(resource: video.resource), duration: 3.0).start())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
self.disposable?.dispose()
|
self.disposable?.dispose()
|
||||||
|
self.preloadDisposableSet.dispose()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -928,6 +952,8 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
|
|||||||
(UIColor(rgb: 0x9674FF), UIColor(rgb: 0x8C7DFF)),
|
(UIColor(rgb: 0x9674FF), UIColor(rgb: 0x8C7DFF)),
|
||||||
(UIColor(rgb: 0x9674FF), UIColor(rgb: 0x8C7DFF)),
|
(UIColor(rgb: 0x9674FF), UIColor(rgb: 0x8C7DFF)),
|
||||||
(UIColor(rgb: 0x7B88FF), UIColor(rgb: 0x7091FF)),
|
(UIColor(rgb: 0x7B88FF), UIColor(rgb: 0x7091FF)),
|
||||||
|
(UIColor(rgb: 0x609DFF), UIColor(rgb: 0x56A5FF)),
|
||||||
|
|
||||||
(UIColor(rgb: 0x609DFF), UIColor(rgb: 0x56A5FF))
|
(UIColor(rgb: 0x609DFF), UIColor(rgb: 0x56A5FF))
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -986,6 +1012,8 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
|
|||||||
demoSubject = .profileBadge
|
demoSubject = .profileBadge
|
||||||
case .animatedUserpics:
|
case .animatedUserpics:
|
||||||
demoSubject = .animatedUserpics
|
demoSubject = .animatedUserpics
|
||||||
|
case .appIcons:
|
||||||
|
demoSubject = .appIcons
|
||||||
}
|
}
|
||||||
|
|
||||||
var dismissImpl: (() -> Void)?
|
var dismissImpl: (() -> Void)?
|
||||||
@ -1091,17 +1119,33 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
|
|||||||
size.height += 6.0
|
size.height += 6.0
|
||||||
|
|
||||||
let termsFont = Font.regular(13.0)
|
let termsFont = Font.regular(13.0)
|
||||||
|
let boldTermsFont = Font.semibold(13.0)
|
||||||
|
let italicTermsFont = Font.italic(13.0)
|
||||||
|
let boldItalicTermsFont = Font.semiboldItalic(13.0)
|
||||||
|
let monospaceTermsFont = Font.monospace(13.0)
|
||||||
let termsTextColor = environment.theme.list.freeTextColor
|
let termsTextColor = environment.theme.list.freeTextColor
|
||||||
let termsMarkdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: termsFont, textColor: termsTextColor), bold: MarkdownAttributeSet(font: termsFont, textColor: termsTextColor), link: MarkdownAttributeSet(font: termsFont, textColor: environment.theme.list.itemAccentColor), linkAttribute: { contents in
|
let termsMarkdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: termsFont, textColor: termsTextColor), bold: MarkdownAttributeSet(font: termsFont, textColor: termsTextColor), link: MarkdownAttributeSet(font: termsFont, textColor: environment.theme.list.itemAccentColor), linkAttribute: { contents in
|
||||||
return (TelegramTextAttributes.URL, contents)
|
return (TelegramTextAttributes.URL, contents)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
let termsString: MultilineTextComponent.TextContent
|
||||||
|
if context.component.isPremium == true {
|
||||||
|
if let promoConfiguration = context.state.promoConfiguration {
|
||||||
|
let attributedString = stringWithAppliedEntities(promoConfiguration.status, entities: promoConfiguration.statusEntities, baseColor: termsTextColor, linkColor: environment.theme.list.itemAccentColor, baseFont: termsFont, linkFont: termsFont, boldFont: boldTermsFont, italicFont: italicTermsFont, boldItalicFont: boldItalicTermsFont, fixedFont: monospaceTermsFont, blockQuoteFont: termsFont)
|
||||||
|
termsString = .plain(attributedString)
|
||||||
|
} else {
|
||||||
|
termsString = .plain(NSAttributedString())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
termsString = .markdown(
|
||||||
|
text: strings.Premium_Terms,
|
||||||
|
attributes: termsMarkdownAttributes
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
let termsText = termsText.update(
|
let termsText = termsText.update(
|
||||||
component: MultilineTextComponent(
|
component: MultilineTextComponent(
|
||||||
text: .markdown(
|
text: termsString,
|
||||||
text: context.component.isPremium == true ? strings.Premium_ChargeInfo("$4.99", "–").string : strings.Premium_Terms,
|
|
||||||
attributes: termsMarkdownAttributes
|
|
||||||
),
|
|
||||||
horizontalAlignment: .natural,
|
horizontalAlignment: .natural,
|
||||||
maximumNumberOfLines: 0,
|
maximumNumberOfLines: 0,
|
||||||
lineSpacing: 0.0,
|
lineSpacing: 0.0,
|
||||||
@ -1448,7 +1492,8 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
|
|||||||
present: context.component.present,
|
present: context.component.present,
|
||||||
buy: { [weak state] in
|
buy: { [weak state] in
|
||||||
state?.buy()
|
state?.buy()
|
||||||
}, updateIsFocused: { [weak state] isFocused in
|
},
|
||||||
|
updateIsFocused: { [weak state] isFocused in
|
||||||
state?.updateIsFocused(isFocused)
|
state?.updateIsFocused(isFocused)
|
||||||
}
|
}
|
||||||
)),
|
)),
|
||||||
|
@ -135,9 +135,14 @@ private final class LimitComponent: CombinedComponent {
|
|||||||
let textFont = Font.regular(13.0)
|
let textFont = Font.regular(13.0)
|
||||||
let boldTextFont = Font.semibold(13.0)
|
let boldTextFont = Font.semibold(13.0)
|
||||||
let textColor = component.textColor
|
let textColor = component.textColor
|
||||||
let markdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor), link: MarkdownAttributeSet(font: textFont, textColor: component.accentColor), linkAttribute: { _ in
|
let markdownAttributes = MarkdownAttributes(
|
||||||
return nil
|
body: MarkdownAttributeSet(font: textFont, textColor: textColor),
|
||||||
})
|
bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor),
|
||||||
|
link: MarkdownAttributeSet(font: textFont, textColor: component.accentColor),
|
||||||
|
linkAttribute: { _ in
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
let text = text.update(
|
let text = text.update(
|
||||||
component: MultilineTextComponent(
|
component: MultilineTextComponent(
|
||||||
|
@ -1,6 +0,0 @@
|
|||||||
import Foundation
|
|
||||||
import UIKit
|
|
||||||
import AsyncDisplayKit
|
|
||||||
import Display
|
|
||||||
|
|
||||||
|
|
@ -45,14 +45,16 @@ class ThemeSettingsAppIconItem: ListViewItem, ItemListItem {
|
|||||||
let theme: PresentationTheme
|
let theme: PresentationTheme
|
||||||
let strings: PresentationStrings
|
let strings: PresentationStrings
|
||||||
let icons: [PresentationAppIcon]
|
let icons: [PresentationAppIcon]
|
||||||
|
let isPremium: Bool
|
||||||
let currentIconName: String?
|
let currentIconName: String?
|
||||||
let updated: (String) -> Void
|
let updated: (PresentationAppIcon) -> Void
|
||||||
let tag: ItemListItemTag?
|
let tag: ItemListItemTag?
|
||||||
|
|
||||||
init(theme: PresentationTheme, strings: PresentationStrings, sectionId: ItemListSectionId, icons: [PresentationAppIcon], currentIconName: String?, updated: @escaping (String) -> Void, tag: ItemListItemTag? = nil) {
|
init(theme: PresentationTheme, strings: PresentationStrings, sectionId: ItemListSectionId, icons: [PresentationAppIcon], isPremium: Bool, currentIconName: String?, updated: @escaping (PresentationAppIcon) -> Void, tag: ItemListItemTag? = nil) {
|
||||||
self.theme = theme
|
self.theme = theme
|
||||||
self.strings = strings
|
self.strings = strings
|
||||||
self.icons = icons
|
self.icons = icons
|
||||||
|
self.isPremium = isPremium
|
||||||
self.currentIconName = currentIconName
|
self.currentIconName = currentIconName
|
||||||
self.updated = updated
|
self.updated = updated
|
||||||
self.tag = tag
|
self.tag = tag
|
||||||
@ -96,6 +98,7 @@ class ThemeSettingsAppIconItem: ListViewItem, ItemListItem {
|
|||||||
private final class ThemeSettingsAppIconNode : ASDisplayNode {
|
private final class ThemeSettingsAppIconNode : ASDisplayNode {
|
||||||
private let iconNode: ASImageNode
|
private let iconNode: ASImageNode
|
||||||
private let overlayNode: ASImageNode
|
private let overlayNode: ASImageNode
|
||||||
|
private let lockNode: ASImageNode
|
||||||
private let textNode: ASTextNode
|
private let textNode: ASTextNode
|
||||||
private var action: (() -> Void)?
|
private var action: (() -> Void)?
|
||||||
|
|
||||||
@ -108,21 +111,27 @@ private final class ThemeSettingsAppIconNode : ASDisplayNode {
|
|||||||
self.overlayNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 62.0, height: 62.0))
|
self.overlayNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 62.0, height: 62.0))
|
||||||
self.overlayNode.isLayerBacked = true
|
self.overlayNode.isLayerBacked = true
|
||||||
|
|
||||||
|
self.lockNode = ASImageNode()
|
||||||
|
self.lockNode.displaysAsynchronously = false
|
||||||
|
self.lockNode.isUserInteractionEnabled = false
|
||||||
|
|
||||||
self.textNode = ASTextNode()
|
self.textNode = ASTextNode()
|
||||||
self.textNode.isUserInteractionEnabled = false
|
self.textNode.isUserInteractionEnabled = false
|
||||||
self.textNode.displaysAsynchronously = true
|
self.textNode.displaysAsynchronously = false
|
||||||
|
|
||||||
super.init()
|
super.init()
|
||||||
|
|
||||||
self.addSubnode(self.iconNode)
|
self.addSubnode(self.iconNode)
|
||||||
self.addSubnode(self.overlayNode)
|
self.addSubnode(self.overlayNode)
|
||||||
self.addSubnode(self.textNode)
|
self.addSubnode(self.textNode)
|
||||||
|
self.addSubnode(self.lockNode)
|
||||||
}
|
}
|
||||||
|
|
||||||
func setup(theme: PresentationTheme, icon: UIImage, title: NSAttributedString, bordered: Bool, selected: Bool, action: @escaping () -> Void) {
|
func setup(theme: PresentationTheme, icon: UIImage, title: NSAttributedString, locked: Bool, color: UIColor, bordered: Bool, selected: Bool, action: @escaping () -> Void) {
|
||||||
self.iconNode.image = icon
|
self.iconNode.image = icon
|
||||||
self.textNode.attributedText = title
|
self.textNode.attributedText = title
|
||||||
self.overlayNode.image = generateBorderImage(theme: theme, bordered: bordered, selected: selected)
|
self.overlayNode.image = generateBorderImage(theme: theme, bordered: bordered, selected: selected)
|
||||||
|
self.lockNode.image = locked ? generateTintedImage(image: UIImage(bundleImageName: "Notification/SecretLock"), color: color) : nil
|
||||||
self.action = {
|
self.action = {
|
||||||
action()
|
action()
|
||||||
}
|
}
|
||||||
@ -147,7 +156,8 @@ private final class ThemeSettingsAppIconNode : ASDisplayNode {
|
|||||||
|
|
||||||
self.iconNode.frame = CGRect(origin: CGPoint(x: 10.0, y: 14.0), size: CGSize(width: 62.0, height: 62.0))
|
self.iconNode.frame = CGRect(origin: CGPoint(x: 10.0, y: 14.0), size: CGSize(width: 62.0, height: 62.0))
|
||||||
self.overlayNode.frame = CGRect(origin: CGPoint(x: 10.0, y: 14.0), size: CGSize(width: 62.0, height: 62.0))
|
self.overlayNode.frame = CGRect(origin: CGPoint(x: 10.0, y: 14.0), size: CGSize(width: 62.0, height: 62.0))
|
||||||
self.textNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 14.0 + 60.0 + 4.0 + 9.0), size: CGSize(width: bounds.size.width, height: 16.0))
|
self.textNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 87.0), size: CGSize(width: bounds.size.width, height: 16.0))
|
||||||
|
self.lockNode.frame = CGRect(x: 9.0, y: 90.0, width: 6.0, height: 8.0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -321,12 +331,18 @@ class ThemeSettingsAppIconItemNode: ListViewItemNode, ItemListItemNode {
|
|||||||
name = item.strings.Appearance_AppIconNew1
|
name = item.strings.Appearance_AppIconNew1
|
||||||
case "New2":
|
case "New2":
|
||||||
name = item.strings.Appearance_AppIconNew2
|
name = item.strings.Appearance_AppIconNew2
|
||||||
|
case "PremiumCosmic":
|
||||||
|
name = "Cosmic"
|
||||||
|
case "PremiumCherry":
|
||||||
|
name = "Cherry"
|
||||||
|
case "PremiumDuck":
|
||||||
|
name = "Duck"
|
||||||
default:
|
default:
|
||||||
break
|
name = icon.name
|
||||||
}
|
}
|
||||||
|
|
||||||
imageNode.setup(theme: item.theme, icon: image, title: NSAttributedString(string: name, font: selected ? selectedTextFont : textFont, textColor: selected ? item.theme.list.itemAccentColor : item.theme.list.itemPrimaryTextColor, paragraphAlignment: .center), bordered: bordered, selected: selected, action: { [weak self, weak imageNode] in
|
imageNode.setup(theme: item.theme, icon: image, title: NSAttributedString(string: name, font: selected ? selectedTextFont : textFont, textColor: selected ? item.theme.list.itemAccentColor : item.theme.list.itemPrimaryTextColor, paragraphAlignment: .center), locked: !item.isPremium && icon.isPremium, color: item.theme.list.itemPrimaryTextColor, bordered: bordered, selected: selected, action: { [weak self, weak imageNode] in
|
||||||
item.updated(icon.name)
|
item.updated(icon)
|
||||||
if let imageNode = imageNode {
|
if let imageNode = imageNode {
|
||||||
self?.scrollToNode(imageNode, animated: true)
|
self?.scrollToNode(imageNode, animated: true)
|
||||||
}
|
}
|
||||||
|
@ -17,6 +17,7 @@ import ShareController
|
|||||||
import AccountContext
|
import AccountContext
|
||||||
import ContextUI
|
import ContextUI
|
||||||
import UndoUI
|
import UndoUI
|
||||||
|
import PremiumUI
|
||||||
|
|
||||||
func themeDisplayName(strings: PresentationStrings, reference: PresentationThemeReference) -> String {
|
func themeDisplayName(strings: PresentationStrings, reference: PresentationThemeReference) -> String {
|
||||||
let name: String
|
let name: String
|
||||||
@ -57,12 +58,12 @@ private final class ThemeSettingsControllerArguments {
|
|||||||
let openBubbleSettings: () -> Void
|
let openBubbleSettings: () -> Void
|
||||||
let toggleLargeEmoji: (Bool) -> Void
|
let toggleLargeEmoji: (Bool) -> Void
|
||||||
let disableAnimations: (Bool) -> Void
|
let disableAnimations: (Bool) -> Void
|
||||||
let selectAppIcon: (String) -> Void
|
let selectAppIcon: (PresentationAppIcon) -> Void
|
||||||
let editTheme: (PresentationCloudTheme) -> Void
|
let editTheme: (PresentationCloudTheme) -> Void
|
||||||
let themeContextAction: (Bool, PresentationThemeReference, ASDisplayNode, ContextGesture?) -> Void
|
let themeContextAction: (Bool, PresentationThemeReference, ASDisplayNode, ContextGesture?) -> Void
|
||||||
let colorContextAction: (Bool, PresentationThemeReference, ThemeSettingsColorOption?, ASDisplayNode, ContextGesture?) -> Void
|
let colorContextAction: (Bool, PresentationThemeReference, ThemeSettingsColorOption?, ASDisplayNode, ContextGesture?) -> Void
|
||||||
|
|
||||||
init(context: AccountContext, selectTheme: @escaping (PresentationThemeReference) -> Void, openThemeSettings: @escaping () -> Void, openWallpaperSettings: @escaping () -> Void, selectAccentColor: @escaping (PresentationThemeAccentColor?) -> Void, openAccentColorPicker: @escaping (PresentationThemeReference, Bool) -> Void, toggleNightTheme: @escaping (Bool) -> Void, openAutoNightTheme: @escaping () -> Void, openTextSize: @escaping () -> Void, openBubbleSettings: @escaping () -> Void, toggleLargeEmoji: @escaping (Bool) -> Void, disableAnimations: @escaping (Bool) -> Void, selectAppIcon: @escaping (String) -> Void, editTheme: @escaping (PresentationCloudTheme) -> Void, themeContextAction: @escaping (Bool, PresentationThemeReference, ASDisplayNode, ContextGesture?) -> Void, colorContextAction: @escaping (Bool, PresentationThemeReference, ThemeSettingsColorOption?, ASDisplayNode, ContextGesture?) -> Void) {
|
init(context: AccountContext, selectTheme: @escaping (PresentationThemeReference) -> Void, openThemeSettings: @escaping () -> Void, openWallpaperSettings: @escaping () -> Void, selectAccentColor: @escaping (PresentationThemeAccentColor?) -> Void, openAccentColorPicker: @escaping (PresentationThemeReference, Bool) -> Void, toggleNightTheme: @escaping (Bool) -> Void, openAutoNightTheme: @escaping () -> Void, openTextSize: @escaping () -> Void, openBubbleSettings: @escaping () -> Void, toggleLargeEmoji: @escaping (Bool) -> Void, disableAnimations: @escaping (Bool) -> Void, selectAppIcon: @escaping (PresentationAppIcon) -> Void, editTheme: @escaping (PresentationCloudTheme) -> Void, themeContextAction: @escaping (Bool, PresentationThemeReference, ASDisplayNode, ContextGesture?) -> Void, colorContextAction: @escaping (Bool, PresentationThemeReference, ThemeSettingsColorOption?, ASDisplayNode, ContextGesture?) -> Void) {
|
||||||
self.context = context
|
self.context = context
|
||||||
self.selectTheme = selectTheme
|
self.selectTheme = selectTheme
|
||||||
self.openThemeSettings = openThemeSettings
|
self.openThemeSettings = openThemeSettings
|
||||||
@ -119,7 +120,7 @@ private enum ThemeSettingsControllerEntry: ItemListNodeEntry {
|
|||||||
case textSize(PresentationTheme, String, String)
|
case textSize(PresentationTheme, String, String)
|
||||||
case bubbleSettings(PresentationTheme, String, String)
|
case bubbleSettings(PresentationTheme, String, String)
|
||||||
case iconHeader(PresentationTheme, String)
|
case iconHeader(PresentationTheme, String)
|
||||||
case iconItem(PresentationTheme, PresentationStrings, [PresentationAppIcon], String?)
|
case iconItem(PresentationTheme, PresentationStrings, [PresentationAppIcon], Bool, String?)
|
||||||
case otherHeader(PresentationTheme, String)
|
case otherHeader(PresentationTheme, String)
|
||||||
case largeEmoji(PresentationTheme, String, Bool)
|
case largeEmoji(PresentationTheme, String, Bool)
|
||||||
case animations(PresentationTheme, String, Bool)
|
case animations(PresentationTheme, String, Bool)
|
||||||
@ -237,8 +238,8 @@ private enum ThemeSettingsControllerEntry: ItemListNodeEntry {
|
|||||||
} else {
|
} else {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
case let .iconItem(lhsTheme, lhsStrings, lhsIcons, lhsValue):
|
case let .iconItem(lhsTheme, lhsStrings, lhsIcons, lhsIsPremium, lhsValue):
|
||||||
if case let .iconItem(rhsTheme, rhsStrings, rhsIcons, rhsValue) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsIcons == rhsIcons, lhsValue == rhsValue {
|
if case let .iconItem(rhsTheme, rhsStrings, rhsIcons, rhsIsPremium, rhsValue) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsIcons == rhsIcons, lhsIsPremium == rhsIsPremium, lhsValue == rhsValue {
|
||||||
return true
|
return true
|
||||||
} else {
|
} else {
|
||||||
return false
|
return false
|
||||||
@ -313,9 +314,9 @@ private enum ThemeSettingsControllerEntry: ItemListNodeEntry {
|
|||||||
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
|
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
|
||||||
case let .iconHeader(_, text):
|
case let .iconHeader(_, text):
|
||||||
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
|
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
|
||||||
case let .iconItem(theme, strings, icons, value):
|
case let .iconItem(theme, strings, icons, isPremium, value):
|
||||||
return ThemeSettingsAppIconItem(theme: theme, strings: strings, sectionId: self.section, icons: icons, currentIconName: value, updated: { iconName in
|
return ThemeSettingsAppIconItem(theme: theme, strings: strings, sectionId: self.section, icons: icons, isPremium: isPremium, currentIconName: value, updated: { icon in
|
||||||
arguments.selectAppIcon(iconName)
|
arguments.selectAppIcon(icon)
|
||||||
})
|
})
|
||||||
case let .otherHeader(_, text):
|
case let .otherHeader(_, text):
|
||||||
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
|
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
|
||||||
@ -333,7 +334,7 @@ private enum ThemeSettingsControllerEntry: ItemListNodeEntry {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func themeSettingsControllerEntries(presentationData: PresentationData, presentationThemeSettings: PresentationThemeSettings, themeReference: PresentationThemeReference, availableThemes: [PresentationThemeReference], availableAppIcons: [PresentationAppIcon], currentAppIconName: String?, chatThemes: [PresentationThemeReference], animatedEmojiStickers: [String: [StickerPackItem]]) -> [ThemeSettingsControllerEntry] {
|
private func themeSettingsControllerEntries(presentationData: PresentationData, presentationThemeSettings: PresentationThemeSettings, themeReference: PresentationThemeReference, availableThemes: [PresentationThemeReference], availableAppIcons: [PresentationAppIcon], currentAppIconName: String?, isPremium: Bool, chatThemes: [PresentationThemeReference], animatedEmojiStickers: [String: [StickerPackItem]]) -> [ThemeSettingsControllerEntry] {
|
||||||
var entries: [ThemeSettingsControllerEntry] = []
|
var entries: [ThemeSettingsControllerEntry] = []
|
||||||
|
|
||||||
let strings = presentationData.strings
|
let strings = presentationData.strings
|
||||||
@ -378,7 +379,7 @@ private func themeSettingsControllerEntries(presentationData: PresentationData,
|
|||||||
|
|
||||||
if !availableAppIcons.isEmpty {
|
if !availableAppIcons.isEmpty {
|
||||||
entries.append(.iconHeader(presentationData.theme, strings.Appearance_AppIcon.uppercased()))
|
entries.append(.iconHeader(presentationData.theme, strings.Appearance_AppIcon.uppercased()))
|
||||||
entries.append(.iconItem(presentationData.theme, presentationData.strings, availableAppIcons, currentAppIconName))
|
entries.append(.iconItem(presentationData.theme, presentationData.strings, availableAppIcons, isPremium, currentAppIconName))
|
||||||
}
|
}
|
||||||
|
|
||||||
entries.append(.otherHeader(presentationData.theme, strings.Appearance_Other.uppercased()))
|
entries.append(.otherHeader(presentationData.theme, strings.Appearance_Other.uppercased()))
|
||||||
@ -494,9 +495,25 @@ public func themeSettingsController(context: AccountContext, focusOnItemTag: The
|
|||||||
let _ = updatePresentationThemeSettingsInteractively(accountManager: context.sharedContext.accountManager, { current in
|
let _ = updatePresentationThemeSettingsInteractively(accountManager: context.sharedContext.accountManager, { current in
|
||||||
return current.withUpdatedReduceMotion(reduceMotion)
|
return current.withUpdatedReduceMotion(reduceMotion)
|
||||||
}).start()
|
}).start()
|
||||||
}, selectAppIcon: { name in
|
}, selectAppIcon: { icon in
|
||||||
currentAppIconName.set(name)
|
let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId))
|
||||||
context.sharedContext.applicationBindings.requestSetAlternateIconName(name, { _ in
|
|> deliverOnMainQueue).start(next: { peer in
|
||||||
|
let isPremium = peer?.isPremium ?? false
|
||||||
|
if icon.isPremium && !isPremium {
|
||||||
|
var replaceImpl: ((ViewController) -> Void)?
|
||||||
|
let controller = PremiumDemoScreen(context: context, subject: .appIcons, source: .other, action: {
|
||||||
|
let controller = PremiumIntroScreen(context: context, source: .appIcons)
|
||||||
|
replaceImpl?(controller)
|
||||||
|
})
|
||||||
|
replaceImpl = { [weak controller] c in
|
||||||
|
controller?.replace(with: c)
|
||||||
|
}
|
||||||
|
pushControllerImpl?(controller)
|
||||||
|
} else {
|
||||||
|
currentAppIconName.set(icon.name)
|
||||||
|
context.sharedContext.applicationBindings.requestSetAlternateIconName(icon.name, { _ in
|
||||||
|
})
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}, editTheme: { theme in
|
}, editTheme: { theme in
|
||||||
let controller = editThemeController(context: context, mode: .edit(theme), navigateToChat: { peerId in
|
let controller = editThemeController(context: context, mode: .edit(theme), navigateToChat: { peerId in
|
||||||
@ -922,10 +939,12 @@ public func themeSettingsController(context: AccountContext, focusOnItemTag: The
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
let signal = combineLatest(queue: .mainQueue(), context.sharedContext.presentationData, context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.presentationThemeSettings, SharedDataKeys.chatThemes]), cloudThemes.get(), availableAppIcons, currentAppIconName.get(), removedThemeIndexesPromise.get(), animatedEmojiStickers)
|
let signal = combineLatest(queue: .mainQueue(), context.sharedContext.presentationData, context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.presentationThemeSettings, SharedDataKeys.chatThemes]), cloudThemes.get(), availableAppIcons, currentAppIconName.get(), removedThemeIndexesPromise.get(), animatedEmojiStickers, context.account.postbox.peerView(id: context.account.peerId))
|
||||||
|> map { presentationData, sharedData, cloudThemes, availableAppIcons, currentAppIconName, removedThemeIndexes, animatedEmojiStickers -> (ItemListControllerState, (ItemListNodeState, Any)) in
|
|> map { presentationData, sharedData, cloudThemes, availableAppIcons, currentAppIconName, removedThemeIndexes, animatedEmojiStickers, peerView -> (ItemListControllerState, (ItemListNodeState, Any)) in
|
||||||
let settings = sharedData.entries[ApplicationSpecificSharedDataKeys.presentationThemeSettings]?.get(PresentationThemeSettings.self) ?? PresentationThemeSettings.defaultSettings
|
let settings = sharedData.entries[ApplicationSpecificSharedDataKeys.presentationThemeSettings]?.get(PresentationThemeSettings.self) ?? PresentationThemeSettings.defaultSettings
|
||||||
|
|
||||||
|
let isPremium = peerView.peers[peerView.peerId]?.isPremium ?? false
|
||||||
|
|
||||||
let themeReference: PresentationThemeReference
|
let themeReference: PresentationThemeReference
|
||||||
if presentationData.autoNightModeTriggered {
|
if presentationData.autoNightModeTriggered {
|
||||||
if let _ = settings.theme.emoticon {
|
if let _ = settings.theme.emoticon {
|
||||||
@ -961,7 +980,7 @@ public func themeSettingsController(context: AccountContext, focusOnItemTag: The
|
|||||||
chatThemes.insert(.builtin(.dayClassic), at: 0)
|
chatThemes.insert(.builtin(.dayClassic), at: 0)
|
||||||
|
|
||||||
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.Appearance_Title), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back))
|
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.Appearance_Title), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back))
|
||||||
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: themeSettingsControllerEntries(presentationData: presentationData, presentationThemeSettings: settings, themeReference: themeReference, availableThemes: availableThemes, availableAppIcons: availableAppIcons, currentAppIconName: currentAppIconName, chatThemes: chatThemes, animatedEmojiStickers: animatedEmojiStickers), style: .blocks, ensureVisibleItemTag: focusOnItemTag, animateChanges: false)
|
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: themeSettingsControllerEntries(presentationData: presentationData, presentationThemeSettings: settings, themeReference: themeReference, availableThemes: availableThemes, availableAppIcons: availableAppIcons, currentAppIconName: currentAppIconName, isPremium: isPremium, chatThemes: chatThemes, animatedEmojiStickers: animatedEmojiStickers), style: .blocks, ensureVisibleItemTag: focusOnItemTag, animateChanges: false)
|
||||||
|
|
||||||
return (controllerState, (listState, arguments))
|
return (controllerState, (listState, arguments))
|
||||||
}
|
}
|
||||||
|
@ -157,6 +157,7 @@ private final class StickerPackContainer: ASDisplayNode {
|
|||||||
|
|
||||||
self.buttonNode = HighlightableButtonNode()
|
self.buttonNode = HighlightableButtonNode()
|
||||||
self.titleNode = ImmediateTextNode()
|
self.titleNode = ImmediateTextNode()
|
||||||
|
self.titleNode.textAlignment = .center
|
||||||
self.titleNode.maximumNumberOfLines = 2
|
self.titleNode.maximumNumberOfLines = 2
|
||||||
self.titleNode.highlightAttributeAction = { attributes in
|
self.titleNode.highlightAttributeAction = { attributes in
|
||||||
if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerTextMention)] {
|
if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerTextMention)] {
|
||||||
|
@ -247,7 +247,7 @@ final class PremiumStickerPackAccessoryNode: SparseNode, PeekControllerAccessory
|
|||||||
UIColor(rgb: 0xe46ace)
|
UIColor(rgb: 0xe46ace)
|
||||||
], foregroundColor: .white), height: 50.0, cornerRadius: 11.0, gloss: true)
|
], foregroundColor: .white), height: 50.0, cornerRadius: 11.0, gloss: true)
|
||||||
self.proceedButton.iconPosition = .right
|
self.proceedButton.iconPosition = .right
|
||||||
self.proceedButton.iconSpacing = 6.0
|
self.proceedButton.iconSpacing = 4.0
|
||||||
self.proceedButton.animation = "premium_unlock"
|
self.proceedButton.animation = "premium_unlock"
|
||||||
|
|
||||||
self.cancelButton = HighlightableButtonNode()
|
self.cancelButton = HighlightableButtonNode()
|
||||||
|
@ -4,6 +4,9 @@ import SwiftSignalKit
|
|||||||
import TelegramApi
|
import TelegramApi
|
||||||
import MtProtoKit
|
import MtProtoKit
|
||||||
|
|
||||||
|
public func updatePremiumPromoConfigurationOnce(account: Account) -> Signal<Void, NoError> {
|
||||||
|
return updatePremiumPromoConfigurationOnce(postbox: account.postbox, network: account.network)
|
||||||
|
}
|
||||||
|
|
||||||
func updatePremiumPromoConfigurationOnce(postbox: Postbox, network: Network) -> Signal<Void, NoError> {
|
func updatePremiumPromoConfigurationOnce(postbox: Postbox, network: Network) -> Signal<Void, NoError> {
|
||||||
return network.request(Api.functions.help.getPremiumPromo())
|
return network.request(Api.functions.help.getPremiumPromo())
|
||||||
|
@ -10,6 +10,7 @@ public final class AttachMenuBots: Equatable, Codable {
|
|||||||
case name
|
case name
|
||||||
case botIcons
|
case botIcons
|
||||||
case peerTypes
|
case peerTypes
|
||||||
|
case hasSettings
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum IconName: Int32, Codable {
|
public enum IconName: Int32, Codable {
|
||||||
@ -93,17 +94,20 @@ public final class AttachMenuBots: Equatable, Codable {
|
|||||||
public let name: String
|
public let name: String
|
||||||
public let icons: [IconName: TelegramMediaFile]
|
public let icons: [IconName: TelegramMediaFile]
|
||||||
public let peerTypes: PeerFlags
|
public let peerTypes: PeerFlags
|
||||||
|
public let hasSettings: Bool
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
peerId: PeerId,
|
peerId: PeerId,
|
||||||
name: String,
|
name: String,
|
||||||
icons: [IconName: TelegramMediaFile],
|
icons: [IconName: TelegramMediaFile],
|
||||||
peerTypes: PeerFlags
|
peerTypes: PeerFlags,
|
||||||
|
hasSettings: Bool
|
||||||
) {
|
) {
|
||||||
self.peerId = peerId
|
self.peerId = peerId
|
||||||
self.name = name
|
self.name = name
|
||||||
self.icons = icons
|
self.icons = icons
|
||||||
self.peerTypes = peerTypes
|
self.peerTypes = peerTypes
|
||||||
|
self.hasSettings = hasSettings
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func ==(lhs: Bot, rhs: Bot) -> Bool {
|
public static func ==(lhs: Bot, rhs: Bot) -> Bool {
|
||||||
@ -119,6 +123,9 @@ public final class AttachMenuBots: Equatable, Codable {
|
|||||||
if lhs.peerTypes != rhs.peerTypes {
|
if lhs.peerTypes != rhs.peerTypes {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
if lhs.hasSettings != rhs.hasSettings {
|
||||||
|
return false
|
||||||
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -139,6 +146,8 @@ public final class AttachMenuBots: Equatable, Codable {
|
|||||||
|
|
||||||
let value = try container.decodeIfPresent(Int32.self, forKey: .peerTypes) ?? Int32(PeerFlags.default.rawValue)
|
let value = try container.decodeIfPresent(Int32.self, forKey: .peerTypes) ?? Int32(PeerFlags.default.rawValue)
|
||||||
self.peerTypes = PeerFlags(rawValue: UInt32(value))
|
self.peerTypes = PeerFlags(rawValue: UInt32(value))
|
||||||
|
|
||||||
|
self.hasSettings = try container.decodeIfPresent(Bool.self, forKey: .hasSettings) ?? false
|
||||||
}
|
}
|
||||||
|
|
||||||
public func encode(to encoder: Encoder) throws {
|
public func encode(to encoder: Encoder) throws {
|
||||||
@ -154,6 +163,8 @@ public final class AttachMenuBots: Equatable, Codable {
|
|||||||
try container.encode(iconPairs, forKey: .botIcons)
|
try container.encode(iconPairs, forKey: .botIcons)
|
||||||
|
|
||||||
try container.encode(Int32(self.peerTypes.rawValue), forKey: .peerTypes)
|
try container.encode(Int32(self.peerTypes.rawValue), forKey: .peerTypes)
|
||||||
|
|
||||||
|
try container.encode(self.hasSettings, forKey: .hasSettings)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -265,7 +276,7 @@ func managedSynchronizeAttachMenuBots(postbox: Postbox, network: Network, force:
|
|||||||
var resultBots: [AttachMenuBots.Bot] = []
|
var resultBots: [AttachMenuBots.Bot] = []
|
||||||
for bot in bots {
|
for bot in bots {
|
||||||
switch bot {
|
switch bot {
|
||||||
case let .attachMenuBot(_, botId, name, apiPeerTypes, botIcons):
|
case let .attachMenuBot(flags, botId, name, apiPeerTypes, botIcons):
|
||||||
var icons: [AttachMenuBots.Bot.IconName: TelegramMediaFile] = [:]
|
var icons: [AttachMenuBots.Bot.IconName: TelegramMediaFile] = [:]
|
||||||
for icon in botIcons {
|
for icon in botIcons {
|
||||||
switch icon {
|
switch icon {
|
||||||
@ -291,7 +302,7 @@ func managedSynchronizeAttachMenuBots(postbox: Postbox, network: Network, force:
|
|||||||
peerTypes.insert(.channel)
|
peerTypes.insert(.channel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
resultBots.append(AttachMenuBots.Bot(peerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(botId)), name: name, icons: icons, peerTypes: peerTypes))
|
resultBots.append(AttachMenuBots.Bot(peerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(botId)), name: name, icons: icons, peerTypes: peerTypes, hasSettings: (flags & (1 << 1)) != 0))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -395,12 +406,14 @@ public struct AttachMenuBot {
|
|||||||
public let shortName: String
|
public let shortName: String
|
||||||
public let icons: [AttachMenuBots.Bot.IconName: TelegramMediaFile]
|
public let icons: [AttachMenuBots.Bot.IconName: TelegramMediaFile]
|
||||||
public let peerTypes: AttachMenuBots.Bot.PeerFlags
|
public let peerTypes: AttachMenuBots.Bot.PeerFlags
|
||||||
|
public let hasSettings: Bool
|
||||||
|
|
||||||
init(peer: Peer, shortName: String, icons: [AttachMenuBots.Bot.IconName: TelegramMediaFile], peerTypes: AttachMenuBots.Bot.PeerFlags) {
|
init(peer: Peer, shortName: String, icons: [AttachMenuBots.Bot.IconName: TelegramMediaFile], peerTypes: AttachMenuBots.Bot.PeerFlags, hasSettings: Bool) {
|
||||||
self.peer = peer
|
self.peer = peer
|
||||||
self.shortName = shortName
|
self.shortName = shortName
|
||||||
self.icons = icons
|
self.icons = icons
|
||||||
self.peerTypes = peerTypes
|
self.peerTypes = peerTypes
|
||||||
|
self.hasSettings = hasSettings
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -412,7 +425,7 @@ func _internal_attachMenuBots(postbox: Postbox) -> Signal<[AttachMenuBot], NoErr
|
|||||||
var resultBots: [AttachMenuBot] = []
|
var resultBots: [AttachMenuBot] = []
|
||||||
for bot in cachedBots {
|
for bot in cachedBots {
|
||||||
if let peer = transaction.getPeer(bot.peerId) {
|
if let peer = transaction.getPeer(bot.peerId) {
|
||||||
resultBots.append(AttachMenuBot(peer: peer, shortName: bot.name, icons: bot.icons, peerTypes: bot.peerTypes))
|
resultBots.append(AttachMenuBot(peer: peer, shortName: bot.name, icons: bot.icons, peerTypes: bot.peerTypes, hasSettings: bot.hasSettings))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return resultBots
|
return resultBots
|
||||||
@ -427,7 +440,7 @@ public func _internal_getAttachMenuBot(postbox: Postbox, network: Network, botId
|
|||||||
return postbox.transaction { transaction -> Signal<AttachMenuBot, GetAttachMenuBotError> in
|
return postbox.transaction { transaction -> Signal<AttachMenuBot, GetAttachMenuBotError> in
|
||||||
if cached, let cachedBots = cachedAttachMenuBots(transaction: transaction)?.bots {
|
if cached, let cachedBots = cachedAttachMenuBots(transaction: transaction)?.bots {
|
||||||
if let bot = cachedBots.first(where: { $0.peerId == botId }), let peer = transaction.getPeer(bot.peerId) {
|
if let bot = cachedBots.first(where: { $0.peerId == botId }), let peer = transaction.getPeer(bot.peerId) {
|
||||||
return .single(AttachMenuBot(peer: peer, shortName: bot.name, icons: bot.icons, peerTypes: bot.peerTypes))
|
return .single(AttachMenuBot(peer: peer, shortName: bot.name, icons: bot.icons, peerTypes: bot.peerTypes, hasSettings: bot.hasSettings))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -461,7 +474,7 @@ public func _internal_getAttachMenuBot(postbox: Postbox, network: Network, botId
|
|||||||
}
|
}
|
||||||
|
|
||||||
switch bot {
|
switch bot {
|
||||||
case let .attachMenuBot(_, _, name, apiPeerTypes, botIcons):
|
case let .attachMenuBot(flags, _, name, apiPeerTypes, botIcons):
|
||||||
var icons: [AttachMenuBots.Bot.IconName: TelegramMediaFile] = [:]
|
var icons: [AttachMenuBots.Bot.IconName: TelegramMediaFile] = [:]
|
||||||
for icon in botIcons {
|
for icon in botIcons {
|
||||||
switch icon {
|
switch icon {
|
||||||
@ -486,7 +499,7 @@ public func _internal_getAttachMenuBot(postbox: Postbox, network: Network, botId
|
|||||||
peerTypes.insert(.channel)
|
peerTypes.insert(.channel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return .single(AttachMenuBot(peer: peer, shortName: name, icons: icons, peerTypes: peerTypes))
|
return .single(AttachMenuBot(peer: peer, shortName: name, icons: icons, peerTypes: peerTypes, hasSettings: (flags & (1 << 1)) != 0))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,25 @@ import Postbox
|
|||||||
import TelegramApi
|
import TelegramApi
|
||||||
import MtProtoKit
|
import MtProtoKit
|
||||||
|
|
||||||
|
public func smallestVideoRepresentation(_ representations: [TelegramMediaImage.VideoRepresentation]) -> TelegramMediaImage.VideoRepresentation? {
|
||||||
|
if representations.count == 0 {
|
||||||
|
return nil
|
||||||
|
} else {
|
||||||
|
var dimensions = representations[0].dimensions
|
||||||
|
var index = 0
|
||||||
|
|
||||||
|
for i in 1 ..< representations.count {
|
||||||
|
let representationDimensions = representations[i].dimensions
|
||||||
|
if representationDimensions.width < dimensions.width && representationDimensions.height < dimensions.height {
|
||||||
|
dimensions = representationDimensions
|
||||||
|
index = i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return representations[index]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public func smallestImageRepresentation(_ representations: [TelegramMediaImageRepresentation]) -> TelegramMediaImageRepresentation? {
|
public func smallestImageRepresentation(_ representations: [TelegramMediaImageRepresentation]) -> TelegramMediaImageRepresentation? {
|
||||||
if representations.count == 0 {
|
if representations.count == 0 {
|
||||||
return nil
|
return nil
|
||||||
|
@ -45,11 +45,13 @@ public struct PresentationAppIcon: Equatable {
|
|||||||
public let name: String
|
public let name: String
|
||||||
public let imageName: String
|
public let imageName: String
|
||||||
public let isDefault: Bool
|
public let isDefault: Bool
|
||||||
|
public let isPremium: Bool
|
||||||
|
|
||||||
public init(name: String, imageName: String, isDefault: Bool = false) {
|
public init(name: String, imageName: String, isDefault: Bool = false, isPremium: Bool = false) {
|
||||||
self.name = name
|
self.name = name
|
||||||
self.imageName = imageName
|
self.imageName = imageName
|
||||||
self.isDefault = isDefault
|
self.isDefault = isDefault
|
||||||
|
self.isPremium = isPremium
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
159
submodules/TelegramUI/Images.xcassets/Premium/Perk/AppIcon.imageset/AppIcon.pdf
vendored
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
%PDF-1.7
|
||||||
|
|
||||||
|
1 0 obj
|
||||||
|
<< >>
|
||||||
|
endobj
|
||||||
|
|
||||||
|
2 0 obj
|
||||||
|
<< /Length 3 0 R >>
|
||||||
|
stream
|
||||||
|
/DeviceRGB CS
|
||||||
|
/DeviceRGB cs
|
||||||
|
q
|
||||||
|
1.000000 0.000000 -0.000000 1.000000 6.000000 6.000000 cm
|
||||||
|
1.000000 1.000000 1.000000 scn
|
||||||
|
9.000000 15.600000 m
|
||||||
|
9.000000 16.440079 9.000000 16.860119 9.163490 17.180986 c
|
||||||
|
9.307301 17.463228 9.536771 17.692699 9.819015 17.836510 c
|
||||||
|
10.139882 18.000000 10.559921 18.000000 11.400000 18.000000 c
|
||||||
|
16.600000 18.000000 l
|
||||||
|
17.440079 18.000000 17.860119 18.000000 18.180984 17.836510 c
|
||||||
|
18.463230 17.692699 18.692699 17.463228 18.836510 17.180986 c
|
||||||
|
19.000000 16.860119 19.000000 16.440079 19.000000 15.600000 c
|
||||||
|
19.000000 10.400000 l
|
||||||
|
19.000000 9.559921 19.000000 9.139882 18.836510 8.819015 c
|
||||||
|
18.692699 8.536771 18.463230 8.307301 18.180984 8.163490 c
|
||||||
|
17.860119 8.000000 17.440079 8.000000 16.600000 8.000000 c
|
||||||
|
11.400000 8.000000 l
|
||||||
|
10.559921 8.000000 10.139882 8.000000 9.819015 8.163490 c
|
||||||
|
9.536771 8.307301 9.307301 8.536771 9.163490 8.819015 c
|
||||||
|
9.000000 9.139882 9.000000 9.559921 9.000000 10.400000 c
|
||||||
|
9.000000 15.600000 l
|
||||||
|
h
|
||||||
|
13.877289 10.424780 m
|
||||||
|
13.972101 10.481474 14.090399 10.481474 14.185211 10.424780 c
|
||||||
|
15.566299 9.598953 l
|
||||||
|
16.097145 9.281532 16.748672 9.758140 16.606907 10.360182 c
|
||||||
|
16.243755 11.902411 l
|
||||||
|
16.218065 12.011508 16.255333 12.125828 16.340385 12.198824 c
|
||||||
|
17.543976 13.231811 l
|
||||||
|
18.015240 13.636273 17.765560 14.408524 17.146713 14.460539 c
|
||||||
|
15.554527 14.594366 l
|
||||||
|
15.443923 14.603662 15.347490 14.673266 15.303833 14.775314 c
|
||||||
|
14.674830 16.245621 l
|
||||||
|
14.432594 16.811852 13.629906 16.811852 13.387671 16.245621 c
|
||||||
|
12.758666 14.775314 l
|
||||||
|
12.715010 14.673266 12.618577 14.603662 12.507973 14.594366 c
|
||||||
|
10.915787 14.460539 l
|
||||||
|
10.296938 14.408524 10.047261 13.636274 10.518523 13.231811 c
|
||||||
|
11.722115 12.198824 l
|
||||||
|
11.807166 12.125828 11.844435 12.011508 11.818745 11.902411 c
|
||||||
|
11.455591 10.360182 l
|
||||||
|
11.313828 9.758140 11.965354 9.281531 12.496199 9.598952 c
|
||||||
|
13.877289 10.424780 l
|
||||||
|
h
|
||||||
|
0.163490 14.180985 m
|
||||||
|
0.000000 13.860119 0.000000 13.440079 0.000000 12.600000 c
|
||||||
|
0.000000 10.400000 l
|
||||||
|
0.000000 9.559921 0.000000 9.139882 0.163490 8.819015 c
|
||||||
|
0.307300 8.536771 0.536771 8.307301 0.819014 8.163490 c
|
||||||
|
1.139882 8.000000 1.559921 8.000000 2.400000 8.000000 c
|
||||||
|
4.600000 8.000000 l
|
||||||
|
5.440079 8.000000 5.860118 8.000000 6.180986 8.163490 c
|
||||||
|
6.463229 8.307301 6.692700 8.536771 6.836510 8.819015 c
|
||||||
|
7.000000 9.139882 7.000000 9.559921 7.000000 10.400000 c
|
||||||
|
7.000000 12.600000 l
|
||||||
|
7.000000 13.440079 7.000000 13.860119 6.836510 14.180985 c
|
||||||
|
6.692700 14.463229 6.463229 14.692699 6.180986 14.836510 c
|
||||||
|
5.860118 15.000000 5.440079 15.000000 4.600000 15.000000 c
|
||||||
|
2.400000 15.000000 l
|
||||||
|
1.559921 15.000000 1.139882 15.000000 0.819014 14.836510 c
|
||||||
|
0.536771 14.692699 0.307300 14.463229 0.163490 14.180985 c
|
||||||
|
h
|
||||||
|
2.163490 5.180985 m
|
||||||
|
2.000000 4.860118 2.000000 4.440079 2.000000 3.600000 c
|
||||||
|
2.000000 3.400000 l
|
||||||
|
2.000000 2.559921 2.000000 2.139882 2.163490 1.819014 c
|
||||||
|
2.307300 1.536772 2.536771 1.307301 2.819014 1.163490 c
|
||||||
|
3.139882 1.000000 3.559921 1.000000 4.400000 1.000000 c
|
||||||
|
4.600000 1.000000 l
|
||||||
|
5.440079 1.000000 5.860118 1.000000 6.180986 1.163490 c
|
||||||
|
6.463229 1.307301 6.692700 1.536772 6.836510 1.819014 c
|
||||||
|
7.000000 2.139882 7.000000 2.559921 7.000000 3.400000 c
|
||||||
|
7.000000 3.600000 l
|
||||||
|
7.000000 4.440079 7.000000 4.860118 6.836510 5.180985 c
|
||||||
|
6.692700 5.463229 6.463229 5.692699 6.180986 5.836510 c
|
||||||
|
5.860118 6.000000 5.440079 6.000000 4.600000 6.000000 c
|
||||||
|
4.400000 6.000000 l
|
||||||
|
3.559921 6.000000 3.139882 6.000000 2.819014 5.836510 c
|
||||||
|
2.536771 5.692699 2.307300 5.463229 2.163490 5.180985 c
|
||||||
|
h
|
||||||
|
9.000000 3.599999 m
|
||||||
|
9.000000 4.440079 9.000000 4.860118 9.163490 5.180985 c
|
||||||
|
9.307301 5.463229 9.536771 5.692699 9.819015 5.836510 c
|
||||||
|
10.139882 6.000000 10.559921 6.000000 11.400000 6.000000 c
|
||||||
|
12.599999 6.000000 l
|
||||||
|
13.440079 6.000000 13.860118 6.000000 14.180985 5.836510 c
|
||||||
|
14.463229 5.692699 14.692699 5.463229 14.836510 5.180985 c
|
||||||
|
15.000000 4.860118 15.000000 4.440079 15.000000 3.600000 c
|
||||||
|
15.000000 2.400001 l
|
||||||
|
15.000000 1.559921 15.000000 1.139881 14.836510 0.819014 c
|
||||||
|
14.692699 0.536772 14.463229 0.307301 14.180985 0.163490 c
|
||||||
|
13.860118 0.000000 13.440079 0.000000 12.600000 0.000000 c
|
||||||
|
11.400001 0.000000 l
|
||||||
|
10.559921 0.000000 10.139882 0.000000 9.819015 0.163490 c
|
||||||
|
9.536771 0.307301 9.307301 0.536772 9.163490 0.819014 c
|
||||||
|
9.000000 1.139881 9.000000 1.559921 9.000000 2.400000 c
|
||||||
|
9.000000 3.599999 l
|
||||||
|
h
|
||||||
|
f*
|
||||||
|
n
|
||||||
|
Q
|
||||||
|
|
||||||
|
endstream
|
||||||
|
endobj
|
||||||
|
|
||||||
|
3 0 obj
|
||||||
|
4178
|
||||||
|
endobj
|
||||||
|
|
||||||
|
4 0 obj
|
||||||
|
<< /Annots []
|
||||||
|
/Type /Page
|
||||||
|
/MediaBox [ 0.000000 0.000000 30.000000 30.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
|
||||||
|
0000000034 00000 n
|
||||||
|
0000004268 00000 n
|
||||||
|
0000004291 00000 n
|
||||||
|
0000004464 00000 n
|
||||||
|
0000004538 00000 n
|
||||||
|
trailer
|
||||||
|
<< /ID [ (some) (id) ]
|
||||||
|
/Root 6 0 R
|
||||||
|
/Size 7
|
||||||
|
>>
|
||||||
|
startxref
|
||||||
|
4597
|
||||||
|
%%EOF
|
12
submodules/TelegramUI/Images.xcassets/Premium/Perk/AppIcon.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "AppIcon.pdf",
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
@ -679,6 +679,11 @@ private func extractAccountManagerState(records: AccountRecordsView<TelegramAcco
|
|||||||
if buildConfig.isInternalBuild {
|
if buildConfig.isInternalBuild {
|
||||||
icons.append(PresentationAppIcon(name: "WhiteFilledIcon", imageName: "WhiteFilledIcon"))
|
icons.append(PresentationAppIcon(name: "WhiteFilledIcon", imageName: "WhiteFilledIcon"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
icons.append(PresentationAppIcon(name: "PremiumCosmic", imageName: "PremiumCosmic", isPremium: true))
|
||||||
|
icons.append(PresentationAppIcon(name: "PremiumCherry", imageName: "PremiumCherry", isPremium: true))
|
||||||
|
icons.append(PresentationAppIcon(name: "PremiumDuck", imageName: "PremiumDuck", isPremium: true))
|
||||||
|
|
||||||
return icons
|
return icons
|
||||||
} else {
|
} else {
|
||||||
return []
|
return []
|
||||||
|
@ -93,7 +93,7 @@ final class ChatAvatarNavigationNode: ASDisplayNode {
|
|||||||
}
|
}
|
||||||
let cachedPeerData = peerView.cachedData
|
let cachedPeerData = peerView.cachedData
|
||||||
if let cachedPeerData = cachedPeerData as? CachedUserData {
|
if let cachedPeerData = cachedPeerData as? CachedUserData {
|
||||||
if let photo = cachedPeerData.photo, let video = photo.videoRepresentations.last, let peerReference = PeerReference(peer._asPeer()) {
|
if let photo = cachedPeerData.photo, let video = smallestVideoRepresentation(photo.videoRepresentations), let peerReference = PeerReference(peer._asPeer()) {
|
||||||
let videoId = photo.id?.id ?? peer.id.id._internalGetInt64Value()
|
let videoId = photo.id?.id ?? peer.id.id._internalGetInt64Value()
|
||||||
let videoFileReference = FileMediaReference.avatarList(peer: peerReference, media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: video.resource, previewRepresentations: photo.representations, videoThumbnails: [], immediateThumbnailData: photo.immediateThumbnailData, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: video.dimensions, flags: [])]))
|
let videoFileReference = FileMediaReference.avatarList(peer: peerReference, media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: video.resource, previewRepresentations: photo.representations, videoThumbnails: [], immediateThumbnailData: photo.immediateThumbnailData, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: video.dimensions, flags: [])]))
|
||||||
let videoContent = NativeVideoContent(id: .profileVideo(videoId, nil), fileReference: videoFileReference, streamVideo: isMediaStreamable(resource: video.resource) ? .conservative : .none, loopVideo: true, enableSound: false, fetchAutomatically: true, onlyFullSizeThumbnail: false, useLargeThumbnail: true, autoFetchFullSizeThumbnail: true, startTimestamp: video.startTimestamp, continuePlayingWithoutSoundOnLostAudioSession: false, placeholderColor: .clear, captureProtected: false)
|
let videoContent = NativeVideoContent(id: .profileVideo(videoId, nil), fileReference: videoFileReference, streamVideo: isMediaStreamable(resource: video.resource) ? .conservative : .none, loopVideo: true, enableSound: false, fetchAutomatically: true, onlyFullSizeThumbnail: false, useLargeThumbnail: true, autoFetchFullSizeThumbnail: true, startTimestamp: video.startTimestamp, continuePlayingWithoutSoundOnLostAudioSession: false, placeholderColor: .clear, captureProtected: false)
|
||||||
|
@ -335,7 +335,7 @@ func chatMediaInputGridEntries(view: ItemCollectionsView, savedStickers: Ordered
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let premiumStickers = premiumStickers, !premiumStickers.items.isEmpty {
|
if let premiumStickers = premiumStickers, !premiumStickers.items.isEmpty && hasPremium {
|
||||||
let packInfo = StickerPackCollectionInfo(id: ItemCollectionId(namespace: ChatMediaInputPanelAuxiliaryNamespace.premium.rawValue, id: 0), flags: [], accessHash: 0, title: strings.Stickers_PremiumStickers.uppercased(), shortName: "", thumbnail: nil, immediateThumbnailData: nil, hash: 0, count: 0)
|
let packInfo = StickerPackCollectionInfo(id: ItemCollectionId(namespace: ChatMediaInputPanelAuxiliaryNamespace.premium.rawValue, id: 0), flags: [], accessHash: 0, title: strings.Stickers_PremiumStickers.uppercased(), shortName: "", thumbnail: nil, immediateThumbnailData: nil, hash: 0, count: 0)
|
||||||
for i in 0 ..< premiumStickers.items.count {
|
for i in 0 ..< premiumStickers.items.count {
|
||||||
if let item = premiumStickers.items[i].contents.get(RecentMediaItem.self) {
|
if let item = premiumStickers.items[i].contents.get(RecentMediaItem.self) {
|
||||||
|
@ -1489,6 +1489,8 @@ public final class SharedAccountContextImpl: SharedAccountContext {
|
|||||||
mappedSource = .folders
|
mappedSource = .folders
|
||||||
case .chatsPerFolder:
|
case .chatsPerFolder:
|
||||||
mappedSource = .chatsPerFolder
|
mappedSource = .chatsPerFolder
|
||||||
|
case .appIcons:
|
||||||
|
mappedSource = .appIcons
|
||||||
case .accounts:
|
case .accounts:
|
||||||
mappedSource = .accounts
|
mappedSource = .accounts
|
||||||
case .about:
|
case .about:
|
||||||
|
@ -40,8 +40,9 @@ public final class NativeVideoContent: UniversalVideoContent {
|
|||||||
let placeholderColor: UIColor
|
let placeholderColor: UIColor
|
||||||
let tempFilePath: String?
|
let tempFilePath: String?
|
||||||
let captureProtected: Bool
|
let captureProtected: Bool
|
||||||
|
let hintDimensions: CGSize?
|
||||||
|
|
||||||
public init(id: NativeVideoContentId, fileReference: FileMediaReference, imageReference: ImageMediaReference? = nil, streamVideo: MediaPlayerStreaming = .none, loopVideo: Bool = false, enableSound: Bool = true, baseRate: Double = 1.0, fetchAutomatically: Bool = true, onlyFullSizeThumbnail: Bool = false, useLargeThumbnail: Bool = false, autoFetchFullSizeThumbnail: Bool = false, startTimestamp: Double? = nil, endTimestamp: Double? = nil, continuePlayingWithoutSoundOnLostAudioSession: Bool = false, placeholderColor: UIColor = .white, tempFilePath: String? = nil, captureProtected: Bool = false) {
|
public init(id: NativeVideoContentId, fileReference: FileMediaReference, imageReference: ImageMediaReference? = nil, streamVideo: MediaPlayerStreaming = .none, loopVideo: Bool = false, enableSound: Bool = true, baseRate: Double = 1.0, fetchAutomatically: Bool = true, onlyFullSizeThumbnail: Bool = false, useLargeThumbnail: Bool = false, autoFetchFullSizeThumbnail: Bool = false, startTimestamp: Double? = nil, endTimestamp: Double? = nil, continuePlayingWithoutSoundOnLostAudioSession: Bool = false, placeholderColor: UIColor = .white, tempFilePath: String? = nil, captureProtected: Bool = false, hintDimensions: CGSize? = nil) {
|
||||||
self.id = id
|
self.id = id
|
||||||
self.nativeId = id
|
self.nativeId = id
|
||||||
self.fileReference = fileReference
|
self.fileReference = fileReference
|
||||||
@ -74,10 +75,11 @@ public final class NativeVideoContent: UniversalVideoContent {
|
|||||||
self.placeholderColor = placeholderColor
|
self.placeholderColor = placeholderColor
|
||||||
self.tempFilePath = tempFilePath
|
self.tempFilePath = tempFilePath
|
||||||
self.captureProtected = captureProtected
|
self.captureProtected = captureProtected
|
||||||
|
self.hintDimensions = hintDimensions
|
||||||
}
|
}
|
||||||
|
|
||||||
public func makeContentNode(postbox: Postbox, audioSession: ManagedAudioSession) -> UniversalVideoContentNode & ASDisplayNode {
|
public func makeContentNode(postbox: Postbox, audioSession: ManagedAudioSession) -> UniversalVideoContentNode & ASDisplayNode {
|
||||||
return NativeVideoContentNode(postbox: postbox, audioSessionManager: audioSession, fileReference: self.fileReference, imageReference: self.imageReference, streamVideo: self.streamVideo, loopVideo: self.loopVideo, enableSound: self.enableSound, baseRate: self.baseRate, fetchAutomatically: self.fetchAutomatically, onlyFullSizeThumbnail: self.onlyFullSizeThumbnail, useLargeThumbnail: self.useLargeThumbnail, autoFetchFullSizeThumbnail: self.autoFetchFullSizeThumbnail, startTimestamp: self.startTimestamp, endTimestamp: self.endTimestamp, continuePlayingWithoutSoundOnLostAudioSession: self.continuePlayingWithoutSoundOnLostAudioSession, placeholderColor: self.placeholderColor, tempFilePath: self.tempFilePath, captureProtected: self.captureProtected)
|
return NativeVideoContentNode(postbox: postbox, audioSessionManager: audioSession, fileReference: self.fileReference, imageReference: self.imageReference, streamVideo: self.streamVideo, loopVideo: self.loopVideo, enableSound: self.enableSound, baseRate: self.baseRate, fetchAutomatically: self.fetchAutomatically, onlyFullSizeThumbnail: self.onlyFullSizeThumbnail, useLargeThumbnail: self.useLargeThumbnail, autoFetchFullSizeThumbnail: self.autoFetchFullSizeThumbnail, startTimestamp: self.startTimestamp, endTimestamp: self.endTimestamp, continuePlayingWithoutSoundOnLostAudioSession: self.continuePlayingWithoutSoundOnLostAudioSession, placeholderColor: self.placeholderColor, tempFilePath: self.tempFilePath, captureProtected: self.captureProtected, hintDimensions: self.hintDimensions)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func isEqual(to other: UniversalVideoContent) -> Bool {
|
public func isEqual(to other: UniversalVideoContent) -> Bool {
|
||||||
@ -151,7 +153,7 @@ private final class NativeVideoContentNode: ASDisplayNode, UniversalVideoContent
|
|||||||
|
|
||||||
private var shouldPlay: Bool = false
|
private var shouldPlay: Bool = false
|
||||||
|
|
||||||
init(postbox: Postbox, audioSessionManager: ManagedAudioSession, fileReference: FileMediaReference, imageReference: ImageMediaReference?, streamVideo: MediaPlayerStreaming, loopVideo: Bool, enableSound: Bool, baseRate: Double, fetchAutomatically: Bool, onlyFullSizeThumbnail: Bool, useLargeThumbnail: Bool, autoFetchFullSizeThumbnail: Bool, startTimestamp: Double?, endTimestamp: Double?, continuePlayingWithoutSoundOnLostAudioSession: Bool = false, placeholderColor: UIColor, tempFilePath: String?, captureProtected: Bool) {
|
init(postbox: Postbox, audioSessionManager: ManagedAudioSession, fileReference: FileMediaReference, imageReference: ImageMediaReference?, streamVideo: MediaPlayerStreaming, loopVideo: Bool, enableSound: Bool, baseRate: Double, fetchAutomatically: Bool, onlyFullSizeThumbnail: Bool, useLargeThumbnail: Bool, autoFetchFullSizeThumbnail: Bool, startTimestamp: Double?, endTimestamp: Double?, continuePlayingWithoutSoundOnLostAudioSession: Bool = false, placeholderColor: UIColor, tempFilePath: String?, captureProtected: Bool, hintDimensions: CGSize?) {
|
||||||
self.postbox = postbox
|
self.postbox = postbox
|
||||||
self.fileReference = fileReference
|
self.fileReference = fileReference
|
||||||
self.placeholderColor = placeholderColor
|
self.placeholderColor = placeholderColor
|
||||||
@ -185,6 +187,11 @@ private final class NativeVideoContentNode: ASDisplayNode, UniversalVideoContent
|
|||||||
|
|
||||||
super.init()
|
super.init()
|
||||||
|
|
||||||
|
if let dimensions = hintDimensions {
|
||||||
|
self.dimensions = dimensions
|
||||||
|
self.dimensionsPromise.set(dimensions)
|
||||||
|
}
|
||||||
|
|
||||||
actionAtEndImpl = { [weak self] in
|
actionAtEndImpl = { [weak self] in
|
||||||
self?.performActionAtEnd()
|
self?.performActionAtEnd()
|
||||||
}
|
}
|
||||||
|
@ -802,6 +802,10 @@ public final class WebAppController: ViewController, AttachmentContainable {
|
|||||||
fileprivate func sendBackButtonEvent() {
|
fileprivate func sendBackButtonEvent() {
|
||||||
self.webView?.sendEvent(name: "back_button_pressed", data: nil)
|
self.webView?.sendEvent(name: "back_button_pressed", data: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fileprivate func sendSettingsButtonEvent() {
|
||||||
|
self.webView?.sendEvent(name: "settings_button_pressed", data: nil)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fileprivate var controllerNode: Node {
|
fileprivate var controllerNode: Node {
|
||||||
@ -931,6 +935,21 @@ public final class WebAppController: ViewController, AttachmentContainable {
|
|||||||
let items = context.engine.messages.attachMenuBots()
|
let items = context.engine.messages.attachMenuBots()
|
||||||
|> map { [weak self] attachMenuBots -> ContextController.Items in
|
|> map { [weak self] attachMenuBots -> ContextController.Items in
|
||||||
var items: [ContextMenuItem] = []
|
var items: [ContextMenuItem] = []
|
||||||
|
|
||||||
|
let attachMenuBot = attachMenuBots.first(where: { $0.peer.id == botId})
|
||||||
|
|
||||||
|
if let attachMenuBot = attachMenuBot, attachMenuBot.hasSettings {
|
||||||
|
items.append(.action(ContextMenuActionItem(text: presentationData.strings.WebApp_Settings, icon: { theme in
|
||||||
|
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Settings"), color: theme.contextMenu.primaryColor)
|
||||||
|
}, action: { [weak self] _, f in
|
||||||
|
f(.default)
|
||||||
|
|
||||||
|
if let strongSelf = self {
|
||||||
|
strongSelf.controllerNode.sendSettingsButtonEvent()
|
||||||
|
}
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
|
||||||
if peerId != botId {
|
if peerId != botId {
|
||||||
items.append(.action(ContextMenuActionItem(text: presentationData.strings.WebApp_OpenBot, icon: { theme in
|
items.append(.action(ContextMenuActionItem(text: presentationData.strings.WebApp_OpenBot, icon: { theme in
|
||||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Bots"), color: theme.contextMenu.primaryColor)
|
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Bots"), color: theme.contextMenu.primaryColor)
|
||||||
@ -952,7 +971,7 @@ public final class WebAppController: ViewController, AttachmentContainable {
|
|||||||
self?.controllerNode.webView?.reload()
|
self?.controllerNode.webView?.reload()
|
||||||
})))
|
})))
|
||||||
|
|
||||||
if let _ = attachMenuBots.firstIndex(where: { $0.peer.id == botId}) {
|
if let _ = attachMenuBot, self?.url == nil {
|
||||||
items.append(.action(ContextMenuActionItem(text: presentationData.strings.WebApp_RemoveBot, textColor: .destructive, icon: { theme in
|
items.append(.action(ContextMenuActionItem(text: presentationData.strings.WebApp_RemoveBot, textColor: .destructive, icon: { theme in
|
||||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor)
|
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor)
|
||||||
}, action: { [weak self] _, f in
|
}, action: { [weak self] _, f in
|
||||||
|