Various improvements
@ -298,6 +298,9 @@ alternate_icon_folders = [
|
||||
"WhiteFilledIcon",
|
||||
"New1",
|
||||
"New2",
|
||||
"PremiumCosmic",
|
||||
"PremiumCherry",
|
||||
"PremiumDuck",
|
||||
]
|
||||
|
||||
[
|
||||
|
BIN
Telegram/Telegram-iOS/PremiumCherry.alticon/PremiumCherry@2x.png
Normal file
After Width: | Height: | Size: 20 KiB |
BIN
Telegram/Telegram-iOS/PremiumCherry.alticon/PremiumCherry@3x.png
Normal file
After Width: | Height: | Size: 34 KiB |
BIN
Telegram/Telegram-iOS/PremiumCosmic.alticon/PremiumCosmic@2x.png
Normal file
After Width: | Height: | Size: 24 KiB |
BIN
Telegram/Telegram-iOS/PremiumCosmic.alticon/PremiumCosmic@3x.png
Normal file
After Width: | Height: | Size: 48 KiB |
BIN
Telegram/Telegram-iOS/PremiumDuck.alticon/PremiumDuck@2x.png
Normal file
After Width: | Height: | Size: 20 KiB |
BIN
Telegram/Telegram-iOS/PremiumDuck.alticon/PremiumDuck@3x.png
Normal file
After Width: | Height: | Size: 33 KiB |
@ -7541,6 +7541,8 @@ Sorry for the inconvenience.";
|
||||
|
||||
"Premium.Reactions.Proceed" = "Unlock Premium Reactions";
|
||||
|
||||
"Premium.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";
|
||||
|
@ -746,6 +746,7 @@ public enum PremiumIntroSource {
|
||||
case folders
|
||||
case chatsPerFolder
|
||||
case accounts
|
||||
case appIcons
|
||||
case about
|
||||
case deeplink(String?)
|
||||
case profile(PeerId)
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -121,8 +121,12 @@ public final class SheetComponent<ChildEnvironmentType: Equatable>: 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<ChildEnvironmentType: Equatable>: 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()
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -47,6 +47,7 @@ swift_library(
|
||||
"//submodules/InstantPageCache:InstantPageCache",
|
||||
"//submodules/MediaPlayer:UniversalMediaPlayer",
|
||||
"//submodules/TelegramUniversalVideoContent:TelegramUniversalVideoContent",
|
||||
"//submodules/RadialStatusNode:RadialStatusNode",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
BIN
submodules/PremiumUI/Resources/lightspeed.scn
Normal file
BIN
submodules/PremiumUI/Resources/lightstreak.png
Normal file
After Width: | Height: | Size: 1.7 KiB |
@ -1 +1 @@
|
||||
{"v":"5.8.1","fr":60,"ip":0,"op":120,"w":30,"h":30,"nm":"unlock","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"lock2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[0]},{"i":{"x":[0.667],"y":[0.805]},"o":{"x":[0.333],"y":[0]},"t":10,"s":[15]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[-0.195]},"t":20,"s":[7.407]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":30,"s":[15]},{"t":40,"s":[0]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[19,14,0],"to":[0,-0.139,0],"ti":[0,0,0]},{"i":{"x":0.903,"y":0},"o":{"x":0.333,"y":0},"t":10,"s":[19,13.167,0],"to":[0,0,0],"ti":[0,-0.124,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.152,"y":1},"t":20,"s":[19,13.383,0],"to":[0,0.216,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":30,"s":[19,13.167,0],"to":[0,0,0],"ti":[0,-0.088,0]},{"t":40,"s":[19,14,0]}],"ix":2,"l":2},"a":{"a":0,"k":[24,0,0],"ix":1,"l":2},"s":{"a":0,"k":[16.667,16.667,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-4,-1.5],[-4,-8],[4,-8],[4,8]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"rd","nm":"Round Corners 1","r":{"a":0,"k":4,"ix":1},"ix":2,"mn":"ADBE Vector Filter - RC","hd":false},{"ty":"st","c":{"a":0,"k":[0,0,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[600,600],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"lock2","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":120,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"lock1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[0]},{"i":{"x":[0.667],"y":[0.805]},"o":{"x":[0.333],"y":[0]},"t":10,"s":[-15]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[-0.195]},"t":20,"s":[-7.407]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":30,"s":[-15]},{"t":40,"s":[0]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[19,16,0],"to":[0,0.139,0],"ti":[0,0,0]},{"i":{"x":0.903,"y":0},"o":{"x":0.333,"y":0},"t":10,"s":[19,16.833,0],"to":[0,0,0],"ti":[0,0.124,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.152,"y":1},"t":20,"s":[19,16.617,0],"to":[0,-0.216,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":30,"s":[19,16.833,0],"to":[0,0,0],"ti":[0,0.088,0]},{"t":40,"s":[19,16,0]}],"ix":2,"l":2},"a":{"a":0,"k":[24,-12,0],"ix":1,"l":2},"s":{"a":0,"k":[16.667,16.667,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[14,12],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":4,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[600,600],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"lock1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":120,"st":0,"bm":0}],"markers":[]}
|
||||
{"v":"5.8.1","fr":60,"ip":0,"op":120,"w":30,"h":30,"nm":"unlock","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"lock2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[0]},{"i":{"x":[0.667],"y":[0.805]},"o":{"x":[0.333],"y":[0]},"t":10,"s":[15]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[-0.195]},"t":20,"s":[7.407]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":30,"s":[15]},{"t":40,"s":[0]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[19,14,0],"to":[0,-0.139,0],"ti":[0,0,0]},{"i":{"x":0.903,"y":0},"o":{"x":0.333,"y":0},"t":10,"s":[19,13.167,0],"to":[0,0,0],"ti":[0,-0.124,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.152,"y":1},"t":20,"s":[19,13.383,0],"to":[0,0.216,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":30,"s":[19,13.167,0],"to":[0,0,0],"ti":[0,-0.088,0]},{"t":40,"s":[19,14,0]}],"ix":2,"l":2},"a":{"a":0,"k":[4,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[-2.209,0],[0,-2.209],[0,0]],"o":[[0,0],[0,-2.209],[2.209,0],[0,0],[0,0]],"v":[[-4,-1],[-4,-4],[0,-8],[4,-4],[4,8]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"rd","nm":"Round Corners 1","r":{"a":0,"k":4,"ix":1},"ix":2,"mn":"ADBE Vector Filter - RC","hd":false},{"ty":"st","c":{"a":0,"k":[0,0,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"lock2","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":120,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"lock1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[0]},{"i":{"x":[0.667],"y":[0.805]},"o":{"x":[0.333],"y":[0]},"t":10,"s":[-15]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[-0.195]},"t":20,"s":[-7.407]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":30,"s":[-15]},{"t":40,"s":[0]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[19,16,0],"to":[0,0.139,0],"ti":[0,0,0]},{"i":{"x":0.903,"y":0},"o":{"x":0.333,"y":0},"t":10,"s":[19,16.833,0],"to":[0,0,0],"ti":[0,0.124,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.152,"y":1},"t":20,"s":[19,16.617,0],"to":[0,-0.216,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":30,"s":[19,16.833,0],"to":[0,0,0],"ti":[0,0.088,0]},{"t":40,"s":[19,16,0]}],"ix":2,"l":2},"a":{"a":0,"k":[24,-12,0],"ix":1,"l":2},"s":{"a":0,"k":[16.667,16.667,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[14,12],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":4,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[600,600],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"lock1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":120,"st":0,"bm":0}],"markers":[]}
|
136
submodules/PremiumUI/Sources/AppIconsDemoComponent.swift
Normal file
@ -0,0 +1,136 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import SwiftSignalKit
|
||||
import ComponentFlow
|
||||
import TelegramCore
|
||||
import AccountContext
|
||||
import TelegramPresentationData
|
||||
import AccountContext
|
||||
import AppBundle
|
||||
|
||||
final class AppIconsDemoComponent: Component {
|
||||
public typealias EnvironmentType = DemoPageEnvironment
|
||||
|
||||
let context: AccountContext
|
||||
let appIcons: [PresentationAppIcon]
|
||||
|
||||
public init(
|
||||
context: AccountContext,
|
||||
appIcons: [PresentationAppIcon]
|
||||
) {
|
||||
self.context = context
|
||||
self.appIcons = appIcons
|
||||
}
|
||||
|
||||
public static func ==(lhs: AppIconsDemoComponent, rhs: AppIconsDemoComponent) -> Bool {
|
||||
if lhs.context !== rhs.context {
|
||||
return false
|
||||
}
|
||||
if lhs.appIcons != rhs.appIcons {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
public final class View: UIView {
|
||||
private var component: AppIconsDemoComponent?
|
||||
|
||||
private var imageViews: [UIImageView] = []
|
||||
|
||||
public func update(component: AppIconsDemoComponent, availableSize: CGSize, environment: Environment<DemoPageEnvironment>, transition: Transition) -> CGSize {
|
||||
// let isDisplaying = environment[DemoPageEnvironment.self].isDisplaying
|
||||
|
||||
// if self.node == nil {
|
||||
// let node = StickersCarouselNode(
|
||||
// context: component.context,
|
||||
// stickers: component.stickers
|
||||
// )
|
||||
// self.node = node
|
||||
// self.addSubnode(node)
|
||||
// }
|
||||
|
||||
// let isFirstTime = self.component == nil
|
||||
self.component = component
|
||||
|
||||
if self.imageViews.isEmpty {
|
||||
for icon in component.appIcons {
|
||||
if let image = UIImage(named: icon.imageName, in: getAppBundle(), compatibleWith: nil) {
|
||||
let imageView = UIImageView(frame: CGRect(origin: .zero, size: CGSize(width: 90.0, height: 90.0)))
|
||||
imageView.clipsToBounds = true
|
||||
imageView.layer.cornerRadius = 24.0
|
||||
imageView.image = image
|
||||
self.addSubview(imageView)
|
||||
|
||||
self.imageViews.append(imageView)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var i = 0
|
||||
for view in self.imageViews {
|
||||
let position: CGPoint
|
||||
switch i {
|
||||
case 0:
|
||||
position = CGPoint(x: availableSize.width * 0.5, y: availableSize.height * 0.333)
|
||||
case 1:
|
||||
position = CGPoint(x: availableSize.width * 0.333, y: availableSize.height * 0.667)
|
||||
case 2:
|
||||
position = CGPoint(x: availableSize.width * 0.667, y: availableSize.height * 0.667)
|
||||
default:
|
||||
position = CGPoint(x: availableSize.width * 0.5, y: availableSize.height * 0.5)
|
||||
}
|
||||
|
||||
view.center = position
|
||||
|
||||
i += 1
|
||||
}
|
||||
|
||||
var mappedPosition = environment[DemoPageEnvironment.self].position
|
||||
mappedPosition *= abs(mappedPosition)
|
||||
|
||||
if let _ = transition.userData(DemoAnimateInTransition.self), abs(mappedPosition) < .ulpOfOne {
|
||||
Queue.mainQueue().after(0.1) {
|
||||
var i = 0
|
||||
for view in self.imageViews {
|
||||
let from: CGPoint
|
||||
let delay: Double
|
||||
switch i {
|
||||
case 0:
|
||||
from = CGPoint(x: -availableSize.width * 0.333, y: -availableSize.height * 0.8)
|
||||
delay = 0.1
|
||||
case 1:
|
||||
from = CGPoint(x: -availableSize.width * 0.75, y: availableSize.height * 0.75)
|
||||
delay = 0.15
|
||||
case 2:
|
||||
from = CGPoint(x: availableSize.width * 0.9, y: availableSize.height * 0.0)
|
||||
delay = 0.0
|
||||
default:
|
||||
from = CGPoint(x: availableSize.width * 0.5, y: availableSize.height * 0.5)
|
||||
delay = 0.0
|
||||
}
|
||||
view.layer.animateScale(from: 3.0, to: 1.0, duration: 0.5, delay: delay, timingFunction: kCAMediaTimingFunctionSpring)
|
||||
view.layer.animatePosition(from: from, to: CGPoint(), duration: 0.5, delay: delay, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
|
||||
|
||||
i += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return availableSize
|
||||
}
|
||||
|
||||
func animateIn() {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public func makeView() -> View {
|
||||
return View()
|
||||
}
|
||||
|
||||
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<DemoPageEnvironment>, transition: Transition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, environment: environment, transition: transition)
|
||||
}
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
import Foundation
|
||||
import 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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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<MediaPlayerStatus?, NoError> {
|
||||
return self.playbackStatusPromise.get()
|
||||
}
|
||||
private var playbackStatusPromise = ValuePromise<MediaPlayerStatus?>(nil)
|
||||
private var playbackStatusValue: MediaPlayerStatus?
|
||||
private var statusDisposable = MetaDisposable()
|
||||
|
||||
var screenRotation: CGFloat = 0.0 {
|
||||
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..<Int64.max)
|
||||
let dummyFile = TelegramMediaFile(fileId: MediaId(namespace: 0, id: id), partialReference: nil, resource: LocalFileReferenceMediaResource(localFilePath: path, randomId: id), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "video/mp4", size: size, attributes: [.Video(duration: 3, size: dimensions, flags: [])])
|
||||
|
||||
let videoContent = NativeVideoContent(id: .message(1, MediaId(namespace: 0, id: id)), fileReference: .standalone(media: dummyFile), streamVideo: .none, loopVideo: true, enableSound: false, fetchAutomatically: true, onlyFullSizeThumbnail: false, continuePlayingWithoutSoundOnLostAudioSession: false, placeholderColor: .clear)
|
||||
|
||||
|
||||
let videoContent = NativeVideoContent(
|
||||
id: .message(1, MediaId(namespace: 0, id: Int64.random(in: 0..<Int64.max))),
|
||||
fileReference: .standalone(media: file),
|
||||
streamVideo: .conservative,
|
||||
loopVideo: true,
|
||||
enableSound: false,
|
||||
fetchAutomatically: true,
|
||||
onlyFullSizeThumbnail: false,
|
||||
continuePlayingWithoutSoundOnLostAudioSession: false,
|
||||
placeholderColor: .darkGray,
|
||||
hintDimensions: CGSize(width: 1170, height: 1754)
|
||||
)
|
||||
let videoNode = UniversalVideoNode(postbox: context.account.postbox, audioSession: context.sharedContext.mediaManager.audioSession, manager: context.sharedContext.mediaManager.universalVideoManager, decoration: VideoDecoration(), content: videoContent, priority: .embedded)
|
||||
videoNode.canAttachContent = true
|
||||
self.videoNode = videoNode
|
||||
|
||||
let status = videoNode.status
|
||||
|> mapToSignal { status -> Signal<MediaPlayerStatus?, NoError> in
|
||||
if let status = status, case .buffering = status.status {
|
||||
return .single(status) |> delay(1.0, queue: Queue.mainQueue())
|
||||
} else {
|
||||
return .single(status)
|
||||
}
|
||||
}
|
||||
|
||||
self.statusDisposable.set((status |> deliverOnMainQueue).start(next: { [weak self] status in
|
||||
if let strongSelf = self {
|
||||
strongSelf.playbackStatusValue = status
|
||||
strongSelf.playbackStatusPromise.set(status)
|
||||
strongSelf.updatePlaybackStatus()
|
||||
}
|
||||
}))
|
||||
|
||||
self.contentContainerView.insertSubview(videoNode.view, at: 0)
|
||||
|
||||
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<Bool, NoError> {
|
||||
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<DemoPageEnvironment>, transition: Transition) -> CGSize {
|
||||
self.component = component
|
||||
|
||||
|
||||
self.phoneView.setup(context: component.context, videoName: component.videoName, position: component.position)
|
||||
|
||||
self.containerView.frame = CGRect(origin: .zero, size: availableSize)
|
||||
self.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 {
|
||||
|
@ -393,7 +393,13 @@ private final class DemoPagerComponent: Component {
|
||||
itemTransition = transition.withAnimation(.none)
|
||||
itemView = ComponentHostView<DemoPageEnvironment>()
|
||||
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 {
|
||||
|
@ -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)
|
||||
}
|
||||
)),
|
||||
|
@ -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(
|
||||
|
@ -1,6 +0,0 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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))
|
||||
}
|
||||
|
@ -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)] {
|
||||
|
@ -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()
|
||||
|
@ -4,6 +4,9 @@ import SwiftSignalKit
|
||||
import TelegramApi
|
||||
import MtProtoKit
|
||||
|
||||
public func updatePremiumPromoConfigurationOnce(account: Account) -> Signal<Void, NoError> {
|
||||
return updatePremiumPromoConfigurationOnce(postbox: account.postbox, network: account.network)
|
||||
}
|
||||
|
||||
func updatePremiumPromoConfigurationOnce(postbox: Postbox, network: Network) -> Signal<Void, NoError> {
|
||||
return network.request(Api.functions.help.getPremiumPromo())
|
||||
|
@ -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<AttachMenuBot, GetAttachMenuBotError> 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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
159
submodules/TelegramUI/Images.xcassets/Premium/Perk/AppIcon.imageset/AppIcon.pdf
vendored
Normal file
@ -0,0 +1,159 @@
|
||||
%PDF-1.7
|
||||
|
||||
1 0 obj
|
||||
<< >>
|
||||
endobj
|
||||
|
||||
2 0 obj
|
||||
<< /Length 3 0 R >>
|
||||
stream
|
||||
/DeviceRGB CS
|
||||
/DeviceRGB cs
|
||||
q
|
||||
1.000000 0.000000 -0.000000 1.000000 6.000000 6.000000 cm
|
||||
1.000000 1.000000 1.000000 scn
|
||||
9.000000 15.600000 m
|
||||
9.000000 16.440079 9.000000 16.860119 9.163490 17.180986 c
|
||||
9.307301 17.463228 9.536771 17.692699 9.819015 17.836510 c
|
||||
10.139882 18.000000 10.559921 18.000000 11.400000 18.000000 c
|
||||
16.600000 18.000000 l
|
||||
17.440079 18.000000 17.860119 18.000000 18.180984 17.836510 c
|
||||
18.463230 17.692699 18.692699 17.463228 18.836510 17.180986 c
|
||||
19.000000 16.860119 19.000000 16.440079 19.000000 15.600000 c
|
||||
19.000000 10.400000 l
|
||||
19.000000 9.559921 19.000000 9.139882 18.836510 8.819015 c
|
||||
18.692699 8.536771 18.463230 8.307301 18.180984 8.163490 c
|
||||
17.860119 8.000000 17.440079 8.000000 16.600000 8.000000 c
|
||||
11.400000 8.000000 l
|
||||
10.559921 8.000000 10.139882 8.000000 9.819015 8.163490 c
|
||||
9.536771 8.307301 9.307301 8.536771 9.163490 8.819015 c
|
||||
9.000000 9.139882 9.000000 9.559921 9.000000 10.400000 c
|
||||
9.000000 15.600000 l
|
||||
h
|
||||
13.877289 10.424780 m
|
||||
13.972101 10.481474 14.090399 10.481474 14.185211 10.424780 c
|
||||
15.566299 9.598953 l
|
||||
16.097145 9.281532 16.748672 9.758140 16.606907 10.360182 c
|
||||
16.243755 11.902411 l
|
||||
16.218065 12.011508 16.255333 12.125828 16.340385 12.198824 c
|
||||
17.543976 13.231811 l
|
||||
18.015240 13.636273 17.765560 14.408524 17.146713 14.460539 c
|
||||
15.554527 14.594366 l
|
||||
15.443923 14.603662 15.347490 14.673266 15.303833 14.775314 c
|
||||
14.674830 16.245621 l
|
||||
14.432594 16.811852 13.629906 16.811852 13.387671 16.245621 c
|
||||
12.758666 14.775314 l
|
||||
12.715010 14.673266 12.618577 14.603662 12.507973 14.594366 c
|
||||
10.915787 14.460539 l
|
||||
10.296938 14.408524 10.047261 13.636274 10.518523 13.231811 c
|
||||
11.722115 12.198824 l
|
||||
11.807166 12.125828 11.844435 12.011508 11.818745 11.902411 c
|
||||
11.455591 10.360182 l
|
||||
11.313828 9.758140 11.965354 9.281531 12.496199 9.598952 c
|
||||
13.877289 10.424780 l
|
||||
h
|
||||
0.163490 14.180985 m
|
||||
0.000000 13.860119 0.000000 13.440079 0.000000 12.600000 c
|
||||
0.000000 10.400000 l
|
||||
0.000000 9.559921 0.000000 9.139882 0.163490 8.819015 c
|
||||
0.307300 8.536771 0.536771 8.307301 0.819014 8.163490 c
|
||||
1.139882 8.000000 1.559921 8.000000 2.400000 8.000000 c
|
||||
4.600000 8.000000 l
|
||||
5.440079 8.000000 5.860118 8.000000 6.180986 8.163490 c
|
||||
6.463229 8.307301 6.692700 8.536771 6.836510 8.819015 c
|
||||
7.000000 9.139882 7.000000 9.559921 7.000000 10.400000 c
|
||||
7.000000 12.600000 l
|
||||
7.000000 13.440079 7.000000 13.860119 6.836510 14.180985 c
|
||||
6.692700 14.463229 6.463229 14.692699 6.180986 14.836510 c
|
||||
5.860118 15.000000 5.440079 15.000000 4.600000 15.000000 c
|
||||
2.400000 15.000000 l
|
||||
1.559921 15.000000 1.139882 15.000000 0.819014 14.836510 c
|
||||
0.536771 14.692699 0.307300 14.463229 0.163490 14.180985 c
|
||||
h
|
||||
2.163490 5.180985 m
|
||||
2.000000 4.860118 2.000000 4.440079 2.000000 3.600000 c
|
||||
2.000000 3.400000 l
|
||||
2.000000 2.559921 2.000000 2.139882 2.163490 1.819014 c
|
||||
2.307300 1.536772 2.536771 1.307301 2.819014 1.163490 c
|
||||
3.139882 1.000000 3.559921 1.000000 4.400000 1.000000 c
|
||||
4.600000 1.000000 l
|
||||
5.440079 1.000000 5.860118 1.000000 6.180986 1.163490 c
|
||||
6.463229 1.307301 6.692700 1.536772 6.836510 1.819014 c
|
||||
7.000000 2.139882 7.000000 2.559921 7.000000 3.400000 c
|
||||
7.000000 3.600000 l
|
||||
7.000000 4.440079 7.000000 4.860118 6.836510 5.180985 c
|
||||
6.692700 5.463229 6.463229 5.692699 6.180986 5.836510 c
|
||||
5.860118 6.000000 5.440079 6.000000 4.600000 6.000000 c
|
||||
4.400000 6.000000 l
|
||||
3.559921 6.000000 3.139882 6.000000 2.819014 5.836510 c
|
||||
2.536771 5.692699 2.307300 5.463229 2.163490 5.180985 c
|
||||
h
|
||||
9.000000 3.599999 m
|
||||
9.000000 4.440079 9.000000 4.860118 9.163490 5.180985 c
|
||||
9.307301 5.463229 9.536771 5.692699 9.819015 5.836510 c
|
||||
10.139882 6.000000 10.559921 6.000000 11.400000 6.000000 c
|
||||
12.599999 6.000000 l
|
||||
13.440079 6.000000 13.860118 6.000000 14.180985 5.836510 c
|
||||
14.463229 5.692699 14.692699 5.463229 14.836510 5.180985 c
|
||||
15.000000 4.860118 15.000000 4.440079 15.000000 3.600000 c
|
||||
15.000000 2.400001 l
|
||||
15.000000 1.559921 15.000000 1.139881 14.836510 0.819014 c
|
||||
14.692699 0.536772 14.463229 0.307301 14.180985 0.163490 c
|
||||
13.860118 0.000000 13.440079 0.000000 12.600000 0.000000 c
|
||||
11.400001 0.000000 l
|
||||
10.559921 0.000000 10.139882 0.000000 9.819015 0.163490 c
|
||||
9.536771 0.307301 9.307301 0.536772 9.163490 0.819014 c
|
||||
9.000000 1.139881 9.000000 1.559921 9.000000 2.400000 c
|
||||
9.000000 3.599999 l
|
||||
h
|
||||
f*
|
||||
n
|
||||
Q
|
||||
|
||||
endstream
|
||||
endobj
|
||||
|
||||
3 0 obj
|
||||
4178
|
||||
endobj
|
||||
|
||||
4 0 obj
|
||||
<< /Annots []
|
||||
/Type /Page
|
||||
/MediaBox [ 0.000000 0.000000 30.000000 30.000000 ]
|
||||
/Resources 1 0 R
|
||||
/Contents 2 0 R
|
||||
/Parent 5 0 R
|
||||
>>
|
||||
endobj
|
||||
|
||||
5 0 obj
|
||||
<< /Kids [ 4 0 R ]
|
||||
/Count 1
|
||||
/Type /Pages
|
||||
>>
|
||||
endobj
|
||||
|
||||
6 0 obj
|
||||
<< /Pages 5 0 R
|
||||
/Type /Catalog
|
||||
>>
|
||||
endobj
|
||||
|
||||
xref
|
||||
0 7
|
||||
0000000000 65535 f
|
||||
0000000010 00000 n
|
||||
0000000034 00000 n
|
||||
0000004268 00000 n
|
||||
0000004291 00000 n
|
||||
0000004464 00000 n
|
||||
0000004538 00000 n
|
||||
trailer
|
||||
<< /ID [ (some) (id) ]
|
||||
/Root 6 0 R
|
||||
/Size 7
|
||||
>>
|
||||
startxref
|
||||
4597
|
||||
%%EOF
|
12
submodules/TelegramUI/Images.xcassets/Premium/Perk/AppIcon.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "AppIcon.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
@ -679,6 +679,11 @@ private func extractAccountManagerState(records: AccountRecordsView<TelegramAcco
|
||||
if buildConfig.isInternalBuild {
|
||||
icons.append(PresentationAppIcon(name: "WhiteFilledIcon", imageName: "WhiteFilledIcon"))
|
||||
}
|
||||
|
||||
icons.append(PresentationAppIcon(name: "PremiumCosmic", imageName: "PremiumCosmic", isPremium: true))
|
||||
icons.append(PresentationAppIcon(name: "PremiumCherry", imageName: "PremiumCherry", isPremium: true))
|
||||
icons.append(PresentationAppIcon(name: "PremiumDuck", imageName: "PremiumDuck", isPremium: true))
|
||||
|
||||
return icons
|
||||
} else {
|
||||
return []
|
||||
|
@ -93,7 +93,7 @@ final class ChatAvatarNavigationNode: ASDisplayNode {
|
||||
}
|
||||
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)
|
||||
|
@ -335,7 +335,7 @@ func chatMediaInputGridEntries(view: ItemCollectionsView, savedStickers: Ordered
|
||||
}
|
||||
}
|
||||
|
||||
if let premiumStickers = premiumStickers, !premiumStickers.items.isEmpty {
|
||||
if let premiumStickers = premiumStickers, !premiumStickers.items.isEmpty && hasPremium {
|
||||
let packInfo = StickerPackCollectionInfo(id: ItemCollectionId(namespace: ChatMediaInputPanelAuxiliaryNamespace.premium.rawValue, id: 0), flags: [], accessHash: 0, title: strings.Stickers_PremiumStickers.uppercased(), shortName: "", thumbnail: nil, immediateThumbnailData: nil, hash: 0, count: 0)
|
||||
for i in 0 ..< premiumStickers.items.count {
|
||||
if let item = premiumStickers.items[i].contents.get(RecentMediaItem.self) {
|
||||
|
@ -1489,6 +1489,8 @@ public final class SharedAccountContextImpl: SharedAccountContext {
|
||||
mappedSource = .folders
|
||||
case .chatsPerFolder:
|
||||
mappedSource = .chatsPerFolder
|
||||
case .appIcons:
|
||||
mappedSource = .appIcons
|
||||
case .accounts:
|
||||
mappedSource = .accounts
|
||||
case .about:
|
||||
|
@ -40,8 +40,9 @@ public final class NativeVideoContent: UniversalVideoContent {
|
||||
let placeholderColor: UIColor
|
||||
let tempFilePath: String?
|
||||
let captureProtected: Bool
|
||||
let hintDimensions: CGSize?
|
||||
|
||||
public init(id: NativeVideoContentId, fileReference: FileMediaReference, imageReference: ImageMediaReference? = nil, streamVideo: MediaPlayerStreaming = .none, loopVideo: Bool = false, enableSound: Bool = true, baseRate: Double = 1.0, fetchAutomatically: Bool = true, onlyFullSizeThumbnail: Bool = false, useLargeThumbnail: Bool = false, autoFetchFullSizeThumbnail: Bool = false, startTimestamp: Double? = nil, endTimestamp: Double? = nil, continuePlayingWithoutSoundOnLostAudioSession: Bool = false, placeholderColor: UIColor = .white, tempFilePath: String? = nil, captureProtected: Bool = false) {
|
||||
public init(id: NativeVideoContentId, fileReference: FileMediaReference, imageReference: ImageMediaReference? = nil, streamVideo: MediaPlayerStreaming = .none, loopVideo: Bool = false, enableSound: Bool = true, baseRate: Double = 1.0, fetchAutomatically: Bool = true, onlyFullSizeThumbnail: Bool = false, useLargeThumbnail: Bool = false, autoFetchFullSizeThumbnail: Bool = false, startTimestamp: Double? = nil, endTimestamp: Double? = nil, continuePlayingWithoutSoundOnLostAudioSession: Bool = false, placeholderColor: UIColor = .white, tempFilePath: String? = nil, captureProtected: Bool = false, hintDimensions: CGSize? = nil) {
|
||||
self.id = id
|
||||
self.nativeId = id
|
||||
self.fileReference = fileReference
|
||||
@ -74,10 +75,11 @@ public final class NativeVideoContent: UniversalVideoContent {
|
||||
self.placeholderColor = placeholderColor
|
||||
self.tempFilePath = tempFilePath
|
||||
self.captureProtected = captureProtected
|
||||
self.hintDimensions = hintDimensions
|
||||
}
|
||||
|
||||
public func makeContentNode(postbox: Postbox, audioSession: ManagedAudioSession) -> UniversalVideoContentNode & ASDisplayNode {
|
||||
return NativeVideoContentNode(postbox: postbox, audioSessionManager: audioSession, fileReference: self.fileReference, imageReference: self.imageReference, streamVideo: self.streamVideo, loopVideo: self.loopVideo, enableSound: self.enableSound, baseRate: self.baseRate, fetchAutomatically: self.fetchAutomatically, onlyFullSizeThumbnail: self.onlyFullSizeThumbnail, useLargeThumbnail: self.useLargeThumbnail, autoFetchFullSizeThumbnail: self.autoFetchFullSizeThumbnail, startTimestamp: self.startTimestamp, endTimestamp: self.endTimestamp, continuePlayingWithoutSoundOnLostAudioSession: self.continuePlayingWithoutSoundOnLostAudioSession, placeholderColor: self.placeholderColor, tempFilePath: self.tempFilePath, captureProtected: self.captureProtected)
|
||||
return NativeVideoContentNode(postbox: postbox, audioSessionManager: audioSession, fileReference: self.fileReference, imageReference: self.imageReference, streamVideo: self.streamVideo, loopVideo: self.loopVideo, enableSound: self.enableSound, baseRate: self.baseRate, fetchAutomatically: self.fetchAutomatically, onlyFullSizeThumbnail: self.onlyFullSizeThumbnail, useLargeThumbnail: self.useLargeThumbnail, autoFetchFullSizeThumbnail: self.autoFetchFullSizeThumbnail, startTimestamp: self.startTimestamp, endTimestamp: self.endTimestamp, continuePlayingWithoutSoundOnLostAudioSession: self.continuePlayingWithoutSoundOnLostAudioSession, placeholderColor: self.placeholderColor, tempFilePath: self.tempFilePath, captureProtected: self.captureProtected, hintDimensions: self.hintDimensions)
|
||||
}
|
||||
|
||||
public func isEqual(to other: UniversalVideoContent) -> Bool {
|
||||
@ -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()
|
||||
}
|
||||
|
@ -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
|
||||
|