diff --git a/Telegram/BUILD b/Telegram/BUILD index ef5301ade1..83f04b0dcf 100644 --- a/Telegram/BUILD +++ b/Telegram/BUILD @@ -298,6 +298,9 @@ alternate_icon_folders = [ "WhiteFilledIcon", "New1", "New2", + "PremiumCosmic", + "PremiumCherry", + "PremiumDuck", ] [ diff --git a/Telegram/Telegram-iOS/PremiumCherry.alticon/PremiumCherry@2x.png b/Telegram/Telegram-iOS/PremiumCherry.alticon/PremiumCherry@2x.png new file mode 100644 index 0000000000..1552a49b63 Binary files /dev/null and b/Telegram/Telegram-iOS/PremiumCherry.alticon/PremiumCherry@2x.png differ diff --git a/Telegram/Telegram-iOS/PremiumCherry.alticon/PremiumCherry@3x.png b/Telegram/Telegram-iOS/PremiumCherry.alticon/PremiumCherry@3x.png new file mode 100644 index 0000000000..78b61d2481 Binary files /dev/null and b/Telegram/Telegram-iOS/PremiumCherry.alticon/PremiumCherry@3x.png differ diff --git a/Telegram/Telegram-iOS/PremiumCosmic.alticon/PremiumCosmic@2x.png b/Telegram/Telegram-iOS/PremiumCosmic.alticon/PremiumCosmic@2x.png new file mode 100644 index 0000000000..e077e61bf4 Binary files /dev/null and b/Telegram/Telegram-iOS/PremiumCosmic.alticon/PremiumCosmic@2x.png differ diff --git a/Telegram/Telegram-iOS/PremiumCosmic.alticon/PremiumCosmic@3x.png b/Telegram/Telegram-iOS/PremiumCosmic.alticon/PremiumCosmic@3x.png new file mode 100644 index 0000000000..41d8434f8b Binary files /dev/null and b/Telegram/Telegram-iOS/PremiumCosmic.alticon/PremiumCosmic@3x.png differ diff --git a/Telegram/Telegram-iOS/PremiumDuck.alticon/PremiumDuck@2x.png b/Telegram/Telegram-iOS/PremiumDuck.alticon/PremiumDuck@2x.png new file mode 100644 index 0000000000..d50c516203 Binary files /dev/null and b/Telegram/Telegram-iOS/PremiumDuck.alticon/PremiumDuck@2x.png differ diff --git a/Telegram/Telegram-iOS/PremiumDuck.alticon/PremiumDuck@3x.png b/Telegram/Telegram-iOS/PremiumDuck.alticon/PremiumDuck@3x.png new file mode 100644 index 0000000000..c24790716f Binary files /dev/null and b/Telegram/Telegram-iOS/PremiumDuck.alticon/PremiumDuck@3x.png differ diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 70d855bea1..d429ee3617 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -7541,6 +7541,8 @@ Sorry for the inconvenience."; "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."; "Chat.MultipleTypingPair" = "%@ and %@"; @@ -7619,6 +7621,12 @@ Sorry for the inconvenience."; "Premium.Avatar" = "Animated Profile Pictures"; "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.AboutTitle" = "ABOUT TELEGRAM PREMIUM"; @@ -7667,3 +7675,5 @@ Sorry for the inconvenience."; "Premium.Limits.FoldersInfo" = "Organize your chats into 20 folders"; "Premium.Limits.ChatsPerFolderInfo" = "Add up to 200 chats into each of your folders"; "Premium.Limits.AccountsInfo" = "Connect 4 accounts with different mobile numbers"; + +"WebApp.Settings" = "Settings"; diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index 5a7c685b25..3d2093292c 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -746,6 +746,7 @@ public enum PremiumIntroSource { case folders case chatsPerFolder case accounts + case appIcons case about case deeplink(String?) case profile(PeerId) diff --git a/submodules/ChatListUI/Sources/ChatListControllerNode.swift b/submodules/ChatListUI/Sources/ChatListControllerNode.swift index f5abfabcf9..162432d9b1 100644 --- a/submodules/ChatListUI/Sources/ChatListControllerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListControllerNode.swift @@ -652,6 +652,7 @@ final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDelegate { @objc private func panGesture(_ recognizer: UIPanGestureRecognizer) { let filtersLimit = self.filtersLimit.flatMap({ $0 + 1 }) ?? Int32(self.availableFilters.count) + let maxFilterIndex = min(Int(filtersLimit), self.availableFilters.count) - 1 switch recognizer.state { case .began: @@ -693,11 +694,11 @@ final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDelegate { 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 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 recognizer.isEnabled = false recognizer.isEnabled = true @@ -744,7 +745,7 @@ final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDelegate { if let directionIsToRight = directionIsToRight { var updatedIndex = selectedIndex if directionIsToRight { - updatedIndex = min(updatedIndex + 1, Int(filtersLimit) - 1) + updatedIndex = min(updatedIndex + 1, maxFilterIndex) } else { updatedIndex = max(updatedIndex - 1, 0) } diff --git a/submodules/ChatListUI/Sources/Node/ChatListItem.swift b/submodules/ChatListUI/Sources/Node/ChatListItem.swift index c4790fbebb..a9ee17e93e 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListItem.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListItem.swift @@ -781,7 +781,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { } let cachedPeerData = peerView.cachedData 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 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) diff --git a/submodules/Components/SheetComponent/Sources/SheetComponent.swift b/submodules/Components/SheetComponent/Sources/SheetComponent.swift index 245e991048..bb79db3b77 100644 --- a/submodules/Components/SheetComponent/Sources/SheetComponent.swift +++ b/submodules/Components/SheetComponent/Sources/SheetComponent.swift @@ -121,8 +121,12 @@ public final class SheetComponent: Component { } } } - + + private var ignoreScrolling: Bool = false public func scrollViewDidScroll(_ scrollView: UIScrollView) { + guard !self.ignoreScrolling else { + return + } let contentOffset = (scrollView.contentOffset.y + scrollView.contentInset.top - scrollView.contentSize.height) * -1.0 if contentOffset >= scrollView.contentSize.height { self.dismiss?(false) @@ -194,11 +198,13 @@ public final class SheetComponent: Component { 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.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) 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.ignoreScrolling = false if environment[SheetComponentEnvironment.self].value.isDisplaying, !self.previousIsDisplaying, let _ = transition.userData(ViewControllerComponentContainer.AnimateInTransition.self) { self.animateIn() diff --git a/submodules/Display/Source/ContainedViewLayoutTransition.swift b/submodules/Display/Source/ContainedViewLayoutTransition.swift index d8eff6b30c..ee118e1014 100644 --- a/submodules/Display/Source/ContainedViewLayoutTransition.swift +++ b/submodules/Display/Source/ContainedViewLayoutTransition.swift @@ -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) { if node.cornerRadius.isEqual(to: cornerRadius) { if let completion = completion { diff --git a/submodules/PeerInfoUI/Sources/ChannelVisibilityController.swift b/submodules/PeerInfoUI/Sources/ChannelVisibilityController.swift index 1e4d87b66c..f0ed748267 100644 --- a/submodules/PeerInfoUI/Sources/ChannelVisibilityController.swift +++ b/submodules/PeerInfoUI/Sources/ChannelVisibilityController.swift @@ -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] = [] if let peer = view.peers[view.peerId] as? TelegramChannel { @@ -730,7 +730,7 @@ private func channelVisibilityControllerEntries(presentationData: PresentationDa if displayAvailability { 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 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 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 - |> 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 (limits, premiumLimits, accountPeer) = data + let isPremium = accountPeer?.isPremium ?? false + var footerItem: ItemListControllerFooterItem? var rightNavigationButton: ItemListNavigationButton? @@ -1630,7 +1644,7 @@ public func channelVisibilityController(context: AccountContext, updatedPresenta 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? if entries.count > 1, let cachedChannelData = view.cachedData as? CachedChannelData, cachedChannelData.peerGeoLocation != nil { diff --git a/submodules/PremiumUI/BUILD b/submodules/PremiumUI/BUILD index 589f0cc29d..956e69ada8 100644 --- a/submodules/PremiumUI/BUILD +++ b/submodules/PremiumUI/BUILD @@ -47,6 +47,7 @@ swift_library( "//submodules/InstantPageCache:InstantPageCache", "//submodules/MediaPlayer:UniversalMediaPlayer", "//submodules/TelegramUniversalVideoContent:TelegramUniversalVideoContent", + "//submodules/RadialStatusNode:RadialStatusNode", ], visibility = [ "//visibility:public", diff --git a/submodules/PremiumUI/Resources/4gb.mp4 b/submodules/PremiumUI/Resources/4gb.mp4 deleted file mode 100644 index 7c880e833f..0000000000 Binary files a/submodules/PremiumUI/Resources/4gb.mp4 and /dev/null differ diff --git a/submodules/PremiumUI/Resources/badge.mp4 b/submodules/PremiumUI/Resources/badge.mp4 deleted file mode 100644 index 86e9b061a7..0000000000 Binary files a/submodules/PremiumUI/Resources/badge.mp4 and /dev/null differ diff --git a/submodules/PremiumUI/Resources/fastdownload.mp4 b/submodules/PremiumUI/Resources/fastdownload.mp4 deleted file mode 100644 index f889234900..0000000000 Binary files a/submodules/PremiumUI/Resources/fastdownload.mp4 and /dev/null differ diff --git a/submodules/PremiumUI/Resources/lightspeed.scn b/submodules/PremiumUI/Resources/lightspeed.scn new file mode 100644 index 0000000000..16d845b8aa Binary files /dev/null and b/submodules/PremiumUI/Resources/lightspeed.scn differ diff --git a/submodules/PremiumUI/Resources/lightstreak.png b/submodules/PremiumUI/Resources/lightstreak.png new file mode 100644 index 0000000000..85a2fd8d4a Binary files /dev/null and b/submodules/PremiumUI/Resources/lightstreak.png differ diff --git a/submodules/PremiumUI/Resources/noads.mp4 b/submodules/PremiumUI/Resources/noads.mp4 deleted file mode 100644 index aaaafdd624..0000000000 Binary files a/submodules/PremiumUI/Resources/noads.mp4 and /dev/null differ diff --git a/submodules/PremiumUI/Resources/premium_unlock.json b/submodules/PremiumUI/Resources/premium_unlock.json index 66733bb8fc..f7334de4a5 100644 --- a/submodules/PremiumUI/Resources/premium_unlock.json +++ b/submodules/PremiumUI/Resources/premium_unlock.json @@ -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":[]} \ No newline at end of file +{"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":[]} \ No newline at end of file diff --git a/submodules/PremiumUI/Resources/voice.mp4 b/submodules/PremiumUI/Resources/voice.mp4 deleted file mode 100644 index eca8757fd2..0000000000 Binary files a/submodules/PremiumUI/Resources/voice.mp4 and /dev/null differ diff --git a/submodules/PremiumUI/Sources/AppIconsDemoComponent.swift b/submodules/PremiumUI/Sources/AppIconsDemoComponent.swift new file mode 100644 index 0000000000..c7daf63b15 --- /dev/null +++ b/submodules/PremiumUI/Sources/AppIconsDemoComponent.swift @@ -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, 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, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, environment: environment, transition: transition) + } +} diff --git a/submodules/PremiumUI/Sources/PageIndicatorComponent.swift b/submodules/PremiumUI/Sources/PageIndicatorComponent.swift index bebb8907c9..ed4af59f87 100644 --- a/submodules/PremiumUI/Sources/PageIndicatorComponent.swift +++ b/submodules/PremiumUI/Sources/PageIndicatorComponent.swift @@ -1,5 +1,6 @@ import Foundation import UIKit +import Display import ComponentFlow public final class PageIndicatorComponent: Component { @@ -336,7 +337,8 @@ private class ItemView: UIView { var dotColor = UIColor.lightGray { didSet { - self.dotView.backgroundColor = dotColor + let transition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .linear) + transition.updateBackgroundColor(layer: self.dotView.layer, color: dotColor) } } diff --git a/submodules/PremiumUI/Sources/PhoneDemoComponent.swift b/submodules/PremiumUI/Sources/PhoneDemoComponent.swift index 0adc14a891..6e062a5d76 100644 --- a/submodules/PremiumUI/Sources/PhoneDemoComponent.swift +++ b/submodules/PremiumUI/Sources/PhoneDemoComponent.swift @@ -1,5 +1,6 @@ import Foundation import UIKit +import SceneKit import Display import AsyncDisplayKit import SwiftSignalKit @@ -7,10 +8,10 @@ import Postbox import TelegramCore import ComponentFlow import AccountContext - -import AppBundle +import RadialStatusNode import UniversalMediaPlayer import TelegramUniversalVideoContent +import AppBundle private let phoneSize = CGSize(width: 262.0, height: 539.0) private var phoneBorderImage = { @@ -42,6 +43,14 @@ private final class PhoneView: UIView { let borderView: UIImageView fileprivate var videoNode: UniversalVideoNode? + private let statusNode: RadialStatusNode + + var playbackStatus: Signal { + return self.playbackStatusPromise.get() + } + private var playbackStatusPromise = ValuePromise(nil) + private var playbackStatusValue: MediaPlayerStatus? + private var statusDisposable = MetaDisposable() var screenRotation: CGFloat = 0.0 { didSet { @@ -50,7 +59,6 @@ private final class PhoneView: UIView { } else { self.overlayView.backgroundColor = .black } - self.contentContainerView.alpha = self.screenRotation > 0.0 ? 1.0 - self.screenRotation : 1.0 self.overlayView.alpha = self.screenRotation > 0.0 ? self.screenRotation * 0.5 : self.screenRotation * -1.0 } } @@ -67,36 +75,66 @@ private final class PhoneView: UIView { 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) self.addSubview(self.contentContainerView) + self.contentContainerView.addSubview(self.statusNode.view) self.contentContainerView.addSubview(self.overlayView) self.addSubview(self.borderView) } + deinit { + self.statusDisposable.dispose() + } 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 - 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 } self.contentContainerView.backgroundColor = .clear - - let dimensions = PixelDimensions(width: 1170, height: 1754) - - let id = Int64.random(in: 0.. mapToSignal { status -> Signal 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) videoNode.pause() @@ -104,6 +142,26 @@ private final class PhoneView: UIView { 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 func play() { if let videoNode = self.videoNode, !self.isPlaying { @@ -133,14 +191,104 @@ private final class PhoneView: UIView { self.overlayView.frame = self.contentContainerView.bounds 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.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 { typealias EnvironmentType = DemoPageEnvironment @@ -151,16 +299,19 @@ final class PhoneDemoComponent: Component { let context: AccountContext let position: Position - let videoName: String? + let videoFile: TelegramMediaFile? + let hasStars: Bool public init( context: AccountContext, position: PhoneDemoComponent.Position, - videoName: String? + videoFile: TelegramMediaFile?, + hasStars: Bool = false ) { self.context = context self.position = position - self.videoName = videoName + self.videoFile = videoFile + self.hasStars = hasStars } public static func ==(lhs: PhoneDemoComponent, rhs: PhoneDemoComponent) -> Bool { @@ -170,7 +321,10 @@ final class PhoneDemoComponent: Component { if lhs.position != rhs.position { return false } - if lhs.videoName != rhs.videoName { + if lhs.videoFile != rhs.videoFile { + return false + } + if lhs.hasStars != rhs.hasStars { return false } return true @@ -190,9 +344,13 @@ final class PhoneDemoComponent: Component { private var isCentral = false private var component: PhoneDemoComponent? + private let starsContainerView: UIView private let containerView: UIView + private var starsView: StarsView? private let phoneView: PhoneView + private var starsDisposable: Disposable? + public var ready: Signal { if let videoNode = self.phoneView.videoNode { return videoNode.ready @@ -205,12 +363,17 @@ final class PhoneDemoComponent: Component { } public override init(frame: CGRect) { + self.starsContainerView = UIView(frame: frame) + self.starsContainerView.clipsToBounds = true + self.containerView = UIView(frame: frame) self.containerView.clipsToBounds = true + self.phoneView = PhoneView(frame: CGRect(origin: .zero, size: phoneSize)) super.init(frame: frame) + self.addSubview(self.starsContainerView) self.addSubview(self.containerView) self.containerView.addSubview(self.phoneView) } @@ -219,15 +382,41 @@ final class PhoneDemoComponent: Component { fatalError("init(coder:) has not been implemented") } + deinit { + self.starsDisposable?.dispose() + } + public func update(component: PhoneDemoComponent, availableSize: CGSize, environment: Environment, transition: Transition) -> CGSize { self.component = component - - self.phoneView.setup(context: component.context, videoName: component.videoName, position: component.position) - self.containerView.frame = CGRect(origin: .zero, size: availableSize) + self.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) - + + 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 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 } - let isVisible = environment[DemoPageEnvironment.self].isDisplaying let isCentral = environment[DemoPageEnvironment.self].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.screenRotation = mappedPosition * -0.7 @@ -258,6 +448,7 @@ final class PhoneDemoComponent: Component { self.phoneView.play() } else if !isVisible { self.phoneView.reset() + self.starsView?.stopAnimation() } if let _ = transition.userData(DemoAnimateInTransition.self), abs(mappedPosition) < .ulpOfOne { diff --git a/submodules/PremiumUI/Sources/PremiumDemoScreen.swift b/submodules/PremiumUI/Sources/PremiumDemoScreen.swift index 613d2b8c00..a0b0fae0ec 100644 --- a/submodules/PremiumUI/Sources/PremiumDemoScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumDemoScreen.swift @@ -393,7 +393,13 @@ private final class DemoPagerComponent: Component { itemTransition = transition.withAnimation(.none) itemView = ComponentHostView() 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) @@ -464,11 +470,18 @@ private final class DemoSheetContent: CombinedComponent { let action: () -> 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.subject = subject 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.dismiss = dismiss } @@ -496,10 +509,14 @@ private final class DemoSheetContent: CombinedComponent { var isPremium: Bool? var reactions: [AvailableReactions.Reaction]? var stickers: [TelegramMediaFile]? + var appIcons: [PresentationAppIcon]? var disposable: Disposable? + var promoConfiguration: PremiumPromoConfiguration? + init(context: AccountContext) { self.context = context + self.appIcons = context.sharedContext.applicationBindings.getAvailableAlternateIcons().filter { $0.isPremium } super.init() @@ -519,9 +536,12 @@ private final class DemoSheetContent: CombinedComponent { return items != nil } |> 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 { var result: [TelegramMediaFile] = [] 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 { - 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 { return } strongSelf.reactions = reactions strongSelf.stickers = stickers strongSelf.isPremium = isPremium + strongSelf.promoConfiguration = promoConfiguration if !reactions.isEmpty && !stickers.isEmpty { strongSelf.updated(transition: Transition(.immediate).withUserData(DemoAnimateInTransition())) } @@ -600,7 +621,7 @@ private final class DemoSheetContent: CombinedComponent { 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 var availableItems: [PremiumPerk: DemoPagerComponent.Item] = [:] @@ -613,7 +634,7 @@ private final class DemoSheetContent: CombinedComponent { content: AnyComponent(PhoneDemoComponent( context: component.context, position: .bottom, - videoName: "4gb" + videoFile: configuration.videos["double_limits"] )), title: strings.Premium_UploadSize, text: strings.Premium_UploadSizeInfo, @@ -630,7 +651,8 @@ private final class DemoSheetContent: CombinedComponent { content: AnyComponent(PhoneDemoComponent( context: component.context, position: .top, - videoName: "fastdownload" + videoFile: configuration.videos["faster_download"], + hasStars: true )), title: strings.Premium_FasterSpeed, text: strings.Premium_FasterSpeedInfo, @@ -647,7 +669,7 @@ private final class DemoSheetContent: CombinedComponent { content: AnyComponent(PhoneDemoComponent( context: component.context, position: .top, - videoName: "voice" + videoFile: configuration.videos["voice_to_text"] )), title: strings.Premium_VoiceToText, text: strings.Premium_VoiceToTextInfo, @@ -664,7 +686,7 @@ private final class DemoSheetContent: CombinedComponent { content: AnyComponent(PhoneDemoComponent( context: component.context, position: .bottom, - videoName: "noads" + videoFile: configuration.videos["no_ads"] )), title: strings.Premium_NoAds, text: strings.Premium_NoAdsInfo, @@ -718,7 +740,7 @@ private final class DemoSheetContent: CombinedComponent { content: AnyComponent(PhoneDemoComponent( context: component.context, position: .top, - videoName: "fastdownload" + videoFile: configuration.videos["chat_management"] )), title: strings.Premium_ChatManagement, text: strings.Premium_ChatManagementInfo, @@ -735,7 +757,7 @@ private final class DemoSheetContent: CombinedComponent { content: AnyComponent(PhoneDemoComponent( context: component.context, position: .top, - videoName: "badge" + videoFile: configuration.videos["profile_badge"] )), title: strings.Premium_Badge, text: strings.Premium_BadgeInfo, @@ -752,7 +774,7 @@ private final class DemoSheetContent: CombinedComponent { content: AnyComponent(PhoneDemoComponent( context: component.context, position: .top, - videoName: "badge" + videoFile: configuration.videos["userpics"] )), title: strings.Premium_Avatar, 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] } let index: Int @@ -827,7 +865,16 @@ private final class DemoSheetContent: CombinedComponent { case let .intro(price): buttonText = strings.Premium_SubscribeFor(price ?? "–").string 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, animationName: isStandalone && component.subject == .uniqueReactions ? "premium_unlock" : nil, iconPosition: .right, - iconSpacing: 6.0, + iconSpacing: 4.0, action: { [weak component] in guard let component = component else { return @@ -989,6 +1036,7 @@ public class PremiumDemoScreen: ViewControllerComponentContainer { case advancedChatManagement case profileBadge case animatedUserpics + case appIcons } public enum Source: Equatable { diff --git a/submodules/PremiumUI/Sources/PremiumIntroScreen.swift b/submodules/PremiumUI/Sources/PremiumIntroScreen.swift index 81bd4a84d4..215055f186 100644 --- a/submodules/PremiumUI/Sources/PremiumIntroScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumIntroScreen.swift @@ -19,6 +19,7 @@ import InAppPurchaseManager import ConfettiEffect import TextFormat import InstantPageCache +import UniversalMediaPlayer public enum PremiumSource: Equatable { case settings @@ -35,6 +36,7 @@ public enum PremiumSource: Equatable { case chatsPerFolder case accounts case about + case appIcons case deeplink(String?) case profile(PeerId) @@ -50,6 +52,8 @@ public enum PremiumSource: Equatable { return "no_ads" case .upload: return "more_upload" + case .appIcons: + return "app_icons" case .groupsAndChannels: return "double_limits__channels" case .pinnedChats: @@ -91,6 +95,7 @@ enum PremiumPerk: CaseIterable { case advancedChatManagement case profileBadge case animatedUserpics + case appIcons static var allCases: [PremiumPerk] { return [ @@ -103,7 +108,8 @@ enum PremiumPerk: CaseIterable { .premiumStickers, .advancedChatManagement, .profileBadge, - .animatedUserpics + .animatedUserpics, + .appIcons ] } @@ -139,6 +145,8 @@ enum PremiumPerk: CaseIterable { return "profile_badge" case .animatedUserpics: return "animated_userpics" + case .appIcons: + return "app_icon" } } @@ -164,6 +172,8 @@ enum PremiumPerk: CaseIterable { return strings.Premium_Badge case .animatedUserpics: return strings.Premium_Avatar + case .appIcons: + return strings.Premium_AppIcon } } @@ -189,6 +199,8 @@ enum PremiumPerk: CaseIterable { return strings.Premium_BadgeInfo case .animatedUserpics: return strings.Premium_AvatarInfo + case .appIcons: + return strings.Premium_AppIconInfo } } @@ -214,6 +226,8 @@ enum PremiumPerk: CaseIterable { return "Premium/Perk/Badge" case .animatedUserpics: return "Premium/Perk/Avatar" + case .appIcons: + return "Premium/Perk/AppIcon" } } } @@ -230,7 +244,8 @@ private struct PremiumIntroConfiguration { .premiumStickers, .advancedChatManagement, .profileBadge, - .animatedUserpics + .animatedUserpics, + .appIcons ]) } @@ -259,6 +274,9 @@ private struct PremiumIntroConfiguration { if perks.count < 4 { perks = PremiumIntroConfiguration.defaultValue.perks } + if !perks.contains(.appIcons) { + perks.append(.appIcons) + } return PremiumIntroConfiguration(perks: perks) } else { return .defaultValue @@ -778,21 +796,23 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { private var disposable: Disposable? private(set) var configuration = PremiumIntroConfiguration.defaultValue + private(set) var promoConfiguration: PremiumPromoConfiguration? + + private var preloadDisposableSet = DisposableSet() init(context: AccountContext, source: PremiumSource) { self.context = context super.init() - self.disposable = (context.account.postbox.preferencesView(keys: [PreferencesKeys.appConfiguration]) - |> map { view -> AppConfiguration in - let appConfiguration: AppConfiguration = view.values[PreferencesKeys.appConfiguration]?.get(AppConfiguration.self) ?? AppConfiguration.defaultValue - return appConfiguration - } - |> take(1) - |> deliverOnMainQueue).start(next: { [weak self] appConfiguration in + self.disposable = (context.engine.data.get( + TelegramEngine.EngineData.Item.Configuration.App(), + TelegramEngine.EngineData.Item.Configuration.PremiumPromo() + ) + |> deliverOnMainQueue).start(next: { [weak self] appConfiguration, promoConfiguration in if let strongSelf = self { strongSelf.configuration = PremiumIntroConfiguration.with(appConfiguration: appConfiguration) + strongSelf.promoConfiguration = promoConfiguration strongSelf.updated(transition: .immediate) var jsonString: String = "{" @@ -809,16 +829,20 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { } jsonString += "]}}" - 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) } + + for (_, video) in promoConfiguration.videos { + strongSelf.preloadDisposableSet.add(preloadVideoResource(postbox: context.account.postbox, resourceReference: .standalone(resource: video.resource), duration: 3.0).start()) + } } }) } deinit { 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: 0x7B88FF), UIColor(rgb: 0x7091FF)), + (UIColor(rgb: 0x609DFF), UIColor(rgb: 0x56A5FF)), + (UIColor(rgb: 0x609DFF), UIColor(rgb: 0x56A5FF)) ] @@ -986,6 +1012,8 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { demoSubject = .profileBadge case .animatedUserpics: demoSubject = .animatedUserpics + case .appIcons: + demoSubject = .appIcons } var dismissImpl: (() -> Void)? @@ -1091,17 +1119,33 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { size.height += 6.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 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) }) - + + 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( component: MultilineTextComponent( - text: .markdown( - text: context.component.isPremium == true ? strings.Premium_ChargeInfo("$4.99", "–").string : strings.Premium_Terms, - attributes: termsMarkdownAttributes - ), + text: termsString, horizontalAlignment: .natural, maximumNumberOfLines: 0, lineSpacing: 0.0, @@ -1448,7 +1492,8 @@ private final class PremiumIntroScreenComponent: CombinedComponent { present: context.component.present, buy: { [weak state] in state?.buy() - }, updateIsFocused: { [weak state] isFocused in + }, + updateIsFocused: { [weak state] isFocused in state?.updateIsFocused(isFocused) } )), diff --git a/submodules/PremiumUI/Sources/PremiumLimitsListScreen.swift b/submodules/PremiumUI/Sources/PremiumLimitsListScreen.swift index 073e49b464..0748e9b586 100644 --- a/submodules/PremiumUI/Sources/PremiumLimitsListScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumLimitsListScreen.swift @@ -135,9 +135,14 @@ private final class LimitComponent: CombinedComponent { let textFont = Font.regular(13.0) let boldTextFont = Font.semibold(13.0) 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 - return nil - }) + let markdownAttributes = MarkdownAttributes( + 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( component: MultilineTextComponent( diff --git a/submodules/RadialStatusNode/Sources/RadialStatusBackgroundNode.swift b/submodules/RadialStatusNode/Sources/RadialStatusBackgroundNode.swift deleted file mode 100644 index 8dad789975..0000000000 --- a/submodules/RadialStatusNode/Sources/RadialStatusBackgroundNode.swift +++ /dev/null @@ -1,6 +0,0 @@ -import Foundation -import UIKit -import AsyncDisplayKit -import Display - - diff --git a/submodules/SettingsUI/Sources/Themes/ThemeSettingsAppIconItem.swift b/submodules/SettingsUI/Sources/Themes/ThemeSettingsAppIconItem.swift index c51b962057..2ccdd27dee 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemeSettingsAppIconItem.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemeSettingsAppIconItem.swift @@ -45,14 +45,16 @@ class ThemeSettingsAppIconItem: ListViewItem, ItemListItem { let theme: PresentationTheme let strings: PresentationStrings let icons: [PresentationAppIcon] + let isPremium: Bool let currentIconName: String? - let updated: (String) -> Void + let updated: (PresentationAppIcon) -> Void 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.strings = strings self.icons = icons + self.isPremium = isPremium self.currentIconName = currentIconName self.updated = updated self.tag = tag @@ -96,6 +98,7 @@ class ThemeSettingsAppIconItem: ListViewItem, ItemListItem { private final class ThemeSettingsAppIconNode : ASDisplayNode { private let iconNode: ASImageNode private let overlayNode: ASImageNode + private let lockNode: ASImageNode private let textNode: ASTextNode 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.isLayerBacked = true + self.lockNode = ASImageNode() + self.lockNode.displaysAsynchronously = false + self.lockNode.isUserInteractionEnabled = false + self.textNode = ASTextNode() self.textNode.isUserInteractionEnabled = false - self.textNode.displaysAsynchronously = true + self.textNode.displaysAsynchronously = false super.init() self.addSubnode(self.iconNode) self.addSubnode(self.overlayNode) 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.textNode.attributedText = title 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 = { 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.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 case "New2": name = item.strings.Appearance_AppIconNew2 + case "PremiumCosmic": + name = "Cosmic" + case "PremiumCherry": + name = "Cherry" + case "PremiumDuck": + name = "Duck" 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 - item.updated(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), locked: !item.isPremium && icon.isPremium, color: item.theme.list.itemPrimaryTextColor, bordered: bordered, selected: selected, action: { [weak self, weak imageNode] in + item.updated(icon) if let imageNode = imageNode { self?.scrollToNode(imageNode, animated: true) } diff --git a/submodules/SettingsUI/Sources/Themes/ThemeSettingsController.swift b/submodules/SettingsUI/Sources/Themes/ThemeSettingsController.swift index 557d42377a..ee06a4a8e9 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemeSettingsController.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemeSettingsController.swift @@ -17,6 +17,7 @@ import ShareController import AccountContext import ContextUI import UndoUI +import PremiumUI func themeDisplayName(strings: PresentationStrings, reference: PresentationThemeReference) -> String { let name: String @@ -57,12 +58,12 @@ private final class ThemeSettingsControllerArguments { let openBubbleSettings: () -> Void let toggleLargeEmoji: (Bool) -> Void let disableAnimations: (Bool) -> Void - let selectAppIcon: (String) -> Void + let selectAppIcon: (PresentationAppIcon) -> Void let editTheme: (PresentationCloudTheme) -> Void let themeContextAction: (Bool, PresentationThemeReference, 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.selectTheme = selectTheme self.openThemeSettings = openThemeSettings @@ -119,7 +120,7 @@ private enum ThemeSettingsControllerEntry: ItemListNodeEntry { case textSize(PresentationTheme, String, String) case bubbleSettings(PresentationTheme, String, String) case iconHeader(PresentationTheme, String) - case iconItem(PresentationTheme, PresentationStrings, [PresentationAppIcon], String?) + case iconItem(PresentationTheme, PresentationStrings, [PresentationAppIcon], Bool, String?) case otherHeader(PresentationTheme, String) case largeEmoji(PresentationTheme, String, Bool) case animations(PresentationTheme, String, Bool) @@ -237,8 +238,8 @@ private enum ThemeSettingsControllerEntry: ItemListNodeEntry { } else { return false } - case let .iconItem(lhsTheme, lhsStrings, lhsIcons, lhsValue): - if case let .iconItem(rhsTheme, rhsStrings, rhsIcons, rhsValue) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsIcons == rhsIcons, lhsValue == rhsValue { + case let .iconItem(lhsTheme, lhsStrings, lhsIcons, lhsIsPremium, lhsValue): + if case let .iconItem(rhsTheme, rhsStrings, rhsIcons, rhsIsPremium, rhsValue) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsIcons == rhsIcons, lhsIsPremium == rhsIsPremium, lhsValue == rhsValue { return true } else { return false @@ -313,9 +314,9 @@ private enum ThemeSettingsControllerEntry: ItemListNodeEntry { return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) case let .iconHeader(_, text): return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) - case let .iconItem(theme, strings, icons, value): - return ThemeSettingsAppIconItem(theme: theme, strings: strings, sectionId: self.section, icons: icons, currentIconName: value, updated: { iconName in - arguments.selectAppIcon(iconName) + case let .iconItem(theme, strings, icons, isPremium, value): + return ThemeSettingsAppIconItem(theme: theme, strings: strings, sectionId: self.section, icons: icons, isPremium: isPremium, currentIconName: value, updated: { icon in + arguments.selectAppIcon(icon) }) case let .otherHeader(_, text): 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] = [] let strings = presentationData.strings @@ -378,7 +379,7 @@ private func themeSettingsControllerEntries(presentationData: PresentationData, if !availableAppIcons.isEmpty { 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())) @@ -494,9 +495,25 @@ public func themeSettingsController(context: AccountContext, focusOnItemTag: The let _ = updatePresentationThemeSettingsInteractively(accountManager: context.sharedContext.accountManager, { current in return current.withUpdatedReduceMotion(reduceMotion) }).start() - }, selectAppIcon: { name in - currentAppIconName.set(name) - context.sharedContext.applicationBindings.requestSetAlternateIconName(name, { _ in + }, selectAppIcon: { icon in + let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) + |> 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 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) - |> map { presentationData, sharedData, cloudThemes, availableAppIcons, currentAppIconName, removedThemeIndexes, animatedEmojiStickers -> (ItemListControllerState, (ItemListNodeState, Any)) in - let settings = sharedData.entries[ApplicationSpecificSharedDataKeys.presentationThemeSettings]?.get(PresentationThemeSettings.self) ?? PresentationThemeSettings.defaultSettings + 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, peerView -> (ItemListControllerState, (ItemListNodeState, Any)) in + let settings = sharedData.entries[ApplicationSpecificSharedDataKeys.presentationThemeSettings]?.get(PresentationThemeSettings.self) ?? PresentationThemeSettings.defaultSettings + let isPremium = peerView.peers[peerView.peerId]?.isPremium ?? false + let themeReference: PresentationThemeReference if presentationData.autoNightModeTriggered { if let _ = settings.theme.emoticon { @@ -961,7 +980,7 @@ public func themeSettingsController(context: AccountContext, focusOnItemTag: The 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 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)) } diff --git a/submodules/StickerPackPreviewUI/Sources/StickerPackScreen.swift b/submodules/StickerPackPreviewUI/Sources/StickerPackScreen.swift index 6d0f4f662e..afbc4aeef8 100644 --- a/submodules/StickerPackPreviewUI/Sources/StickerPackScreen.swift +++ b/submodules/StickerPackPreviewUI/Sources/StickerPackScreen.swift @@ -157,6 +157,7 @@ private final class StickerPackContainer: ASDisplayNode { self.buttonNode = HighlightableButtonNode() self.titleNode = ImmediateTextNode() + self.titleNode.textAlignment = .center self.titleNode.maximumNumberOfLines = 2 self.titleNode.highlightAttributeAction = { attributes in if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerTextMention)] { diff --git a/submodules/StickerPackPreviewUI/Sources/StickerPreviewPeekContent.swift b/submodules/StickerPackPreviewUI/Sources/StickerPreviewPeekContent.swift index 728912f9b7..24f13d50ec 100644 --- a/submodules/StickerPackPreviewUI/Sources/StickerPreviewPeekContent.swift +++ b/submodules/StickerPackPreviewUI/Sources/StickerPreviewPeekContent.swift @@ -247,7 +247,7 @@ final class PremiumStickerPackAccessoryNode: SparseNode, PeekControllerAccessory UIColor(rgb: 0xe46ace) ], foregroundColor: .white), height: 50.0, cornerRadius: 11.0, gloss: true) self.proceedButton.iconPosition = .right - self.proceedButton.iconSpacing = 6.0 + self.proceedButton.iconSpacing = 4.0 self.proceedButton.animation = "premium_unlock" self.cancelButton = HighlightableButtonNode() diff --git a/submodules/TelegramCore/Sources/State/ManagedPremiumPromoConfigurationUpdates.swift b/submodules/TelegramCore/Sources/State/ManagedPremiumPromoConfigurationUpdates.swift index b328b464b8..910c11c1ec 100644 --- a/submodules/TelegramCore/Sources/State/ManagedPremiumPromoConfigurationUpdates.swift +++ b/submodules/TelegramCore/Sources/State/ManagedPremiumPromoConfigurationUpdates.swift @@ -4,6 +4,9 @@ import SwiftSignalKit import TelegramApi import MtProtoKit +public func updatePremiumPromoConfigurationOnce(account: Account) -> Signal { + return updatePremiumPromoConfigurationOnce(postbox: account.postbox, network: account.network) +} func updatePremiumPromoConfigurationOnce(postbox: Postbox, network: Network) -> Signal { return network.request(Api.functions.help.getPremiumPromo()) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/AttachMenuBots.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/AttachMenuBots.swift index ee62b054b8..cfd6ff6e33 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/AttachMenuBots.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/AttachMenuBots.swift @@ -10,6 +10,7 @@ public final class AttachMenuBots: Equatable, Codable { case name case botIcons case peerTypes + case hasSettings } public enum IconName: Int32, Codable { @@ -93,17 +94,20 @@ public final class AttachMenuBots: Equatable, Codable { public let name: String public let icons: [IconName: TelegramMediaFile] public let peerTypes: PeerFlags + public let hasSettings: Bool public init( peerId: PeerId, name: String, icons: [IconName: TelegramMediaFile], - peerTypes: PeerFlags + peerTypes: PeerFlags, + hasSettings: Bool ) { self.peerId = peerId self.name = name self.icons = icons self.peerTypes = peerTypes + self.hasSettings = hasSettings } public static func ==(lhs: Bot, rhs: Bot) -> Bool { @@ -119,6 +123,9 @@ public final class AttachMenuBots: Equatable, Codable { if lhs.peerTypes != rhs.peerTypes { return false } + if lhs.hasSettings != rhs.hasSettings { + return false + } 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) self.peerTypes = PeerFlags(rawValue: UInt32(value)) + + self.hasSettings = try container.decodeIfPresent(Bool.self, forKey: .hasSettings) ?? false } 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(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] = [] for bot in bots { switch bot { - case let .attachMenuBot(_, botId, name, apiPeerTypes, botIcons): + case let .attachMenuBot(flags, botId, name, apiPeerTypes, botIcons): var icons: [AttachMenuBots.Bot.IconName: TelegramMediaFile] = [:] for icon in botIcons { switch icon { @@ -291,7 +302,7 @@ func managedSynchronizeAttachMenuBots(postbox: Postbox, network: Network, force: 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 icons: [AttachMenuBots.Bot.IconName: TelegramMediaFile] 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.shortName = shortName self.icons = icons self.peerTypes = peerTypes + self.hasSettings = hasSettings } } @@ -412,7 +425,7 @@ func _internal_attachMenuBots(postbox: Postbox) -> Signal<[AttachMenuBot], NoErr var resultBots: [AttachMenuBot] = [] for bot in cachedBots { 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 @@ -427,7 +440,7 @@ public func _internal_getAttachMenuBot(postbox: Postbox, network: Network, botId return postbox.transaction { transaction -> Signal in if cached, let cachedBots = cachedAttachMenuBots(transaction: transaction)?.bots { 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 { - case let .attachMenuBot(_, _, name, apiPeerTypes, botIcons): + case let .attachMenuBot(flags, _, name, apiPeerTypes, botIcons): var icons: [AttachMenuBots.Bot.IconName: TelegramMediaFile] = [:] for icon in botIcons { switch icon { @@ -486,7 +499,7 @@ public func _internal_getAttachMenuBot(postbox: Postbox, network: Network, botId 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)) } } } diff --git a/submodules/TelegramCore/Sources/Utils/ImageRepresentationsUtils.swift b/submodules/TelegramCore/Sources/Utils/ImageRepresentationsUtils.swift index da457a4f47..6b71965fb9 100644 --- a/submodules/TelegramCore/Sources/Utils/ImageRepresentationsUtils.swift +++ b/submodules/TelegramCore/Sources/Utils/ImageRepresentationsUtils.swift @@ -2,6 +2,25 @@ import Postbox import TelegramApi 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? { if representations.count == 0 { return nil diff --git a/submodules/TelegramPresentationData/Sources/PresentationData.swift b/submodules/TelegramPresentationData/Sources/PresentationData.swift index 12f73d381a..8ee1dda8b7 100644 --- a/submodules/TelegramPresentationData/Sources/PresentationData.swift +++ b/submodules/TelegramPresentationData/Sources/PresentationData.swift @@ -45,11 +45,13 @@ public struct PresentationAppIcon: Equatable { public let name: String public let imageName: String 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.imageName = imageName self.isDefault = isDefault + self.isPremium = isPremium } } diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Perk/AppIcon.imageset/AppIcon.pdf b/submodules/TelegramUI/Images.xcassets/Premium/Perk/AppIcon.imageset/AppIcon.pdf new file mode 100644 index 0000000000..f44283f77f --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/Perk/AppIcon.imageset/AppIcon.pdf @@ -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 \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Perk/AppIcon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/Perk/AppIcon.imageset/Contents.json new file mode 100644 index 0000000000..90677f49b1 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/Perk/AppIcon.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "AppIcon.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Sources/AppDelegate.swift b/submodules/TelegramUI/Sources/AppDelegate.swift index 3e68f6a460..1e166dcfbf 100644 --- a/submodules/TelegramUI/Sources/AppDelegate.swift +++ b/submodules/TelegramUI/Sources/AppDelegate.swift @@ -679,6 +679,11 @@ private func extractAccountManagerState(records: AccountRecordsView 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 { @@ -151,7 +153,7 @@ private final class NativeVideoContentNode: ASDisplayNode, UniversalVideoContent 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.fileReference = fileReference self.placeholderColor = placeholderColor @@ -185,6 +187,11 @@ private final class NativeVideoContentNode: ASDisplayNode, UniversalVideoContent super.init() + if let dimensions = hintDimensions { + self.dimensions = dimensions + self.dimensionsPromise.set(dimensions) + } + actionAtEndImpl = { [weak self] in self?.performActionAtEnd() } diff --git a/submodules/WebUI/Sources/WebAppController.swift b/submodules/WebUI/Sources/WebAppController.swift index 6a312e9077..fcaeb0c054 100644 --- a/submodules/WebUI/Sources/WebAppController.swift +++ b/submodules/WebUI/Sources/WebAppController.swift @@ -802,6 +802,10 @@ public final class WebAppController: ViewController, AttachmentContainable { fileprivate func sendBackButtonEvent() { 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 { @@ -931,6 +935,21 @@ public final class WebAppController: ViewController, AttachmentContainable { let items = context.engine.messages.attachMenuBots() |> map { [weak self] attachMenuBots -> ContextController.Items in 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 { 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) @@ -952,7 +971,7 @@ public final class WebAppController: ViewController, AttachmentContainable { 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 return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor) }, action: { [weak self] _, f in