Various improvements

This commit is contained in:
Ilya Laktyushin 2022-06-01 11:09:20 +04:00
parent 6827584cd8
commit f9c0eeba55
46 changed files with 884 additions and 119 deletions

View File

@ -298,6 +298,9 @@ alternate_icon_folders = [
"WhiteFilledIcon", "WhiteFilledIcon",
"New1", "New1",
"New2", "New2",
"PremiumCosmic",
"PremiumCherry",
"PremiumDuck",
] ]
[ [

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

View File

@ -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";

View File

@ -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)

View File

@ -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)
} }

View File

@ -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)

View File

@ -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()

View File

@ -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 {

View File

@ -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 {

View File

@ -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",

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -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":[]}

View 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)
}
}

View File

@ -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)
} }
} }

View File

@ -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 {

View File

@ -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 {

View File

@ -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)
} }
)), )),

View File

@ -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(

View File

@ -1,6 +0,0 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display

View File

@ -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)
} }

View File

@ -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))
} }

View File

@ -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)] {

View File

@ -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()

View File

@ -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())

View File

@ -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))
} }
} }
} }

View File

@ -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

View File

@ -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
} }
} }

View 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

View File

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

View File

@ -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 []

View File

@ -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)

View File

@ -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) {

View File

@ -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:

View File

@ -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()
} }

View File

@ -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