mirror of
https://github.com/TelegramMessenger/Telegram-iOS.git
synced 2026-06-20 18:24:43 +00:00
Merge commit 'e6ba681c030590e5fa095f9828610889abf08ef3'
This commit is contained in:
@@ -10694,7 +10694,7 @@ Sorry for the inconvenience.";
|
||||
|
||||
"Premium.Gift.ApplyLink" = "Apply for Free";
|
||||
"Premium.Gift.ApplyLink.AlreadyHasPremium.Title" = "You already have Telegram Premium";
|
||||
"Premium.Gift.ApplyLink.AlreadyHasPremium.Text" = "You can activate this gift link after %@ or [send the link]() to a friend.";
|
||||
"Premium.Gift.ApplyLink.AlreadyHasPremium.Text" = "You can activate this gift link after **%@** or [send the link]() to a friend.";
|
||||
|
||||
"Premium.Gift.Link.Text" = "This link allows you or [anyone you choose]() to activate a **Telegram Premium** subscription.";
|
||||
"Premium.Gift.UsedLink.Text" = "This link was used to activate a **Telegram Premium** subscription.";
|
||||
@@ -10742,17 +10742,17 @@ Sorry for the inconvenience.";
|
||||
"ChannelBoost.EmojiStatus" = "Use Emoji Statuses";
|
||||
"ChannelBoost.EnableEmojiStatusLevelText" = "Your channel needs **Level %1$@** to use emoji statuses.";
|
||||
|
||||
"ChannelBoost.Wallpaper" = "Set Channel Background";
|
||||
"ChannelBoost.EnableWallpaperLevelText" = "Your channel needs **Level %1$@** to set channel background.";
|
||||
"ChannelBoost.Wallpaper" = "Set Channel Wallpaper";
|
||||
"ChannelBoost.EnableWallpaperLevelText" = "Your channel needs **Level %1$@** to set channel wallpaper.";
|
||||
|
||||
"ChannelBoost.CustomWallpaper" = "Set Custom Channel Background";
|
||||
"ChannelBoost.EnableCustomWallpaperLevelText" = "Your channel needs **Level %1$@** to set custom channel background.";
|
||||
"ChannelBoost.CustomWallpaper" = "Set Custom Channel Wallpaper";
|
||||
"ChannelBoost.EnableCustomWallpaperLevelText" = "Your channel needs **Level %1$@** to set custom channel wallpaper.";
|
||||
|
||||
"WallpaperPreview.ChannelHeader" = "All subscribers will see this wallpaper";
|
||||
"WallpaperPreview.ChannelTopText" = "Details to follow shortly.\nStay tuned!";
|
||||
"WallpaperPreview.ChannelReplyText" = "Breaking News";
|
||||
|
||||
"Wallpaper.ApplyForChannel" = "Apply Background";
|
||||
"Wallpaper.ApplyForChannel" = "Apply Wallpaper";
|
||||
"Notification.ChannelChangedWallpaper" = "Channel set a new wallpaper";
|
||||
|
||||
"Story.MessageReposted.Personal" = "Message reposted to your stories.";
|
||||
@@ -10763,3 +10763,13 @@ Sorry for the inconvenience.";
|
||||
"Share.RepostToStory" = "Repost\nto Story";
|
||||
|
||||
"Conversation.ReadMore" = "Read More";
|
||||
|
||||
"Wallpaper.ChannelTitle" = "Channel Wallpaper";
|
||||
"Wallpaper.ChannelCustomBackgroundInfo" = "Upload your own background image for the channel.";
|
||||
"Wallpaper.ChannelRemoveBackground" = "Remove Wallpaper";
|
||||
"Wallpaper.NoWallpaper" = "No\nWallpaper";
|
||||
|
||||
"ChatList.PremiumXmasGiftTitle" = "Send gifts to **your friends**! 🎄";
|
||||
"ChatList.PremiumXmasGiftText" = "Gift Telegram Premium for Christmas.";
|
||||
|
||||
"ReassignBoost.DescriptionWithLink" = "To boost **%1$@**, reassign a previous boost or [gift Telegram Premium]() to a friend to get **%2$@** additional boosts.";
|
||||
|
||||
@@ -52,6 +52,7 @@ public final class ChatMessageItemAssociatedData: Equatable {
|
||||
public let maxReadStoryId: Int32?
|
||||
public let recommendedChannels: RecommendedChannels?
|
||||
public let audioTranscriptionTrial: AudioTranscription.TrialState
|
||||
public let chatThemes: [TelegramTheme]
|
||||
|
||||
public init(
|
||||
automaticDownloadPeerType: MediaAutoDownloadPeerType,
|
||||
@@ -77,7 +78,8 @@ public final class ChatMessageItemAssociatedData: Equatable {
|
||||
translateToLanguage: String? = nil,
|
||||
maxReadStoryId: Int32? = nil,
|
||||
recommendedChannels: RecommendedChannels? = nil,
|
||||
audioTranscriptionTrial: AudioTranscription.TrialState = .defaultValue
|
||||
audioTranscriptionTrial: AudioTranscription.TrialState = .defaultValue,
|
||||
chatThemes: [TelegramTheme] = []
|
||||
) {
|
||||
self.automaticDownloadPeerType = automaticDownloadPeerType
|
||||
self.automaticDownloadPeerId = automaticDownloadPeerId
|
||||
@@ -103,6 +105,7 @@ public final class ChatMessageItemAssociatedData: Equatable {
|
||||
self.maxReadStoryId = maxReadStoryId
|
||||
self.recommendedChannels = recommendedChannels
|
||||
self.audioTranscriptionTrial = audioTranscriptionTrial
|
||||
self.chatThemes = chatThemes
|
||||
}
|
||||
|
||||
public static func == (lhs: ChatMessageItemAssociatedData, rhs: ChatMessageItemAssociatedData) -> Bool {
|
||||
@@ -175,6 +178,9 @@ public final class ChatMessageItemAssociatedData: Equatable {
|
||||
if lhs.audioTranscriptionTrial != rhs.audioTranscriptionTrial {
|
||||
return false
|
||||
}
|
||||
if lhs.chatThemes != rhs.chatThemes {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2236,6 +2236,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
|
||||
}, openStorageManagement: {
|
||||
}, openPasswordSetup: {
|
||||
}, openPremiumIntro: {
|
||||
}, openPremiumGift: {
|
||||
}, openActiveSessions: {
|
||||
}, performActiveSessionAction: { _, _ in
|
||||
}, openChatFolderUpdates: {
|
||||
@@ -3556,7 +3557,7 @@ public final class ChatListSearchShimmerNode: ASDisplayNode {
|
||||
let interaction = ChatListNodeInteraction(context: context, animationCache: animationCache, animationRenderer: animationRenderer, activateSearch: {}, peerSelected: { _, _, _, _ in }, disabledPeerSelected: { _, _ in }, togglePeerSelected: { _, _ in }, togglePeersSelection: { _, _ in }, additionalCategorySelected: { _ in
|
||||
}, messageSelected: { _, _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, setPeerThreadMuted: { _, _, _ in }, deletePeer: { _, _ in }, deletePeerThread: { _, _ in }, setPeerThreadStopped: { _, _, _ in }, setPeerThreadPinned: { _, _, _ in }, setPeerThreadHidden: { _, _, _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, toggleThreadsSelection: { _, _ in }, hidePsa: { _ in }, activateChatPreview: { _, _, _, gesture, _ in
|
||||
gesture?.cancel()
|
||||
}, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openActiveSessions: {
|
||||
}, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openPremiumGift: {}, openActiveSessions: {
|
||||
}, performActiveSessionAction: { _, _ in
|
||||
}, openChatFolderUpdates: {}, hideChatFolderUpdates: {
|
||||
}, openStories: { _, _ in
|
||||
|
||||
@@ -156,7 +156,7 @@ final class ChatListShimmerNode: ASDisplayNode {
|
||||
let interaction = ChatListNodeInteraction(context: context, animationCache: animationCache, animationRenderer: animationRenderer, activateSearch: {}, peerSelected: { _, _, _, _ in }, disabledPeerSelected: { _, _ in }, togglePeerSelected: { _, _ in }, togglePeersSelection: { _, _ in }, additionalCategorySelected: { _ in
|
||||
}, messageSelected: { _, _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, setPeerThreadMuted: { _, _, _ in }, deletePeer: { _, _ in }, deletePeerThread: { _, _ in }, setPeerThreadStopped: { _, _, _ in }, setPeerThreadPinned: { _, _, _ in }, setPeerThreadHidden: { _, _, _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, toggleThreadsSelection: { _, _ in }, hidePsa: { _ in }, activateChatPreview: { _, _, _, gesture, _ in
|
||||
gesture?.cancel()
|
||||
}, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openActiveSessions: {}, performActiveSessionAction: { _, _ in }, openChatFolderUpdates: {}, hideChatFolderUpdates: {}, openStories: { _, _ in })
|
||||
}, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openPremiumGift: {}, openActiveSessions: {}, performActiveSessionAction: { _, _ in }, openChatFolderUpdates: {}, hideChatFolderUpdates: {}, openStories: { _, _ in })
|
||||
interaction.isInlineMode = isInlineMode
|
||||
|
||||
let items = (0 ..< 2).map { _ -> ChatListItem in
|
||||
|
||||
@@ -100,6 +100,7 @@ public final class ChatListNodeInteraction {
|
||||
let openStorageManagement: () -> Void
|
||||
let openPasswordSetup: () -> Void
|
||||
let openPremiumIntro: () -> Void
|
||||
let openPremiumGift: () -> Void
|
||||
let openActiveSessions: () -> Void
|
||||
let performActiveSessionAction: (NewSessionReview, Bool) -> Void
|
||||
let openChatFolderUpdates: () -> Void
|
||||
@@ -150,6 +151,7 @@ public final class ChatListNodeInteraction {
|
||||
openStorageManagement: @escaping () -> Void,
|
||||
openPasswordSetup: @escaping () -> Void,
|
||||
openPremiumIntro: @escaping () -> Void,
|
||||
openPremiumGift: @escaping () -> Void,
|
||||
openActiveSessions: @escaping () -> Void,
|
||||
performActiveSessionAction: @escaping (NewSessionReview, Bool) -> Void,
|
||||
openChatFolderUpdates: @escaping () -> Void,
|
||||
@@ -187,6 +189,7 @@ public final class ChatListNodeInteraction {
|
||||
self.openStorageManagement = openStorageManagement
|
||||
self.openPasswordSetup = openPasswordSetup
|
||||
self.openPremiumIntro = openPremiumIntro
|
||||
self.openPremiumGift = openPremiumGift
|
||||
self.openActiveSessions = openActiveSessions
|
||||
self.performActiveSessionAction = performActiveSessionAction
|
||||
self.openChatFolderUpdates = openChatFolderUpdates
|
||||
@@ -708,6 +711,8 @@ private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatL
|
||||
nodeInteraction?.openPasswordSetup()
|
||||
case .premiumUpgrade, .premiumAnnualDiscount, .premiumRestore:
|
||||
nodeInteraction?.openPremiumIntro()
|
||||
case .xmasPremiumGift:
|
||||
nodeInteraction?.openPremiumGift()
|
||||
case .reviewLogin:
|
||||
break
|
||||
}
|
||||
@@ -1028,6 +1033,8 @@ private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatL
|
||||
nodeInteraction?.openPasswordSetup()
|
||||
case .premiumUpgrade, .premiumAnnualDiscount, .premiumRestore:
|
||||
nodeInteraction?.openPremiumIntro()
|
||||
case .xmasPremiumGift:
|
||||
nodeInteraction?.openPremiumGift()
|
||||
case .reviewLogin:
|
||||
break
|
||||
}
|
||||
@@ -1604,6 +1611,12 @@ public final class ChatListNode: ListView {
|
||||
}
|
||||
let controller = self.context.sharedContext.makePremiumIntroController(context: self.context, source: .ads, forceDark: false, dismissed: nil)
|
||||
self.push?(controller)
|
||||
}, openPremiumGift: { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
let controller = self.context.sharedContext.makePremiumGiftController(context: self.context)
|
||||
self.push?(controller)
|
||||
}, openActiveSessions: { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
@@ -1793,7 +1806,9 @@ public final class ChatListNode: ListView {
|
||||
return .single(.setupPassword)
|
||||
}
|
||||
}
|
||||
if suggestions.contains(.annualPremium) || suggestions.contains(.upgradePremium) || suggestions.contains(.restorePremium), let inAppPurchaseManager = context.inAppPurchaseManager {
|
||||
if suggestions.contains(.xmasPremiumGift) {
|
||||
return .single(.xmasPremiumGift)
|
||||
} else if suggestions.contains(.annualPremium) || suggestions.contains(.upgradePremium) || suggestions.contains(.restorePremium), let inAppPurchaseManager = context.inAppPurchaseManager {
|
||||
return inAppPurchaseManager.availableProducts
|
||||
|> map { products -> ChatListNotice? in
|
||||
if products.count > 1 {
|
||||
|
||||
@@ -85,6 +85,7 @@ enum ChatListNotice: Equatable {
|
||||
case premiumUpgrade(discount: Int32)
|
||||
case premiumAnnualDiscount(discount: Int32)
|
||||
case premiumRestore(discount: Int32)
|
||||
case xmasPremiumGift
|
||||
case reviewLogin(newSessionReview: NewSessionReview, totalCount: Int)
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import TelegramPresentationData
|
||||
import ListSectionHeaderNode
|
||||
import AppBundle
|
||||
import ItemListUI
|
||||
import Markdown
|
||||
|
||||
class ChatListStorageInfoItem: ListViewItem {
|
||||
enum Action {
|
||||
@@ -203,6 +204,9 @@ class ChatListStorageInfoItemNode: ItemListRevealOptionsItemNode {
|
||||
titleString = titleStringValue
|
||||
|
||||
textString = NSAttributedString(string: item.strings.ChatList_PremiumRestoreDiscountText, font: textFont, textColor: item.theme.rootController.navigationBar.secondaryTextColor)
|
||||
case .xmasPremiumGift:
|
||||
titleString = parseMarkdownIntoAttributedString(item.strings.ChatList_PremiumXmasGiftTitle, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: titleFont, textColor: item.theme.rootController.navigationBar.primaryTextColor), bold: MarkdownAttributeSet(font: titleFont, textColor: item.theme.rootController.navigationBar.accentTextColor), link: MarkdownAttributeSet(font: titleFont, textColor: item.theme.rootController.navigationBar.primaryTextColor), linkAttribute: { _ in return nil }))
|
||||
textString = NSAttributedString(string: item.strings.ChatList_PremiumXmasGiftText, font: textFont, textColor: item.theme.rootController.navigationBar.secondaryTextColor)
|
||||
case let .reviewLogin(newSessionReview, totalCount):
|
||||
spacing = 2.0
|
||||
alignment = .center
|
||||
|
||||
@@ -101,7 +101,9 @@ public protocol CustomViewControllerNavigationDataSummary: AnyObject {
|
||||
public final var supportedOrientations: ViewControllerSupportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .allButUpsideDown) {
|
||||
didSet {
|
||||
if self.supportedOrientations != oldValue {
|
||||
self.window?.invalidateSupportedOrientations()
|
||||
if self.isNodeLoaded {
|
||||
self.window?.invalidateSupportedOrientations()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ private func makeEntityView(context: AccountContext, entity: DrawingEntity) -> D
|
||||
private func prepareForRendering(entityView: DrawingEntityView) {
|
||||
if let entityView = entityView as? DrawingStickerEntityView {
|
||||
entityView.entity.renderImage = entityView.getRenderImage()
|
||||
entityView.entity.renderSubEntities = entityView.getRenderSubEntities()
|
||||
}
|
||||
if let entityView = entityView as? DrawingBubbleEntityView {
|
||||
entityView.entity.renderImage = entityView.getRenderImage()
|
||||
|
||||
@@ -3152,12 +3152,13 @@ public final class DrawingToolsInteraction {
|
||||
}))
|
||||
}
|
||||
}
|
||||
#if DEBUG
|
||||
if isRectangleImage {
|
||||
|
||||
|
||||
if #available(iOS 17.0, *), isRectangleImage {
|
||||
actions.append(ContextMenuAction(content: .text(title: presentationData.strings.Paint_CutOut, accessibilityLabel: presentationData.strings.Paint_CutOut), action: { [weak self, weak entityView] in
|
||||
if let self, let entityView, let entity = entityView.entity as? DrawingStickerEntity, case let .image(image, _) = entity.content {
|
||||
let _ = (cutoutStickerImage(from: image)
|
||||
|> deliverOnMainQueue).start(next: { [weak entity] result in
|
||||
|> deliverOnMainQueue).start(next: { [weak entity] result in
|
||||
if let result, let entity {
|
||||
let newEntity = DrawingStickerEntity(content: .image(result, .sticker))
|
||||
newEntity.referenceDrawingSize = entity.referenceDrawingSize
|
||||
@@ -3180,7 +3181,7 @@ public final class DrawingToolsInteraction {
|
||||
}
|
||||
}))
|
||||
}
|
||||
#endif
|
||||
|
||||
let entityFrame = entityView.convert(entityView.selectionBounds, to: node.view).offsetBy(dx: 0.0, dy: -6.0)
|
||||
let controller = makeContextMenuController(actions: actions)
|
||||
let bounds = node.bounds.insetBy(dx: 0.0, dy: 160.0)
|
||||
|
||||
@@ -154,7 +154,7 @@ public class DrawingStickerEntityView: DrawingEntityView {
|
||||
return file.dimensions?.cgSize ?? CGSize(width: 512.0, height: 512.0)
|
||||
case .dualVideoReference:
|
||||
return CGSize(width: 512.0, height: 512.0)
|
||||
case let .message(_, size):
|
||||
case let .message(_, _, size):
|
||||
return size
|
||||
}
|
||||
}
|
||||
@@ -267,45 +267,7 @@ public class DrawingStickerEntityView: DrawingEntityView {
|
||||
}), attemptSynchronously: synchronous)
|
||||
self.setNeedsLayout()
|
||||
} else if case let .video(file) = self.stickerEntity.content {
|
||||
let videoNode = UniversalVideoNode(
|
||||
postbox: self.context.account.postbox,
|
||||
audioSession: self.context.sharedContext.mediaManager.audioSession,
|
||||
manager: self.context.sharedContext.mediaManager.universalVideoManager,
|
||||
decoration: StickerVideoDecoration(),
|
||||
content: NativeVideoContent(
|
||||
id: .contextResult(0, "\(UInt64.random(in: 0 ... UInt64.max))"),
|
||||
userLocation: .other,
|
||||
fileReference: .standalone(media: file),
|
||||
imageReference: nil,
|
||||
streamVideo: .story,
|
||||
loopVideo: true,
|
||||
enableSound: false,
|
||||
soundMuted: true,
|
||||
beginWithAmbientSound: false,
|
||||
mixWithOthers: true,
|
||||
useLargeThumbnail: false,
|
||||
autoFetchFullSizeThumbnail: false,
|
||||
tempFilePath: nil,
|
||||
captureProtected: false,
|
||||
hintDimensions: file.dimensions?.cgSize,
|
||||
storeAfterDownload: nil,
|
||||
displayImage: false,
|
||||
hasSentFramesToDisplay: { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.videoNode?.isHidden = false
|
||||
}
|
||||
),
|
||||
priority: .gallery
|
||||
)
|
||||
videoNode.canAttachContent = true
|
||||
videoNode.isUserInteractionEnabled = false
|
||||
videoNode.clipsToBounds = true
|
||||
self.addSubnode(videoNode)
|
||||
self.videoNode = videoNode
|
||||
self.setNeedsLayout()
|
||||
videoNode.play()
|
||||
self.setupWithVideo(file)
|
||||
} else if case let .animatedImage(data, thumbnailImage) = self.stickerEntity.content {
|
||||
let imageView = UIImageView()
|
||||
imageView.contentMode = .scaleAspectFit
|
||||
@@ -314,16 +276,63 @@ public class DrawingStickerEntityView: DrawingEntityView {
|
||||
self.animatedImageView = imageView
|
||||
self.addSubview(imageView)
|
||||
self.setNeedsLayout()
|
||||
} else if case .message = self.stickerEntity.content {
|
||||
} else if case let .message(_, innerFile, _) = self.stickerEntity.content {
|
||||
let imageView = UIImageView()
|
||||
imageView.contentMode = .scaleAspectFit
|
||||
imageView.image = self.stickerEntity.renderImage
|
||||
self.animatedImageView = imageView
|
||||
self.addSubview(imageView)
|
||||
self.setNeedsLayout()
|
||||
|
||||
let _ = innerFile
|
||||
// if let innerFile, innerFile.isAnimated {
|
||||
// self.setupWithVideo(innerFile)
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
private func setupWithVideo(_ file: TelegramMediaFile) {
|
||||
let videoNode = UniversalVideoNode(
|
||||
postbox: self.context.account.postbox,
|
||||
audioSession: self.context.sharedContext.mediaManager.audioSession,
|
||||
manager: self.context.sharedContext.mediaManager.universalVideoManager,
|
||||
decoration: StickerVideoDecoration(),
|
||||
content: NativeVideoContent(
|
||||
id: .contextResult(0, "\(UInt64.random(in: 0 ... UInt64.max))"),
|
||||
userLocation: .other,
|
||||
fileReference: .standalone(media: file),
|
||||
imageReference: nil,
|
||||
streamVideo: .story,
|
||||
loopVideo: true,
|
||||
enableSound: false,
|
||||
soundMuted: true,
|
||||
beginWithAmbientSound: false,
|
||||
mixWithOthers: true,
|
||||
useLargeThumbnail: false,
|
||||
autoFetchFullSizeThumbnail: false,
|
||||
tempFilePath: nil,
|
||||
captureProtected: false,
|
||||
hintDimensions: file.dimensions?.cgSize,
|
||||
storeAfterDownload: nil,
|
||||
displayImage: false,
|
||||
hasSentFramesToDisplay: { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.videoNode?.isHidden = false
|
||||
}
|
||||
),
|
||||
priority: .gallery
|
||||
)
|
||||
videoNode.canAttachContent = true
|
||||
videoNode.isUserInteractionEnabled = false
|
||||
videoNode.clipsToBounds = true
|
||||
self.addSubnode(videoNode)
|
||||
self.videoNode = videoNode
|
||||
self.setNeedsLayout()
|
||||
videoNode.play()
|
||||
}
|
||||
|
||||
public override func play() {
|
||||
self.isVisible = true
|
||||
self.applyVisibility()
|
||||
@@ -494,6 +503,13 @@ public class DrawingStickerEntityView: DrawingEntityView {
|
||||
guard let containerView = self.containerView, case let .image(image, _) = self.stickerEntity.content else {
|
||||
return
|
||||
}
|
||||
|
||||
let scaledSize = image.size.aspectFitted(CGSize(width: 180.0, height: 180.0))
|
||||
guard let scaledImage = generateScaledImage(image: image, size: scaledSize) else {
|
||||
self.isHidden = true
|
||||
completion()
|
||||
return
|
||||
}
|
||||
|
||||
let dustEffectLayer = DustEffectLayer()
|
||||
dustEffectLayer.position = containerView.bounds.center
|
||||
@@ -506,7 +522,7 @@ public class DrawingStickerEntityView: DrawingEntityView {
|
||||
containerView.layer.insertSublayer(dustEffectLayer, below: self.layer)
|
||||
|
||||
let itemFrame = self.layer.convert(self.bounds, to: dustEffectLayer)
|
||||
dustEffectLayer.addItem(frame: itemFrame, image: image)
|
||||
dustEffectLayer.addItem(frame: itemFrame, image: scaledImage)
|
||||
|
||||
self.isHidden = true
|
||||
}
|
||||
@@ -515,79 +531,6 @@ public class DrawingStickerEntityView: DrawingEntityView {
|
||||
let values = [self.entity.scale, self.entity.scale * 1.1, self.entity.scale]
|
||||
let keyTimes = [0.0, 0.67, 1.0]
|
||||
self.layer.animateKeyframes(values: values as [NSNumber], keyTimes: keyTimes as [NSNumber], duration: 0.35, keyPath: "transform.scale")
|
||||
// func blob(pointsCount: Int, randomness: CGFloat) -> [CGPoint] {
|
||||
// let angle = (CGFloat.pi * 2) / CGFloat(pointsCount)
|
||||
//
|
||||
// let rgen = { () -> CGFloat in
|
||||
// let accuracy: UInt32 = 1000
|
||||
// let random = arc4random_uniform(accuracy)
|
||||
// return CGFloat(random) / CGFloat(accuracy)
|
||||
// }
|
||||
// let rangeStart: CGFloat = 1 / (1 + randomness / 10)
|
||||
//
|
||||
// let startAngle = angle * CGFloat(arc4random_uniform(100)) / CGFloat(100)
|
||||
// let points = (0 ..< pointsCount).map { i -> CGPoint in
|
||||
// let randPointOffset = (rangeStart + CGFloat(rgen()) * (1 - rangeStart)) / 2
|
||||
// let angleRandomness: CGFloat = angle * 0.1
|
||||
// let randAngle = angle + angle * ((angleRandomness * CGFloat(arc4random_uniform(100)) / CGFloat(100)) - angleRandomness * 0.5)
|
||||
// let pointX = sin(startAngle + CGFloat(i) * randAngle)
|
||||
// let pointY = cos(startAngle + CGFloat(i) * randAngle)
|
||||
// return CGPoint(
|
||||
// x: pointX * randPointOffset,
|
||||
// y: pointY * randPointOffset
|
||||
// )
|
||||
// }
|
||||
// return points
|
||||
// }
|
||||
//
|
||||
// func generateNextBlob(for size: CGSize) -> [CGPoint] {
|
||||
// let pointsCount = 8
|
||||
// let minRandomness = 1.0
|
||||
// let maxRandomness = 1.0
|
||||
// let speedLevel = 0.8
|
||||
// let randomness = minRandomness + (maxRandomness - minRandomness) * speedLevel
|
||||
// return blob(pointsCount: pointsCount, randomness: randomness)
|
||||
// .map {
|
||||
// return CGPoint(
|
||||
// x: $0.x * CGFloat(size.width),
|
||||
// y: $0.y * CGFloat(size.height)
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// guard case let .image(image, _) = self.stickerEntity.content else {
|
||||
// return
|
||||
// }
|
||||
// let maskView = UIImageView()
|
||||
// maskView.frame = self.bounds
|
||||
// maskView.image = image
|
||||
// self.mask = maskView
|
||||
//
|
||||
// let blobLayer = CAShapeLayer()
|
||||
// blobLayer.strokeColor = UIColor.red.cgColor
|
||||
// blobLayer.fillColor = UIColor.clear.cgColor
|
||||
// blobLayer.lineWidth = 2.0
|
||||
// blobLayer.shadowRadius = 3.0
|
||||
// blobLayer.shadowOpacity = 0.8
|
||||
// blobLayer.shadowColor = UIColor.white.cgColor
|
||||
// blobLayer.position = CGPoint(
|
||||
// x: CGFloat.random(in: self.bounds.width * 0.33 ..< self.bounds.width * 0.5),
|
||||
// y: self.bounds.height * 0.5
|
||||
// )
|
||||
//
|
||||
//
|
||||
// let minSide = min(self.bounds.width, self.bounds.height)
|
||||
// let size = CGSize(width: minSide * 0.5, height: minSide * 0.5)
|
||||
// blobLayer.bounds = CGRect(origin: .zero, size: size)
|
||||
//
|
||||
// let points = generateNextBlob(for: size)
|
||||
// blobLayer.path = UIBezierPath.smoothCurve(through: points, length: size.width).cgPath
|
||||
// self.layer.addSublayer(blobLayer)
|
||||
//
|
||||
// blobLayer.animateScale(from: 0.01, to: 3.0, duration: 1.0, removeOnCompletion: false, completion: { _ in
|
||||
// blobLayer.removeFromSuperlayer()
|
||||
// self.mask = nil
|
||||
// })
|
||||
}
|
||||
|
||||
private var didApplyVisibility = false
|
||||
@@ -621,7 +564,14 @@ public class DrawingStickerEntityView: DrawingEntityView {
|
||||
}
|
||||
|
||||
if let videoNode = self.videoNode {
|
||||
videoNode.cornerRadius = floor(imageSize.width * 0.03)
|
||||
var imageSize = imageSize
|
||||
if case let .message(_, file, _) = self.stickerEntity.content, let dimensions = file?.dimensions {
|
||||
let fittedDimensions = dimensions.cgSize.aspectFitted(boundingSize)
|
||||
imageSize = fittedDimensions
|
||||
videoNode.cornerRadius = 0.0
|
||||
} else {
|
||||
videoNode.cornerRadius = floor(imageSize.width * 0.03)
|
||||
}
|
||||
videoNode.frame = CGRect(origin: CGPoint(x: floor((size.width - imageSize.width) * 0.5), y: floor((size.height - imageSize.height) * 0.5)), size: imageSize)
|
||||
videoNode.updateLayout(size: imageSize, transition: .immediate)
|
||||
}
|
||||
@@ -743,6 +693,15 @@ public class DrawingStickerEntityView: DrawingEntityView {
|
||||
selectionView.entityView = self
|
||||
return selectionView
|
||||
}
|
||||
|
||||
func getRenderSubEntities() -> [DrawingEntity] {
|
||||
guard case let .message(_, file, _) = self.stickerEntity.content else {
|
||||
return []
|
||||
}
|
||||
|
||||
let _ = file
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
final class DrawingStickerEntititySelectionView: DrawingEntitySelectionView {
|
||||
@@ -1277,3 +1236,48 @@ extension UIImageView {
|
||||
self.animationRepeatCount = 0
|
||||
}
|
||||
}
|
||||
|
||||
//private func prerenderEntityTransformations(entity: DrawingEntity, image: UIImage, colorSpace: CGColorSpace) -> UIImage {
|
||||
// let imageSize = image.size
|
||||
//
|
||||
// let angle: CGFloat
|
||||
// var scale: CGFloat
|
||||
// let position: CGPoint
|
||||
//
|
||||
// if let entity = entity as? DrawingStickerEntity {
|
||||
// angle = -entity.rotation
|
||||
// scale = entity.scale
|
||||
// position = entity.position
|
||||
// } else {
|
||||
// fatalError()
|
||||
// }
|
||||
//
|
||||
// let rotatedSize = CGSize(
|
||||
// width: abs(imageSize.width * cos(angle)) + abs(imageSize.height * sin(angle)),
|
||||
// height: abs(imageSize.width * sin(angle)) + abs(imageSize.height * cos(angle))
|
||||
// )
|
||||
// let newSize = CGSize(width: rotatedSize.width * scale, height: rotatedSize.height * scale)
|
||||
//
|
||||
// let newImage = generateImage(newSize, contextGenerator: { size, context in
|
||||
// context.setAllowsAntialiasing(true)
|
||||
// context.setShouldAntialias(true)
|
||||
// context.interpolationQuality = .high
|
||||
// context.clear(CGRect(origin: .zero, size: size))
|
||||
// context.translateBy(x: newSize.width * 0.5, y: newSize.height * 0.5)
|
||||
// context.rotate(by: angle)
|
||||
// context.scaleBy(x: scale, y: scale)
|
||||
// let drawRect = CGRect(
|
||||
// x: -imageSize.width * 0.5,
|
||||
// y: -imageSize.height * 0.5,
|
||||
// width: imageSize.width,
|
||||
// height: imageSize.height
|
||||
// )
|
||||
// if let cgImage = image.cgImage {
|
||||
// context.draw(cgImage, in: drawRect)
|
||||
// }
|
||||
// }, scale: 1.0)!
|
||||
//
|
||||
// let _ = position
|
||||
//
|
||||
// return newImage
|
||||
//}
|
||||
|
||||
@@ -68,37 +68,14 @@ public extension String {
|
||||
|
||||
var isSingleEmoji: Bool {
|
||||
return self.count == 1 && self.containsEmoji
|
||||
// return self.emojis.count == 1 && self.containsEmoji
|
||||
}
|
||||
|
||||
var containsEmoji: Bool {
|
||||
return self.contains { $0.isEmoji }
|
||||
//return self.unicodeScalars.contains { $0.isEmoji }
|
||||
}
|
||||
|
||||
var containsOnlyEmoji: Bool {
|
||||
return !self.isEmpty && !self.contains { !$0.isEmoji }
|
||||
// guard !self.isEmpty else {
|
||||
// return false
|
||||
// }
|
||||
// var nextShouldBeVariationSelector = false
|
||||
// for scalar in self.unicodeScalars {
|
||||
// if nextShouldBeVariationSelector {
|
||||
// if scalar == UnicodeScalar.VariationSelector {
|
||||
// nextShouldBeVariationSelector = false
|
||||
// continue
|
||||
// } else {
|
||||
// return false
|
||||
// }
|
||||
// }
|
||||
// if !scalar.isEmoji && scalar.maybeEmoji {
|
||||
// nextShouldBeVariationSelector = true
|
||||
// }
|
||||
// else if !scalar.isEmoji && scalar != UnicodeScalar.ZeroWidthJoiner {
|
||||
// return false
|
||||
// }
|
||||
// }
|
||||
// return !nextShouldBeVariationSelector
|
||||
}
|
||||
|
||||
var emojis: [String] {
|
||||
|
||||
@@ -95,6 +95,7 @@ public final class HashtagSearchController: TelegramBaseController {
|
||||
}, openStorageManagement: {
|
||||
}, openPasswordSetup: {
|
||||
}, openPremiumIntro: {
|
||||
}, openPremiumGift: {
|
||||
}, openActiveSessions: {
|
||||
}, performActiveSessionAction: { _, _ in
|
||||
}, openChatFolderUpdates: {
|
||||
|
||||
@@ -24,19 +24,21 @@ public class ItemListPeerActionItem: ListViewItem, ItemListItem {
|
||||
let icon: UIImage?
|
||||
let iconSignal: Signal<UIImage?, NoError>?
|
||||
let title: String
|
||||
let additionalBadgeIcon: UIImage?
|
||||
public let alwaysPlain: Bool
|
||||
let hasSeparator: Bool
|
||||
let editing: Bool
|
||||
let height: ItemListPeerActionItemHeight
|
||||
let color: ItemListPeerActionItemColor
|
||||
public let sectionId: ItemListSectionId
|
||||
let action: (() -> Void)?
|
||||
public let action: (() -> Void)?
|
||||
|
||||
public init(presentationData: ItemListPresentationData, icon: UIImage?, iconSignal: Signal<UIImage?, NoError>? = nil, title: String, alwaysPlain: Bool = false, hasSeparator: Bool = true, sectionId: ItemListSectionId, height: ItemListPeerActionItemHeight = .peerList, color: ItemListPeerActionItemColor = .accent, editing: Bool = false, action: (() -> Void)?) {
|
||||
public init(presentationData: ItemListPresentationData, icon: UIImage?, iconSignal: Signal<UIImage?, NoError>? = nil, title: String, additionalBadgeIcon: UIImage? = nil, alwaysPlain: Bool = false, hasSeparator: Bool = true, sectionId: ItemListSectionId, height: ItemListPeerActionItemHeight = .peerList, color: ItemListPeerActionItemColor = .accent, editing: Bool = false, action: (() -> Void)?) {
|
||||
self.presentationData = presentationData
|
||||
self.icon = icon
|
||||
self.iconSignal = iconSignal
|
||||
self.title = title
|
||||
self.additionalBadgeIcon = additionalBadgeIcon
|
||||
self.alwaysPlain = alwaysPlain
|
||||
self.hasSeparator = hasSeparator
|
||||
self.editing = editing
|
||||
@@ -102,7 +104,7 @@ public class ItemListPeerActionItem: ListViewItem, ItemListItem {
|
||||
}
|
||||
}
|
||||
|
||||
class ItemListPeerActionItemNode: ListViewItemNode {
|
||||
public final class ItemListPeerActionItemNode: ListViewItemNode {
|
||||
private let backgroundNode: ASDisplayNode
|
||||
private let topStripeNode: ASDisplayNode
|
||||
private let bottomStripeNode: ASDisplayNode
|
||||
@@ -111,6 +113,7 @@ class ItemListPeerActionItemNode: ListViewItemNode {
|
||||
|
||||
private let iconNode: ASImageNode
|
||||
private let titleNode: TextNode
|
||||
private var additionalLabelBadgeNode: ASImageNode?
|
||||
|
||||
private let activateArea: AccessibilityAreaNode
|
||||
|
||||
@@ -118,7 +121,7 @@ class ItemListPeerActionItemNode: ListViewItemNode {
|
||||
|
||||
private let iconDisposable = MetaDisposable()
|
||||
|
||||
init() {
|
||||
public init() {
|
||||
self.backgroundNode = ASDisplayNode()
|
||||
self.backgroundNode.isLayerBacked = true
|
||||
|
||||
@@ -157,7 +160,7 @@ class ItemListPeerActionItemNode: ListViewItemNode {
|
||||
self.iconDisposable.dispose()
|
||||
}
|
||||
|
||||
func asyncLayout() -> (_ item: ItemListPeerActionItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, (Bool) -> Void) {
|
||||
public func asyncLayout() -> (_ item: ItemListPeerActionItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, (Bool) -> Void) {
|
||||
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
|
||||
|
||||
let currentItem = self.item
|
||||
@@ -177,7 +180,7 @@ class ItemListPeerActionItemNode: ListViewItemNode {
|
||||
switch item.height {
|
||||
case .generic:
|
||||
iconOffset = 1.0
|
||||
verticalInset = 11.0
|
||||
verticalInset = 12.0
|
||||
verticalOffset = 0.0
|
||||
leftInset = (item.icon == nil && item.iconSignal == nil ? 16.0 : 59.0) + params.leftInset
|
||||
case .peerList:
|
||||
@@ -296,15 +299,37 @@ class ItemListPeerActionItemNode: ListViewItemNode {
|
||||
strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight))
|
||||
transition.updateFrame(node: strongSelf.bottomStripeNode, frame: CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight)))
|
||||
|
||||
transition.updateFrame(node: strongSelf.titleNode, frame: CGRect(origin: CGPoint(x: leftInset + editingOffset, y: verticalInset + verticalOffset), size: titleLayout.size))
|
||||
let titleFrame = CGRect(origin: CGPoint(x: leftInset + editingOffset, y: verticalInset + verticalOffset), size: titleLayout.size)
|
||||
transition.updateFrame(node: strongSelf.titleNode, frame: titleFrame)
|
||||
|
||||
strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: layout.contentSize.height + UIScreenPixel + UIScreenPixel))
|
||||
|
||||
if let additionalBadgeIcon = item.additionalBadgeIcon {
|
||||
let additionalLabelBadgeNode: ASImageNode
|
||||
if let current = strongSelf.additionalLabelBadgeNode {
|
||||
additionalLabelBadgeNode = current
|
||||
} else {
|
||||
additionalLabelBadgeNode = ASImageNode()
|
||||
additionalLabelBadgeNode.isUserInteractionEnabled = false
|
||||
strongSelf.additionalLabelBadgeNode = additionalLabelBadgeNode
|
||||
strongSelf.addSubnode(additionalLabelBadgeNode)
|
||||
}
|
||||
additionalLabelBadgeNode.image = additionalBadgeIcon
|
||||
|
||||
let additionalLabelSize = additionalBadgeIcon.size
|
||||
additionalLabelBadgeNode.frame = CGRect(origin: CGPoint(x: titleFrame.maxX + 6.0, y: floor((contentSize.height - additionalLabelSize.height) / 2.0)), size: additionalLabelSize)
|
||||
} else {
|
||||
if let additionalLabelBadgeNode = strongSelf.additionalLabelBadgeNode {
|
||||
strongSelf.additionalLabelBadgeNode = nil
|
||||
additionalLabelBadgeNode.removeFromSupernode()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
override func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) {
|
||||
public override func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) {
|
||||
super.setHighlighted(highlighted, at: point, animated: animated)
|
||||
|
||||
if highlighted {
|
||||
@@ -342,11 +367,11 @@ class ItemListPeerActionItemNode: ListViewItemNode {
|
||||
}
|
||||
}
|
||||
|
||||
override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) {
|
||||
public override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) {
|
||||
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
|
||||
}
|
||||
|
||||
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
|
||||
public override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
|
||||
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ public class ItemListActionItem: ListViewItem, ItemListItem {
|
||||
|
||||
Queue.mainQueue().async {
|
||||
completion(node, {
|
||||
return (nil, { _ in apply() })
|
||||
return (nil, { _ in apply(false) })
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -67,7 +67,7 @@ public class ItemListActionItem: ListViewItem, ItemListItem {
|
||||
let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
|
||||
Queue.mainQueue().async {
|
||||
completion(layout, { _ in
|
||||
apply()
|
||||
apply(false)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -130,7 +130,7 @@ public class ItemListActionItemNode: ListViewItemNode, ItemListItemNode {
|
||||
self.addSubnode(self.activateArea)
|
||||
}
|
||||
|
||||
public func asyncLayout() -> (_ item: ItemListActionItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
|
||||
public func asyncLayout() -> (_ item: ItemListActionItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, (Bool) -> Void) {
|
||||
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
|
||||
|
||||
let currentItem = self.item
|
||||
@@ -179,7 +179,7 @@ public class ItemListActionItemNode: ListViewItemNode, ItemListItemNode {
|
||||
|
||||
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
|
||||
|
||||
return (layout, { [weak self] in
|
||||
return (layout, { [weak self] _ in
|
||||
if let strongSelf = self {
|
||||
strongSelf.item = item
|
||||
|
||||
|
||||
@@ -93,6 +93,7 @@ public final class ListSectionHeaderNode: ASDisplayNode {
|
||||
self.label = ImmediateTextNode()
|
||||
self.label.isUserInteractionEnabled = false
|
||||
self.label.isAccessibilityElement = true
|
||||
self.label.displaysAsynchronously = false
|
||||
|
||||
super.init()
|
||||
|
||||
|
||||
@@ -182,17 +182,17 @@ private enum CreateGiveawayEntry: ItemListNodeEntry {
|
||||
return 202
|
||||
case .prizeDescriptionInfo:
|
||||
return 203
|
||||
case .timeHeader:
|
||||
return 204
|
||||
case .timeExpiryDate:
|
||||
return 205
|
||||
case .timeCustomPicker:
|
||||
return 206
|
||||
case .timeInfo:
|
||||
return 207
|
||||
case .winners:
|
||||
return 208
|
||||
return 204
|
||||
case .winnersInfo:
|
||||
return 205
|
||||
case .timeHeader:
|
||||
return 206
|
||||
case .timeExpiryDate:
|
||||
return 207
|
||||
case .timeCustomPicker:
|
||||
return 208
|
||||
case .timeInfo:
|
||||
return 209
|
||||
}
|
||||
}
|
||||
@@ -777,12 +777,12 @@ private func createGiveawayControllerEntries(
|
||||
}
|
||||
entries.append(.prizeDescriptionInfo(presentationData.theme, prizeDescriptionInfoText))
|
||||
|
||||
entries.append(.winners(presentationData.theme, presentationData.strings.BoostGift_Winners, state.showWinners))
|
||||
entries.append(.winnersInfo(presentationData.theme, presentationData.strings.BoostGift_WinnersInfo))
|
||||
|
||||
entries.append(.timeHeader(presentationData.theme, presentationData.strings.BoostGift_DateTitle.uppercased()))
|
||||
entries.append(.timeCustomPicker(presentationData.theme, presentationData.dateTimeFormat, state.time, minDate, maxDate, state.pickingExpiryDate, state.pickingExpiryTime))
|
||||
entries.append(.timeInfo(presentationData.theme, presentationData.strings.BoostGift_DateInfo(presentationData.strings.BoostGift_DateInfoSubscribers(Int32(state.subscriptions))).string))
|
||||
|
||||
entries.append(.winners(presentationData.theme, presentationData.strings.BoostGift_Winners, state.showWinners))
|
||||
entries.append(.winnersInfo(presentationData.theme, presentationData.strings.BoostGift_WinnersInfo))
|
||||
case .gift:
|
||||
appendDurationEntries()
|
||||
}
|
||||
@@ -803,7 +803,7 @@ private struct CreateGiveawayControllerState: Equatable {
|
||||
var selectedMonths: Int32?
|
||||
var countries: [String] = []
|
||||
var onlyNewEligible: Bool = false
|
||||
var showWinners: Bool = false
|
||||
var showWinners: Bool = true
|
||||
var showPrizeDescription: Bool = false
|
||||
var prizeDescription: String = ""
|
||||
var time: Int32
|
||||
|
||||
@@ -722,15 +722,38 @@ private final class LimitSheetContent: CombinedComponent {
|
||||
|
||||
var levelsHeight: CGFloat = 0.0
|
||||
var levelItems: [AnyComponentWithIdentity<Empty>] = []
|
||||
|
||||
var nameColorsAtLevel: [(Int32, Int32)] = []
|
||||
var nameColorsCountMap: [Int32: Int32] = [:]
|
||||
for color in context.component.context.peerNameColors.displayOrder {
|
||||
if let level = context.component.context.peerNameColors.nameColorsChannelMinRequiredBoostLevel[color] {
|
||||
if let current = nameColorsCountMap[level] {
|
||||
nameColorsCountMap[level] = current + 1
|
||||
} else {
|
||||
nameColorsCountMap[level] = 1
|
||||
}
|
||||
}
|
||||
}
|
||||
for (key, value) in nameColorsCountMap {
|
||||
nameColorsAtLevel.append((key, value))
|
||||
}
|
||||
|
||||
if let nextLevels {
|
||||
for level in nextLevels {
|
||||
var perks: [LevelSectionComponent.Perk] = []
|
||||
perks.append(.story(level))
|
||||
perks.append(.reaction(level))
|
||||
|
||||
if level >= premiumConfiguration.minChannelNameColorLevel {
|
||||
perks.append(.nameColor(7))
|
||||
|
||||
var nameColorsCount: Int32 = 0
|
||||
for (colorLevel, count) in nameColorsAtLevel {
|
||||
if level >= colorLevel && colorLevel == 1 {
|
||||
nameColorsCount = count
|
||||
}
|
||||
}
|
||||
if nameColorsCount > 0 {
|
||||
perks.append(.nameColor(nameColorsCount))
|
||||
}
|
||||
|
||||
if level >= premiumConfiguration.minChannelProfileColorLevel {
|
||||
let delta = min(level - premiumConfiguration.minChannelProfileColorLevel + 1, 2)
|
||||
perks.append(.profileColor(8 * delta))
|
||||
@@ -738,10 +761,17 @@ private final class LimitSheetContent: CombinedComponent {
|
||||
if level >= premiumConfiguration.minChannelProfileIconLevel {
|
||||
perks.append(.profileIcon)
|
||||
}
|
||||
if level >= premiumConfiguration.minChannelNameColorLevel {
|
||||
let delta = min(level - premiumConfiguration.minChannelNameColorLevel + 1, 3)
|
||||
perks.append(.linkColor(7 * delta))
|
||||
|
||||
var linkColorsCount: Int32 = 0
|
||||
for (colorLevel, count) in nameColorsAtLevel {
|
||||
if level >= colorLevel {
|
||||
linkColorsCount += count
|
||||
}
|
||||
}
|
||||
if linkColorsCount > 0 {
|
||||
perks.append(.linkColor(linkColorsCount))
|
||||
}
|
||||
|
||||
if level >= premiumConfiguration.minChannelNameIconLevel {
|
||||
perks.append(.linkIcon)
|
||||
}
|
||||
|
||||
@@ -28,14 +28,16 @@ private final class ReplaceBoostScreenComponent: CombinedComponent {
|
||||
let initiallySelectedSlot: Int32?
|
||||
let selectedSlotsUpdated: ([Int32]) -> Void
|
||||
let presentController: (ViewController) -> Void
|
||||
let giftPremium: () -> Void
|
||||
|
||||
init(context: AccountContext, peerId: EnginePeer.Id, myBoostStatus: MyBoostStatus, initiallySelectedSlot: Int32?, selectedSlotsUpdated: @escaping ([Int32]) -> Void, presentController: @escaping (ViewController) -> Void) {
|
||||
init(context: AccountContext, peerId: EnginePeer.Id, myBoostStatus: MyBoostStatus, initiallySelectedSlot: Int32?, selectedSlotsUpdated: @escaping ([Int32]) -> Void, presentController: @escaping (ViewController) -> Void, giftPremium: @escaping () -> Void) {
|
||||
self.context = context
|
||||
self.peerId = peerId
|
||||
self.myBoostStatus = myBoostStatus
|
||||
self.initiallySelectedSlot = initiallySelectedSlot
|
||||
self.selectedSlotsUpdated = selectedSlotsUpdated
|
||||
self.presentController = presentController
|
||||
self.giftPremium = giftPremium
|
||||
}
|
||||
|
||||
static func ==(lhs: ReplaceBoostScreenComponent, rhs: ReplaceBoostScreenComponent) -> Bool {
|
||||
@@ -170,14 +172,26 @@ private final class ReplaceBoostScreenComponent: CombinedComponent {
|
||||
if channelName.count > 48 {
|
||||
channelName = "\(channelName.prefix(48))..."
|
||||
}
|
||||
let descriptionString = strings.ReassignBoost_Description(channelName, "\(premiumConfiguration.boostsPerGiftCount)").string
|
||||
let descriptionString = strings.ReassignBoost_DescriptionWithLink(channelName, "\(premiumConfiguration.boostsPerGiftCount)").string
|
||||
|
||||
let giftPremium = context.component.giftPremium
|
||||
let description = description.update(
|
||||
component: MultilineTextComponent(
|
||||
text: .markdown(text: descriptionString, attributes: markdownAttributes),
|
||||
horizontalAlignment: .center,
|
||||
maximumNumberOfLines: 0,
|
||||
lineSpacing: 0.1
|
||||
lineSpacing: 0.1,
|
||||
highlightColor: environment.theme.list.itemAccentColor.withAlphaComponent(0.2),
|
||||
highlightAction: { attributes in
|
||||
if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] {
|
||||
return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
},
|
||||
tapAction: { _, _ in
|
||||
giftPremium()
|
||||
}
|
||||
),
|
||||
environment: {},
|
||||
availableSize: CGSize(width: availableSize.width - sideInset * 2.0 - textSideInset, height: availableSize.height),
|
||||
@@ -229,7 +243,7 @@ private final class ReplaceBoostScreenComponent: CombinedComponent {
|
||||
selectionPosition: .right,
|
||||
isEnabled: isEnabled,
|
||||
hasNext: i != occupiedBoosts.count - 1,
|
||||
action: { [weak state] _, _ in
|
||||
action: { [weak state] _, _, _ in
|
||||
guard let state, hasSelection else {
|
||||
return
|
||||
}
|
||||
@@ -831,10 +845,13 @@ public class ReplaceBoostScreen: ViewController {
|
||||
|
||||
var selectedSlotsUpdatedImpl: (([Int32]) -> Void)?
|
||||
var presentControllerImpl: ((ViewController) -> Void)?
|
||||
var giftPremiumImpl: (() -> Void)?
|
||||
self.init(context: context, component: ReplaceBoostScreenComponent(context: context, peerId: peerId, myBoostStatus: myBoostStatus, initiallySelectedSlot: initiallySelectedSlot, selectedSlotsUpdated: { slots in
|
||||
selectedSlotsUpdatedImpl?(slots)
|
||||
}, presentController: { c in
|
||||
presentControllerImpl?(c)
|
||||
}, giftPremium: {
|
||||
giftPremiumImpl?()
|
||||
}))
|
||||
|
||||
self.title = presentationData.strings.ReassignBoost_Title
|
||||
@@ -856,6 +873,17 @@ public class ReplaceBoostScreen: ViewController {
|
||||
if let initiallySelectedSlot {
|
||||
self.node.selectedSlots = [initiallySelectedSlot]
|
||||
}
|
||||
|
||||
giftPremiumImpl = { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
let navigationController = self.navigationController
|
||||
self.dismiss(animated: true, completion: {
|
||||
let giftController = context.sharedContext.makePremiumGiftController(context: context)
|
||||
navigationController?.pushViewController(giftController, animated: true)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private init<C: Component>(context: AccountContext, component: C, theme: PresentationTheme? = nil) where C.EnvironmentType == ViewControllerComponentContainer.Environment {
|
||||
|
||||
@@ -202,6 +202,12 @@ public func combineLatest<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13
|
||||
}, initialValues: [:], queue: queue)
|
||||
}
|
||||
|
||||
public func combineLatest<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20, E>(queue: Queue? = nil, _ s1: Signal<T1, E>, _ s2: Signal<T2, E>, _ s3: Signal<T3, E>, _ s4: Signal<T4, E>, _ s5: Signal<T5, E>, _ s6: Signal<T6, E>, _ s7: Signal<T7, E>, _ s8: Signal<T8, E>, _ s9: Signal<T9, E>, _ s10: Signal<T10, E>, _ s11: Signal<T11, E>, _ s12: Signal<T12, E>, _ s13: Signal<T13, E>, _ s14: Signal<T14, E>, _ s15: Signal<T15, E>, _ s16: Signal<T16, E>, _ s17: Signal<T17, E>, _ s18: Signal<T18, E>, _ s19: Signal<T19, E>, _ s20: Signal<T20, E>) -> Signal<(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20), E> {
|
||||
return combineLatestAny([signalOfAny(s1), signalOfAny(s2), signalOfAny(s3), signalOfAny(s4), signalOfAny(s5), signalOfAny(s6), signalOfAny(s7), signalOfAny(s8), signalOfAny(s9), signalOfAny(s10), signalOfAny(s11), signalOfAny(s12), signalOfAny(s13), signalOfAny(s14), signalOfAny(s15), signalOfAny(s16), signalOfAny(s17), signalOfAny(s18), signalOfAny(s19), signalOfAny(s20)], combine: { values in
|
||||
return (values[0] as! T1, values[1] as! T2, values[2] as! T3, values[3] as! T4, values[4] as! T5, values[5] as! T6, values[6] as! T7, values[7] as! T8, values[8] as! T9, values[9] as! T10, values[10] as! T11, values[11] as! T12, values[12] as! T13, values[13] as! T14, values[14] as! T15, values[15] as! T16, values[16] as! T17, values[17] as! T18, values[18] as! T19, values[19] as! T20)
|
||||
}, initialValues: [:], queue: queue)
|
||||
}
|
||||
|
||||
public func combineLatest<T, E>(queue: Queue? = nil, _ signals: [Signal<T, E>]) -> Signal<[T], E> {
|
||||
if signals.count == 0 {
|
||||
return single([T](), E.self)
|
||||
|
||||
@@ -121,6 +121,9 @@ swift_library(
|
||||
"//submodules/TelegramUI/Components/Settings/ThemeSettingsThemeItem",
|
||||
"//submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen",
|
||||
"//submodules/TelegramUI/Components/Settings/SettingsThemeWallpaperNode",
|
||||
"//submodules/TelegramUI/Components/Settings/WallpaperGridScreen",
|
||||
"//submodules/TelegramUI/Components/Settings/ThemeAccentColorScreen",
|
||||
"//submodules/TelegramUI/Components/Settings/GenerateThemeName",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
||||
@@ -22,6 +22,7 @@ import QrCodeUI
|
||||
import PremiumUI
|
||||
import StorageUsageScreen
|
||||
import PeerInfoStoryGridScreen
|
||||
import WallpaperGridScreen
|
||||
|
||||
enum SettingsSearchableItemIcon {
|
||||
case profile
|
||||
|
||||
@@ -222,7 +222,7 @@ private final class TextSizeSelectionControllerNode: ASDisplayNode, UIScrollView
|
||||
}, messageSelected: { _, _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, setPeerThreadMuted: { _, _, _ in }, deletePeer: { _, _ in }, deletePeerThread: { _, _ in }, setPeerThreadStopped: { _, _, _ in }, setPeerThreadPinned: { _, _, _ in }, setPeerThreadHidden: { _, _, _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, toggleThreadsSelection: { _, _ in }, hidePsa: { _ in
|
||||
}, activateChatPreview: { _, _, _, gesture, _ in
|
||||
gesture?.cancel()
|
||||
}, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openActiveSessions: {
|
||||
}, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openPremiumGift: {}, openActiveSessions: {
|
||||
}, performActiveSessionAction: { _, _ in
|
||||
}, openChatFolderUpdates: {}, hideChatFolderUpdates: {
|
||||
}, openStories: { _, _ in
|
||||
|
||||
@@ -20,6 +20,7 @@ import UndoUI
|
||||
import ItemListPeerActionItem
|
||||
import AnimationUI
|
||||
import ThemeSettingsThemeItem
|
||||
import ThemeAccentColorScreen
|
||||
|
||||
private final class ThemePickerControllerArguments {
|
||||
let context: AccountContext
|
||||
|
||||
@@ -10,8 +10,10 @@ import MediaResources
|
||||
import AccountContext
|
||||
import LegacyUI
|
||||
import LegacyMediaPickerUI
|
||||
import LegacyComponents
|
||||
import LocalMediaResources
|
||||
import ImageBlur
|
||||
import WallpaperGridScreen
|
||||
import WallpaperGalleryScreen
|
||||
|
||||
func presentCustomWallpaperPicker(context: AccountContext, present: @escaping (ViewController) -> Void, push: @escaping (ViewController) -> Void) {
|
||||
@@ -47,275 +49,3 @@ func presentCustomWallpaperPicker(context: AccountContext, present: @escaping (V
|
||||
present(legacyController)
|
||||
})
|
||||
}
|
||||
|
||||
func uploadCustomWallpaper(context: AccountContext, wallpaper: WallpaperGalleryEntry, mode: WallpaperPresentationOptions, editedImage: UIImage?, cropRect: CGRect?, brightness: CGFloat?, completion: @escaping () -> Void) {
|
||||
var imageSignal: Signal<UIImage, NoError>
|
||||
switch wallpaper {
|
||||
case let .wallpaper(wallpaper, _):
|
||||
switch wallpaper {
|
||||
case let .file(file):
|
||||
if let path = context.account.postbox.mediaBox.completedResourcePath(file.file.resource), let data = try? Data(contentsOf: URL(fileURLWithPath: path), options: .mappedRead) {
|
||||
context.sharedContext.accountManager.mediaBox.storeResourceData(file.file.resource.id, data: data)
|
||||
let _ = context.sharedContext.accountManager.mediaBox.cachedResourceRepresentation(file.file.resource, representation: CachedScaledImageRepresentation(size: CGSize(width: 720.0, height: 720.0), mode: .aspectFit), complete: true, fetch: true).start()
|
||||
let _ = context.sharedContext.accountManager.mediaBox.cachedResourceRepresentation(file.file.resource, representation: CachedBlurredWallpaperRepresentation(), complete: true, fetch: true).start()
|
||||
}
|
||||
case let .image(representations, _):
|
||||
for representation in representations {
|
||||
let resource = representation.resource
|
||||
if let path = context.account.postbox.mediaBox.completedResourcePath(resource), let data = try? Data(contentsOf: URL(fileURLWithPath: path), options: .mappedRead) {
|
||||
context.sharedContext.accountManager.mediaBox.storeResourceData(resource.id, data: data)
|
||||
let _ = context.sharedContext.accountManager.mediaBox.cachedResourceRepresentation(resource, representation: CachedScaledImageRepresentation(size: CGSize(width: 720.0, height: 720.0), mode: .aspectFit), complete: true, fetch: true).start()
|
||||
}
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
imageSignal = .complete()
|
||||
completion()
|
||||
case let .asset(asset):
|
||||
imageSignal = fetchPhotoLibraryImage(localIdentifier: asset.localIdentifier, thumbnail: false)
|
||||
|> filter { value in
|
||||
return !(value?.1 ?? true)
|
||||
}
|
||||
|> mapToSignal { result -> Signal<UIImage, NoError> in
|
||||
if let result = result {
|
||||
return .single(result.0)
|
||||
} else {
|
||||
return .complete()
|
||||
}
|
||||
}
|
||||
case let .contextResult(result):
|
||||
var imageResource: TelegramMediaResource?
|
||||
switch result {
|
||||
case let .externalReference(externalReference):
|
||||
if let content = externalReference.content {
|
||||
imageResource = content.resource
|
||||
}
|
||||
case let .internalReference(internalReference):
|
||||
if let image = internalReference.image {
|
||||
if let imageRepresentation = imageRepresentationLargerThan(image.representations, size: PixelDimensions(width: 1000, height: 800)) {
|
||||
imageResource = imageRepresentation.resource
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let imageResource = imageResource {
|
||||
imageSignal = .single(context.account.postbox.mediaBox.completedResourcePath(imageResource))
|
||||
|> mapToSignal { path -> Signal<UIImage, NoError> in
|
||||
if let path = path, let data = try? Data(contentsOf: URL(fileURLWithPath: path), options: [.mappedIfSafe]), let image = UIImage(data: data) {
|
||||
return .single(image)
|
||||
} else {
|
||||
return .complete()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
imageSignal = .complete()
|
||||
}
|
||||
}
|
||||
|
||||
if let editedImage {
|
||||
imageSignal = .single(editedImage)
|
||||
}
|
||||
|
||||
let _ = (imageSignal
|
||||
|> map { image -> UIImage in
|
||||
var croppedImage = UIImage()
|
||||
|
||||
let finalCropRect: CGRect
|
||||
if let cropRect = cropRect {
|
||||
finalCropRect = cropRect
|
||||
} else {
|
||||
let screenSize = TGScreenSize()
|
||||
let fittedSize = TGScaleToFit(screenSize, image.size)
|
||||
finalCropRect = CGRect(x: (image.size.width - fittedSize.width) / 2.0, y: (image.size.height - fittedSize.height) / 2.0, width: fittedSize.width, height: fittedSize.height)
|
||||
}
|
||||
croppedImage = TGPhotoEditorCrop(image, nil, .up, 0.0, finalCropRect, false, CGSize(width: 1440.0, height: 2960.0), image.size, true)
|
||||
|
||||
let thumbnailDimensions = finalCropRect.size.fitted(CGSize(width: 320.0, height: 320.0))
|
||||
let thumbnailImage = generateScaledImage(image: croppedImage, size: thumbnailDimensions, scale: 1.0)
|
||||
|
||||
if let data = croppedImage.jpegData(compressionQuality: 0.8), let thumbnailImage = thumbnailImage, let thumbnailData = thumbnailImage.jpegData(compressionQuality: 0.4) {
|
||||
let thumbnailResource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max))
|
||||
context.sharedContext.accountManager.mediaBox.storeResourceData(thumbnailResource.id, data: thumbnailData, synchronous: true)
|
||||
context.account.postbox.mediaBox.storeResourceData(thumbnailResource.id, data: thumbnailData, synchronous: true)
|
||||
|
||||
let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max))
|
||||
context.sharedContext.accountManager.mediaBox.storeResourceData(resource.id, data: data, synchronous: true)
|
||||
context.account.postbox.mediaBox.storeResourceData(resource.id, data: data, synchronous: true)
|
||||
|
||||
let autoNightModeTriggered = context.sharedContext.currentPresentationData.with {$0 }.autoNightModeTriggered
|
||||
let accountManager = context.sharedContext.accountManager
|
||||
let account = context.account
|
||||
let updateWallpaper: (TelegramWallpaper) -> Void = { wallpaper in
|
||||
var resource: MediaResource?
|
||||
if case let .image(representations, _) = wallpaper, let representation = largestImageRepresentation(representations) {
|
||||
resource = representation.resource
|
||||
} else if case let .file(file) = wallpaper {
|
||||
resource = file.file.resource
|
||||
}
|
||||
|
||||
if let resource = resource {
|
||||
let _ = accountManager.mediaBox.cachedResourceRepresentation(resource, representation: CachedScaledImageRepresentation(size: CGSize(width: 720.0, height: 720.0), mode: .aspectFit), complete: true, fetch: true).start(completed: {})
|
||||
let _ = account.postbox.mediaBox.cachedResourceRepresentation(resource, representation: CachedScaledImageRepresentation(size: CGSize(width: 720.0, height: 720.0), mode: .aspectFit), complete: true, fetch: true).start(completed: {})
|
||||
}
|
||||
|
||||
let _ = (updatePresentationThemeSettingsInteractively(accountManager: accountManager, { current in
|
||||
var themeSpecificChatWallpapers = current.themeSpecificChatWallpapers
|
||||
let themeReference: PresentationThemeReference
|
||||
if autoNightModeTriggered {
|
||||
themeReference = current.automaticThemeSwitchSetting.theme
|
||||
} else {
|
||||
themeReference = current.theme
|
||||
}
|
||||
let accentColor = current.themeSpecificAccentColors[themeReference.index]
|
||||
if let accentColor = accentColor, accentColor.baseColor == .custom {
|
||||
themeSpecificChatWallpapers[coloredThemeIndex(reference: themeReference, accentColor: accentColor)] = wallpaper
|
||||
} else {
|
||||
themeSpecificChatWallpapers[coloredThemeIndex(reference: themeReference, accentColor: accentColor)] = nil
|
||||
themeSpecificChatWallpapers[themeReference.index] = wallpaper
|
||||
}
|
||||
return current.withUpdatedThemeSpecificChatWallpapers(themeSpecificChatWallpapers)
|
||||
})).start()
|
||||
}
|
||||
|
||||
let apply: () -> Void = {
|
||||
let settings = WallpaperSettings(blur: mode.contains(.blur), motion: mode.contains(.motion), colors: [], intensity: nil)
|
||||
let wallpaper: TelegramWallpaper = .image([TelegramMediaImageRepresentation(dimensions: PixelDimensions(thumbnailDimensions), resource: thumbnailResource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false), TelegramMediaImageRepresentation(dimensions: PixelDimensions(croppedImage.size), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)], settings)
|
||||
updateWallpaper(wallpaper)
|
||||
DispatchQueue.main.async {
|
||||
completion()
|
||||
}
|
||||
}
|
||||
|
||||
if mode.contains(.blur) {
|
||||
let representation = CachedBlurredWallpaperRepresentation()
|
||||
let _ = context.account.postbox.mediaBox.cachedResourceRepresentation(resource, representation: representation, complete: true, fetch: true).start()
|
||||
let _ = context.sharedContext.accountManager.mediaBox.cachedResourceRepresentation(resource, representation: representation, complete: true, fetch: true).start(completed: {
|
||||
apply()
|
||||
})
|
||||
} else {
|
||||
apply()
|
||||
}
|
||||
}
|
||||
return croppedImage
|
||||
}).start()
|
||||
}
|
||||
|
||||
public func uploadCustomPeerWallpaper(context: AccountContext, wallpaper: WallpaperGalleryEntry, mode: WallpaperPresentationOptions, editedImage: UIImage?, cropRect: CGRect?, brightness: CGFloat?, peerId: PeerId, forBoth: Bool, completion: @escaping () -> Void) {
|
||||
var imageSignal: Signal<UIImage, NoError>
|
||||
switch wallpaper {
|
||||
case let .wallpaper(wallpaper, _):
|
||||
imageSignal = .complete()
|
||||
switch wallpaper {
|
||||
case let .file(file):
|
||||
if let path = context.account.postbox.mediaBox.completedResourcePath(file.file.resource), let data = try? Data(contentsOf: URL(fileURLWithPath: path), options: .mappedRead), let image = UIImage(data: data) {
|
||||
context.sharedContext.accountManager.mediaBox.storeResourceData(file.file.resource.id, data: data, synchronous: true)
|
||||
let _ = context.sharedContext.accountManager.mediaBox.cachedResourceRepresentation(file.file.resource, representation: CachedScaledImageRepresentation(size: CGSize(width: 720.0, height: 720.0), mode: .aspectFit), complete: true, fetch: true).start()
|
||||
let _ = context.sharedContext.accountManager.mediaBox.cachedResourceRepresentation(file.file.resource, representation: CachedBlurredWallpaperRepresentation(), complete: true, fetch: true).start()
|
||||
|
||||
imageSignal = .single(image)
|
||||
}
|
||||
case let .image(representations, _):
|
||||
for representation in representations {
|
||||
let resource = representation.resource
|
||||
if let path = context.account.postbox.mediaBox.completedResourcePath(resource), let data = try? Data(contentsOf: URL(fileURLWithPath: path), options: .mappedRead) {
|
||||
context.sharedContext.accountManager.mediaBox.storeResourceData(resource.id, data: data, synchronous: true)
|
||||
let _ = context.sharedContext.accountManager.mediaBox.cachedResourceRepresentation(resource, representation: CachedScaledImageRepresentation(size: CGSize(width: 720.0, height: 720.0), mode: .aspectFit), complete: true, fetch: true).start()
|
||||
}
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
completion()
|
||||
case let .asset(asset):
|
||||
imageSignal = fetchPhotoLibraryImage(localIdentifier: asset.localIdentifier, thumbnail: false)
|
||||
|> filter { value in
|
||||
return !(value?.1 ?? true)
|
||||
}
|
||||
|> mapToSignal { result -> Signal<UIImage, NoError> in
|
||||
if let result = result {
|
||||
return .single(result.0)
|
||||
} else {
|
||||
return .complete()
|
||||
}
|
||||
}
|
||||
case let .contextResult(result):
|
||||
var imageResource: TelegramMediaResource?
|
||||
switch result {
|
||||
case let .externalReference(externalReference):
|
||||
if let content = externalReference.content {
|
||||
imageResource = content.resource
|
||||
}
|
||||
case let .internalReference(internalReference):
|
||||
if let image = internalReference.image {
|
||||
if let imageRepresentation = imageRepresentationLargerThan(image.representations, size: PixelDimensions(width: 1000, height: 800)) {
|
||||
imageResource = imageRepresentation.resource
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let imageResource = imageResource {
|
||||
imageSignal = .single(context.account.postbox.mediaBox.completedResourcePath(imageResource))
|
||||
|> mapToSignal { path -> Signal<UIImage, NoError> in
|
||||
if let path = path, let data = try? Data(contentsOf: URL(fileURLWithPath: path), options: [.mappedIfSafe]), let image = UIImage(data: data) {
|
||||
return .single(image)
|
||||
} else {
|
||||
return .complete()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
imageSignal = .complete()
|
||||
}
|
||||
}
|
||||
|
||||
if let editedImage {
|
||||
imageSignal = .single(editedImage)
|
||||
}
|
||||
|
||||
let _ = (imageSignal
|
||||
|> map { image -> UIImage in
|
||||
var croppedImage = UIImage()
|
||||
|
||||
let finalCropRect: CGRect
|
||||
if let cropRect = cropRect {
|
||||
finalCropRect = cropRect
|
||||
} else {
|
||||
let screenSize = TGScreenSize()
|
||||
let fittedSize = TGScaleToFit(screenSize, image.size)
|
||||
finalCropRect = CGRect(x: (image.size.width - fittedSize.width) / 2.0, y: (image.size.height - fittedSize.height) / 2.0, width: fittedSize.width, height: fittedSize.height)
|
||||
}
|
||||
croppedImage = TGPhotoEditorCrop(image, nil, .up, 0.0, finalCropRect, false, CGSize(width: 1440.0, height: 2960.0), image.size, true)
|
||||
|
||||
let thumbnailDimensions = finalCropRect.size.fitted(CGSize(width: 320.0, height: 320.0))
|
||||
let thumbnailImage = generateScaledImage(image: croppedImage, size: thumbnailDimensions, scale: 1.0)
|
||||
|
||||
if let data = croppedImage.jpegData(compressionQuality: 0.8), let thumbnailImage = thumbnailImage, let thumbnailData = thumbnailImage.jpegData(compressionQuality: 0.4) {
|
||||
let thumbnailResource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max))
|
||||
context.sharedContext.accountManager.mediaBox.storeResourceData(thumbnailResource.id, data: thumbnailData, synchronous: true)
|
||||
context.account.postbox.mediaBox.storeResourceData(thumbnailResource.id, data: thumbnailData, synchronous: true)
|
||||
|
||||
let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max))
|
||||
context.sharedContext.accountManager.mediaBox.storeResourceData(resource.id, data: data, synchronous: true)
|
||||
context.account.postbox.mediaBox.storeResourceData(resource.id, data: data, synchronous: true)
|
||||
|
||||
let _ = context.sharedContext.accountManager.mediaBox.cachedResourceRepresentation(resource, representation: CachedBlurredWallpaperRepresentation(), complete: true, fetch: true).start()
|
||||
|
||||
var intensity: Int32?
|
||||
if let brightness {
|
||||
intensity = max(0, min(100, Int32(brightness * 100.0)))
|
||||
}
|
||||
|
||||
Queue.mainQueue().after(0.05) {
|
||||
let settings = WallpaperSettings(blur: mode.contains(.blur), motion: mode.contains(.motion), colors: [], intensity: intensity)
|
||||
let temporaryWallpaper: TelegramWallpaper = .image([TelegramMediaImageRepresentation(dimensions: PixelDimensions(thumbnailDimensions), resource: thumbnailResource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false), TelegramMediaImageRepresentation(dimensions: PixelDimensions(croppedImage.size), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)], settings)
|
||||
|
||||
context.account.pendingPeerMediaUploadManager.add(peerId: peerId, content: .wallpaper(wallpaper: temporaryWallpaper, forBoth: forBoth))
|
||||
|
||||
Queue.mainQueue().after(0.05) {
|
||||
completion()
|
||||
}
|
||||
}
|
||||
}
|
||||
return croppedImage
|
||||
}).start()
|
||||
}
|
||||
|
||||
@@ -14,6 +14,8 @@ import LegacyMediaPickerUI
|
||||
import WallpaperResources
|
||||
import AccountContext
|
||||
import MediaResources
|
||||
import ThemeAccentColorScreen
|
||||
import GenerateThemeName
|
||||
|
||||
private final class EditThemeControllerArguments {
|
||||
let context: AccountContext
|
||||
|
||||
@@ -1,134 +0,0 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import TelegramCore
|
||||
import SwiftSignalKit
|
||||
import AsyncDisplayKit
|
||||
import Postbox
|
||||
import AccountContext
|
||||
import GridMessageSelectionNode
|
||||
import SettingsThemeWallpaperNode
|
||||
|
||||
final class ThemeGridControllerItem: GridItem {
|
||||
let context: AccountContext
|
||||
let wallpaper: TelegramWallpaper
|
||||
let wallpaperId: ThemeGridControllerEntry.StableId
|
||||
let index: Int
|
||||
let editable: Bool
|
||||
let selected: Bool
|
||||
let interaction: ThemeGridControllerInteraction
|
||||
|
||||
let section: GridSection? = nil
|
||||
|
||||
init(context: AccountContext, wallpaper: TelegramWallpaper, wallpaperId: ThemeGridControllerEntry.StableId, index: Int, editable: Bool, selected: Bool, interaction: ThemeGridControllerInteraction) {
|
||||
self.context = context
|
||||
self.wallpaper = wallpaper
|
||||
self.wallpaperId = wallpaperId
|
||||
self.index = index
|
||||
self.editable = editable
|
||||
self.selected = selected
|
||||
self.interaction = interaction
|
||||
}
|
||||
|
||||
func node(layout: GridNodeLayout, synchronousLoad: Bool) -> GridItemNode {
|
||||
let node = ThemeGridControllerItemNode()
|
||||
node.setup(item: self, synchronousLoad: synchronousLoad)
|
||||
return node
|
||||
}
|
||||
|
||||
func update(node: GridItemNode) {
|
||||
guard let node = node as? ThemeGridControllerItemNode else {
|
||||
assertionFailure()
|
||||
return
|
||||
}
|
||||
node.setup(item: self, synchronousLoad: false)
|
||||
}
|
||||
}
|
||||
|
||||
final class ThemeGridControllerItemNode: GridItemNode {
|
||||
private let wallpaperNode: SettingsThemeWallpaperNode
|
||||
private var selectionNode: GridMessageSelectionNode?
|
||||
|
||||
private var item: ThemeGridControllerItem?
|
||||
|
||||
override init() {
|
||||
self.wallpaperNode = SettingsThemeWallpaperNode(displayLoading: false)
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.wallpaperNode)
|
||||
}
|
||||
|
||||
override func didLoad() {
|
||||
super.didLoad()
|
||||
|
||||
self.view.isExclusiveTouch = true
|
||||
self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))))
|
||||
}
|
||||
|
||||
func setup(item: ThemeGridControllerItem, synchronousLoad: Bool) {
|
||||
self.item = item
|
||||
self.updateSelectionState(animated: false)
|
||||
self.setNeedsLayout()
|
||||
}
|
||||
|
||||
@objc func tapGesture(_ recognizer: UITapGestureRecognizer) {
|
||||
if case .ended = recognizer.state {
|
||||
if let item = self.item {
|
||||
item.interaction.openWallpaper(item.wallpaper)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func updateSelectionState(animated: Bool) {
|
||||
if let item = self.item {
|
||||
let (editing, selectedIds) = item.interaction.selectionState
|
||||
|
||||
if editing && item.editable {
|
||||
let selected = selectedIds.contains(item.wallpaperId)
|
||||
|
||||
if let selectionNode = self.selectionNode {
|
||||
selectionNode.updateSelected(selected, animated: animated)
|
||||
selectionNode.frame = CGRect(origin: CGPoint(), size: self.bounds.size)
|
||||
} else {
|
||||
let theme = item.context.sharedContext.currentPresentationData.with { $0 }.theme
|
||||
let selectionNode = GridMessageSelectionNode(theme: theme, toggle: { [weak self] value in
|
||||
if let strongSelf = self {
|
||||
strongSelf.item?.interaction.toggleWallpaperSelection(item.wallpaperId, value)
|
||||
}
|
||||
})
|
||||
|
||||
selectionNode.frame = CGRect(origin: CGPoint(), size: self.bounds.size)
|
||||
self.addSubnode(selectionNode)
|
||||
self.selectionNode = selectionNode
|
||||
selectionNode.updateSelected(selected, animated: false)
|
||||
if animated {
|
||||
selectionNode.animateIn()
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
if let selectionNode = self.selectionNode {
|
||||
self.selectionNode = nil
|
||||
if animated {
|
||||
selectionNode.animateOut { [weak selectionNode] in
|
||||
selectionNode?.removeFromSupernode()
|
||||
}
|
||||
} else {
|
||||
selectionNode.removeFromSupernode()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override func layout() {
|
||||
super.layout()
|
||||
|
||||
let bounds = self.bounds
|
||||
if let item = self.item {
|
||||
self.wallpaperNode.setWallpaper(context: item.context, wallpaper: item.wallpaper, selected: item.selected, size: bounds.size, synchronousLoad: false)
|
||||
self.selectionNode?.frame = CGRect(origin: CGPoint(), size: bounds.size)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -371,7 +371,7 @@ final class ThemePreviewControllerNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
}, activateChatPreview: { _, _, _, gesture, _ in
|
||||
gesture?.cancel()
|
||||
}, present: { _ in
|
||||
}, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openActiveSessions: {
|
||||
}, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openPremiumGift: {}, openActiveSessions: {
|
||||
}, performActiveSessionAction: { _, _ in
|
||||
}, openChatFolderUpdates: {}, hideChatFolderUpdates: {
|
||||
}, openStories: { _, _ in
|
||||
|
||||
@@ -20,6 +20,8 @@ import UndoUI
|
||||
import PremiumUI
|
||||
import PeerNameColorScreen
|
||||
import ThemeCarouselItem
|
||||
import ThemeAccentColorScreen
|
||||
import WallpaperGridScreen
|
||||
|
||||
private final class ThemeSettingsControllerArguments {
|
||||
let context: AccountContext
|
||||
|
||||
@@ -7,7 +7,7 @@ import TelegramApi
|
||||
extension WallpaperSettings {
|
||||
init(apiWallpaperSettings: Api.WallPaperSettings) {
|
||||
switch apiWallpaperSettings {
|
||||
case let .wallPaperSettings(flags, backgroundColor, secondBackgroundColor, thirdBackgroundColor, fourthBackgroundColor, intensity, rotation, _):
|
||||
case let .wallPaperSettings(flags, backgroundColor, secondBackgroundColor, thirdBackgroundColor, fourthBackgroundColor, intensity, rotation, emoticon):
|
||||
var colors: [UInt32] = []
|
||||
if let backgroundColor = backgroundColor {
|
||||
colors.append(UInt32(bitPattern: backgroundColor))
|
||||
@@ -21,7 +21,7 @@ extension WallpaperSettings {
|
||||
if let fourthBackgroundColor = fourthBackgroundColor {
|
||||
colors.append(UInt32(bitPattern: fourthBackgroundColor))
|
||||
}
|
||||
self = WallpaperSettings(blur: (flags & 1 << 1) != 0, motion: (flags & 1 << 2) != 0, colors: colors, intensity: intensity, rotation: rotation)
|
||||
self = WallpaperSettings(blur: (flags & 1 << 1) != 0, motion: (flags & 1 << 2) != 0, colors: colors, intensity: intensity, rotation: rotation, emoticon: emoticon)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -42,6 +42,9 @@ func apiWallpaperSettings(_ wallpaperSettings: WallpaperSettings) -> Api.WallPap
|
||||
if let _ = wallpaperSettings.intensity {
|
||||
flags |= (1 << 3)
|
||||
}
|
||||
if let _ = wallpaperSettings.emoticon {
|
||||
flags |= (1 << 7)
|
||||
}
|
||||
var secondBackgroundColor: Int32?
|
||||
if wallpaperSettings.colors.count >= 2 {
|
||||
flags |= (1 << 4)
|
||||
@@ -57,7 +60,7 @@ func apiWallpaperSettings(_ wallpaperSettings: WallpaperSettings) -> Api.WallPap
|
||||
flags |= (1 << 6)
|
||||
fourthBackgroundColor = Int32(bitPattern: wallpaperSettings.colors[3])
|
||||
}
|
||||
return .wallPaperSettings(flags: flags, backgroundColor: backgroundColor, secondBackgroundColor: secondBackgroundColor, thirdBackgroundColor: thirdBackgroundColor, fourthBackgroundColor: fourthBackgroundColor, intensity: wallpaperSettings.intensity, rotation: wallpaperSettings.rotation ?? 0, emoticon: nil)
|
||||
return .wallPaperSettings(flags: flags, backgroundColor: backgroundColor, secondBackgroundColor: secondBackgroundColor, thirdBackgroundColor: thirdBackgroundColor, fourthBackgroundColor: fourthBackgroundColor, intensity: wallpaperSettings.intensity, rotation: wallpaperSettings.rotation ?? 0, emoticon: wallpaperSettings.emoticon)
|
||||
}
|
||||
|
||||
extension TelegramWallpaper {
|
||||
@@ -77,7 +80,11 @@ extension TelegramWallpaper {
|
||||
self = .color(0xffffff)
|
||||
}
|
||||
case let .wallPaperNoFile(id, _, settings):
|
||||
if let settings = settings, case let .wallPaperSettings(_, backgroundColor, secondBackgroundColor, thirdBackgroundColor, fourthBackgroundColor, _, rotation, _) = settings {
|
||||
if let settings = settings, case let .wallPaperSettings(_, backgroundColor, secondBackgroundColor, thirdBackgroundColor, fourthBackgroundColor, _, rotation, emoticon) = settings {
|
||||
if id == 0, let emoticon = emoticon {
|
||||
self = .emoticon(emoticon)
|
||||
return
|
||||
}
|
||||
let colors: [UInt32] = ([backgroundColor, secondBackgroundColor, thirdBackgroundColor, fourthBackgroundColor] as [Int32?]).compactMap({ color -> UInt32? in
|
||||
return color.flatMap(UInt32.init(bitPattern:))
|
||||
})
|
||||
@@ -105,6 +112,8 @@ extension TelegramWallpaper {
|
||||
return (.inputWallPaperNoFile(id: 0), apiWallpaperSettings(WallpaperSettings(colors: [color])))
|
||||
case let .gradient(gradient):
|
||||
return (.inputWallPaperNoFile(id: gradient.id ?? 0), apiWallpaperSettings(WallpaperSettings(colors: gradient.colors, rotation: gradient.settings.rotation)))
|
||||
case let .emoticon(emoticon):
|
||||
return (.inputWallPaperNoFile(id: 0), apiWallpaperSettings(WallpaperSettings(emoticon: emoticon)))
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ public enum ServerProvidedSuggestion: String {
|
||||
case upgradePremium = "PREMIUM_UPGRADE"
|
||||
case annualPremium = "PREMIUM_ANNUAL"
|
||||
case restorePremium = "PREMIUM_RESTORE"
|
||||
case xmasPremiumGift = "PREMIUM_CHRISTMAS"
|
||||
}
|
||||
|
||||
private var dismissedSuggestionsPromise = ValuePromise<[AccountRecordId: Set<ServerProvidedSuggestion>]>([:])
|
||||
|
||||
@@ -6,13 +6,15 @@ public struct WallpaperSettings: Codable, Equatable {
|
||||
public var colors: [UInt32]
|
||||
public var intensity: Int32?
|
||||
public var rotation: Int32?
|
||||
public var emoticon: String?
|
||||
|
||||
public init(blur: Bool = false, motion: Bool = false, colors: [UInt32] = [], intensity: Int32? = nil, rotation: Int32? = nil) {
|
||||
public init(blur: Bool = false, motion: Bool = false, colors: [UInt32] = [], intensity: Int32? = nil, rotation: Int32? = nil, emoticon: String? = nil) {
|
||||
self.blur = blur
|
||||
self.motion = motion
|
||||
self.colors = colors
|
||||
self.intensity = intensity
|
||||
self.rotation = rotation
|
||||
self.emoticon = emoticon
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
@@ -32,6 +34,7 @@ public struct WallpaperSettings: Codable, Equatable {
|
||||
|
||||
self.intensity = try container.decodeIfPresent(Int32.self, forKey: "i")
|
||||
self.rotation = try container.decodeIfPresent(Int32.self, forKey: "r")
|
||||
self.emoticon = try container.decodeIfPresent(String.self, forKey: "e")
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
@@ -42,6 +45,7 @@ public struct WallpaperSettings: Codable, Equatable {
|
||||
try container.encode(self.colors.map(Int32.init(bitPattern:)), forKey: "colors")
|
||||
try container.encodeIfPresent(self.intensity, forKey: "i")
|
||||
try container.encodeIfPresent(self.rotation, forKey: "r")
|
||||
try container.encodeIfPresent(self.emoticon, forKey: "e")
|
||||
}
|
||||
|
||||
public static func ==(lhs: WallpaperSettings, rhs: WallpaperSettings) -> Bool {
|
||||
@@ -60,6 +64,9 @@ public struct WallpaperSettings: Codable, Equatable {
|
||||
if lhs.rotation != rhs.rotation {
|
||||
return false
|
||||
}
|
||||
if lhs.emoticon != rhs.emoticon {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -73,7 +80,7 @@ public struct TelegramWallpaperNativeCodable: Codable {
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: StringCodingKey.self)
|
||||
|
||||
|
||||
switch try container.decode(Int32.self, forKey: "v") {
|
||||
case 0:
|
||||
let settings = try container.decode(WallpaperSettings.self, forKey: "settings")
|
||||
@@ -106,9 +113,9 @@ public struct TelegramWallpaperNativeCodable: Codable {
|
||||
}
|
||||
case 4:
|
||||
let settings = try container.decode(WallpaperSettings.self, forKey: "settings")
|
||||
|
||||
|
||||
var colors: [UInt32] = []
|
||||
|
||||
|
||||
if let topColor = (try container.decodeIfPresent(Int32.self, forKey: "c1")).flatMap(UInt32.init(bitPattern:)) {
|
||||
colors.append(topColor)
|
||||
if let bottomColor = (try container.decodeIfPresent(Int32.self, forKey: "c2")).flatMap(UInt32.init(bitPattern:)) {
|
||||
@@ -117,12 +124,14 @@ public struct TelegramWallpaperNativeCodable: Codable {
|
||||
} else {
|
||||
colors = (try container.decode([Int32].self, forKey: "colors")).map(UInt32.init(bitPattern:))
|
||||
}
|
||||
|
||||
|
||||
self.value = .gradient(TelegramWallpaper.Gradient(
|
||||
id: try container.decodeIfPresent(Int64.self, forKey: "id"),
|
||||
colors: colors,
|
||||
settings: settings
|
||||
))
|
||||
case 5:
|
||||
self.value = .emoticon(try container.decode(String.self, forKey: "e"))
|
||||
default:
|
||||
assertionFailure()
|
||||
self.value = .color(0xffffff)
|
||||
@@ -131,41 +140,48 @@ public struct TelegramWallpaperNativeCodable: Codable {
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: StringCodingKey.self)
|
||||
|
||||
|
||||
switch self.value {
|
||||
case let .builtin(settings):
|
||||
try container.encode(0 as Int32, forKey: "v")
|
||||
try container.encode(settings, forKey: "settings")
|
||||
case let .color(color):
|
||||
try container.encode(1 as Int32, forKey: "v")
|
||||
try container.encode(Int32(bitPattern: color), forKey: "c")
|
||||
case let .gradient(gradient):
|
||||
try container.encode(4 as Int32, forKey: "v")
|
||||
try container.encodeIfPresent(gradient.id, forKey: "id")
|
||||
try container.encode(gradient.colors.map(Int32.init(bitPattern:)), forKey: "colors")
|
||||
try container.encode(gradient.settings, forKey: "settings")
|
||||
case let .image(representations, settings):
|
||||
try container.encode(2 as Int32, forKey: "v")
|
||||
try container.encode(representations.map { item in
|
||||
return PostboxEncoder().encodeObjectToRawData(item)
|
||||
}, forKey: "i")
|
||||
try container.encode(settings, forKey: "settings")
|
||||
case let .file(file):
|
||||
try container.encode(3 as Int32, forKey: "v")
|
||||
try container.encode(file.id, forKey: "id")
|
||||
try container.encode(file.accessHash, forKey: "accessHash")
|
||||
try container.encode((file.isCreator ? 1 : 0) as Int32, forKey: "isCreator")
|
||||
try container.encode((file.isDefault ? 1 : 0) as Int32, forKey: "isDefault")
|
||||
try container.encode((file.isPattern ? 1 : 0) as Int32, forKey: "isPattern")
|
||||
try container.encode((file.isDark ? 1 : 0) as Int32, forKey: "isDark")
|
||||
try container.encode(file.slug, forKey: "slug")
|
||||
try container.encode(PostboxEncoder().encodeObjectToRawData(file.file), forKey: "file")
|
||||
try container.encode(file.settings, forKey: "settings")
|
||||
case let .builtin(settings):
|
||||
try container.encode(0 as Int32, forKey: "v")
|
||||
try container.encode(settings, forKey: "settings")
|
||||
case let .color(color):
|
||||
try container.encode(1 as Int32, forKey: "v")
|
||||
try container.encode(Int32(bitPattern: color), forKey: "c")
|
||||
case let .gradient(gradient):
|
||||
try container.encode(4 as Int32, forKey: "v")
|
||||
try container.encodeIfPresent(gradient.id, forKey: "id")
|
||||
try container.encode(gradient.colors.map(Int32.init(bitPattern:)), forKey: "colors")
|
||||
try container.encode(gradient.settings, forKey: "settings")
|
||||
case let .image(representations, settings):
|
||||
try container.encode(2 as Int32, forKey: "v")
|
||||
try container.encode(representations.map { item in
|
||||
return PostboxEncoder().encodeObjectToRawData(item)
|
||||
}, forKey: "i")
|
||||
try container.encode(settings, forKey: "settings")
|
||||
case let .file(file):
|
||||
try container.encode(3 as Int32, forKey: "v")
|
||||
try container.encode(file.id, forKey: "id")
|
||||
try container.encode(file.accessHash, forKey: "accessHash")
|
||||
try container.encode((file.isCreator ? 1 : 0) as Int32, forKey: "isCreator")
|
||||
try container.encode((file.isDefault ? 1 : 0) as Int32, forKey: "isDefault")
|
||||
try container.encode((file.isPattern ? 1 : 0) as Int32, forKey: "isPattern")
|
||||
try container.encode((file.isDark ? 1 : 0) as Int32, forKey: "isDark")
|
||||
try container.encode(file.slug, forKey: "slug")
|
||||
try container.encode(PostboxEncoder().encodeObjectToRawData(file.file), forKey: "file")
|
||||
try container.encode(file.settings, forKey: "settings")
|
||||
case let .emoticon(emoticon):
|
||||
try container.encode(5 as Int32, forKey: "v")
|
||||
try container.encode(emoticon, forKey: "e")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum TelegramWallpaper: Equatable {
|
||||
public static func emoticonWallpaper(emoticon: String) -> TelegramWallpaper {
|
||||
return .file(File(id: -1, accessHash: -1, isCreator: false, isDefault: false, isPattern: false, isDark: false, slug: "", file: TelegramMediaFile(fileId: MediaId(namespace: 0, id: 0), partialReference: nil, resource: EmptyMediaResource(), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "", size: nil, attributes: []), settings: WallpaperSettings(emoticon: emoticon)))
|
||||
}
|
||||
|
||||
public struct Gradient: Equatable {
|
||||
public var id: Int64?
|
||||
public var colors: [UInt32]
|
||||
@@ -221,6 +237,7 @@ public enum TelegramWallpaper: Equatable {
|
||||
case gradient(Gradient)
|
||||
case image([TelegramMediaImageRepresentation], WallpaperSettings)
|
||||
case file(File)
|
||||
case emoticon(String)
|
||||
|
||||
public var hasWallpaper: Bool {
|
||||
switch self {
|
||||
@@ -263,6 +280,12 @@ public enum TelegramWallpaper: Equatable {
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .emoticon(emoticon):
|
||||
if case .emoticon(emoticon) = rhs {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -298,6 +321,12 @@ public enum TelegramWallpaper: Equatable {
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .emoticon(emoticon):
|
||||
if case .emoticon(emoticon) = wallpaper {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -328,6 +357,8 @@ public enum TelegramWallpaper: Equatable {
|
||||
case var .file(file):
|
||||
file.settings = settings
|
||||
return .file(file)
|
||||
case .emoticon:
|
||||
return self
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+4
-2
@@ -427,7 +427,7 @@ public final class EngineStoryViewListContext {
|
||||
let sortMode = self.sortMode
|
||||
let searchQuery = self.searchQuery
|
||||
let currentOffset = state.nextOffset
|
||||
let limit = state.items.isEmpty ? 50 : 100
|
||||
let limit = 50
|
||||
|
||||
let signal: Signal<InternalState, NoError>
|
||||
|
||||
@@ -662,6 +662,9 @@ public final class EngineStoryViewListContext {
|
||||
}
|
||||
|
||||
var flags: Int32 = 0
|
||||
if let _ = currentOffset {
|
||||
flags |= (1 << 1)
|
||||
}
|
||||
if case .repostsFirst = sortMode {
|
||||
flags |= (1 << 2)
|
||||
}
|
||||
@@ -887,4 +890,3 @@ public final class EngineStoryViewListContext {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+18
-5
@@ -14,7 +14,11 @@ public enum GetMessagesStrategy {
|
||||
case cloud(skipLocal: Bool)
|
||||
}
|
||||
|
||||
func _internal_getMessagesLoadIfNecessary(_ messageIds: [MessageId], postbox: Postbox, network: Network, accountPeerId: PeerId, strategy: GetMessagesStrategy = .cloud(skipLocal: false)) -> Signal<GetMessagesResult, NoError> {
|
||||
public enum GetMessagesError {
|
||||
case privateChannel
|
||||
}
|
||||
|
||||
func _internal_getMessagesLoadIfNecessary(_ messageIds: [MessageId], postbox: Postbox, network: Network, accountPeerId: PeerId, strategy: GetMessagesStrategy = .cloud(skipLocal: false)) -> Signal<GetMessagesResult, GetMessagesError> {
|
||||
let postboxSignal = postbox.transaction { transaction -> ([Message], Set<MessageId>, SimpleDictionary<PeerId, Peer>) in
|
||||
var ids = messageIds
|
||||
|
||||
@@ -50,8 +54,9 @@ func _internal_getMessagesLoadIfNecessary(_ messageIds: [MessageId], postbox: Po
|
||||
|
||||
if case .cloud = strategy {
|
||||
return postboxSignal
|
||||
|> castError(GetMessagesError.self)
|
||||
|> mapToSignal { (existMessages, missingMessageIds, supportPeers) in
|
||||
var signals: [Signal<(Peer, [Api.Message], [Api.Chat], [Api.User]), NoError>] = []
|
||||
var signals: [Signal<(Peer, [Api.Message], [Api.Chat], [Api.User]), GetMessagesError>] = []
|
||||
for (peerId, messageIds) in messagesIdsGroupedByPeerId(missingMessageIds) {
|
||||
if let peer = supportPeers[peerId] {
|
||||
var signal: Signal<Api.messages.Messages, MTRpcError>?
|
||||
@@ -75,14 +80,20 @@ func _internal_getMessagesLoadIfNecessary(_ messageIds: [MessageId], postbox: Po
|
||||
case .messagesNotModified:
|
||||
return (peer, [], [], [])
|
||||
}
|
||||
} |> `catch` { _ in
|
||||
return Signal<(Peer, [Api.Message], [Api.Chat], [Api.User]), NoError>.single((peer, [], [], []))
|
||||
} |> `catch` { error in
|
||||
if error.errorDescription == "CHANNEL_PRIVATE" {
|
||||
return .fail(.privateChannel)
|
||||
} else {
|
||||
return Signal<(Peer, [Api.Message], [Api.Chat], [Api.User]), GetMessagesError>.single((peer, [], [], []))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return .single(.progress) |> then(combineLatest(signals) |> mapToSignal { results -> Signal<GetMessagesResult, NoError> in
|
||||
return .single(.progress)
|
||||
|> castError(GetMessagesError.self)
|
||||
|> then(combineLatest(signals) |> mapToSignal { results -> Signal<GetMessagesResult, GetMessagesError> in
|
||||
return postbox.transaction { transaction -> GetMessagesResult in
|
||||
for (peer, messages, chats, users) in results {
|
||||
if !messages.isEmpty {
|
||||
@@ -108,10 +119,12 @@ func _internal_getMessagesLoadIfNecessary(_ messageIds: [MessageId], postbox: Po
|
||||
|
||||
return .result(existMessages + loadedMessages)
|
||||
}
|
||||
|> castError(GetMessagesError.self)
|
||||
})
|
||||
}
|
||||
} else {
|
||||
return postboxSignal
|
||||
|> castError(GetMessagesError.self)
|
||||
|> map {
|
||||
return .result($0.0)
|
||||
}
|
||||
|
||||
+1
-1
@@ -162,7 +162,7 @@ public extension TelegramEngine {
|
||||
return _internal_markAllChatsAsRead(postbox: self.account.postbox, network: self.account.network, stateManager: self.account.stateManager)
|
||||
}
|
||||
|
||||
public func getMessagesLoadIfNecessary(_ messageIds: [MessageId], strategy: GetMessagesStrategy = .cloud(skipLocal: false)) -> Signal<GetMessagesResult, NoError> {
|
||||
public func getMessagesLoadIfNecessary(_ messageIds: [MessageId], strategy: GetMessagesStrategy = .cloud(skipLocal: false)) -> Signal<GetMessagesResult, GetMessagesError> {
|
||||
return _internal_getMessagesLoadIfNecessary(messageIds, postbox: self.account.postbox, network: self.account.network, accountPeerId: self.account.peerId, strategy: strategy)
|
||||
}
|
||||
|
||||
|
||||
@@ -134,6 +134,8 @@ func _internal_setChatWallpaper(postbox: Postbox, network: Network, stateManager
|
||||
transaction.updatePeerCachedData(peerIds: Set([peerId]), update: { _, current in
|
||||
if let current = current as? CachedUserData {
|
||||
return current.withUpdatedWallpaper(wallpaper)
|
||||
} else if let current = current as? CachedChannelData {
|
||||
return current.withUpdatedWallpaper(wallpaper)
|
||||
} else {
|
||||
return current
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ public func chatControllerBackgroundImage(theme: PresentationTheme?, wallpaper i
|
||||
} else {
|
||||
var succeed = true
|
||||
switch wallpaper {
|
||||
case .builtin:
|
||||
case .builtin, .emoticon:
|
||||
if let filePath = getAppBundle().path(forResource: "ChatWallpaperBuiltin0", ofType: "jpg") {
|
||||
backgroundImage = UIImage(contentsOfFile: filePath)?.precomposed()
|
||||
}
|
||||
@@ -114,7 +114,7 @@ public func chatControllerBackgroundImageSignal(wallpaper: TelegramWallpaper, me
|
||||
}
|
||||
|
||||
switch wallpaper {
|
||||
case .builtin:
|
||||
case .builtin, .emoticon:
|
||||
if let filePath = getAppBundle().path(forResource: "ChatWallpaperBuiltin0", ofType: "jpg") {
|
||||
return .single((UIImage(contentsOfFile: filePath)?.precomposed(), true))
|
||||
|> afterNext { image in
|
||||
|
||||
@@ -624,6 +624,8 @@ public func serviceColor(for wallpaper: (TelegramWallpaper, UIImage?)) -> UIColo
|
||||
} else {
|
||||
return UIColor(rgb: 0x000000, alpha: 0.3)
|
||||
}
|
||||
case .emoticon:
|
||||
return UIColor(rgb: 0x000000, alpha: 0.3)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -708,6 +710,8 @@ public func chatServiceBackgroundColor(wallpaper: TelegramWallpaper, mediaBox: M
|
||||
serviceBackgroundColorForWallpaper = (wallpaper, color)
|
||||
}
|
||||
}
|
||||
case .emoticon:
|
||||
return .single(UIColor(rgb: 0x000000, alpha: 0.3))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@ public extension TelegramWallpaper {
|
||||
switch self {
|
||||
case .image:
|
||||
return false
|
||||
case .emoticon:
|
||||
return false
|
||||
case let .file(file):
|
||||
if self.isPattern, file.settings.colors.count == 1 && (file.settings.colors[0] == 0xffffff || file.settings.colors[0] == 0xffffffff) {
|
||||
return true
|
||||
@@ -47,6 +49,15 @@ public extension TelegramWallpaper {
|
||||
}
|
||||
}
|
||||
|
||||
var isEmoticon: Bool {
|
||||
switch self {
|
||||
case .emoticon:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
var dimensions: CGSize? {
|
||||
if case let .file(file) = self {
|
||||
return file.file.dimensions?.cgSize
|
||||
|
||||
@@ -418,6 +418,7 @@ swift_library(
|
||||
"//submodules/UIKitRuntimeUtils",
|
||||
"//submodules/TelegramUI/Components/SavedMessages/SavedMessagesScreen",
|
||||
"//submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen",
|
||||
"//submodules/TelegramUI/Components/Settings/WallpaperGridScreen",
|
||||
] + select({
|
||||
"@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets,
|
||||
"//build-system:ios_sim_arm64": [],
|
||||
|
||||
+8
-3
@@ -686,7 +686,7 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
candidateTitleString = NSAttributedString(string: title ?? (arguments.file.fileName ?? "Unknown Track"), font: titleFont, textColor: arguments.customTintColor ?? messageTheme.fileTitleColor)
|
||||
let descriptionText: String
|
||||
if let performer = performer {
|
||||
descriptionText = performer
|
||||
descriptionText = performer.trimmingTrailingSpaces()
|
||||
} else if let size = arguments.file.size, size > 0 && size != .max {
|
||||
descriptionText = dataSizeString(size, formatting: DataSizeStringFormatting(chatPresentationData: arguments.presentationData))
|
||||
} else {
|
||||
@@ -876,7 +876,7 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
var viewCount: Int?
|
||||
var dateReplies = 0
|
||||
var dateReactionsAndPeers = mergedMessageReactionsAndPeers(accountPeer: arguments.associatedData.accountPeer, message: arguments.topMessage)
|
||||
if arguments.topMessage.isRestricted(platform: "ios", contentSettings: arguments.context.currentContentSettings.with { $0 }) {
|
||||
if arguments.topMessage.isRestricted(platform: "ios", contentSettings: arguments.context.currentContentSettings.with { $0 }) || arguments.presentationData.isPreview {
|
||||
dateReactionsAndPeers = ([], [])
|
||||
}
|
||||
for attribute in arguments.message.attributes {
|
||||
@@ -954,7 +954,11 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
minLayoutWidth = max(titleLayout.size.width, descriptionMaxWidth) + 44.0 + 8.0
|
||||
}
|
||||
|
||||
var statusHeightAddition: CGFloat = 0.0
|
||||
if let statusSuggestedWidthAndContinue = statusSuggestedWidthAndContinue {
|
||||
if statusSuggestedWidthAndContinue.0 > minLayoutWidth {
|
||||
statusHeightAddition = 6.0
|
||||
}
|
||||
minLayoutWidth = max(minLayoutWidth, statusSuggestedWidthAndContinue.0)
|
||||
}
|
||||
|
||||
@@ -1020,6 +1024,7 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
statusOffset = -10.0
|
||||
fittedLayoutSize.height += statusOffset
|
||||
}
|
||||
fittedLayoutSize.height += statusHeightAddition
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1223,7 +1228,7 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||
if textString != nil {
|
||||
statusFrame = CGRect(origin: CGPoint(x: fittedLayoutSize.width - 6.0 - statusSizeAndApply.0.width, y: textFrame.maxY + 4.0), size: statusSizeAndApply.0)
|
||||
} else {
|
||||
statusFrame = CGRect(origin: CGPoint(x: statusReferenceFrame.minX, y: statusReferenceFrame.maxY + statusOffset), size: statusSizeAndApply.0)
|
||||
statusFrame = CGRect(origin: CGPoint(x: statusReferenceFrame.minX, y: statusReferenceFrame.maxY + statusOffset + statusHeightAddition), size: statusSizeAndApply.0)
|
||||
}
|
||||
if strongSelf.dateAndStatusNode.supernode == nil {
|
||||
strongSelf.dateAndStatusNode.frame = statusFrame
|
||||
|
||||
+6
-4
@@ -791,7 +791,7 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr
|
||||
}
|
||||
case .themeSettings, .image:
|
||||
unboundSize = CGSize(width: 160.0, height: 240.0).fitted(CGSize(width: 240.0, height: 240.0))
|
||||
case .color, .gradient:
|
||||
case .color, .gradient, .emoticon:
|
||||
unboundSize = CGSize(width: 128.0, height: 128.0)
|
||||
}
|
||||
} else if let invoice = media as? TelegramMediaInvoice, let extendedMedia = invoice.extendedMedia {
|
||||
@@ -1312,7 +1312,7 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr
|
||||
switch wallpaper.content {
|
||||
case let .file(file, _, _, _, isTheme, _):
|
||||
if isTheme {
|
||||
return themeImage(account: context.account, accountManager: context.sharedContext.accountManager, source: .file(FileMediaReference.message(message: MessageReference(message), media: file)))
|
||||
return themeImage(account: context.account, accountManager: context.sharedContext.accountManager, source: .file(FileMediaReference.message(message: MessageReference(message), media: file)), synchronousLoad: synchronousLoad)
|
||||
} else {
|
||||
var representations: [ImageRepresentationWithReference] = file.previewRepresentations.map({ ImageRepresentationWithReference(representation: $0, reference: AnyMediaReference.message(message: MessageReference(message), media: file).resourceReference($0.resource)) })
|
||||
if file.mimeType == "image/svg+xml" || file.mimeType == "application/x-tgwallpattern" {
|
||||
@@ -1328,7 +1328,7 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return wallpaperImage(account: context.account, accountManager: context.sharedContext.accountManager, fileReference: FileMediaReference.message(message: MessageReference(message), media: file), representations: representations, alwaysShowThumbnailFirst: false, thumbnail: true, autoFetchFullSize: true)
|
||||
return wallpaperImage(account: context.account, accountManager: context.sharedContext.accountManager, fileReference: FileMediaReference.message(message: MessageReference(message), media: file), representations: representations, alwaysShowThumbnailFirst: false, thumbnail: true, autoFetchFullSize: true, synchronousLoad: synchronousLoad)
|
||||
}
|
||||
}
|
||||
case let .image(representations):
|
||||
@@ -1339,6 +1339,8 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr
|
||||
return solidColorImage(color)
|
||||
case let .gradient(colors, rotation):
|
||||
return gradientImage(colors.map(UIColor.init(rgb:)), rotation: rotation ?? 0)
|
||||
case .emoticon:
|
||||
return solidColorImage(.black)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1404,7 +1406,7 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr
|
||||
|> map { resourceStatus -> (MediaResourceStatus, MediaResourceStatus?) in
|
||||
return (resourceStatus, nil)
|
||||
}
|
||||
case .themeSettings, .color, .gradient, .image:
|
||||
case .themeSettings, .color, .gradient, .image, .emoticon:
|
||||
updatedStatusSignal = .single((.Local, nil))
|
||||
}
|
||||
}
|
||||
|
||||
+3
-1
@@ -512,7 +512,9 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
var cutout: TextNodeCutout? = nil
|
||||
if item.presentationData.isPreview {
|
||||
moreLayoutAndApply = moreLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.presentationData.strings.Conversation_ReadMore, font: textFont, textColor: messageTheme.accentTextColor), maximumNumberOfLines: 1, truncationType: .end, constrainedSize: textConstrainedSize))
|
||||
cutout = TextNodeCutout(bottomRight: moreLayoutAndApply?.0.size)
|
||||
if let moreSize = moreLayoutAndApply?.0.size {
|
||||
cutout = TextNodeCutout(bottomRight: CGSize(width: moreSize.width + 8.0, height: moreSize.height))
|
||||
}
|
||||
}
|
||||
|
||||
let textInsets = UIEdgeInsets(top: 2.0, left: 2.0, bottom: 5.0, right: 2.0)
|
||||
|
||||
+9
-1
@@ -332,7 +332,13 @@ public class ChatMessageWallpaperBubbleContentNode: ChatMessageBubbleContentNode
|
||||
var imageSize = boundingSize
|
||||
let updateImageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>
|
||||
var patternArguments: PatternWallpaperArguments?
|
||||
switch media.content {
|
||||
|
||||
var mediaContent = media.content
|
||||
if case let .emoticon(emoticon) = mediaContent, let theme = item.associatedData.chatThemes.first(where: { $0.emoticon?.strippedEmoji == emoticon.strippedEmoji }), let themeWallpaper = theme.settings?.first?.wallpaper, let themeWallpaperContent = WallpaperPreviewMedia(wallpaper: themeWallpaper)?.content {
|
||||
mediaContent = themeWallpaperContent
|
||||
}
|
||||
|
||||
switch mediaContent {
|
||||
case let .file(file, patternColors, rotation, intensity, _, _):
|
||||
var representations: [ImageRepresentationWithReference] = file.previewRepresentations.map({ ImageRepresentationWithReference(representation: $0, reference: AnyMediaReference.message(message: MessageReference(item.message), media: file).resourceReference($0.resource)) })
|
||||
if file.mimeType == "image/svg+xml" || file.mimeType == "application/x-tgwallpattern" {
|
||||
@@ -386,6 +392,8 @@ public class ChatMessageWallpaperBubbleContentNode: ChatMessageBubbleContentNode
|
||||
updateImageSignal = gradientImage(colors.map(UIColor.init(rgb:)), rotation: rotation ?? 0)
|
||||
case .themeSettings:
|
||||
updateImageSignal = .complete()
|
||||
case .emoticon:
|
||||
updateImageSignal = .complete()
|
||||
}
|
||||
|
||||
strongSelf.imageNode.setSignal(updateImageSignal, attemptSynchronously: synchronousLoads)
|
||||
|
||||
+21
-4
@@ -342,12 +342,16 @@ public final class EditableTokenListNode: ASDisplayNode, UITextFieldDelegate {
|
||||
public func updateLayout(tokens: [EditableTokenListToken], width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat {
|
||||
let validTokens = Set<AnyHashable>(tokens.map { $0.id })
|
||||
|
||||
var placeholderSnapshot: UIView?
|
||||
if let shortPlaceholder = self.shortPlaceholder {
|
||||
if validTokens.count > 0 {
|
||||
self.placeholderNode.attributedText = NSAttributedString(string: shortPlaceholder, font: Font.regular(15.0), textColor: self.theme.placeholderTextColor)
|
||||
} else {
|
||||
self.placeholderNode.attributedText = NSAttributedString(string: self.placeholder, font: Font.regular(15.0), textColor: self.theme.placeholderTextColor)
|
||||
let previousPlaceholder = self.placeholderNode.attributedText?.string ?? ""
|
||||
let placeholder = validTokens.count > 0 ? shortPlaceholder : self.placeholder
|
||||
|
||||
if !previousPlaceholder.isEmpty && placeholder != previousPlaceholder {
|
||||
placeholderSnapshot = self.placeholderNode.layer.snapshotContentTreeAsView()
|
||||
placeholderSnapshot?.frame = self.placeholderNode.frame
|
||||
}
|
||||
self.placeholderNode.attributedText = NSAttributedString(string: placeholder, font: Font.regular(15.0), textColor: self.theme.placeholderTextColor)
|
||||
}
|
||||
|
||||
for i in (0 ..< self.tokenNodes.count).reversed() {
|
||||
@@ -444,10 +448,23 @@ public final class EditableTokenListNode: ASDisplayNode, UITextFieldDelegate {
|
||||
currentOffset.y += 28.0
|
||||
currentOffset.x = sideInset
|
||||
}
|
||||
|
||||
let previousPlaceholderWidth = self.placeholderNode.bounds.width
|
||||
let placeholderFrame = CGRect(origin: CGPoint(x: currentOffset.x + 4.0, y: currentOffset.y + floor((28.0 - placeholderSize.height) / 2.0)), size: placeholderSize)
|
||||
self.placeholderNode.bounds = CGRect(origin: .zero, size: placeholderSize)
|
||||
transition.updatePosition(node: self.placeholderNode, position: placeholderFrame.center)
|
||||
|
||||
if let placeholderSnapshot {
|
||||
self.placeholderNode.view.superview?.insertSubview(placeholderSnapshot, belowSubview: self.placeholderNode.view)
|
||||
self.placeholderNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
|
||||
placeholderSnapshot.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in
|
||||
placeholderSnapshot.removeFromSuperview()
|
||||
})
|
||||
let delta = (placeholderSize.width - previousPlaceholderWidth) / 2.0
|
||||
transition.updatePosition(layer: placeholderSnapshot.layer, position: CGPoint(x: placeholderFrame.center.x - delta, y: placeholderFrame.center.y))
|
||||
transition.animatePositionAdditive(node: self.placeholderNode, offset: CGPoint(x: delta, y: 0.0))
|
||||
}
|
||||
|
||||
let textNodeFrame = CGRect(origin: CGPoint(x: currentOffset.x + 4.0, y: currentOffset.y + UIScreenPixel), size: CGSize(width: width - currentOffset.x - sideInset - 8.0, height: 28.0))
|
||||
let caretNodeFrame = CGRect(origin: CGPoint(x: textNodeFrame.minX, y: textNodeFrame.minY + 4.0 - UIScreenPixel), size: CGSize(width: 2.0, height: 19.0 + UIScreenPixel))
|
||||
if case .immediate = transition {
|
||||
|
||||
@@ -64,6 +64,7 @@ private final class ArrowNode: HighlightTrackingButtonNode {
|
||||
}
|
||||
|
||||
final class ContextMenuNode: ASDisplayNode {
|
||||
private let blurred: Bool
|
||||
private let isDark: Bool
|
||||
|
||||
private let actions: [ContextMenuAction]
|
||||
@@ -93,6 +94,7 @@ final class ContextMenuNode: ASDisplayNode {
|
||||
private let feedback: HapticFeedback?
|
||||
|
||||
init(actions: [ContextMenuAction], dismiss: @escaping () -> Void, dismissOnTap: @escaping (UIView, CGPoint) -> Bool, catchTapsOutside: Bool, hasHapticFeedback: Bool, blurred: Bool = false, isDark: Bool = true) {
|
||||
self.blurred = blurred
|
||||
self.isDark = isDark
|
||||
|
||||
self.actions = actions
|
||||
@@ -264,9 +266,11 @@ final class ContextMenuNode: ASDisplayNode {
|
||||
self.containerNode.layer.animateSpring(from: NSValue(cgPoint: CGPoint(x: containerPosition.x, y: containerPosition.y + (self.arrowOnBottom ? 1.0 : -1.0) * self.containerNode.bounds.size.height / 2.0)), to: NSValue(cgPoint: containerPosition), keyPath: "position", duration: 0.4)
|
||||
}
|
||||
|
||||
self.allowsGroupOpacity = true
|
||||
self.layer.rasterizationScale = UIScreen.main.scale
|
||||
self.layer.shouldRasterize = true
|
||||
if !(self.blurred && self.isDark) {
|
||||
self.allowsGroupOpacity = true
|
||||
self.layer.rasterizationScale = UIScreen.main.scale
|
||||
self.layer.shouldRasterize = true
|
||||
}
|
||||
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1, completion: { [weak self] _ in
|
||||
self?.allowsGroupOpacity = false
|
||||
self?.layer.shouldRasterize = false
|
||||
@@ -278,9 +282,11 @@ final class ContextMenuNode: ASDisplayNode {
|
||||
}
|
||||
|
||||
func animateOut(bounce: Bool, completion: @escaping () -> Void) {
|
||||
self.allowsGroupOpacity = true
|
||||
self.layer.rasterizationScale = UIScreen.main.scale
|
||||
self.layer.shouldRasterize = true
|
||||
if !(self.blurred && self.isDark) {
|
||||
self.allowsGroupOpacity = true
|
||||
self.layer.rasterizationScale = UIScreen.main.scale
|
||||
self.layer.shouldRasterize = true
|
||||
}
|
||||
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak self] _ in
|
||||
self?.allowsGroupOpacity = false
|
||||
self?.layer.shouldRasterize = false
|
||||
|
||||
+1
-1
@@ -123,7 +123,7 @@ public enum CodableDrawingEntity: Equatable {
|
||||
reaction: reaction,
|
||||
flags: flags
|
||||
)
|
||||
} else if case let .message(messageIds, _) = entity.content, let messageId = messageIds.first {
|
||||
} else if case let .message(messageIds, _, _) = entity.content, let messageId = messageIds.first {
|
||||
return .channelMessage(coordinates: coordinates, messageId: messageId)
|
||||
} else {
|
||||
return nil
|
||||
|
||||
+10
-7
@@ -33,7 +33,7 @@ public final class DrawingStickerEntity: DrawingEntity, Codable {
|
||||
case animatedImage(Data, UIImage)
|
||||
case video(TelegramMediaFile)
|
||||
case dualVideoReference(Bool)
|
||||
case message([MessageId], CGSize)
|
||||
case message([MessageId], TelegramMediaFile?, CGSize)
|
||||
|
||||
public static func == (lhs: Content, rhs: Content) -> Bool {
|
||||
switch lhs {
|
||||
@@ -67,8 +67,8 @@ public final class DrawingStickerEntity: DrawingEntity, Codable {
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .message(messageIds, size):
|
||||
if case .message(messageIds, size) = rhs {
|
||||
case let .message(messageIds, innerFile, size):
|
||||
if case .message(messageIds, innerFile, size) = rhs {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
@@ -108,6 +108,8 @@ public final class DrawingStickerEntity: DrawingEntity, Codable {
|
||||
didSet {
|
||||
if case let .file(_, type) = self.content, case .reaction = type {
|
||||
self.scale = max(0.59, min(1.77, self.scale))
|
||||
} else if case .message = self.content {
|
||||
self.scale = max(1.0, min(4.0, self.scale))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -144,7 +146,7 @@ public final class DrawingStickerEntity: DrawingEntity, Codable {
|
||||
dimensions = file.dimensions?.cgSize ?? CGSize(width: 512.0, height: 512.0)
|
||||
case .dualVideoReference:
|
||||
dimensions = CGSize(width: 512.0, height: 512.0)
|
||||
case let .message(_, size):
|
||||
case let .message(_, _, size):
|
||||
dimensions = size
|
||||
}
|
||||
|
||||
@@ -174,7 +176,7 @@ public final class DrawingStickerEntity: DrawingEntity, Codable {
|
||||
case .dualVideoReference:
|
||||
return true
|
||||
case .message:
|
||||
return false
|
||||
return !(self.renderSubEntities ?? []).isEmpty
|
||||
}
|
||||
}
|
||||
|
||||
@@ -216,7 +218,7 @@ public final class DrawingStickerEntity: DrawingEntity, Codable {
|
||||
self.uuid = try container.decode(UUID.self, forKey: .uuid)
|
||||
if let messageIds = try container.decodeIfPresent([MessageId].self, forKey: .messageIds) {
|
||||
let size = try container.decodeIfPresent(CGSize.self, forKey: .explicitSize) ?? .zero
|
||||
self.content = .message(messageIds, size)
|
||||
self.content = .message(messageIds, nil, size)
|
||||
} else if let _ = try container.decodeIfPresent(Bool.self, forKey: .dualVideo) {
|
||||
let isAdditional = try container.decodeIfPresent(Bool.self, forKey: .isAdditionalVideo) ?? false
|
||||
self.content = .dualVideoReference(isAdditional)
|
||||
@@ -311,8 +313,9 @@ public final class DrawingStickerEntity: DrawingEntity, Codable {
|
||||
case let .dualVideoReference(isAdditional):
|
||||
try container.encode(true, forKey: .dualVideo)
|
||||
try container.encode(isAdditional, forKey: .isAdditionalVideo)
|
||||
case let .message(messageIds, size):
|
||||
case let .message(messageIds, innerFile, size):
|
||||
try container.encode(messageIds, forKey: .messageIds)
|
||||
let _ = innerFile
|
||||
try container.encode(size, forKey: .explicitSize)
|
||||
}
|
||||
try container.encode(self.referenceDrawingSize, forKey: .referenceDrawingSize)
|
||||
|
||||
@@ -34,17 +34,17 @@ public final class DrawingWallpaperRenderer {
|
||||
self.darkWallpaperBackgroundNode.update(wallpaper: darkWallpaper, animated: false)
|
||||
}
|
||||
|
||||
public func render(completion: @escaping (CGSize, UIImage?, UIImage?) -> Void) {
|
||||
public func render(completion: @escaping (CGSize, UIImage?, UIImage?, CGRect?) -> Void) {
|
||||
self.updateLayout(size: CGSize(width: 360.0, height: 640.0))
|
||||
|
||||
let resultSize = CGSize(width: 1080, height: 1920)
|
||||
self.generate(view: self.wallpaperBackgroundNode.view) { dayImage in
|
||||
if self.customWallpaper != nil {
|
||||
completion(resultSize, dayImage, nil)
|
||||
completion(resultSize, dayImage, nil, nil)
|
||||
} else {
|
||||
Queue.mainQueue().justDispatch {
|
||||
self.generate(view: self.darkWallpaperBackgroundNode.view) { nightImage in
|
||||
completion(resultSize, dayImage, nightImage)
|
||||
completion(resultSize, dayImage, nightImage, nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,7 +140,7 @@ public func getChatWallpaperImage(context: AccountContext, messageId: EngineMess
|
||||
return Signal { subscriber in
|
||||
Queue.mainQueue().async {
|
||||
let wallpaperRenderer = DrawingWallpaperRenderer(context: context, customWallpaper: customWallpaper)
|
||||
wallpaperRenderer.render { size, image, darkImage in
|
||||
wallpaperRenderer.render { size, image, darkImage, mediaRect in
|
||||
subscriber.putNext((size, image, darkImage))
|
||||
subscriber.putCompletion()
|
||||
}
|
||||
|
||||
+100
-97
@@ -1575,6 +1575,102 @@ final class MediaEditorScreenComponent: Component {
|
||||
}
|
||||
|
||||
var topButtonOffsetX: CGFloat = 0.0
|
||||
|
||||
if let subject = controller.node.subject, case .message = subject {
|
||||
let isNightTheme = mediaEditor?.values.nightTheme == true
|
||||
|
||||
let dayNightContentComponent: AnyComponentWithIdentity<Empty>
|
||||
if component.hasAppeared {
|
||||
dayNightContentComponent = AnyComponentWithIdentity(
|
||||
id: "animatedIcon",
|
||||
component: AnyComponent(
|
||||
LottieAnimationComponent(
|
||||
animation: LottieAnimationComponent.AnimationItem(
|
||||
name: isNightTheme ? "anim_sun" : "anim_sun_reverse",
|
||||
mode: state.dayNightDidChange ? .animating(loop: false) : .still(position: .end)
|
||||
),
|
||||
colors: ["__allcolors__": .white],
|
||||
size: CGSize(width: 30.0, height: 30.0)
|
||||
).tagged(dayNightButtonTag)
|
||||
)
|
||||
)
|
||||
} else {
|
||||
dayNightContentComponent = AnyComponentWithIdentity(
|
||||
id: "staticIcon",
|
||||
component: AnyComponent(
|
||||
BundleIconComponent(
|
||||
name: "Media Editor/MuteIcon",
|
||||
tintColor: nil
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
let dayNightButtonSize = self.dayNightButton.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(CameraButton(
|
||||
content: dayNightContentComponent,
|
||||
action: { [weak self, weak state, weak mediaEditor] in
|
||||
guard let environment = self?.environment, let controller = environment.controller() as? MediaEditorScreen else {
|
||||
return
|
||||
}
|
||||
guard !controller.node.recording.isActive else {
|
||||
return
|
||||
}
|
||||
|
||||
if let mediaEditor {
|
||||
state?.dayNightDidChange = true
|
||||
|
||||
if let snapshotView = controller.node.previewContainerView.snapshotView(afterScreenUpdates: false) {
|
||||
controller.node.previewContainerView.addSubview(snapshotView)
|
||||
|
||||
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, delay: 0.1, removeOnCompletion: false, completion: { _ in
|
||||
snapshotView.removeFromSuperview()
|
||||
})
|
||||
}
|
||||
|
||||
Queue.mainQueue().after(0.1) {
|
||||
mediaEditor.toggleNightTheme()
|
||||
controller.node.entitiesView.eachView { view in
|
||||
if let stickerEntityView = view as? DrawingStickerEntityView {
|
||||
stickerEntityView.toggleNightTheme()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: 44.0, height: 44.0)
|
||||
)
|
||||
let dayNightButtonFrame = CGRect(
|
||||
origin: CGPoint(x: availableSize.width - 20.0 - dayNightButtonSize.width - 50.0, y: max(environment.statusBarHeight + 10.0, environment.safeInsets.top + 20.0)),
|
||||
size: dayNightButtonSize
|
||||
)
|
||||
if let dayNightButtonView = self.dayNightButton.view {
|
||||
if dayNightButtonView.superview == nil {
|
||||
setupButtonShadow(dayNightButtonView)
|
||||
self.addSubview(dayNightButtonView)
|
||||
|
||||
dayNightButtonView.layer.animateAlpha(from: 0.0, to: dayNightButtonView.alpha, duration: self.animatingButtons ? 0.1 : 0.2)
|
||||
dayNightButtonView.layer.animateScale(from: 0.4, to: 1.0, duration: self.animatingButtons ? 0.1 : 0.2)
|
||||
}
|
||||
transition.setPosition(view: dayNightButtonView, position: dayNightButtonFrame.center)
|
||||
transition.setBounds(view: dayNightButtonView, bounds: CGRect(origin: .zero, size: dayNightButtonFrame.size))
|
||||
transition.setScale(view: dayNightButtonView, scale: displayTopButtons ? 1.0 : 0.01)
|
||||
transition.setAlpha(view: dayNightButtonView, alpha: displayTopButtons && !component.isDismissing && !component.isInteractingWithEntities ? topButtonsAlpha : 0.0)
|
||||
}
|
||||
|
||||
topButtonOffsetX += 50.0
|
||||
} else {
|
||||
if let dayNightButtonView = self.dayNightButton.view, dayNightButtonView.superview != nil {
|
||||
dayNightButtonView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak dayNightButtonView] _ in
|
||||
dayNightButtonView?.removeFromSuperview()
|
||||
})
|
||||
dayNightButtonView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false)
|
||||
}
|
||||
}
|
||||
|
||||
if let playerState = state.playerState, playerState.hasAudio {
|
||||
let isVideoMuted = mediaEditor?.values.videoIsMuted ?? false
|
||||
|
||||
@@ -1632,7 +1728,7 @@ final class MediaEditorScreenComponent: Component {
|
||||
containerSize: CGSize(width: 44.0, height: 44.0)
|
||||
)
|
||||
let muteButtonFrame = CGRect(
|
||||
origin: CGPoint(x: availableSize.width - 20.0 - muteButtonSize.width - 50.0, y: max(environment.statusBarHeight + 10.0, environment.safeInsets.top + 20.0)),
|
||||
origin: CGPoint(x: availableSize.width - 20.0 - muteButtonSize.width - 50.0 - topButtonOffsetX, y: max(environment.statusBarHeight + 10.0, environment.safeInsets.top + 20.0)),
|
||||
size: muteButtonSize
|
||||
)
|
||||
if let muteButtonView = self.muteButton.view {
|
||||
@@ -1736,101 +1832,6 @@ final class MediaEditorScreenComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
if let subject = controller.node.subject, case .message = subject {
|
||||
let isNightTheme = mediaEditor?.values.nightTheme == true
|
||||
|
||||
let dayNightContentComponent: AnyComponentWithIdentity<Empty>
|
||||
if component.hasAppeared {
|
||||
dayNightContentComponent = AnyComponentWithIdentity(
|
||||
id: "animatedIcon",
|
||||
component: AnyComponent(
|
||||
LottieAnimationComponent(
|
||||
animation: LottieAnimationComponent.AnimationItem(
|
||||
name: isNightTheme ? "anim_sun" : "anim_sun_reverse",
|
||||
mode: state.dayNightDidChange ? .animating(loop: false) : .still(position: .end)
|
||||
),
|
||||
colors: ["__allcolors__": .white],
|
||||
size: CGSize(width: 30.0, height: 30.0)
|
||||
).tagged(dayNightButtonTag)
|
||||
)
|
||||
)
|
||||
} else {
|
||||
dayNightContentComponent = AnyComponentWithIdentity(
|
||||
id: "staticIcon",
|
||||
component: AnyComponent(
|
||||
BundleIconComponent(
|
||||
name: "Media Editor/MuteIcon",
|
||||
tintColor: nil
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
let dayNightButtonSize = self.dayNightButton.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(CameraButton(
|
||||
content: dayNightContentComponent,
|
||||
action: { [weak self, weak state, weak mediaEditor] in
|
||||
guard let environment = self?.environment, let controller = environment.controller() as? MediaEditorScreen else {
|
||||
return
|
||||
}
|
||||
guard !controller.node.recording.isActive else {
|
||||
return
|
||||
}
|
||||
|
||||
if let mediaEditor {
|
||||
state?.dayNightDidChange = true
|
||||
|
||||
if let snapshotView = controller.node.previewContainerView.snapshotView(afterScreenUpdates: false) {
|
||||
controller.node.previewContainerView.addSubview(snapshotView)
|
||||
|
||||
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, delay: 0.1, removeOnCompletion: false, completion: { _ in
|
||||
snapshotView.removeFromSuperview()
|
||||
})
|
||||
}
|
||||
|
||||
Queue.mainQueue().after(0.1) {
|
||||
mediaEditor.toggleNightTheme()
|
||||
controller.node.entitiesView.eachView { view in
|
||||
if let stickerEntityView = view as? DrawingStickerEntityView {
|
||||
stickerEntityView.toggleNightTheme()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: 44.0, height: 44.0)
|
||||
)
|
||||
let dayNightButtonFrame = CGRect(
|
||||
origin: CGPoint(x: availableSize.width - 20.0 - dayNightButtonSize.width - 50.0, y: max(environment.statusBarHeight + 10.0, environment.safeInsets.top + 20.0)),
|
||||
size: dayNightButtonSize
|
||||
)
|
||||
if let dayNightButtonView = self.dayNightButton.view {
|
||||
if dayNightButtonView.superview == nil {
|
||||
setupButtonShadow(dayNightButtonView)
|
||||
self.addSubview(dayNightButtonView)
|
||||
|
||||
dayNightButtonView.layer.animateAlpha(from: 0.0, to: dayNightButtonView.alpha, duration: self.animatingButtons ? 0.1 : 0.2)
|
||||
dayNightButtonView.layer.animateScale(from: 0.4, to: 1.0, duration: self.animatingButtons ? 0.1 : 0.2)
|
||||
}
|
||||
transition.setPosition(view: dayNightButtonView, position: dayNightButtonFrame.center)
|
||||
transition.setBounds(view: dayNightButtonView, bounds: CGRect(origin: .zero, size: dayNightButtonFrame.size))
|
||||
transition.setScale(view: dayNightButtonView, scale: displayTopButtons ? 1.0 : 0.01)
|
||||
transition.setAlpha(view: dayNightButtonView, alpha: displayTopButtons && !component.isDismissing && !component.isInteractingWithEntities ? topButtonsAlpha : 0.0)
|
||||
}
|
||||
|
||||
topButtonOffsetX += 50.0
|
||||
} else {
|
||||
if let dayNightButtonView = self.dayNightButton.view, dayNightButtonView.superview != nil {
|
||||
dayNightButtonView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak dayNightButtonView] _ in
|
||||
dayNightButtonView?.removeFromSuperview()
|
||||
})
|
||||
dayNightButtonView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false)
|
||||
}
|
||||
}
|
||||
|
||||
let switchCameraButtonSize = self.switchCameraButton.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(Button(
|
||||
@@ -2426,9 +2427,11 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
||||
}
|
||||
}
|
||||
|
||||
let maybeFile = messages.first?.media.first(where: { $0 is TelegramMediaFile }) as? TelegramMediaFile
|
||||
|
||||
let renderer = DrawingMessageRenderer(context: self.context, messages: messages)
|
||||
renderer.render(completion: { size, dayImage, nightImage in
|
||||
let messageEntity = DrawingStickerEntity(content: .message(messageIds, size))
|
||||
let messageEntity = DrawingStickerEntity(content: .message(messageIds, maybeFile?.isVideo == true ? maybeFile : nil, size))
|
||||
messageEntity.renderImage = dayImage
|
||||
messageEntity.secondaryRenderImage = nightImage
|
||||
messageEntity.referenceDrawingSize = storyDimensions
|
||||
|
||||
+2
-2
@@ -230,7 +230,7 @@ final class ContextResultPanelComponent: Component {
|
||||
presence: nil,
|
||||
selectionState: .none,
|
||||
hasNext: index != peers.count - 1,
|
||||
action: { [weak self] peer, _ in
|
||||
action: { [weak self] peer, _, _ in
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
}
|
||||
@@ -299,7 +299,7 @@ final class ContextResultPanelComponent: Component {
|
||||
presence: nil,
|
||||
selectionState: .none,
|
||||
hasNext: true,
|
||||
action: { _, _ in
|
||||
action: { _, _, _ in
|
||||
}
|
||||
)),
|
||||
environment: {},
|
||||
|
||||
@@ -135,6 +135,7 @@ swift_library(
|
||||
"//submodules/TelegramUI/Components/PeerInfo/PeerInfoChatListPaneNode",
|
||||
"//submodules/MediaPickerUI",
|
||||
"//submodules/AttachmentUI",
|
||||
"//submodules/TelegramUI/Components/Settings/BoostLevelIconComponent",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
||||
@@ -101,6 +101,7 @@ import ChatHistorySearchContainerNode
|
||||
import PeerInfoPaneNode
|
||||
import MediaPickerUI
|
||||
import AttachmentUI
|
||||
import BoostLevelIconComponent
|
||||
|
||||
public enum PeerInfoAvatarEditingMode {
|
||||
case generic
|
||||
|
||||
+10
-6
@@ -8,20 +8,24 @@ import AnimatedTextComponent
|
||||
|
||||
public final class PremiumLockButtonSubtitleComponent: CombinedComponent {
|
||||
public let count: Int
|
||||
public let theme: PresentationTheme
|
||||
public let color: UIColor
|
||||
public let strings: PresentationStrings
|
||||
|
||||
public init(count: Int, theme: PresentationTheme, strings: PresentationStrings) {
|
||||
public init(count: Int, color: UIColor, strings: PresentationStrings) {
|
||||
self.count = count
|
||||
self.theme = theme
|
||||
self.color = color
|
||||
self.strings = strings
|
||||
}
|
||||
|
||||
public convenience init(count: Int, theme: PresentationTheme, strings: PresentationStrings) {
|
||||
self.init(count: count, color: theme.list.itemCheckColors.foregroundColor.withMultipliedAlpha(0.7), strings: strings)
|
||||
}
|
||||
|
||||
public static func ==(lhs: PremiumLockButtonSubtitleComponent, rhs: PremiumLockButtonSubtitleComponent) -> Bool {
|
||||
if lhs.count != rhs.count {
|
||||
return false
|
||||
}
|
||||
if lhs.theme !== rhs.theme {
|
||||
if lhs.color !== rhs.color {
|
||||
return false
|
||||
}
|
||||
if lhs.strings !== rhs.strings {
|
||||
@@ -38,7 +42,7 @@ public final class PremiumLockButtonSubtitleComponent: CombinedComponent {
|
||||
let icon = icon.update(
|
||||
component: BundleIconComponent(
|
||||
name: "Chat/Input/Accessory Panels/TextLockIcon",
|
||||
tintColor: context.component.theme.list.itemCheckColors.foregroundColor.withMultipliedAlpha(0.7),
|
||||
tintColor: context.component.color,
|
||||
maxSize: CGSize(width: 10.0, height: 10.0)
|
||||
),
|
||||
availableSize: CGSize(width: 100.0, height: 100.0),
|
||||
@@ -63,7 +67,7 @@ public final class PremiumLockButtonSubtitleComponent: CombinedComponent {
|
||||
}
|
||||
|
||||
let text = text.update(
|
||||
component: AnimatedTextComponent(font: Font.medium(11.0), color: context.component.theme.list.itemCheckColors.foregroundColor.withMultipliedAlpha(0.7), items: textItems),
|
||||
component: AnimatedTextComponent(font: Font.medium(11.0), color: context.component.color, items: textItems),
|
||||
availableSize: CGSize(width: context.availableSize.width - 20.0, height: 100.0),
|
||||
transition: context.transition
|
||||
)
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "BoostLevelIconComponent",
|
||||
module_name = "BoostLevelIconComponent",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
"//submodules/Display",
|
||||
"//submodules/ComponentFlow",
|
||||
"//submodules/TelegramPresentationData",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import ComponentFlow
|
||||
import TelegramPresentationData
|
||||
|
||||
public func generateDisclosureActionBoostLevelBadgeImage(text: String) -> UIImage {
|
||||
let attributedText = NSAttributedString(string: text, attributes: [
|
||||
.font: Font.medium(12.0),
|
||||
.foregroundColor: UIColor.white
|
||||
])
|
||||
let bounds = attributedText.boundingRect(with: CGSize(width: 100.0, height: 100.0), options: .usesLineFragmentOrigin, context: nil)
|
||||
let leftInset: CGFloat = 16.0
|
||||
let rightInset: CGFloat = 4.0
|
||||
let size = CGSize(width: leftInset + rightInset + ceil(bounds.width), height: 20.0)
|
||||
return generateImage(size, rotatedContext: { size, context in
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
context.addPath(UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: size), cornerRadius: 6.0).cgPath)
|
||||
context.clip()
|
||||
|
||||
var locations: [CGFloat] = [0.0, 1.0]
|
||||
let colors: [CGColor] = [UIColor(rgb: 0x9076FF).cgColor, UIColor(rgb: 0xB86DEA).cgColor]
|
||||
let colorSpace = CGColorSpaceCreateDeviceRGB()
|
||||
let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)!
|
||||
context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: size.width, y: 0.0), options: CGGradientDrawingOptions())
|
||||
|
||||
context.resetClip()
|
||||
|
||||
UIGraphicsPushContext(context)
|
||||
|
||||
if let image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Media/PanelBadgeLock"), color: .white) {
|
||||
let imageFit: CGFloat = 14.0
|
||||
let imageSize = image.size.aspectFitted(CGSize(width: imageFit, height: imageFit))
|
||||
let imageRect = CGRect(origin: CGPoint(x: 2.0, y: UIScreenPixel + floorToScreenPixels((size.height - imageSize.height) * 0.5)), size: imageSize)
|
||||
image.draw(in: imageRect)
|
||||
}
|
||||
|
||||
attributedText.draw(at: CGPoint(x: leftInset, y: floorToScreenPixels((size.height - bounds.height) * 0.5)))
|
||||
|
||||
UIGraphicsPopContext()
|
||||
})!
|
||||
}
|
||||
|
||||
public final class BoostLevelIconComponent: Component {
|
||||
let strings: PresentationStrings
|
||||
let level: Int
|
||||
|
||||
public init(
|
||||
strings: PresentationStrings,
|
||||
level: Int
|
||||
) {
|
||||
self.strings = strings
|
||||
self.level = level
|
||||
}
|
||||
|
||||
public static func ==(lhs: BoostLevelIconComponent, rhs: BoostLevelIconComponent) -> Bool {
|
||||
if lhs.strings !== rhs.strings {
|
||||
return false
|
||||
}
|
||||
if lhs.level != rhs.level {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
public final class View: UIView {
|
||||
private let imageView: UIImageView
|
||||
|
||||
private var component: BoostLevelIconComponent?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.imageView = UIImageView()
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.addSubview(self.imageView)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func update(component: BoostLevelIconComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||
if self.component != component {
|
||||
//TODO:localize
|
||||
self.imageView.image = generateDisclosureActionBoostLevelBadgeImage(text: "Level \(component.level)")
|
||||
}
|
||||
self.component = component
|
||||
|
||||
if let image = self.imageView.image {
|
||||
self.imageView.frame = CGRect(origin: CGPoint(), size: image.size)
|
||||
return image.size
|
||||
} else {
|
||||
return CGSize(width: 1.0, height: 20.0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func makeView() -> View {
|
||||
return View(frame: CGRect())
|
||||
}
|
||||
|
||||
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "GenerateThemeName",
|
||||
module_name = "GenerateThemeName",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
"//submodules/Display",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
||||
+1
-1
@@ -302,7 +302,7 @@ private let colorPairs: [(UInt32, UInt32)] = [
|
||||
(0x00416a, 0xe4e5e6)
|
||||
]
|
||||
|
||||
func generateGradientColors(color: UIColor) -> (UIColor, UIColor) {
|
||||
public func generateGradientColors(color: UIColor) -> (UIColor, UIColor) {
|
||||
var nearest: (colors: (lhs: UInt32, rhs: UInt32), distance: Int32)?
|
||||
for (lhs, rhs) in colorPairs {
|
||||
let lhsDistance = color.distance(to: UIColor(rgb: lhs))
|
||||
+1
-1
@@ -297,7 +297,7 @@ private let subjectives: [String] = [
|
||||
"Zone"
|
||||
]
|
||||
|
||||
func generateThemeName(accentColor: UIColor) -> String {
|
||||
public func generateThemeName(accentColor: UIColor) -> String {
|
||||
var nearest: (color: UInt32, distance: Int32)?
|
||||
for (color, _) in colors {
|
||||
let distance = accentColor.distance(to: UIColor(rgb: color))
|
||||
@@ -45,6 +45,8 @@ swift_library(
|
||||
"//submodules/WallpaperResources",
|
||||
"//submodules/MediaPickerUI",
|
||||
"//submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen",
|
||||
"//submodules/TelegramUI/Components/Settings/WallpaperGridScreen",
|
||||
"//submodules/TelegramUI/Components/Settings/BoostLevelIconComponent",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
||||
+104
-183
@@ -32,6 +32,8 @@ import ComponentDisplayAdapters
|
||||
import WallpaperResources
|
||||
import MediaPickerUI
|
||||
import WallpaperGalleryScreen
|
||||
import WallpaperGridScreen
|
||||
import BoostLevelIconComponent
|
||||
|
||||
private final class EmojiActionIconComponent: Component {
|
||||
let context: AccountContext
|
||||
@@ -127,107 +129,6 @@ private final class EmojiActionIconComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
public func generateDisclosureActionBoostLevelBadgeImage(text: String) -> UIImage {
|
||||
let attributedText = NSAttributedString(string: text, attributes: [
|
||||
.font: Font.medium(12.0),
|
||||
.foregroundColor: UIColor.white
|
||||
])
|
||||
let bounds = attributedText.boundingRect(with: CGSize(width: 100.0, height: 100.0), options: .usesLineFragmentOrigin, context: nil)
|
||||
let leftInset: CGFloat = 16.0
|
||||
let rightInset: CGFloat = 4.0
|
||||
let size = CGSize(width: leftInset + rightInset + ceil(bounds.width), height: 20.0)
|
||||
return generateImage(size, rotatedContext: { size, context in
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
context.addPath(UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: size), cornerRadius: 6.0).cgPath)
|
||||
context.clip()
|
||||
|
||||
var locations: [CGFloat] = [0.0, 1.0]
|
||||
let colors: [CGColor] = [UIColor(rgb: 0x9076FF).cgColor, UIColor(rgb: 0xB86DEA).cgColor]
|
||||
let colorSpace = CGColorSpaceCreateDeviceRGB()
|
||||
let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)!
|
||||
context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: size.width, y: 0.0), options: CGGradientDrawingOptions())
|
||||
|
||||
context.resetClip()
|
||||
|
||||
UIGraphicsPushContext(context)
|
||||
|
||||
if let image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Media/PanelBadgeLock"), color: .white) {
|
||||
let imageFit: CGFloat = 14.0
|
||||
let imageSize = image.size.aspectFitted(CGSize(width: imageFit, height: imageFit))
|
||||
let imageRect = CGRect(origin: CGPoint(x: 2.0, y: UIScreenPixel + floorToScreenPixels((size.height - imageSize.height) * 0.5)), size: imageSize)
|
||||
image.draw(in: imageRect)
|
||||
}
|
||||
|
||||
attributedText.draw(at: CGPoint(x: leftInset, y: floorToScreenPixels((size.height - bounds.height) * 0.5)))
|
||||
|
||||
UIGraphicsPopContext()
|
||||
})!
|
||||
}
|
||||
|
||||
private final class BoostLevelIconComponent: Component {
|
||||
let strings: PresentationStrings
|
||||
let level: Int
|
||||
|
||||
init(
|
||||
strings: PresentationStrings,
|
||||
level: Int
|
||||
) {
|
||||
self.strings = strings
|
||||
self.level = level
|
||||
}
|
||||
|
||||
static func ==(lhs: BoostLevelIconComponent, rhs: BoostLevelIconComponent) -> Bool {
|
||||
if lhs.strings !== rhs.strings {
|
||||
return false
|
||||
}
|
||||
if lhs.level != rhs.level {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
final class View: UIView {
|
||||
private let imageView: UIImageView
|
||||
|
||||
private var component: BoostLevelIconComponent?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.imageView = UIImageView()
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.addSubview(self.imageView)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func update(component: BoostLevelIconComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||
if self.component != component {
|
||||
//TODO:localize
|
||||
self.imageView.image = generateDisclosureActionBoostLevelBadgeImage(text: "Level \(component.level)")
|
||||
}
|
||||
self.component = component
|
||||
|
||||
if let image = self.imageView.image {
|
||||
self.imageView.frame = CGRect(origin: CGPoint(), size: image.size)
|
||||
return image.size
|
||||
} else {
|
||||
return CGSize(width: 1.0, height: 20.0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func makeView() -> View {
|
||||
return View(frame: CGRect())
|
||||
}
|
||||
|
||||
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||
}
|
||||
}
|
||||
|
||||
final class ChannelAppearanceScreenComponent: Component {
|
||||
typealias EnvironmentType = ViewControllerComponentContainer.Environment
|
||||
|
||||
@@ -360,6 +261,8 @@ final class ChannelAppearanceScreenComponent: Component {
|
||||
private var updatedPeerProfileColor: PeerNameColor??
|
||||
private var updatedPeerProfileEmoji: Int64??
|
||||
private var updatedPeerStatus: PeerEmojiStatus??
|
||||
private var updatedPeerWallpaper: WallpaperSelectionResult?
|
||||
private var temporaryPeerWallpaper: TelegramWallpaper?
|
||||
|
||||
private var requiredBoostSubject: BoostSubject?
|
||||
|
||||
@@ -502,9 +405,19 @@ final class ChannelAppearanceScreenComponent: Component {
|
||||
changes.insert(.emojiStatus)
|
||||
}
|
||||
|
||||
let wallpaper = self.resolvedCurrentTheme?.wallpaper
|
||||
if wallpaper != contentsData.peerWallpaper {
|
||||
let wallpaper: TelegramWallpaper?
|
||||
if let updatedPeerWallpaper = self.updatedPeerWallpaper {
|
||||
switch updatedPeerWallpaper {
|
||||
case .remove:
|
||||
wallpaper = nil
|
||||
case let .emoticon(emoticon):
|
||||
wallpaper = .emoticon(emoticon)
|
||||
case .custom:
|
||||
wallpaper = self.temporaryPeerWallpaper
|
||||
}
|
||||
changes.insert(.wallpaper)
|
||||
} else {
|
||||
wallpaper = contentsData.peerWallpaper
|
||||
}
|
||||
|
||||
return ResolvedState(
|
||||
@@ -544,16 +457,6 @@ final class ChannelAppearanceScreenComponent: Component {
|
||||
|
||||
let statusFileId = resolvedState.emojiStatus?.fileId
|
||||
|
||||
var wallpaperEmoji: String?
|
||||
if let currentTheme = self.currentTheme {
|
||||
switch currentTheme {
|
||||
case let .cloud(cloudTheme):
|
||||
wallpaperEmoji = cloudTheme.theme.emoticon
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
enum ApplyError {
|
||||
case generic
|
||||
}
|
||||
@@ -574,11 +477,24 @@ final class ChannelAppearanceScreenComponent: Component {
|
||||
})
|
||||
}
|
||||
if resolvedState.changes.contains(.wallpaper) {
|
||||
let _ = wallpaperEmoji
|
||||
/*signals.append(component.context.engine.themes.setBuiltinChannelWallpaper(peerId: component.peerId, emoji: wallpaperEmoji, wallpaper: self.resolvedCurrentTheme?.wallpaper)
|
||||
|> mapError { _ -> ApplyError in
|
||||
return .generic
|
||||
})*/
|
||||
if let updatedPeerWallpaper {
|
||||
switch updatedPeerWallpaper {
|
||||
case .remove:
|
||||
signals.append(component.context.engine.themes.setChatWallpaper(peerId: component.peerId, wallpaper: nil, forBoth: false)
|
||||
|> ignoreValues
|
||||
|> mapError { _ -> ApplyError in
|
||||
return .generic
|
||||
})
|
||||
case let .emoticon(emoticon):
|
||||
signals.append(component.context.engine.themes.setChatWallpaper(peerId: component.peerId, wallpaper: .emoticon(emoticon), forBoth: false)
|
||||
|> ignoreValues
|
||||
|> mapError { _ -> ApplyError in
|
||||
return .generic
|
||||
})
|
||||
case let .custom(wallpaperEntry, options, editedImage, cropRect, brightness):
|
||||
uploadCustomPeerWallpaper(context: component.context, wallpaper: wallpaperEntry, mode: options, editedImage: editedImage, cropRect: cropRect, brightness: brightness, peerId: component.peerId, forBoth: false, completion: {})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.applyDisposable = (combineLatest(signals)
|
||||
@@ -644,55 +560,44 @@ final class ChannelAppearanceScreenComponent: Component {
|
||||
}
|
||||
|
||||
private func openCustomWallpaperSetup() {
|
||||
guard let _ = self.component, let contentsData = self.contentsData else {
|
||||
guard let component = self.component, let contentsData = self.contentsData, let peer = contentsData.peer, let premiumConfiguration = self.premiumConfiguration, let boostStatus = self.boostStatus, let resolvedState = self.resolveState() else {
|
||||
return
|
||||
}
|
||||
// let dismissControllers = { [weak self] in
|
||||
// if let self, let navigationController = self.controller?.navigationController as? NavigationController {
|
||||
// let controllers = navigationController.viewControllers.filter({ controller in
|
||||
// if controller is WallpaperGalleryController || controller is AttachmentController || controller is PeerInfoScreenImpl {
|
||||
// return false
|
||||
// }
|
||||
// return true
|
||||
// })
|
||||
// navigationController.setViewControllers(controllers, animated: true)
|
||||
// }
|
||||
// }
|
||||
// var openWallpaperPickerImpl: ((Bool) -> Void)?
|
||||
let openWallpaperPicker: (Bool) -> Void = { [weak self] animateAppearance in
|
||||
guard let self, let component = self.component, let peer = contentsData.peer else {
|
||||
let level = boostStatus.level
|
||||
let requiredWallpaperLevel = Int(BoostSubject.wallpaper.requiredLevel(context: component.context, configuration: premiumConfiguration))
|
||||
let requiredCustomWallpaperLevel = Int(BoostSubject.customWallpaper.requiredLevel(context: component.context, configuration: premiumConfiguration))
|
||||
|
||||
let selectedWallpaper = resolvedState.wallpaper
|
||||
|
||||
let controller = ThemeGridController(
|
||||
context: component.context,
|
||||
mode: .peer(peer, contentsData.availableThemes, selectedWallpaper, level < requiredWallpaperLevel ? requiredWallpaperLevel : nil, level < requiredCustomWallpaperLevel ? requiredCustomWallpaperLevel : nil)
|
||||
)
|
||||
controller.completion = { [weak self] result in
|
||||
guard let self, let component = self.component, let contentsData = self.contentsData else {
|
||||
return
|
||||
}
|
||||
let controller = wallpaperMediaPickerController(
|
||||
context: component.context,
|
||||
updatedPresentationData: nil,
|
||||
peer: peer,
|
||||
animateAppearance: true,
|
||||
completion: { [weak self] _, result in
|
||||
guard let self, let component = self.component, let asset = result as? PHAsset else {
|
||||
return
|
||||
}
|
||||
let controller = WallpaperGalleryController(context: component.context, source: .asset(asset), mode: .peer(peer, false))
|
||||
controller.navigationPresentation = .modal
|
||||
controller.apply = { wallpaper, options, editedImage, cropRect, brightness, forBoth in
|
||||
// if let strongSelf = self {
|
||||
// uploadCustomPeerWallpaper(context: strongSelf.context, wallpaper: wallpaper, mode: options, editedImage: editedImage, cropRect: cropRect, brightness: brightness, peerId: peer.id, forBoth: forBoth, completion: {
|
||||
// Queue.mainQueue().after(0.3, {
|
||||
// dismissControllers()
|
||||
// })
|
||||
// })
|
||||
// }
|
||||
}
|
||||
self.environment?.controller()?.push(controller)
|
||||
},
|
||||
openColors: {
|
||||
self.updatedPeerWallpaper = result
|
||||
switch result {
|
||||
case let .emoticon(emoticon):
|
||||
if let selectedTheme = contentsData.availableThemes.first(where: { $0.emoticon?.strippedEmoji == emoticon.strippedEmoji }) {
|
||||
self.currentTheme = .cloud(PresentationCloudTheme(theme: selectedTheme, resolvedWallpaper: nil, creatorAccountId: nil))
|
||||
}
|
||||
)
|
||||
controller.navigationPresentation = .flatModal
|
||||
self.environment?.controller()?.push(controller)
|
||||
self.temporaryPeerWallpaper = nil
|
||||
case .remove:
|
||||
self.currentTheme = nil
|
||||
self.temporaryPeerWallpaper = nil
|
||||
case let .custom(wallpaperEntry, options, editedImage, cropRect, brightness):
|
||||
let _ = (getTemporaryCustomPeerWallpaper(context: component.context, wallpaper: wallpaperEntry, mode: options, editedImage: editedImage, cropRect: cropRect, brightness: brightness)
|
||||
|> deliverOnMainQueue).startStandalone(next: { [weak self] wallpaper in
|
||||
self?.temporaryPeerWallpaper = wallpaper
|
||||
self?.state?.updated(transition: .immediate)
|
||||
})
|
||||
self.currentTheme = nil
|
||||
}
|
||||
self.state?.updated(transition: .immediate)
|
||||
}
|
||||
// openWallpaperPickerImpl = openWallpaperPicker
|
||||
openWallpaperPicker(true)
|
||||
self.environment?.controller()?.push(controller)
|
||||
}
|
||||
|
||||
private enum EmojiSetupSubject {
|
||||
@@ -833,20 +738,9 @@ final class ChannelAppearanceScreenComponent: Component {
|
||||
}
|
||||
if self.contentsData == nil, let peerWallpaper = contentsData.peerWallpaper {
|
||||
for cloudTheme in contentsData.availableThemes {
|
||||
let settings: TelegramThemeSettings?
|
||||
if let exactSettings = cloudTheme.settings?.first(where: { ($0.baseTheme == .night || $0.baseTheme == .tinted || $0.baseTheme == .classic || $0.baseTheme == .day) }) {
|
||||
settings = exactSettings
|
||||
} else if let firstSettings = cloudTheme.settings?.first {
|
||||
settings = firstSettings
|
||||
} else {
|
||||
settings = nil
|
||||
}
|
||||
|
||||
if let settings {
|
||||
if settings.wallpaper == peerWallpaper {
|
||||
self.currentTheme = .cloud(PresentationCloudTheme(theme: cloudTheme, resolvedWallpaper: nil, creatorAccountId: cloudTheme.isCreator ? component.context.account.id : nil))
|
||||
break
|
||||
}
|
||||
if case let .emoticon(emoticon) = peerWallpaper, cloudTheme.emoticon?.strippedEmoji == emoticon.strippedEmoji {
|
||||
self.currentTheme = .cloud(PresentationCloudTheme(theme: cloudTheme, resolvedWallpaper: nil, creatorAccountId: cloudTheme.isCreator ? component.context.account.id : nil))
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -906,7 +800,7 @@ final class ChannelAppearanceScreenComponent: Component {
|
||||
|
||||
let cloudThemes: [PresentationThemeReference] = contentsData.availableThemes.map { .cloud(PresentationCloudTheme(theme: $0, resolvedWallpaper: nil, creatorAccountId: $0.isCreator ? component.context.account.id : nil)) }
|
||||
let chatThemes = cloudThemes.filter { $0.emoticon != nil }
|
||||
|
||||
|
||||
if let currentTheme = self.currentTheme, (self.resolvedCurrentTheme?.reference != currentTheme || self.resolvedCurrentTheme?.isDark != environment.theme.overallDarkAppearance), (self.resolvingCurrentTheme?.reference != currentTheme || self.resolvingCurrentTheme?.isDark != environment.theme.overallDarkAppearance) {
|
||||
self.resolvingCurrentTheme?.disposable.dispose()
|
||||
|
||||
@@ -924,7 +818,9 @@ final class ChannelAppearanceScreenComponent: Component {
|
||||
}
|
||||
if let presentationTheme {
|
||||
let resolvedWallpaper: Signal<TelegramWallpaper?, NoError>
|
||||
if case let .file(file) = presentationTheme.chat.defaultWallpaper, file.id == 0 {
|
||||
if let temporaryPeerWallpaper = self.temporaryPeerWallpaper {
|
||||
resolvedWallpaper = .single(temporaryPeerWallpaper)
|
||||
} else if case let .file(file) = presentationTheme.chat.defaultWallpaper, file.id == 0 {
|
||||
resolvedWallpaper = cachedWallpaper(account: component.context.account, slug: file.slug, settings: file.settings)
|
||||
|> map { wallpaper -> TelegramWallpaper? in
|
||||
return wallpaper?.wallpaper
|
||||
@@ -946,12 +842,15 @@ final class ChannelAppearanceScreenComponent: Component {
|
||||
} else if self.currentTheme == nil {
|
||||
self.resolvingCurrentTheme?.disposable.dispose()
|
||||
self.resolvingCurrentTheme = nil
|
||||
|
||||
self.resolvedCurrentTheme = nil
|
||||
}
|
||||
|
||||
if self.currentTheme != nil {
|
||||
requiredBoostSubjects.append(.wallpaper)
|
||||
if let wallpaper = resolvedState.wallpaper {
|
||||
if wallpaper.isEmoticon {
|
||||
requiredBoostSubjects.append(.wallpaper)
|
||||
} else {
|
||||
requiredBoostSubjects.append(.customWallpaper)
|
||||
}
|
||||
}
|
||||
|
||||
if case let .user(user) = peer {
|
||||
@@ -1025,11 +924,16 @@ final class ChannelAppearanceScreenComponent: Component {
|
||||
|
||||
var chatPreviewTheme: PresentationTheme = environment.theme
|
||||
var chatPreviewWallpaper: TelegramWallpaper = presentationData.chatWallpaper
|
||||
if let resolvedCurrentTheme = self.resolvedCurrentTheme {
|
||||
if let updatedWallpaper = self.updatedPeerWallpaper, case .remove = updatedWallpaper {
|
||||
} else if let temporaryPeerWallpaper = self.temporaryPeerWallpaper {
|
||||
chatPreviewWallpaper = temporaryPeerWallpaper
|
||||
} else if let resolvedCurrentTheme = self.resolvedCurrentTheme {
|
||||
chatPreviewTheme = resolvedCurrentTheme.theme
|
||||
if let wallpaper = resolvedCurrentTheme.wallpaper {
|
||||
chatPreviewWallpaper = wallpaper
|
||||
}
|
||||
} else if let initialWallpaper = contentsData.peerWallpaper, !initialWallpaper.isEmoticon {
|
||||
chatPreviewWallpaper = initialWallpaper
|
||||
}
|
||||
|
||||
let replySectionSize = self.replySection.update(
|
||||
@@ -1129,6 +1033,14 @@ final class ChannelAppearanceScreenComponent: Component {
|
||||
))))
|
||||
}
|
||||
|
||||
var currentTheme = self.currentTheme
|
||||
var selectedWallpaper: TelegramWallpaper?
|
||||
if currentTheme == nil, let wallpaper = resolvedState.wallpaper, !wallpaper.isEmoticon {
|
||||
let theme: PresentationThemeReference = .builtin(.day)
|
||||
currentTheme = theme
|
||||
selectedWallpaper = wallpaper
|
||||
}
|
||||
|
||||
let wallpaperSectionSize = self.wallpaperSection.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(ListSectionComponent(
|
||||
@@ -1156,12 +1068,19 @@ final class ChannelAppearanceScreenComponent: Component {
|
||||
themeSpecificChatWallpapers: [:],
|
||||
nightMode: environment.theme.overallDarkAppearance,
|
||||
channelMode: true,
|
||||
currentTheme: self.currentTheme,
|
||||
selectedWallpaper: selectedWallpaper,
|
||||
currentTheme: currentTheme,
|
||||
updatedTheme: { [weak self] value in
|
||||
guard let self else {
|
||||
guard let self, value != .builtin(.day) else {
|
||||
return
|
||||
}
|
||||
self.currentTheme = value
|
||||
self.temporaryPeerWallpaper = nil
|
||||
if let value {
|
||||
self.updatedPeerWallpaper = .emoticon(value.emoticon ?? "")
|
||||
} else {
|
||||
self.updatedPeerWallpaper = .remove
|
||||
}
|
||||
self.state?.updated(transition: .spring(duration: 0.4))
|
||||
},
|
||||
contextAction: nil
|
||||
@@ -1538,8 +1457,10 @@ public class ChannelAppearanceScreen: ViewControllerComponentContainer {
|
||||
peerId: peerId
|
||||
), navigationBarAppearance: .default, theme: .default, updatedPresentationData: updatedPresentationData)
|
||||
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
//TODO:localize
|
||||
self.title = "Appearance"
|
||||
self.navigationItem.backBarButtonItem = UIBarButtonItem(title: presentationData.strings.Common_Back, style: .plain, target: nil, action: nil)
|
||||
|
||||
self.ready.set(.never())
|
||||
|
||||
|
||||
@@ -18,8 +18,11 @@ swift_library(
|
||||
"//submodules/TelegramPresentationData",
|
||||
"//submodules/GradientBackground",
|
||||
"//submodules/WallpaperResources",
|
||||
"//submodules/StickerResources",
|
||||
"//submodules/AccountContext",
|
||||
"//submodules/RadialStatusNode",
|
||||
"//submodules/AnimatedStickerNode",
|
||||
"//submodules/TelegramAnimatedStickerNode",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
||||
+89
-3
@@ -10,6 +10,9 @@ import AccountContext
|
||||
import RadialStatusNode
|
||||
import WallpaperResources
|
||||
import GradientBackground
|
||||
import StickerResources
|
||||
import AnimatedStickerNode
|
||||
import TelegramAnimatedStickerNode
|
||||
|
||||
private func whiteColorImage(theme: PresentationTheme, color: UIColor) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> {
|
||||
return .single({ arguments in
|
||||
@@ -46,12 +49,19 @@ public final class SettingsThemeWallpaperNode: ASDisplayNode {
|
||||
public var wallpaper: TelegramWallpaper?
|
||||
private var arguments: PatternWallpaperArguments?
|
||||
|
||||
private var emojiFile: TelegramMediaFile?
|
||||
|
||||
public let buttonNode = HighlightTrackingButtonNode()
|
||||
public let backgroundNode = ASImageNode()
|
||||
public let imageNode = TransformImageNode()
|
||||
private var gradientNode: GradientBackgroundNode?
|
||||
private let statusNode: RadialStatusNode
|
||||
|
||||
private let emojiContainerNode: ASDisplayNode
|
||||
private let emojiImageNode: TransformImageNode
|
||||
private var animatedStickerNode: AnimatedStickerNode?
|
||||
private let stickerFetchedDisposable = MetaDisposable()
|
||||
|
||||
public var pressed: (() -> Void)?
|
||||
|
||||
private let displayLoading: Bool
|
||||
@@ -69,6 +79,9 @@ public final class SettingsThemeWallpaperNode: ASDisplayNode {
|
||||
self.statusNode.frame = CGRect(x: 0.0, y: 0.0, width: progressDiameter, height: progressDiameter)
|
||||
self.statusNode.isUserInteractionEnabled = false
|
||||
|
||||
self.emojiContainerNode = ASDisplayNode()
|
||||
self.emojiImageNode = TransformImageNode()
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.backgroundNode)
|
||||
@@ -76,11 +89,34 @@ public final class SettingsThemeWallpaperNode: ASDisplayNode {
|
||||
self.addSubnode(self.buttonNode)
|
||||
self.addSubnode(self.statusNode)
|
||||
|
||||
self.addSubnode(self.emojiContainerNode)
|
||||
// self.emojiContainerNode.addSubnode(self.emojiNode)
|
||||
self.emojiContainerNode.addSubnode(self.emojiImageNode)
|
||||
|
||||
self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside)
|
||||
|
||||
var firstTime = true
|
||||
self.emojiImageNode.imageUpdated = { [weak self] image in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
if image != nil {
|
||||
strongSelf.removePlaceholder(animated: !firstTime)
|
||||
if firstTime {
|
||||
strongSelf.emojiImageNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
}
|
||||
}
|
||||
firstTime = false
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.isLoadedDisposable.dispose()
|
||||
self.stickerFetchedDisposable.dispose()
|
||||
}
|
||||
|
||||
private func removePlaceholder(animated: Bool) {
|
||||
|
||||
}
|
||||
|
||||
public func setSelected(_ selected: Bool, animated: Bool = false) {
|
||||
@@ -114,7 +150,7 @@ public final class SettingsThemeWallpaperNode: ASDisplayNode {
|
||||
self.statusNode.backgroundNodeColor = color
|
||||
}
|
||||
|
||||
public func setWallpaper(context: AccountContext, wallpaper: TelegramWallpaper, selected: Bool, size: CGSize, cornerRadius: CGFloat = 0.0, synchronousLoad: Bool = false) {
|
||||
public func setWallpaper(context: AccountContext, theme: PresentationTheme? = nil, wallpaper: TelegramWallpaper, isEmpty: Bool = false, emojiFile: TelegramMediaFile? = nil, selected: Bool, size: CGSize, cornerRadius: CGFloat = 0.0, synchronousLoad: Bool = false) {
|
||||
self.buttonNode.frame = CGRect(origin: CGPoint(), size: size)
|
||||
self.backgroundNode.frame = CGRect(origin: CGPoint(), size: size)
|
||||
self.imageNode.frame = CGRect(origin: CGPoint(), size: size)
|
||||
@@ -175,6 +211,11 @@ public final class SettingsThemeWallpaperNode: ASDisplayNode {
|
||||
self.backgroundNode.backgroundColor = UIColor(rgb: colors[0])
|
||||
}
|
||||
}
|
||||
|
||||
if isEmpty, let theme {
|
||||
self.backgroundNode.image = nil
|
||||
self.backgroundNode.backgroundColor = theme.list.mediaPlaceholderColor
|
||||
}
|
||||
|
||||
if let gradientNode = self.gradientNode {
|
||||
gradientNode.frame = CGRect(origin: CGPoint(), size: size)
|
||||
@@ -186,7 +227,7 @@ public final class SettingsThemeWallpaperNode: ASDisplayNode {
|
||||
|
||||
let corners = ImageCorners(radius: cornerRadius)
|
||||
|
||||
if self.wallpaper != wallpaper {
|
||||
if self.wallpaper != wallpaper && !isEmpty {
|
||||
self.wallpaper = wallpaper
|
||||
switch wallpaper {
|
||||
case .builtin:
|
||||
@@ -286,7 +327,7 @@ public final class SettingsThemeWallpaperNode: ASDisplayNode {
|
||||
}
|
||||
} else if let wallpaper = self.wallpaper {
|
||||
switch wallpaper {
|
||||
case .builtin, .color, .gradient:
|
||||
case .builtin, .color, .gradient, .emoticon:
|
||||
let apply = self.imageNode.asyncLayout()(TransformImageArguments(corners: corners, imageSize: CGSize(), boundingSize: size, intrinsicInsets: UIEdgeInsets()))
|
||||
apply()
|
||||
case let .image(representations, _):
|
||||
@@ -300,6 +341,51 @@ public final class SettingsThemeWallpaperNode: ASDisplayNode {
|
||||
}
|
||||
|
||||
self.setSelected(selected, animated: false)
|
||||
|
||||
|
||||
self.emojiContainerNode.frame = self.backgroundNode.frame
|
||||
|
||||
var emojiFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - 42.0) / 2.0), y: 98.0), size: CGSize(width: 42.0, height: 42.0))
|
||||
if isEmpty {
|
||||
emojiFrame = emojiFrame.insetBy(dx: 3.0, dy: 3.0)
|
||||
}
|
||||
if let file = emojiFile, self.emojiFile?.id != emojiFile?.id {
|
||||
self.emojiFile = file
|
||||
|
||||
let imageApply = self.emojiImageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: emojiFrame.size, boundingSize: emojiFrame.size, intrinsicInsets: UIEdgeInsets()))
|
||||
imageApply()
|
||||
self.emojiImageNode.setSignal(chatMessageStickerPackThumbnail(postbox: context.account.postbox, resource: file.resource, animated: true, nilIfEmpty: true))
|
||||
self.emojiImageNode.frame = emojiFrame
|
||||
|
||||
let animatedStickerNode: AnimatedStickerNode
|
||||
if let current = self.animatedStickerNode {
|
||||
animatedStickerNode = current
|
||||
} else {
|
||||
animatedStickerNode = DefaultAnimatedStickerNodeImpl()
|
||||
animatedStickerNode.started = { [weak self] in
|
||||
self?.emojiImageNode.isHidden = true
|
||||
}
|
||||
self.animatedStickerNode = animatedStickerNode
|
||||
self.emojiContainerNode.addSubnode(animatedStickerNode)
|
||||
let pathPrefix = context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(file.resource.id)
|
||||
animatedStickerNode.setup(source: AnimatedStickerResourceSource(account: context.account, resource: file.resource), width: 128, height: 128, playbackMode: .still(.start), mode: .direct(cachePathPrefix: pathPrefix))
|
||||
|
||||
animatedStickerNode.anchorPoint = CGPoint(x: 0.5, y: 1.0)
|
||||
}
|
||||
animatedStickerNode.autoplay = true
|
||||
animatedStickerNode.visibility = true
|
||||
|
||||
self.stickerFetchedDisposable.set(fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, userLocation: .other, userContentType: .other, reference: MediaResourceReference.media(media: .standalone(media: file), resource: file.resource)).start())
|
||||
|
||||
// let thumbnailDimensions = PixelDimensions(width: 512, height: 512)
|
||||
// self.placeholderNode.update(backgroundColor: nil, foregroundColor: UIColor(rgb: 0xffffff, alpha: 0.2), shimmeringColor: UIColor(rgb: 0xffffff, alpha: 0.3), data: file.immediateThumbnailData, size: emojiFrame.size, enableEffect: item.context.sharedContext.energyUsageSettings.fullTranslucency, imageSize: thumbnailDimensions.cgSize)
|
||||
// self.placeholderNode.frame = emojiFrame
|
||||
}
|
||||
|
||||
if let animatedStickerNode = self.animatedStickerNode {
|
||||
animatedStickerNode.frame = emojiFrame
|
||||
animatedStickerNode.updateLayout(size: emojiFrame.size)
|
||||
}
|
||||
}
|
||||
|
||||
@objc func buttonPressed() {
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "ThemeAccentColorScreen",
|
||||
module_name = "ThemeAccentColorScreen",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
"//submodules/AsyncDisplayKit",
|
||||
"//submodules/Display",
|
||||
"//submodules/Postbox",
|
||||
"//submodules/TelegramCore",
|
||||
"//submodules/SSignalKit/SwiftSignalKit",
|
||||
"//submodules/TelegramPresentationData",
|
||||
"//submodules/AccountContext",
|
||||
"//submodules/PresentationDataUtils",
|
||||
"//submodules/WallpaperBackgroundNode",
|
||||
"//submodules/ComponentFlow",
|
||||
"//submodules/SolidRoundedButtonNode",
|
||||
"//submodules/AppBundle",
|
||||
"//submodules/PremiumUI",
|
||||
"//submodules/WallpaperResources",
|
||||
"//submodules/HexColor",
|
||||
"//submodules/MergeLists",
|
||||
"//submodules/ShareController",
|
||||
"//submodules/GalleryUI",
|
||||
"//submodules/ChatListUI",
|
||||
"//submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen",
|
||||
"//submodules/TelegramUI/Components/Settings/SettingsThemeWallpaperNode",
|
||||
"//submodules/TelegramUI/Components/Settings/GenerateThemeName",
|
||||
"//submodules/TelegramUI/Components/PremiumLockButtonSubtitleComponent",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
||||
+11
-10
@@ -11,16 +11,17 @@ import AccountContext
|
||||
import PresentationDataUtils
|
||||
import MediaResources
|
||||
import WallpaperGalleryScreen
|
||||
import GenerateThemeName
|
||||
|
||||
private let randomBackgroundColors: [Int32] = [0x007aff, 0x00c2ed, 0x29b327, 0xeb6ca4, 0xf08200, 0x9472ee, 0xd33213, 0xedb400, 0x6d839e]
|
||||
|
||||
extension TelegramThemeSettings {
|
||||
public extension TelegramThemeSettings {
|
||||
convenience init(baseTheme: TelegramBaseTheme, accentColor: UIColor, outgoingAccentColor: UIColor?, messageColors: [UInt32], animateMessageColors: Bool, wallpaper: TelegramWallpaper?) {
|
||||
self.init(baseTheme: baseTheme, accentColor: accentColor.argb, outgoingAccentColor: outgoingAccentColor?.argb, messageColors: messageColors, animateMessageColors: animateMessageColors, wallpaper: wallpaper)
|
||||
}
|
||||
}
|
||||
|
||||
enum ThemeAccentColorControllerMode {
|
||||
public enum ThemeAccentColorControllerMode {
|
||||
case colors(themeReference: PresentationThemeReference, create: Bool)
|
||||
case background(themeReference: PresentationThemeReference)
|
||||
case edit(settings: TelegramThemeSettings?, theme: PresentationTheme, wallpaper: TelegramWallpaper?, generalThemeReference: PresentationThemeReference?, defaultThemeReference: PresentationThemeReference?, create: Bool, completion: (PresentationTheme, TelegramThemeSettings?) -> Void)
|
||||
@@ -35,8 +36,8 @@ enum ThemeAccentColorControllerMode {
|
||||
}
|
||||
}
|
||||
|
||||
final class ThemeAccentColorController: ViewController {
|
||||
enum ResultMode {
|
||||
public final class ThemeAccentColorController: ViewController {
|
||||
public enum ResultMode {
|
||||
case `default`
|
||||
case peer(EnginePeer)
|
||||
}
|
||||
@@ -53,7 +54,7 @@ final class ThemeAccentColorController: ViewController {
|
||||
}
|
||||
|
||||
private let _ready = Promise<Bool>()
|
||||
override public var ready: Promise<Bool> {
|
||||
public override var ready: Promise<Bool> {
|
||||
return self._ready
|
||||
}
|
||||
|
||||
@@ -61,9 +62,9 @@ final class ThemeAccentColorController: ViewController {
|
||||
|
||||
private var applyDisposable = MetaDisposable()
|
||||
|
||||
var completion: (() -> Void)?
|
||||
public var completion: (() -> Void)?
|
||||
|
||||
init(context: AccountContext, mode: ThemeAccentColorControllerMode, resultMode: ResultMode = .default) {
|
||||
public init(context: AccountContext, mode: ThemeAccentColorControllerMode, resultMode: ResultMode = .default) {
|
||||
self.context = context
|
||||
self.mode = mode
|
||||
self.resultMode = resultMode
|
||||
@@ -136,13 +137,13 @@ final class ThemeAccentColorController: ViewController {
|
||||
self.dismiss()
|
||||
}
|
||||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
public override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
|
||||
self.controllerNode.animateWallpaperAppeared()
|
||||
}
|
||||
|
||||
override func loadDisplayNode() {
|
||||
public override func loadDisplayNode() {
|
||||
super.loadDisplayNode()
|
||||
|
||||
let theme: PresentationTheme
|
||||
@@ -631,7 +632,7 @@ final class ThemeAccentColorController: ViewController {
|
||||
self.displayNodeDidLoad()
|
||||
}
|
||||
|
||||
override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
|
||||
public override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
|
||||
super.containerLayoutUpdated(layout, transition: transition)
|
||||
|
||||
self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition)
|
||||
+1
-1
@@ -865,7 +865,7 @@ final class ThemeAccentColorControllerNode: ASDisplayNode, UIScrollViewDelegate
|
||||
}, activateChatPreview: { _, _, _, gesture, _ in
|
||||
gesture?.cancel()
|
||||
}, present: { _ in
|
||||
}, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openActiveSessions: {
|
||||
}, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openPremiumGift: {}, openActiveSessions: {
|
||||
}, performActiveSessionAction: { _, _ in
|
||||
}, openChatFolderUpdates: {}, hideChatFolderUpdates: {
|
||||
}, openStories: { _, _ in
|
||||
+7
-5
@@ -369,16 +369,16 @@ private final class ThemeCarouselThemeItemIconNode : ListViewItemNode {
|
||||
updatedSelected = true
|
||||
}
|
||||
|
||||
//TODO:localize
|
||||
let text = NSAttributedString(string: "No\nWallpaper", font: Font.semibold(15.0), textColor: item.theme.actionSheet.controlAccentColor)
|
||||
let text = NSAttributedString(string: item.strings.Wallpaper_NoWallpaper, font: Font.regular(13.0), textColor: item.theme.list.itemSecondaryTextColor)
|
||||
let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: text, backgroundColor: nil, maximumNumberOfLines: 2, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
var string: String?
|
||||
if item.themeReference == nil {
|
||||
string = "❌"
|
||||
self?.imageNode.backgroundColor = item.theme.list.mediaPlaceholderColor
|
||||
} else if let _ = item.themeReference?.emoticon {
|
||||
} else {
|
||||
string = "🎨"
|
||||
string = item.channelMode ? "" : "🎨"
|
||||
}
|
||||
|
||||
let emojiTitle = NSAttributedString(string: string ?? "", font: Font.regular(20.0), textColor: .black)
|
||||
@@ -544,12 +544,13 @@ public class ThemeCarouselThemeItem: ListViewItem, ItemListItem, ListItemCompone
|
||||
public let themeSpecificChatWallpapers: [Int64: TelegramWallpaper]
|
||||
public let nightMode: Bool
|
||||
public let channelMode: Bool
|
||||
public let selectedWallpaper: TelegramWallpaper?
|
||||
public let currentTheme: PresentationThemeReference?
|
||||
public let updatedTheme: (PresentationThemeReference?) -> Void
|
||||
public let contextAction: ((PresentationThemeReference, ASDisplayNode, ContextGesture?) -> Void)?
|
||||
public let tag: ItemListItemTag?
|
||||
|
||||
public init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, sectionId: ItemListSectionId, themes: [PresentationThemeReference], hasNoTheme: Bool, animatedEmojiStickers: [String: [StickerPackItem]], themeSpecificAccentColors: [Int64: PresentationThemeAccentColor], themeSpecificChatWallpapers: [Int64: TelegramWallpaper], nightMode: Bool, channelMode: Bool = false, currentTheme: PresentationThemeReference?, updatedTheme: @escaping (PresentationThemeReference?) -> Void, contextAction: ((PresentationThemeReference, ASDisplayNode, ContextGesture?) -> Void)?, tag: ItemListItemTag? = nil) {
|
||||
public init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, sectionId: ItemListSectionId, themes: [PresentationThemeReference], hasNoTheme: Bool, animatedEmojiStickers: [String: [StickerPackItem]], themeSpecificAccentColors: [Int64: PresentationThemeAccentColor], themeSpecificChatWallpapers: [Int64: TelegramWallpaper], nightMode: Bool, channelMode: Bool = false, selectedWallpaper: TelegramWallpaper? = nil, currentTheme: PresentationThemeReference?, updatedTheme: @escaping (PresentationThemeReference?) -> Void, contextAction: ((PresentationThemeReference, ASDisplayNode, ContextGesture?) -> Void)?, tag: ItemListItemTag? = nil) {
|
||||
self.context = context
|
||||
self.theme = theme
|
||||
self.strings = strings
|
||||
@@ -560,6 +561,7 @@ public class ThemeCarouselThemeItem: ListViewItem, ItemListItem, ListItemCompone
|
||||
self.themeSpecificChatWallpapers = themeSpecificChatWallpapers
|
||||
self.nightMode = nightMode
|
||||
self.channelMode = channelMode
|
||||
self.selectedWallpaper = selectedWallpaper
|
||||
self.currentTheme = currentTheme
|
||||
self.updatedTheme = updatedTheme
|
||||
self.contextAction = contextAction
|
||||
@@ -870,7 +872,7 @@ public class ThemeCarouselThemeItemNode: ListViewItemNode, ItemListItemNode {
|
||||
}
|
||||
|
||||
if !hasCurrentTheme {
|
||||
entries.append(ThemeCarouselThemeEntry(index: index, emojiFile: nil, themeReference: item.currentTheme, nightMode: false, channelMode: item.channelMode, themeSpecificAccentColors: item.themeSpecificAccentColors, themeSpecificChatWallpapers: item.themeSpecificChatWallpapers, selected: true, theme: item.theme, strings: item.strings, wallpaper: nil))
|
||||
entries.insert(ThemeCarouselThemeEntry(index: index, emojiFile: nil, themeReference: item.currentTheme, nightMode: false, channelMode: item.channelMode, themeSpecificAccentColors: item.themeSpecificAccentColors, themeSpecificChatWallpapers: item.themeSpecificChatWallpapers, selected: true, theme: item.theme, strings: item.strings, wallpaper: item.hasNoTheme ? item.selectedWallpaper : nil), at: item.hasNoTheme ? 1 : entries.count)
|
||||
}
|
||||
|
||||
let action: (PresentationThemeReference?) -> Void = { [weak self] themeReference in
|
||||
|
||||
@@ -31,6 +31,7 @@ swift_library(
|
||||
"//submodules/CounterContollerTitleView",
|
||||
"//submodules/LegacyMediaPickerUI",
|
||||
"//submodules/TelegramUI/Components/Settings/SettingsThemeWallpaperNode",
|
||||
"//submodules/TelegramUI/Components/PremiumLockButtonSubtitleComponent",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
||||
+3
@@ -234,6 +234,8 @@ public class WallpaperGalleryController: ViewController {
|
||||
private var savedPatternWallpaper: TelegramWallpaper?
|
||||
private var savedPatternIntensity: Int32?
|
||||
|
||||
public var requiredLevel: Int?
|
||||
|
||||
public init(context: AccountContext, source: WallpaperListSource, mode: Mode = .default) {
|
||||
self.context = context
|
||||
self.source = source
|
||||
@@ -506,6 +508,7 @@ public class WallpaperGalleryController: ViewController {
|
||||
}
|
||||
|
||||
let toolbarNode = WallpaperGalleryToolbarNode(theme: presentationData.theme, strings: presentationData.strings, doneButtonType: doneButtonType)
|
||||
toolbarNode.requiredLevel = self.requiredLevel
|
||||
switch self.source {
|
||||
case .asset, .contextResult:
|
||||
toolbarNode.dark = false
|
||||
|
||||
+21
-5
@@ -716,7 +716,7 @@ final class WallpaperGalleryItemNode: GalleryItemNode {
|
||||
self.initialWallpaper = wallpaper
|
||||
|
||||
switch wallpaper {
|
||||
case .builtin:
|
||||
case .builtin, .emoticon:
|
||||
displaySize = CGSize(width: 1308.0, height: 2688.0).fitted(CGSize(width: 1280.0, height: 1280.0)).dividedByScreenScale().integralFloor
|
||||
contentSize = displaySize
|
||||
signal = settingsBuiltinWallpaperImage(account: self.context.account)
|
||||
@@ -1371,6 +1371,7 @@ final class WallpaperGalleryItemNode: GalleryItemNode {
|
||||
|
||||
let centerOffset: CGFloat = 32.0
|
||||
|
||||
var isPattern = false
|
||||
if let entry = self.entry {
|
||||
switch entry {
|
||||
case .asset:
|
||||
@@ -1385,7 +1386,7 @@ final class WallpaperGalleryItemNode: GalleryItemNode {
|
||||
motionFrame = rightButtonFrame
|
||||
case let .wallpaper(wallpaper, _):
|
||||
switch wallpaper {
|
||||
case .builtin:
|
||||
case .builtin, .emoticon:
|
||||
motionAlpha = 1.0
|
||||
motionFrame = centerButtonFrame
|
||||
case .color:
|
||||
@@ -1417,6 +1418,8 @@ final class WallpaperGalleryItemNode: GalleryItemNode {
|
||||
colorsAlpha = 1.0
|
||||
case let .file(file):
|
||||
if file.isPattern {
|
||||
isPattern = true
|
||||
|
||||
motionAlpha = 0.0
|
||||
patternAlpha = 1.0
|
||||
|
||||
@@ -1453,6 +1456,13 @@ final class WallpaperGalleryItemNode: GalleryItemNode {
|
||||
|
||||
if let mode = self.mode, case let .peer(peer, _) = mode, case .channel = peer {
|
||||
motionAlpha = 0.0
|
||||
if isPattern {
|
||||
patternAlpha = 0.0
|
||||
colorsAlpha = 0.0
|
||||
blurAlpha = 0.0
|
||||
playAlpha = 0.0
|
||||
self.shareButtonNode.isHidden = true
|
||||
}
|
||||
blurFrame = centerButtonFrame
|
||||
}
|
||||
|
||||
@@ -1487,8 +1497,12 @@ final class WallpaperGalleryItemNode: GalleryItemNode {
|
||||
self.nativeNode.updateBubbleTheme(bubbleTheme: self.presentationData.theme, bubbleCorners: self.presentationData.chatBubbleCorners)
|
||||
|
||||
var bottomInset: CGFloat = 132.0
|
||||
if let mode = self.mode, case let .peer(peer, _) = mode, case .user = peer {
|
||||
bottomInset += 58.0
|
||||
if let mode = self.mode, case let .peer(peer, _) = mode {
|
||||
if case .user = peer {
|
||||
bottomInset += 58.0
|
||||
} else if case .channel = peer, let entry = self.entry, case let .wallpaper(wallpaper, _) = entry, case let .file(file) = wallpaper, file.isPattern {
|
||||
bottomInset -= 42.0
|
||||
}
|
||||
}
|
||||
|
||||
var items: [ListViewItem] = []
|
||||
@@ -1667,7 +1681,9 @@ final class WallpaperGalleryItemNode: GalleryItemNode {
|
||||
|
||||
if let _ = serviceMessageText, let messageNodes = self.messageNodes, let node = messageNodes.last {
|
||||
if let backgroundNode = node.subnodes?.first?.subnodes?.first?.subnodes?.first?.subnodes?.first, let backdropNode = node.subnodes?.first?.subnodes?.first?.subnodes?.first?.subnodes?.last?.subnodes?.last?.subnodes?.first {
|
||||
backdropNode.isHidden = true
|
||||
if !(backdropNode is TextNode) {
|
||||
backdropNode.isHidden = true
|
||||
}
|
||||
let serviceBackgroundFrame = backgroundNode.view.convert(backgroundNode.bounds, to: self.view).offsetBy(dx: 0.0, dy: -1.0).insetBy(dx: 0.0, dy: -1.0)
|
||||
transition.updateFrame(node: self.serviceBackgroundNode, frame: serviceBackgroundFrame)
|
||||
self.serviceBackgroundNode.update(size: serviceBackgroundFrame.size, cornerRadius: serviceBackgroundFrame.height / 2.0, transition: transition)
|
||||
|
||||
+67
-4
@@ -4,6 +4,8 @@ import AsyncDisplayKit
|
||||
import Display
|
||||
import TelegramPresentationData
|
||||
import ManagedAnimationNode
|
||||
import ComponentFlow
|
||||
import PremiumLockButtonSubtitleComponent
|
||||
|
||||
public enum WallpaperGalleryToolbarCancelButtonType {
|
||||
case cancel
|
||||
@@ -33,9 +35,13 @@ public protocol WallpaperGalleryToolbar: ASDisplayNode {
|
||||
|
||||
public final class WallpaperGalleryToolbarNode: ASDisplayNode, WallpaperGalleryToolbar {
|
||||
class ButtonNode: ASDisplayNode {
|
||||
private let strings: PresentationStrings
|
||||
|
||||
private let doneButton = HighlightTrackingButtonNode()
|
||||
private var doneButtonBackgroundNode: ASDisplayNode
|
||||
private let doneButtonTitleNode: ImmediateTextNode
|
||||
private var doneButtonSubtitle: ComponentView<Empty>?
|
||||
|
||||
private let doneButtonSolidBackgroundNode: ASDisplayNode
|
||||
private let doneButtonSolidTitleNode: ImmediateTextNode
|
||||
|
||||
@@ -49,7 +55,11 @@ public final class WallpaperGalleryToolbarNode: ASDisplayNode, WallpaperGalleryT
|
||||
}
|
||||
}
|
||||
|
||||
override init() {
|
||||
var requiredLevel: Int?
|
||||
|
||||
init(strings: PresentationStrings) {
|
||||
self.strings = strings
|
||||
|
||||
self.doneButtonBackgroundNode = WallpaperLightButtonBackgroundNode()
|
||||
self.doneButtonBackgroundNode.cornerRadius = 14.0
|
||||
|
||||
@@ -102,6 +112,9 @@ public final class WallpaperGalleryToolbarNode: ASDisplayNode, WallpaperGalleryT
|
||||
strongSelf.doneButtonBackgroundNode.alpha = 0.55
|
||||
strongSelf.doneButtonTitleNode.layer.removeAnimation(forKey: "opacity")
|
||||
strongSelf.doneButtonTitleNode.alpha = 0.55
|
||||
|
||||
strongSelf.doneButtonSubtitle?.view?.layer.removeAnimation(forKey: "opacity")
|
||||
strongSelf.doneButtonSubtitle?.view?.alpha = 0.55
|
||||
}
|
||||
} else {
|
||||
if strongSelf.isSolid {
|
||||
@@ -114,6 +127,9 @@ public final class WallpaperGalleryToolbarNode: ASDisplayNode, WallpaperGalleryT
|
||||
strongSelf.doneButtonBackgroundNode.layer.animateAlpha(from: 0.55, to: 1.0, duration: 0.2)
|
||||
strongSelf.doneButtonTitleNode.alpha = 1.0
|
||||
strongSelf.doneButtonTitleNode.layer.animateAlpha(from: 0.55, to: 1.0, duration: 0.2)
|
||||
|
||||
strongSelf.doneButtonSubtitle?.view?.alpha = 1.0
|
||||
strongSelf.doneButtonSubtitle?.view?.layer.animateAlpha(from: 0.55, to: 1.0, duration: 0.2)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -168,7 +184,45 @@ public final class WallpaperGalleryToolbarNode: ASDisplayNode, WallpaperGalleryT
|
||||
let titleOriginX = floorToScreenPixels((bounds.width - totalWidth) / 2.0)
|
||||
|
||||
self.animationNode.frame = CGRect(origin: CGPoint(x: titleOriginX, y: floorToScreenPixels((bounds.height - iconSize.height) / 2.0)), size: iconSize)
|
||||
self.doneButtonTitleNode.frame = CGRect(origin: CGPoint(x: titleOriginX + totalWidth - doneTitleSize.width, y: floorToScreenPixels((bounds.height - doneTitleSize.height) / 2.0)), size: doneTitleSize).offsetBy(dx: bounds.minX, dy: bounds.minY)
|
||||
|
||||
var titleFrame = CGRect(origin: CGPoint(x: titleOriginX + totalWidth - doneTitleSize.width, y: floorToScreenPixels((bounds.height - doneTitleSize.height) / 2.0)), size: doneTitleSize).offsetBy(dx: bounds.minX, dy: bounds.minY)
|
||||
|
||||
if let requiredLevel = self.requiredLevel {
|
||||
let subtitle: ComponentView<Empty>
|
||||
if let current = self.doneButtonSubtitle {
|
||||
subtitle = current
|
||||
} else {
|
||||
subtitle = ComponentView<Empty>()
|
||||
self.doneButtonSubtitle = subtitle
|
||||
}
|
||||
|
||||
let subtitleSize = subtitle.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(
|
||||
PremiumLockButtonSubtitleComponent(
|
||||
count: requiredLevel,
|
||||
color: UIColor(rgb: 0xffffff, alpha: 0.7),
|
||||
strings: self.strings
|
||||
)
|
||||
),
|
||||
environment: {},
|
||||
containerSize: size
|
||||
)
|
||||
|
||||
if let view = subtitle.view {
|
||||
if view.superview == nil {
|
||||
view.isUserInteractionEnabled = false
|
||||
self.view.addSubview(view)
|
||||
}
|
||||
|
||||
titleFrame.origin.y -= 8.0
|
||||
|
||||
let subtitleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((bounds.width - subtitleSize.width) / 2.0), y: titleFrame.maxY + 3.0), size: subtitleSize)
|
||||
view.frame = subtitleFrame
|
||||
}
|
||||
}
|
||||
|
||||
self.doneButtonTitleNode.frame = titleFrame
|
||||
|
||||
let _ = self.doneButtonSolidTitleNode.updateLayout(constrainedSize)
|
||||
self.doneButtonSolidTitleNode.frame = self.doneButtonTitleNode.frame
|
||||
@@ -223,18 +277,27 @@ public final class WallpaperGalleryToolbarNode: ASDisplayNode, WallpaperGalleryT
|
||||
}
|
||||
}
|
||||
|
||||
private let applyButton = ButtonNode()
|
||||
private let applyForBothButton = ButtonNode()
|
||||
private let applyButton: ButtonNode
|
||||
private let applyForBothButton: ButtonNode
|
||||
|
||||
public var cancel: (() -> Void)?
|
||||
public var done: ((Bool) -> Void)?
|
||||
|
||||
var requiredLevel: Int? {
|
||||
didSet {
|
||||
self.applyButton.requiredLevel = self.requiredLevel
|
||||
}
|
||||
}
|
||||
|
||||
public init(theme: PresentationTheme, strings: PresentationStrings, cancelButtonType: WallpaperGalleryToolbarCancelButtonType = .cancel, doneButtonType: WallpaperGalleryToolbarDoneButtonType = .set) {
|
||||
self.theme = theme
|
||||
self.strings = strings
|
||||
self.cancelButtonType = cancelButtonType
|
||||
self.doneButtonType = doneButtonType
|
||||
|
||||
self.applyButton = ButtonNode(strings: strings)
|
||||
self.applyForBothButton = ButtonNode(strings: strings)
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.applyButton)
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "WallpaperGridScreen",
|
||||
module_name = "WallpaperGridScreen",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
"//submodules/AsyncDisplayKit",
|
||||
"//submodules/Display",
|
||||
"//submodules/Postbox",
|
||||
"//submodules/TelegramCore",
|
||||
"//submodules/SSignalKit/SwiftSignalKit",
|
||||
"//submodules/TelegramPresentationData",
|
||||
"//submodules/AccountContext",
|
||||
"//submodules/PresentationDataUtils",
|
||||
"//submodules/WallpaperBackgroundNode",
|
||||
"//submodules/ComponentFlow",
|
||||
"//submodules/SolidRoundedButtonNode",
|
||||
"//submodules/AppBundle",
|
||||
"//submodules/PremiumUI",
|
||||
"//submodules/WallpaperResources",
|
||||
"//submodules/HexColor",
|
||||
"//submodules/MergeLists",
|
||||
"//submodules/ShareController",
|
||||
"//submodules/GalleryUI",
|
||||
"//submodules/GridMessageSelectionNode",
|
||||
"//submodules/SearchUI",
|
||||
"//submodules/MediaPickerUI",
|
||||
"//submodules/ItemListPeerActionItem",
|
||||
"//submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen",
|
||||
"//submodules/TelegramUI/Components/Settings/SettingsThemeWallpaperNode",
|
||||
"//submodules/TelegramUI/Components/Settings/ThemeAccentColorScreen",
|
||||
"//submodules/TelegramUI/Components/PremiumLockButtonSubtitleComponent",
|
||||
"//submodules/TelegramUI/Components/Settings/BoostLevelIconComponent",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
||||
+1
@@ -10,6 +10,7 @@ import TelegramUIPreferences
|
||||
import AccountContext
|
||||
import AttachmentUI
|
||||
import WallpaperGalleryScreen
|
||||
import ThemeAccentColorScreen
|
||||
|
||||
private func availableGradients(dark: Bool) -> [[UInt32]] {
|
||||
if dark {
|
||||
+1
-1
@@ -378,7 +378,7 @@ final class ThemeColorsGridControllerNode: ASDisplayNode {
|
||||
let makeColorLayout = self.customColorItemNode.asyncLayout()
|
||||
let params = ListViewItemLayoutParams(width: layout.size.width, leftInset: listInsets.left, rightInset: listInsets.right, availableHeight: layout.size.height)
|
||||
let (colorLayout, colorApply) = makeColorLayout(self.customColorItem, params, ItemListNeighbors(top: .none, bottom: .none))
|
||||
colorApply()
|
||||
colorApply(false)
|
||||
|
||||
transition.updateFrame(node: self.topBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -buttonOffset - 500.0), size: CGSize(width: layout.size.width, height: buttonInset + 500.0)))
|
||||
transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -buttonOffset + buttonInset - UIScreenPixel), size: CGSize(width: layout.size.width, height: UIScreenPixel)))
|
||||
+146
-48
@@ -17,7 +17,17 @@ import PresentationDataUtils
|
||||
import MediaPickerUI
|
||||
import WallpaperGalleryScreen
|
||||
|
||||
public enum WallpaperSelectionResult {
|
||||
case remove
|
||||
case emoticon(String)
|
||||
case custom(wallpaperEntry: WallpaperGalleryEntry, options: WallpaperPresentationOptions, editedImage: UIImage?, cropRect: CGRect?, brightness: CGFloat?)
|
||||
}
|
||||
|
||||
public final class ThemeGridController: ViewController {
|
||||
public enum Mode {
|
||||
case generic
|
||||
case peer(EnginePeer, [TelegramTheme], TelegramWallpaper?, Int?, Int?)
|
||||
}
|
||||
private var controllerNode: ThemeGridControllerNode {
|
||||
return self.displayNode as! ThemeGridControllerNode
|
||||
}
|
||||
@@ -28,6 +38,7 @@ public final class ThemeGridController: ViewController {
|
||||
}
|
||||
|
||||
private let context: AccountContext
|
||||
private let mode: Mode
|
||||
|
||||
private var presentationData: PresentationData
|
||||
private let presentationDataPromise = Promise<PresentationData>()
|
||||
@@ -46,14 +57,24 @@ public final class ThemeGridController: ViewController {
|
||||
|
||||
private var previousContentOffset: GridNodeVisibleContentOffset?
|
||||
|
||||
public init(context: AccountContext) {
|
||||
public var completion: (WallpaperSelectionResult) -> Void = { _ in }
|
||||
|
||||
public init(context: AccountContext, mode: Mode = .generic) {
|
||||
self.context = context
|
||||
self.mode = mode
|
||||
|
||||
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
self.presentationDataPromise.set(.single(self.presentationData))
|
||||
|
||||
super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationData: self.presentationData))
|
||||
|
||||
self.title = self.presentationData.strings.Wallpaper_Title
|
||||
switch mode {
|
||||
case .generic:
|
||||
self.title = self.presentationData.strings.Wallpaper_Title
|
||||
case .peer:
|
||||
self.title = self.presentationData.strings.Wallpaper_ChannelTitle
|
||||
}
|
||||
|
||||
self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Back, style: .plain, target: nil, action: nil)
|
||||
self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style
|
||||
|
||||
@@ -81,10 +102,12 @@ public final class ThemeGridController: ViewController {
|
||||
}
|
||||
})
|
||||
|
||||
self.searchContentNode = NavigationBarSearchContentNode(theme: self.presentationData.theme, placeholder: self.presentationData.strings.Wallpaper_Search, activate: { [weak self] in
|
||||
self?.activateSearch()
|
||||
})
|
||||
self.navigationBar?.setContentNode(self.searchContentNode, animated: false)
|
||||
if case .generic = mode {
|
||||
self.searchContentNode = NavigationBarSearchContentNode(theme: self.presentationData.theme, placeholder: self.presentationData.strings.Wallpaper_Search, activate: { [weak self] in
|
||||
self?.activateSearch()
|
||||
})
|
||||
self.navigationBar?.setContentNode(self.searchContentNode, animated: false)
|
||||
}
|
||||
}
|
||||
|
||||
required public init(coder aDecoder: NSCoder) {
|
||||
@@ -99,12 +122,14 @@ public final class ThemeGridController: ViewController {
|
||||
self.title = self.presentationData.strings.Wallpaper_Title
|
||||
self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Back, style: .plain, target: nil, action: nil)
|
||||
|
||||
if let isEmpty = self.isEmpty, isEmpty {
|
||||
} else {
|
||||
if self.editingMode {
|
||||
self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Done, style: .done, target: self, action: #selector(self.donePressed))
|
||||
if case .generic = self.mode {
|
||||
if let isEmpty = self.isEmpty, isEmpty {
|
||||
} else {
|
||||
self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Edit, style: .plain, target: self, action: #selector(self.editPressed))
|
||||
if self.editingMode {
|
||||
self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Done, style: .done, target: self, action: #selector(self.donePressed))
|
||||
} else {
|
||||
self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Edit, style: .plain, target: self, action: #selector(self.editPressed))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,35 +143,29 @@ public final class ThemeGridController: ViewController {
|
||||
}
|
||||
|
||||
public override func loadDisplayNode() {
|
||||
self.displayNode = ThemeGridControllerNode(context: self.context, presentationData: self.presentationData, presentPreviewController: { [weak self] source in
|
||||
if let strongSelf = self {
|
||||
let controller = WallpaperGalleryController(context: strongSelf.context, source: source)
|
||||
controller.apply = { [weak self, weak controller] wallpaper, options, editedImage, cropRect, brightness, _ in
|
||||
if let strongSelf = self {
|
||||
uploadCustomWallpaper(context: strongSelf.context, wallpaper: wallpaper, mode: options, editedImage: editedImage, cropRect: cropRect, brightness: brightness, completion: { [weak self, weak controller] in
|
||||
if let strongSelf = self {
|
||||
strongSelf.deactivateSearch(animated: false)
|
||||
strongSelf.controllerNode.scrollToTop(animated: false)
|
||||
}
|
||||
if let controller = controller {
|
||||
switch wallpaper {
|
||||
case .asset, .contextResult:
|
||||
controller.dismiss(animated: true)
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
self?.push(controller)
|
||||
}
|
||||
}, presentGallery: { [weak self] in
|
||||
var mode: WallpaperGalleryController.Mode = .default
|
||||
var requiredLevel: Int?
|
||||
var requiredCustomLevel: Int?
|
||||
if case let .peer(peer, _, _, requiredLevelValue, requiredCustomLevelValue) = self.mode {
|
||||
mode = .peer(peer, false)
|
||||
requiredLevel = requiredLevelValue
|
||||
requiredCustomLevel = requiredCustomLevelValue
|
||||
}
|
||||
|
||||
self.displayNode = ThemeGridControllerNode(context: self.context, mode: self.mode, presentationData: self.presentationData, presentPreviewController: { [weak self] source in
|
||||
if let strongSelf = self {
|
||||
let dismissControllers = { [weak self] in
|
||||
if let self, let navigationController = self.navigationController as? NavigationController {
|
||||
let controllers = navigationController.viewControllers.filter({ controller in
|
||||
if controller is WallpaperGalleryController || controller is MediaPickerScreen {
|
||||
var controllers = navigationController.viewControllers.filter({ controller in
|
||||
if controller is ThemeGridController {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
navigationController.setViewControllers(controllers, animated: false)
|
||||
|
||||
controllers = navigationController.viewControllers.filter({ controller in
|
||||
if controller is WallpaperGalleryController {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
@@ -155,17 +174,89 @@ public final class ThemeGridController: ViewController {
|
||||
}
|
||||
}
|
||||
|
||||
let controller = WallpaperGalleryController(context: strongSelf.context, source: source, mode: mode)
|
||||
controller.requiredLevel = requiredLevel
|
||||
controller.apply = { [weak self, weak controller] wallpaper, options, editedImage, cropRect, brightness, _ in
|
||||
if let strongSelf = self {
|
||||
if case .peer = mode {
|
||||
var emoticon = ""
|
||||
if case let .wallpaper(wallpaper, _) = wallpaper {
|
||||
emoticon = wallpaper.settings?.emoticon ?? ""
|
||||
}
|
||||
strongSelf.completion(.emoticon(emoticon))
|
||||
dismissControllers()
|
||||
} else {
|
||||
uploadCustomWallpaper(context: strongSelf.context, wallpaper: wallpaper, mode: options, editedImage: editedImage, cropRect: cropRect, brightness: brightness, completion: { [weak self, weak controller] in
|
||||
if let strongSelf = self {
|
||||
strongSelf.deactivateSearch(animated: false)
|
||||
strongSelf.controllerNode.scrollToTop(animated: false)
|
||||
}
|
||||
if let controller = controller {
|
||||
switch wallpaper {
|
||||
case .asset, .contextResult:
|
||||
controller.dismiss(animated: true)
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
self?.push(controller)
|
||||
}
|
||||
}, presentGallery: { [weak self] in
|
||||
if let strongSelf = self {
|
||||
let dismissControllers = { [weak self] in
|
||||
if let self, let navigationController = self.navigationController as? NavigationController {
|
||||
if case .peer = mode {
|
||||
var controllers = navigationController.viewControllers.filter({ controller in
|
||||
if controller is ThemeGridController || controller is MediaPickerScreen {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
navigationController.setViewControllers(controllers, animated: false)
|
||||
|
||||
controllers = navigationController.viewControllers.filter({ controller in
|
||||
if controller is WallpaperGalleryController {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
navigationController.setViewControllers(controllers, animated: true)
|
||||
} else {
|
||||
let controllers = navigationController.viewControllers.filter({ controller in
|
||||
if controller is WallpaperGalleryController || controller is MediaPickerScreen {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
navigationController.setViewControllers(controllers, animated: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let controller = MediaPickerScreen(context: strongSelf.context, peer: nil, threadTitle: nil, chatLocation: nil, bannedSendPhotos: nil, bannedSendVideos: nil, subject: .assets(nil, .wallpaper))
|
||||
controller.customSelection = { [weak self] _, asset in
|
||||
guard let strongSelf = self, let asset = asset as? PHAsset else {
|
||||
return
|
||||
}
|
||||
let controller = WallpaperGalleryController(context: strongSelf.context, source: .asset(asset))
|
||||
let controller = WallpaperGalleryController(context: strongSelf.context, source: .asset(asset), mode: mode)
|
||||
controller.requiredLevel = requiredCustomLevel
|
||||
controller.apply = { [weak self] wallpaper, options, editedImage, cropRect, brightness, _ in
|
||||
if let strongSelf = self {
|
||||
uploadCustomWallpaper(context: strongSelf.context, wallpaper: wallpaper, mode: options, editedImage: editedImage, cropRect: cropRect, brightness: brightness, completion: {
|
||||
dismissControllers()
|
||||
})
|
||||
if case .peer = mode {
|
||||
strongSelf.completion(.custom(wallpaperEntry: wallpaper, options: options, editedImage: editedImage, cropRect: cropRect, brightness: brightness))
|
||||
Queue.mainQueue().after(0.15) {
|
||||
dismissControllers()
|
||||
}
|
||||
} else {
|
||||
uploadCustomWallpaper(context: strongSelf.context, wallpaper: wallpaper, mode: options, editedImage: editedImage, cropRect: cropRect, brightness: brightness, completion: {
|
||||
dismissControllers()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
strongSelf.push(controller)
|
||||
@@ -182,13 +273,15 @@ public final class ThemeGridController: ViewController {
|
||||
if empty != strongSelf.isEmpty {
|
||||
strongSelf.isEmpty = empty
|
||||
|
||||
if empty {
|
||||
strongSelf.navigationItem.setRightBarButton(nil, animated: true)
|
||||
} else {
|
||||
if strongSelf.editingMode {
|
||||
strongSelf.navigationItem.rightBarButtonItem = UIBarButtonItem(title: strongSelf.presentationData.strings.Common_Done, style: .done, target: strongSelf, action: #selector(strongSelf.donePressed))
|
||||
if case .generic = strongSelf.mode {
|
||||
if empty {
|
||||
strongSelf.navigationItem.setRightBarButton(nil, animated: true)
|
||||
} else {
|
||||
strongSelf.navigationItem.rightBarButtonItem = UIBarButtonItem(title: strongSelf.presentationData.strings.Common_Edit, style: .plain, target: strongSelf, action: #selector(strongSelf.editPressed))
|
||||
if strongSelf.editingMode {
|
||||
strongSelf.navigationItem.rightBarButtonItem = UIBarButtonItem(title: strongSelf.presentationData.strings.Common_Done, style: .done, target: strongSelf, action: #selector(strongSelf.donePressed))
|
||||
} else {
|
||||
strongSelf.navigationItem.rightBarButtonItem = UIBarButtonItem(title: strongSelf.presentationData.strings.Common_Edit, style: .plain, target: strongSelf, action: #selector(strongSelf.editPressed))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -308,7 +401,12 @@ public final class ThemeGridController: ViewController {
|
||||
self.controllerNode.requestDeactivateSearch = { [weak self] in
|
||||
self?.deactivateSearch(animated: true)
|
||||
}
|
||||
|
||||
self.controllerNode.requestWallpaperRemoval = { [weak self] in
|
||||
if let self {
|
||||
self.completion(.remove)
|
||||
self.dismiss()
|
||||
}
|
||||
}
|
||||
self.controllerNode.gridNode.visibleContentOffsetChanged = { [weak self] offset in
|
||||
if let strongSelf = self {
|
||||
if let searchContentNode = strongSelf.searchContentNode {
|
||||
+229
@@ -0,0 +1,229 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import TelegramCore
|
||||
import SwiftSignalKit
|
||||
import AsyncDisplayKit
|
||||
import Postbox
|
||||
import AccountContext
|
||||
import GridMessageSelectionNode
|
||||
import SettingsThemeWallpaperNode
|
||||
import TelegramPresentationData
|
||||
|
||||
private var cachedBorderImages: [String: UIImage] = [:]
|
||||
private func generateBorderImage(theme: PresentationTheme, bordered: Bool, selected: Bool) -> UIImage? {
|
||||
let key = "\(theme.list.itemBlocksBackgroundColor.hexString)_\(selected ? "s" + theme.list.itemAccentColor.hexString : theme.list.disclosureArrowColor.hexString)"
|
||||
if let image = cachedBorderImages[key] {
|
||||
return image
|
||||
} else {
|
||||
let image = generateImage(CGSize(width: 20.0, height: 20.0), rotatedContext: { size, context in
|
||||
let bounds = CGRect(origin: CGPoint(), size: size)
|
||||
context.clear(bounds)
|
||||
|
||||
let lineWidth: CGFloat
|
||||
if selected {
|
||||
lineWidth = 2.0
|
||||
context.setLineWidth(lineWidth)
|
||||
context.setStrokeColor(theme.list.itemBlocksBackgroundColor.cgColor)
|
||||
|
||||
context.strokeEllipse(in: bounds.insetBy(dx: 2.0 + lineWidth / 2.0, dy: 2.0 + lineWidth / 2.0))
|
||||
|
||||
var accentColor = theme.list.itemAccentColor
|
||||
if accentColor.rgb == 0xffffff {
|
||||
accentColor = UIColor(rgb: 0x999999)
|
||||
}
|
||||
context.setStrokeColor(accentColor.cgColor)
|
||||
} else {
|
||||
context.setStrokeColor(theme.list.disclosureArrowColor.withAlphaComponent(0.4).cgColor)
|
||||
lineWidth = 1.0
|
||||
}
|
||||
|
||||
if bordered || selected {
|
||||
context.setLineWidth(lineWidth)
|
||||
context.strokeEllipse(in: bounds.insetBy(dx: lineWidth / 2.0, dy: lineWidth / 2.0))
|
||||
}
|
||||
})?.stretchableImage(withLeftCapWidth: 10, topCapHeight: 10)
|
||||
cachedBorderImages[key] = image
|
||||
return image
|
||||
}
|
||||
}
|
||||
|
||||
final class ThemeGridControllerItem: GridItem {
|
||||
let context: AccountContext
|
||||
let theme: PresentationTheme?
|
||||
let wallpaper: TelegramWallpaper
|
||||
let wallpaperId: ThemeGridControllerEntry.StableId
|
||||
let isEmpty: Bool
|
||||
let emojiFile: TelegramMediaFile?
|
||||
let channelMode: Bool
|
||||
let index: Int
|
||||
let editable: Bool
|
||||
let selected: Bool
|
||||
let interaction: ThemeGridControllerInteraction
|
||||
|
||||
let section: GridSection? = nil
|
||||
|
||||
init(context: AccountContext, theme: PresentationTheme? = nil, wallpaper: TelegramWallpaper, wallpaperId: ThemeGridControllerEntry.StableId, isEmpty: Bool = false, emojiFile: TelegramMediaFile? = nil, channelMode: Bool = false, index: Int, editable: Bool, selected: Bool, interaction: ThemeGridControllerInteraction) {
|
||||
self.context = context
|
||||
self.theme = theme
|
||||
self.wallpaper = wallpaper
|
||||
self.wallpaperId = wallpaperId
|
||||
self.isEmpty = isEmpty
|
||||
self.emojiFile = emojiFile
|
||||
self.channelMode = channelMode
|
||||
self.index = index
|
||||
self.editable = editable
|
||||
self.selected = selected
|
||||
self.interaction = interaction
|
||||
}
|
||||
|
||||
func node(layout: GridNodeLayout, synchronousLoad: Bool) -> GridItemNode {
|
||||
let node = ThemeGridControllerItemNode()
|
||||
node.setup(item: self, synchronousLoad: synchronousLoad)
|
||||
return node
|
||||
}
|
||||
|
||||
func update(node: GridItemNode) {
|
||||
guard let node = node as? ThemeGridControllerItemNode else {
|
||||
assertionFailure()
|
||||
return
|
||||
}
|
||||
node.setup(item: self, synchronousLoad: false)
|
||||
}
|
||||
}
|
||||
|
||||
final class ThemeGridControllerItemNode: GridItemNode {
|
||||
private let wallpaperNode: SettingsThemeWallpaperNode
|
||||
private var selectionNode: GridMessageSelectionNode?
|
||||
private var selectionBorderNode: ASImageNode?
|
||||
|
||||
private var textNode: ImmediateTextNode?
|
||||
|
||||
private var item: ThemeGridControllerItem?
|
||||
|
||||
override init() {
|
||||
self.wallpaperNode = SettingsThemeWallpaperNode(displayLoading: false)
|
||||
|
||||
super.init()
|
||||
|
||||
self.clipsToBounds = true
|
||||
|
||||
self.addSubnode(self.wallpaperNode)
|
||||
}
|
||||
|
||||
override func didLoad() {
|
||||
super.didLoad()
|
||||
|
||||
self.view.layer.cornerRadius = 10.0
|
||||
|
||||
self.view.isExclusiveTouch = true
|
||||
self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))))
|
||||
}
|
||||
|
||||
func setup(item: ThemeGridControllerItem, synchronousLoad: Bool) {
|
||||
self.item = item
|
||||
self.updateSelectionState(animated: false)
|
||||
|
||||
if item.channelMode, item.selected, let theme = item.theme {
|
||||
let selectionBorderNode: ASImageNode
|
||||
if let current = self.selectionBorderNode {
|
||||
selectionBorderNode = current
|
||||
} else {
|
||||
selectionBorderNode = ASImageNode()
|
||||
selectionBorderNode.displaysAsynchronously = false
|
||||
self.selectionBorderNode = selectionBorderNode
|
||||
|
||||
self.addSubnode(selectionBorderNode)
|
||||
}
|
||||
|
||||
selectionBorderNode.image = generateBorderImage(theme: theme, bordered: true, selected: true)
|
||||
} else {
|
||||
self.selectionBorderNode?.removeFromSupernode()
|
||||
}
|
||||
|
||||
if item.channelMode, item.isEmpty, let theme = item.theme {
|
||||
let textNode: ImmediateTextNode
|
||||
if let current = self.textNode {
|
||||
textNode = current
|
||||
} else {
|
||||
textNode = ImmediateTextNode()
|
||||
textNode.maximumNumberOfLines = 2
|
||||
textNode.textAlignment = .center
|
||||
self.textNode = textNode
|
||||
|
||||
self.addSubnode(textNode)
|
||||
}
|
||||
|
||||
let strings = item.context.sharedContext.currentPresentationData.with { $0 }.strings
|
||||
textNode.attributedText = NSAttributedString(string: strings.Wallpaper_NoWallpaper, font: Font.regular(15.0), textColor: theme.list.itemSecondaryTextColor)
|
||||
}
|
||||
|
||||
self.setNeedsLayout()
|
||||
}
|
||||
|
||||
@objc func tapGesture(_ recognizer: UITapGestureRecognizer) {
|
||||
if case .ended = recognizer.state {
|
||||
if let item = self.item, !item.isEmpty {
|
||||
item.interaction.openWallpaper(item.wallpaper)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func updateSelectionState(animated: Bool) {
|
||||
if let item = self.item {
|
||||
let (editing, selectedIds) = item.interaction.selectionState
|
||||
|
||||
if editing && item.editable {
|
||||
let selected = selectedIds.contains(item.wallpaperId)
|
||||
|
||||
if let selectionNode = self.selectionNode {
|
||||
selectionNode.updateSelected(selected, animated: animated)
|
||||
selectionNode.frame = CGRect(origin: CGPoint(), size: self.bounds.size)
|
||||
} else {
|
||||
let theme = item.context.sharedContext.currentPresentationData.with { $0 }.theme
|
||||
let selectionNode = GridMessageSelectionNode(theme: theme, toggle: { [weak self] value in
|
||||
if let strongSelf = self {
|
||||
strongSelf.item?.interaction.toggleWallpaperSelection(item.wallpaperId, value)
|
||||
}
|
||||
})
|
||||
|
||||
selectionNode.frame = CGRect(origin: CGPoint(), size: self.bounds.size)
|
||||
self.addSubnode(selectionNode)
|
||||
self.selectionNode = selectionNode
|
||||
selectionNode.updateSelected(selected, animated: false)
|
||||
if animated {
|
||||
selectionNode.animateIn()
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
if let selectionNode = self.selectionNode {
|
||||
self.selectionNode = nil
|
||||
if animated {
|
||||
selectionNode.animateOut { [weak selectionNode] in
|
||||
selectionNode?.removeFromSupernode()
|
||||
}
|
||||
} else {
|
||||
selectionNode.removeFromSupernode()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override func layout() {
|
||||
super.layout()
|
||||
|
||||
let bounds = self.bounds
|
||||
if let item = self.item {
|
||||
self.wallpaperNode.setWallpaper(context: item.context, theme: item.theme, wallpaper: item.wallpaper, isEmpty: item.isEmpty, emojiFile: item.emojiFile, selected: !item.channelMode && item.selected, size: bounds.size, synchronousLoad: false)
|
||||
self.selectionNode?.frame = CGRect(origin: CGPoint(), size: bounds.size)
|
||||
}
|
||||
self.selectionBorderNode?.frame = CGRect(origin: CGPoint(), size: bounds.size)
|
||||
|
||||
if let textNode = self.textNode {
|
||||
let textSize = textNode.updateLayout(bounds.size)
|
||||
textNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((bounds.width - textSize.width) / 2.0), y: floorToScreenPixels((bounds.height - textSize.height) / 2.0) - 18.0), size: textSize)
|
||||
}
|
||||
}
|
||||
}
|
||||
+316
-129
@@ -10,12 +10,14 @@ import TelegramPresentationData
|
||||
import TelegramUIPreferences
|
||||
import MergeLists
|
||||
import ItemListUI
|
||||
import ItemListPeerActionItem
|
||||
import PresentationDataUtils
|
||||
import AccountContext
|
||||
import SearchBarNode
|
||||
import SearchUI
|
||||
import WallpaperResources
|
||||
import WallpaperGalleryScreen
|
||||
import BoostLevelIconComponent
|
||||
|
||||
struct ThemeGridControllerNodeState: Equatable {
|
||||
var editing: Bool
|
||||
@@ -28,12 +30,14 @@ final class ThemeGridControllerInteraction {
|
||||
let deleteSelectedWallpapers: () -> Void
|
||||
let shareSelectedWallpapers: () -> Void
|
||||
var selectionState: (Bool, Set<ThemeGridControllerEntry.StableId>) = (false, Set())
|
||||
var removeWallpaper: () -> Void
|
||||
|
||||
init(openWallpaper: @escaping (TelegramWallpaper) -> Void, toggleWallpaperSelection: @escaping (ThemeGridControllerEntry.StableId, Bool) -> Void, deleteSelectedWallpapers: @escaping () -> Void, shareSelectedWallpapers: @escaping () -> Void) {
|
||||
init(openWallpaper: @escaping (TelegramWallpaper) -> Void, toggleWallpaperSelection: @escaping (ThemeGridControllerEntry.StableId, Bool) -> Void, deleteSelectedWallpapers: @escaping () -> Void, shareSelectedWallpapers: @escaping () -> Void, removeWallpaper: @escaping () -> Void) {
|
||||
self.openWallpaper = openWallpaper
|
||||
self.toggleWallpaperSelection = toggleWallpaperSelection
|
||||
self.deleteSelectedWallpapers = deleteSelectedWallpapers
|
||||
self.shareSelectedWallpapers = shareSelectedWallpapers
|
||||
self.removeWallpaper = removeWallpaper
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,10 +48,15 @@ struct ThemeGridControllerEntry: Comparable, Identifiable {
|
||||
case gradient([UInt32])
|
||||
case file(Int64, [UInt32], Int32)
|
||||
case image(String)
|
||||
case emoticon(String)
|
||||
}
|
||||
|
||||
var index: Int
|
||||
var theme: PresentationTheme?
|
||||
var wallpaper: TelegramWallpaper
|
||||
var isEmpty: Bool = false
|
||||
var emoji: TelegramMediaFile?
|
||||
var channelMode: Bool = false
|
||||
var isEditable: Bool
|
||||
var isSelected: Bool
|
||||
|
||||
@@ -71,11 +80,13 @@ struct ThemeGridControllerEntry: Comparable, Identifiable {
|
||||
} else {
|
||||
return .image("")
|
||||
}
|
||||
case let .emoticon(emoticon):
|
||||
return .emoticon(emoticon)
|
||||
}
|
||||
}
|
||||
|
||||
func item(context: AccountContext, interaction: ThemeGridControllerInteraction) -> ThemeGridControllerItem {
|
||||
return ThemeGridControllerItem(context: context, wallpaper: self.wallpaper, wallpaperId: self.stableId, index: self.index, editable: self.isEditable, selected: self.isSelected, interaction: interaction)
|
||||
return ThemeGridControllerItem(context: context, theme: self.theme, wallpaper: self.wallpaper, wallpaperId: self.stableId, isEmpty: self.isEmpty, emojiFile: self.emoji, channelMode: self.channelMode, index: self.index, editable: self.isEditable, selected: self.isSelected, interaction: interaction)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,6 +152,7 @@ final class ThemeGridControllerNode: ASDisplayNode {
|
||||
}
|
||||
|
||||
private let context: AccountContext
|
||||
private let mode: ThemeGridController.Mode
|
||||
private var presentationData: PresentationData
|
||||
private var controllerInteraction: ThemeGridControllerInteraction?
|
||||
|
||||
@@ -151,19 +163,24 @@ final class ThemeGridControllerNode: ASDisplayNode {
|
||||
private let resetWallpapers: () -> Void
|
||||
|
||||
var requestDeactivateSearch: (() -> Void)?
|
||||
var requestWallpaperRemoval: (() -> Void)?
|
||||
|
||||
let ready = ValuePromise<Bool>()
|
||||
private let wallpapersPromise: Promise<[Wallpaper]>
|
||||
private let wallpapersPromise = Promise<[Wallpaper]>()
|
||||
private let themesPromise = Promise<[TelegramTheme]>()
|
||||
|
||||
private var backgroundNode: ASDisplayNode
|
||||
private var separatorNode: ASDisplayNode
|
||||
private var bottomBackgroundNode: ASDisplayNode
|
||||
private var bottomSeparatorNode: ASDisplayNode
|
||||
private let maskNode: ASImageNode
|
||||
|
||||
private let colorItemNode: ItemListActionItemNode
|
||||
private var colorItem: ItemListActionItem
|
||||
private let galleryItemNode: ItemListActionItemNode
|
||||
private var galleryItem: ItemListActionItem
|
||||
private let galleryItemNode: ListViewItemNode
|
||||
private var galleryItem: ItemListItem
|
||||
private let removeItemNode: ItemListPeerActionItemNode
|
||||
private var removeItem: ItemListPeerActionItem
|
||||
private let descriptionItemNode: ItemListTextItemNode
|
||||
private var descriptionItem: ItemListTextItem
|
||||
private let resetItemNode: ItemListActionItemNode
|
||||
@@ -193,8 +210,9 @@ final class ThemeGridControllerNode: ASDisplayNode {
|
||||
|
||||
private var disposable: Disposable?
|
||||
|
||||
init(context: AccountContext, presentationData: PresentationData, presentPreviewController: @escaping (WallpaperListSource) -> Void, presentGallery: @escaping () -> Void, presentColors: @escaping () -> Void, emptyStateUpdated: @escaping (Bool) -> Void, deleteWallpapers: @escaping ([TelegramWallpaper], @escaping () -> Void) -> Void, shareWallpapers: @escaping ([TelegramWallpaper]) -> Void, resetWallpapers: @escaping () -> Void, popViewController: @escaping () -> Void) {
|
||||
init(context: AccountContext, mode: ThemeGridController.Mode, presentationData: PresentationData, presentPreviewController: @escaping (WallpaperListSource) -> Void, presentGallery: @escaping () -> Void, presentColors: @escaping () -> Void, emptyStateUpdated: @escaping (Bool) -> Void, deleteWallpapers: @escaping ([TelegramWallpaper], @escaping () -> Void) -> Void, shareWallpapers: @escaping ([TelegramWallpaper]) -> Void, resetWallpapers: @escaping () -> Void, popViewController: @escaping () -> Void) {
|
||||
self.context = context
|
||||
self.mode = mode
|
||||
self.presentationData = presentationData
|
||||
self.presentPreviewController = presentPreviewController
|
||||
self.presentGallery = presentGallery
|
||||
@@ -221,16 +239,49 @@ final class ThemeGridControllerNode: ASDisplayNode {
|
||||
self.bottomSeparatorNode = ASDisplayNode()
|
||||
self.bottomSeparatorNode.backgroundColor = presentationData.theme.list.itemBlocksSeparatorColor
|
||||
|
||||
self.maskNode = ASImageNode()
|
||||
self.maskNode.isUserInteractionEnabled = false
|
||||
|
||||
self.colorItemNode = ItemListActionItemNode()
|
||||
self.colorItem = ItemListActionItem(presentationData: ItemListPresentationData(presentationData), title: presentationData.strings.Wallpaper_SetColor, kind: .generic, alignment: .natural, sectionId: 0, style: .blocks, action: {
|
||||
presentColors()
|
||||
})
|
||||
self.galleryItemNode = ItemListActionItemNode()
|
||||
self.galleryItem = ItemListActionItem(presentationData: ItemListPresentationData(presentationData), title: presentationData.strings.Wallpaper_SetCustomBackground, kind: .generic, alignment: .natural, sectionId: 0, style: .blocks, action: {
|
||||
presentGallery()
|
||||
|
||||
switch mode {
|
||||
case .generic:
|
||||
self.galleryItem = ItemListActionItem(presentationData: ItemListPresentationData(presentationData), title: presentationData.strings.Wallpaper_SetCustomBackground, kind: .generic, alignment: .natural, sectionId: 0, style: .blocks, action: {
|
||||
presentGallery()
|
||||
})
|
||||
self.galleryItemNode = ItemListActionItemNode()
|
||||
case .peer:
|
||||
var requiredCustomWallpaperLevel: Int?
|
||||
if case let .peer(_, _, _, _, customLevel) = mode {
|
||||
requiredCustomWallpaperLevel = customLevel
|
||||
}
|
||||
//TODO:localize
|
||||
self.galleryItem = ItemListPeerActionItem(presentationData: ItemListPresentationData(presentationData), icon: generateTintedImage(image: UIImage(bundleImageName: "Chat/Attach Menu/Image"), color: presentationData.theme.list.itemAccentColor), title: presentationData.strings.Wallpaper_SetCustomBackground, additionalBadgeIcon: requiredCustomWallpaperLevel.flatMap { generateDisclosureActionBoostLevelBadgeImage(text: "Level \($0)") }, alwaysPlain: false, hasSeparator: true, sectionId: 0, height: .generic, color: .accent, editing: false, action: {
|
||||
presentGallery()
|
||||
})
|
||||
self.galleryItemNode = ItemListPeerActionItemNode()
|
||||
}
|
||||
|
||||
var removeImpl: (() -> Void)?
|
||||
self.removeItem = ItemListPeerActionItem(presentationData: ItemListPresentationData(presentationData), icon: generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionTrash"), color: presentationData.theme.list.itemDestructiveColor), title: presentationData.strings.Wallpaper_ChannelRemoveBackground, alwaysPlain: false, hasSeparator: true, sectionId: 0, height: .generic, color: .destructive, editing: false, action: {
|
||||
removeImpl?()
|
||||
})
|
||||
self.removeItemNode = ItemListPeerActionItemNode()
|
||||
|
||||
self.descriptionItemNode = ItemListTextItemNode()
|
||||
self.descriptionItem = ItemListTextItem(presentationData: ItemListPresentationData(presentationData), text: .plain(presentationData.strings.Wallpaper_SetCustomBackgroundInfo), sectionId: 0)
|
||||
|
||||
let descriptionText: String
|
||||
switch mode {
|
||||
case .generic:
|
||||
descriptionText = presentationData.strings.Wallpaper_SetCustomBackgroundInfo
|
||||
case .peer:
|
||||
descriptionText = presentationData.strings.Wallpaper_ChannelCustomBackgroundInfo
|
||||
}
|
||||
self.descriptionItem = ItemListTextItem(presentationData: ItemListPresentationData(presentationData), text: .plain(descriptionText), sectionId: 0)
|
||||
|
||||
self.resetItemNode = ItemListActionItemNode()
|
||||
self.resetItem = ItemListActionItem(presentationData: ItemListPresentationData(presentationData), title: presentationData.strings.Wallpaper_ResetWallpapers, kind: .generic, alignment: .natural, sectionId: 0, style: .blocks, action: {
|
||||
resetWallpapers()
|
||||
@@ -241,9 +292,6 @@ final class ThemeGridControllerNode: ASDisplayNode {
|
||||
self.currentState = ThemeGridControllerNodeState(editing: false, selectedIds: Set())
|
||||
self.statePromise = ValuePromise(self.currentState, ignoreRepeated: true)
|
||||
|
||||
let wallpapersPromise = Promise<[Wallpaper]>()
|
||||
self.wallpapersPromise = wallpapersPromise
|
||||
|
||||
let deletedWallpaperIdsValue = Atomic<Set<ThemeGridControllerEntry.StableId>>(value: Set())
|
||||
let deletedWallpaperIdsPromise = ValuePromise<Set<ThemeGridControllerEntry.StableId>>(Set())
|
||||
|
||||
@@ -256,16 +304,25 @@ final class ThemeGridControllerNode: ASDisplayNode {
|
||||
self.backgroundColor = presentationData.theme.list.itemBlocksBackgroundColor
|
||||
|
||||
self.gridNode.addSubnode(self.backgroundNode)
|
||||
self.gridNode.addSubnode(self.separatorNode)
|
||||
self.gridNode.addSubnode(self.bottomBackgroundNode)
|
||||
self.gridNode.addSubnode(self.bottomSeparatorNode)
|
||||
self.gridNode.addSubnode(self.colorItemNode)
|
||||
// self.gridNode.addSubnode(self.bottomSeparatorNode)
|
||||
if case .generic = mode {
|
||||
self.gridNode.addSubnode(self.colorItemNode)
|
||||
}
|
||||
self.gridNode.addSubnode(self.galleryItemNode)
|
||||
if case let .peer(_, _, wallpaper, _, _) = mode, let wallpaper, !wallpaper.isEmoticon {
|
||||
self.gridNode.addSubnode(self.removeItemNode)
|
||||
}
|
||||
self.gridNode.addSubnode(self.descriptionItemNode)
|
||||
self.gridNode.addSubnode(self.resetItemNode)
|
||||
self.gridNode.addSubnode(self.resetDescriptionItemNode)
|
||||
|
||||
if case .generic = mode {
|
||||
self.gridNode.addSubnode(self.resetItemNode)
|
||||
self.gridNode.addSubnode(self.resetDescriptionItemNode)
|
||||
}
|
||||
|
||||
self.addSubnode(self.gridNode)
|
||||
self.gridNode.addSubnode(self.maskNode)
|
||||
self.maskNode.image = PresentationResourcesItemList.cornersImage(presentationData.theme, top: true, bottom: true)
|
||||
|
||||
let previousEntries = Atomic<[ThemeGridControllerEntry]?>(value: nil)
|
||||
let interaction = ThemeGridControllerInteraction(openWallpaper: { [weak self] wallpaper in
|
||||
@@ -337,83 +394,125 @@ final class ThemeGridControllerNode: ASDisplayNode {
|
||||
if let strongSelf = self, let entries = entries {
|
||||
shareWallpapers(selectedWallpapers(entries: entries, state: strongSelf.currentState))
|
||||
}
|
||||
}, removeWallpaper: { [weak self] in
|
||||
if let self {
|
||||
self.requestWallpaperRemoval?()
|
||||
}
|
||||
})
|
||||
self.controllerInteraction = interaction
|
||||
|
||||
let transition = combineLatest(self.wallpapersPromise.get(), deletedWallpaperIdsPromise.get(), context.sharedContext.presentationData)
|
||||
|> map { wallpapers, deletedWallpaperIds, presentationData -> (ThemeGridEntryTransition, Bool) in
|
||||
let transition = combineLatest(self.wallpapersPromise.get(), self.themesPromise.get(), deletedWallpaperIdsPromise.get(), context.sharedContext.presentationData)
|
||||
|> map { wallpapers, themes, deletedWallpaperIds, presentationData -> (ThemeGridEntryTransition, Bool) in
|
||||
var entries: [ThemeGridControllerEntry] = []
|
||||
var index: Int = 0
|
||||
|
||||
entries.insert(ThemeGridControllerEntry(index: 0, wallpaper: presentationData.chatWallpaper, isEditable: false, isSelected: true), at: 0)
|
||||
index += 1
|
||||
|
||||
var defaultWallpaper: TelegramWallpaper?
|
||||
if !presentationData.chatWallpaper.isBasicallyEqual(to: presentationData.theme.chat.defaultWallpaper) {
|
||||
let entry = ThemeGridControllerEntry(index: 1, wallpaper: presentationData.theme.chat.defaultWallpaper, isEditable: false, isSelected: false)
|
||||
if !entries.contains(where: { $0.stableId == entry.stableId }) {
|
||||
defaultWallpaper = presentationData.theme.chat.defaultWallpaper
|
||||
entries.insert(entry, at: index)
|
||||
index += 1
|
||||
if !themes.isEmpty {
|
||||
var selectedWallpaper: TelegramWallpaper?
|
||||
if case let .peer(_, _, wallpaper, _, _) = mode {
|
||||
selectedWallpaper = wallpaper
|
||||
}
|
||||
}
|
||||
|
||||
var sortedWallpapers: [TelegramWallpaper] = []
|
||||
if presentationData.theme.overallDarkAppearance {
|
||||
var localWallpapers: [TelegramWallpaper] = []
|
||||
var darkWallpapers: [TelegramWallpaper] = []
|
||||
for wallpaper in wallpapers {
|
||||
if wallpaper.isLocal {
|
||||
localWallpapers.append(wallpaper.wallpaper)
|
||||
} else {
|
||||
if case let .file(file) = wallpaper.wallpaper, file.isDark {
|
||||
darkWallpapers.append(wallpaper.wallpaper)
|
||||
} else {
|
||||
sortedWallpapers.append(wallpaper.wallpaper)
|
||||
}
|
||||
}
|
||||
}
|
||||
sortedWallpapers = localWallpapers + darkWallpapers + sortedWallpapers
|
||||
} else {
|
||||
sortedWallpapers = wallpapers.map(\.wallpaper)
|
||||
}
|
||||
|
||||
if let builtinIndex = sortedWallpapers.firstIndex(where: { wallpaper in
|
||||
if case .builtin = wallpaper {
|
||||
return true
|
||||
|
||||
if let selectedWallpaper, !selectedWallpaper.isEmoticon {
|
||||
entries.append(ThemeGridControllerEntry(index: index, theme: presentationData.theme, wallpaper: selectedWallpaper, channelMode: true, isEditable: false, isSelected: true))
|
||||
} else {
|
||||
return false
|
||||
let emojiFile = context.animatedEmojiStickers["❌"]?.first?.file
|
||||
|
||||
entries.append(ThemeGridControllerEntry(index: index, theme: presentationData.theme, wallpaper: .color(0), isEmpty: true, emoji: emojiFile, channelMode: true, isEditable: false, isSelected: selectedWallpaper == nil))
|
||||
}
|
||||
}) {
|
||||
sortedWallpapers[builtinIndex] = defaultBuiltinWallpaper(data: .legacy, colors: legacyBuiltinWallpaperGradientColors.map(\.rgb))
|
||||
}
|
||||
|
||||
for wallpaper in sortedWallpapers {
|
||||
if case let .file(file) = wallpaper, (wallpaper.isPattern && file.settings.colors.isEmpty) {
|
||||
continue
|
||||
}
|
||||
let selected = presentationData.chatWallpaper.isBasicallyEqual(to: wallpaper)
|
||||
var isDefault = false
|
||||
if let defaultWallpaper = defaultWallpaper, defaultWallpaper.isBasicallyEqual(to: wallpaper) {
|
||||
isDefault = true
|
||||
}
|
||||
var isEditable = true
|
||||
if case .builtin = wallpaper {
|
||||
isEditable = false
|
||||
}
|
||||
if isDefault || presentationData.chatWallpaper.isBasicallyEqual(to: wallpaper) {
|
||||
isEditable = false
|
||||
}
|
||||
if !selected && !isDefault {
|
||||
let entry = ThemeGridControllerEntry(index: index, wallpaper: wallpaper, isEditable: isEditable, isSelected: false)
|
||||
if deletedWallpaperIds.contains(entry.stableId) {
|
||||
index += 1
|
||||
|
||||
for theme in themes {
|
||||
guard let wallpaper = theme.settings?.first?.wallpaper, let themeEmoticon = theme.emoticon else {
|
||||
continue
|
||||
}
|
||||
|
||||
var updatedWallpaper = wallpaper
|
||||
if let settings = wallpaper.settings {
|
||||
var updatedSettings = settings
|
||||
updatedSettings.emoticon = themeEmoticon
|
||||
updatedWallpaper = wallpaper.withUpdatedSettings(updatedSettings)
|
||||
}
|
||||
|
||||
var isSelected = false
|
||||
if let selectedWallpaper, case let .emoticon(emoticon) = selectedWallpaper, emoticon.strippedEmoji == themeEmoticon.strippedEmoji {
|
||||
isSelected = true
|
||||
}
|
||||
|
||||
let emoji = context.animatedEmojiStickers[themeEmoticon]
|
||||
entries.append(ThemeGridControllerEntry(index: index, theme: presentationData.theme, wallpaper: updatedWallpaper, emoji: emoji?.first?.file, channelMode: true, isEditable: false, isSelected: isSelected))
|
||||
index += 1
|
||||
}
|
||||
} else {
|
||||
entries.insert(ThemeGridControllerEntry(index: 0, wallpaper: presentationData.chatWallpaper, emoji: nil, isEditable: false, isSelected: true), at: 0)
|
||||
index += 1
|
||||
|
||||
var defaultWallpaper: TelegramWallpaper?
|
||||
if !presentationData.chatWallpaper.isBasicallyEqual(to: presentationData.theme.chat.defaultWallpaper) {
|
||||
let entry = ThemeGridControllerEntry(index: 1, wallpaper: presentationData.theme.chat.defaultWallpaper, emoji: nil, isEditable: false, isSelected: false)
|
||||
if !entries.contains(where: { $0.stableId == entry.stableId }) {
|
||||
entries.append(entry)
|
||||
defaultWallpaper = presentationData.theme.chat.defaultWallpaper
|
||||
entries.insert(entry, at: index)
|
||||
index += 1
|
||||
}
|
||||
}
|
||||
|
||||
var sortedWallpapers: [TelegramWallpaper] = []
|
||||
if presentationData.theme.overallDarkAppearance {
|
||||
var localWallpapers: [TelegramWallpaper] = []
|
||||
var darkWallpapers: [TelegramWallpaper] = []
|
||||
for wallpaper in wallpapers {
|
||||
if wallpaper.isLocal {
|
||||
localWallpapers.append(wallpaper.wallpaper)
|
||||
} else {
|
||||
if case let .file(file) = wallpaper.wallpaper, file.isDark {
|
||||
darkWallpapers.append(wallpaper.wallpaper)
|
||||
} else {
|
||||
sortedWallpapers.append(wallpaper.wallpaper)
|
||||
}
|
||||
}
|
||||
}
|
||||
sortedWallpapers = localWallpapers + darkWallpapers + sortedWallpapers
|
||||
} else {
|
||||
sortedWallpapers = wallpapers.map(\.wallpaper)
|
||||
}
|
||||
|
||||
if let builtinIndex = sortedWallpapers.firstIndex(where: { wallpaper in
|
||||
if case .builtin = wallpaper {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}) {
|
||||
sortedWallpapers[builtinIndex] = defaultBuiltinWallpaper(data: .legacy, colors: legacyBuiltinWallpaperGradientColors.map(\.rgb))
|
||||
}
|
||||
|
||||
for wallpaper in sortedWallpapers {
|
||||
if case let .file(file) = wallpaper, (wallpaper.isPattern && file.settings.colors.isEmpty) {
|
||||
continue
|
||||
}
|
||||
let selected = presentationData.chatWallpaper.isBasicallyEqual(to: wallpaper)
|
||||
var isDefault = false
|
||||
if let defaultWallpaper = defaultWallpaper, defaultWallpaper.isBasicallyEqual(to: wallpaper) {
|
||||
isDefault = true
|
||||
}
|
||||
var isEditable = true
|
||||
if case .builtin = wallpaper {
|
||||
isEditable = false
|
||||
}
|
||||
if isDefault || presentationData.chatWallpaper.isBasicallyEqual(to: wallpaper) {
|
||||
isEditable = false
|
||||
}
|
||||
if !selected && !isDefault {
|
||||
let entry = ThemeGridControllerEntry(index: index, wallpaper: wallpaper, isEditable: isEditable, isSelected: false)
|
||||
if deletedWallpaperIds.contains(entry.stableId) {
|
||||
continue
|
||||
}
|
||||
if !entries.contains(where: { $0.stableId == entry.stableId }) {
|
||||
entries.append(entry)
|
||||
index += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let previous = previousEntries.swap(entries)
|
||||
@@ -424,6 +523,10 @@ final class ThemeGridControllerNode: ASDisplayNode {
|
||||
strongSelf.enqueueTransition(transition)
|
||||
}
|
||||
})
|
||||
|
||||
removeImpl = { [weak self] in
|
||||
self?.controllerInteraction?.removeWallpaper()
|
||||
}
|
||||
|
||||
self.updateWallpapers()
|
||||
}
|
||||
@@ -450,6 +553,8 @@ final class ThemeGridControllerNode: ASDisplayNode {
|
||||
highlightedNode = strongSelf.galleryItemNode
|
||||
} else if strongSelf.resetItemNode.frame.contains(point) {
|
||||
highlightedNode = strongSelf.resetItemNode
|
||||
} else if strongSelf.removeItemNode.frame.contains(point) {
|
||||
highlightedNode = strongSelf.removeItemNode
|
||||
}
|
||||
}
|
||||
|
||||
@@ -459,6 +564,7 @@ final class ThemeGridControllerNode: ASDisplayNode {
|
||||
strongSelf.colorItemNode.setHighlighted(false, at: CGPoint(), animated: true)
|
||||
strongSelf.galleryItemNode.setHighlighted(false, at: CGPoint(), animated: true)
|
||||
strongSelf.resetItemNode.setHighlighted(false, at: CGPoint(), animated: true)
|
||||
strongSelf.removeItemNode.setHighlighted(false, at: CGPoint(), animated: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -476,11 +582,14 @@ final class ThemeGridControllerNode: ASDisplayNode {
|
||||
let (resetLayout, resetApply) = makeResetLayout(strongSelf.resetItem, params, ItemListNeighbors(top: .none, bottom: .sameSection(alwaysPlain: true)))
|
||||
let (resetDescriptionLayout, resetDescriptionApply) = makeResetDescriptionLayout(strongSelf.resetDescriptionItem, params, ItemListNeighbors(top: .none, bottom: .none))
|
||||
|
||||
resetApply()
|
||||
resetApply(false)
|
||||
resetDescriptionApply()
|
||||
|
||||
transition.updateFrame(node: strongSelf.resetItemNode, frame: CGRect(origin: CGPoint(x: 0.0, y: gridLayout.contentSize.height + 35.0), size: resetLayout.contentSize))
|
||||
transition.updateFrame(node: strongSelf.resetDescriptionItemNode, frame: CGRect(origin: CGPoint(x: 0.0, y: gridLayout.contentSize.height + 35.0 + resetLayout.contentSize.height), size: resetDescriptionLayout.contentSize))
|
||||
|
||||
let sideInset = strongSelf.leftOverlayNode.frame.maxX
|
||||
strongSelf.maskNode.frame = CGRect(origin: CGPoint(x: sideInset, y: strongSelf.separatorNode.frame.minY + UIScreenPixel + 4.0), size: CGSize(width: layout.size.width - sideInset * 2.0, height: gridLayout.contentSize.height + 6.0))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -494,9 +603,15 @@ final class ThemeGridControllerNode: ASDisplayNode {
|
||||
if self.colorItemNode.frame.contains(location) {
|
||||
self.colorItem.action()
|
||||
} else if self.galleryItemNode.frame.contains(location) {
|
||||
self.galleryItem.action()
|
||||
if let galleryItem = self.galleryItem as? ItemListActionItem {
|
||||
galleryItem.action()
|
||||
} else if let galleryItem = self.galleryItem as? ItemListPeerActionItem {
|
||||
galleryItem.action?()
|
||||
}
|
||||
} else if self.resetItemNode.frame.contains(location) {
|
||||
self.resetItem.action()
|
||||
} else if self.removeItemNode.frame.contains(location) {
|
||||
self.removeItem.action?()
|
||||
}
|
||||
default:
|
||||
break
|
||||
@@ -508,31 +623,38 @@ final class ThemeGridControllerNode: ASDisplayNode {
|
||||
}
|
||||
|
||||
func updateWallpapers() {
|
||||
self.wallpapersPromise.set(combineLatest(queue: .mainQueue(),
|
||||
telegramWallpapers(postbox: self.context.account.postbox, network: self.context.account.network),
|
||||
self.context.sharedContext.accountManager.sharedData(keys: [SharedDataKeys.wallapersState])
|
||||
)
|
||||
|> map { remoteWallpapers, sharedData -> [Wallpaper] in
|
||||
let localState = sharedData.entries[SharedDataKeys.wallapersState]?.get(WallpapersState.self) ?? WallpapersState.default
|
||||
switch self.mode {
|
||||
case .generic:
|
||||
self.wallpapersPromise.set(combineLatest(queue: .mainQueue(),
|
||||
telegramWallpapers(postbox: self.context.account.postbox, network: self.context.account.network),
|
||||
self.context.sharedContext.accountManager.sharedData(keys: [SharedDataKeys.wallapersState])
|
||||
)
|
||||
|> map { remoteWallpapers, sharedData -> [Wallpaper] in
|
||||
let localState = sharedData.entries[SharedDataKeys.wallapersState]?.get(WallpapersState.self) ?? WallpapersState.default
|
||||
|
||||
var wallpapers: [Wallpaper] = []
|
||||
for wallpaper in localState.wallpapers {
|
||||
if !wallpapers.contains(where: {
|
||||
$0.wallpaper.isBasicallyEqual(to: wallpaper)
|
||||
}) {
|
||||
wallpapers.append(Wallpaper(wallpaper: wallpaper, isLocal: true))
|
||||
var wallpapers: [Wallpaper] = []
|
||||
for wallpaper in localState.wallpapers {
|
||||
if !wallpapers.contains(where: {
|
||||
$0.wallpaper.isBasicallyEqual(to: wallpaper)
|
||||
}) {
|
||||
wallpapers.append(Wallpaper(wallpaper: wallpaper, isLocal: true))
|
||||
}
|
||||
}
|
||||
}
|
||||
for wallpaper in remoteWallpapers {
|
||||
if !wallpapers.contains(where: {
|
||||
$0.wallpaper.isBasicallyEqual(to: wallpaper)
|
||||
}) {
|
||||
wallpapers.append(Wallpaper(wallpaper: wallpaper, isLocal: false))
|
||||
for wallpaper in remoteWallpapers {
|
||||
if !wallpapers.contains(where: {
|
||||
$0.wallpaper.isBasicallyEqual(to: wallpaper)
|
||||
}) {
|
||||
wallpapers.append(Wallpaper(wallpaper: wallpaper, isLocal: false))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return wallpapers
|
||||
})
|
||||
return wallpapers
|
||||
})
|
||||
self.themesPromise.set(.single([]))
|
||||
case let .peer(_, themes, _, _, _):
|
||||
self.themesPromise.set(.single(themes))
|
||||
self.wallpapersPromise.set(.single([]))
|
||||
}
|
||||
}
|
||||
|
||||
func updatePresentationData(_ presentationData: PresentationData) {
|
||||
@@ -551,9 +673,25 @@ final class ThemeGridControllerNode: ASDisplayNode {
|
||||
self.colorItem = ItemListActionItem(presentationData: ItemListPresentationData(presentationData), title: presentationData.strings.Wallpaper_SetColor, kind: .generic, alignment: .natural, sectionId: 0, style: .blocks, action: { [weak self] in
|
||||
self?.presentColors()
|
||||
})
|
||||
self.galleryItem = ItemListActionItem(presentationData: ItemListPresentationData(presentationData), title: presentationData.strings.Wallpaper_SetCustomBackground, kind: .generic, alignment: .natural, sectionId: 0, style: .blocks, action: { [weak self] in
|
||||
self?.presentGallery()
|
||||
})
|
||||
|
||||
switch self.mode {
|
||||
case .generic:
|
||||
self.galleryItem = ItemListActionItem(presentationData: ItemListPresentationData(presentationData), title: presentationData.strings.Wallpaper_SetCustomBackground, kind: .generic, alignment: .natural, sectionId: 0, style: .blocks, action: { [weak self] in
|
||||
self?.presentGallery()
|
||||
})
|
||||
case .peer:
|
||||
var requiredCustomWallpaperLevel: Int?
|
||||
if case let .peer(_, _, _, _, customLevel) = mode {
|
||||
requiredCustomWallpaperLevel = customLevel
|
||||
}
|
||||
self.galleryItem = ItemListPeerActionItem(presentationData: ItemListPresentationData(presentationData), icon: generateTintedImage(image: UIImage(bundleImageName: "Chat/Attach Menu/Image"), color: presentationData.theme.list.itemAccentColor), title: presentationData.strings.Wallpaper_SetCustomBackground, additionalBadgeIcon: requiredCustomWallpaperLevel.flatMap { generateDisclosureActionBoostLevelBadgeImage(text: "Level \($0)") }, alwaysPlain: false, hasSeparator: true, sectionId: 0, height: .generic, color: .accent, editing: false, action: { [weak self] in
|
||||
self?.presentGallery()
|
||||
})
|
||||
self.removeItem = ItemListPeerActionItem(presentationData: ItemListPresentationData(presentationData), icon: generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionTrash"), color: presentationData.theme.list.itemDestructiveColor), title: presentationData.strings.Wallpaper_ChannelRemoveBackground, alwaysPlain: false, hasSeparator: true, sectionId: 0, height: .generic, color: .destructive, editing: false, action: { [weak self] in
|
||||
self?.controllerInteraction?.removeWallpaper()
|
||||
})
|
||||
}
|
||||
|
||||
self.descriptionItem = ItemListTextItem(presentationData: ItemListPresentationData(presentationData), text: .plain(presentationData.strings.Wallpaper_SetCustomBackgroundInfo), sectionId: 0)
|
||||
self.resetItem = ItemListActionItem(presentationData: ItemListPresentationData(presentationData), title: presentationData.strings.Wallpaper_ResetWallpapers, kind: .generic, alignment: .natural, sectionId: 0, style: .blocks, action: { [weak self] in
|
||||
self?.resetWallpapers()
|
||||
@@ -621,28 +759,33 @@ final class ThemeGridControllerNode: ASDisplayNode {
|
||||
|
||||
var scrollIndicatorInsets = insets
|
||||
|
||||
let minSpacing: CGFloat = 8.0
|
||||
let minSpacing: CGFloat = 6.0
|
||||
let referenceImageSize: CGSize
|
||||
let screenWidth = min(layout.size.width, layout.size.height)
|
||||
if screenWidth >= 390.0 {
|
||||
referenceImageSize = CGSize(width: 108.0, height: 230.0)
|
||||
referenceImageSize = CGSize(width: 112.0, height: 150.0)
|
||||
} else {
|
||||
referenceImageSize = CGSize(width: 91.0, height: 161.0)
|
||||
}
|
||||
let imageCount = Int((layout.size.width - insets.left - insets.right - minSpacing * 2.0) / (referenceImageSize.width + minSpacing))
|
||||
let imageSize = referenceImageSize.aspectFilled(CGSize(width: floor((layout.size.width - CGFloat(imageCount + 1) * minSpacing) / CGFloat(imageCount)), height: referenceImageSize.height))
|
||||
let spacing = floor((layout.size.width - CGFloat(imageCount) * imageSize.width) / CGFloat(imageCount + 1))
|
||||
|
||||
let sideInset = max(16.0, floor((layout.size.width - 674.0) / 2.0))
|
||||
|
||||
let gridWidth = layout.size.width - sideInset * 2.0
|
||||
|
||||
let imageCount = Int((gridWidth - minSpacing * 2.0) / (referenceImageSize.width))
|
||||
let imageSize = referenceImageSize.aspectFilled(CGSize(width: floor((gridWidth - CGFloat(imageCount + 1) * minSpacing) / CGFloat(imageCount)), height: referenceImageSize.height))
|
||||
let spacing = floor((gridWidth - CGFloat(imageCount) * imageSize.width) / CGFloat(imageCount + 1))
|
||||
|
||||
let makeColorLayout = self.colorItemNode.asyncLayout()
|
||||
let makeGalleryLayout = self.galleryItemNode.asyncLayout()
|
||||
let makeGalleryLayout = (self.galleryItemNode as? ItemListActionItemNode)?.asyncLayout()
|
||||
let makeGalleryIconLayout = (self.galleryItemNode as? ItemListPeerActionItemNode)?.asyncLayout()
|
||||
let makeRemoveLayout = self.removeItemNode.asyncLayout()
|
||||
let makeDescriptionLayout = self.descriptionItemNode.asyncLayout()
|
||||
|
||||
var listInsets = insets
|
||||
if layout.size.width >= 375.0 {
|
||||
let inset = max(16.0, floor((layout.size.width - 674.0) / 2.0))
|
||||
listInsets.left += inset
|
||||
listInsets.right += inset
|
||||
|
||||
listInsets.left = sideInset
|
||||
listInsets.right = sideInset
|
||||
if self.leftOverlayNode.supernode == nil {
|
||||
self.gridNode.addSubnode(self.leftOverlayNode)
|
||||
}
|
||||
@@ -658,33 +801,75 @@ final class ThemeGridControllerNode: ASDisplayNode {
|
||||
}
|
||||
}
|
||||
|
||||
var isChannel = false
|
||||
var hasCustomWallpaper = false
|
||||
if case let .peer(_, _, wallpaper, _, _) = self.mode {
|
||||
isChannel = true
|
||||
if let wallpaper, !wallpaper.isPattern {
|
||||
hasCustomWallpaper = true
|
||||
}
|
||||
}
|
||||
|
||||
let params = ListViewItemLayoutParams(width: layout.size.width, leftInset: listInsets.left, rightInset: listInsets.right, availableHeight: layout.size.height)
|
||||
let (colorLayout, colorApply) = makeColorLayout(self.colorItem, params, ItemListNeighbors(top: .none, bottom: .sameSection(alwaysPlain: false)))
|
||||
let (galleryLayout, galleryApply) = makeGalleryLayout(self.galleryItem, params, ItemListNeighbors(top: .sameSection(alwaysPlain: false), bottom: .sameSection(alwaysPlain: true)))
|
||||
let (galleryLayout, galleryApply): (ListViewItemNodeLayout, (Bool) -> Void)
|
||||
if let makeGalleryIconLayout, let galleryItem = self.galleryItem as? ItemListPeerActionItem {
|
||||
(galleryLayout, galleryApply) = makeGalleryIconLayout(galleryItem, params, ItemListNeighbors(top: isChannel ? .none : .sameSection(alwaysPlain: false), bottom: .sameSection(alwaysPlain: !hasCustomWallpaper)))
|
||||
} else if let makeGalleryLayout, let galleryItem = self.galleryItem as? ItemListActionItem {
|
||||
(galleryLayout, galleryApply) = makeGalleryLayout(galleryItem, params, ItemListNeighbors(top: isChannel ? .none : .sameSection(alwaysPlain: false), bottom: .sameSection(alwaysPlain: false)))
|
||||
} else {
|
||||
fatalError()
|
||||
}
|
||||
let (removeLayout, removeApply) = makeRemoveLayout(self.removeItem, params, ItemListNeighbors(top: .sameSection(alwaysPlain: false), bottom: .none))
|
||||
|
||||
let (descriptionLayout, descriptionApply) = makeDescriptionLayout(self.descriptionItem, params, ItemListNeighbors(top: .none, bottom: .none))
|
||||
|
||||
colorApply()
|
||||
galleryApply()
|
||||
|
||||
colorApply(false)
|
||||
galleryApply(false)
|
||||
removeApply(false)
|
||||
descriptionApply()
|
||||
|
||||
let buttonTopInset: CGFloat = 32.0
|
||||
let buttonHeight: CGFloat = 44.0
|
||||
let buttonBottomInset: CGFloat = descriptionLayout.contentSize.height + 17.0
|
||||
var buttonBottomInset: CGFloat = descriptionLayout.contentSize.height + 17.0
|
||||
if hasCustomWallpaper {
|
||||
buttonBottomInset = 17.0
|
||||
}
|
||||
|
||||
let buttonInset: CGFloat = buttonTopInset + buttonHeight * 2.0 + buttonBottomInset
|
||||
var buttonInset: CGFloat = buttonTopInset + buttonHeight + buttonBottomInset
|
||||
if !isChannel || hasCustomWallpaper {
|
||||
buttonInset += buttonHeight
|
||||
}
|
||||
let buttonOffset = buttonInset + 10.0
|
||||
|
||||
transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -buttonOffset - 500.0), size: CGSize(width: layout.size.width, height: buttonInset + 500.0)))
|
||||
transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -buttonOffset - 500.0), size: CGSize(width: layout.size.width, height: buttonInset + 504.0)))
|
||||
transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -buttonOffset + buttonInset - UIScreenPixel), size: CGSize(width: layout.size.width, height: UIScreenPixel)))
|
||||
|
||||
transition.updateFrame(node: self.colorItemNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -buttonOffset + buttonTopInset), size: colorLayout.contentSize))
|
||||
transition.updateFrame(node: self.galleryItemNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -buttonOffset + buttonTopInset + colorLayout.contentSize.height), size: galleryLayout.contentSize))
|
||||
transition.updateFrame(node: self.descriptionItemNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -buttonOffset + buttonTopInset + colorLayout.contentSize.height + galleryLayout.contentSize.height), size: descriptionLayout.contentSize))
|
||||
var originY = -buttonOffset + buttonTopInset
|
||||
if !isChannel {
|
||||
transition.updateFrame(node: self.colorItemNode, frame: CGRect(origin: CGPoint(x: 0.0, y: originY), size: colorLayout.contentSize))
|
||||
originY += colorLayout.contentSize.height
|
||||
}
|
||||
transition.updateFrame(node: self.galleryItemNode, frame: CGRect(origin: CGPoint(x: 0.0, y: originY), size: galleryLayout.contentSize))
|
||||
originY += galleryLayout.contentSize.height
|
||||
|
||||
self.leftOverlayNode.frame = CGRect(x: 0.0, y: -buttonOffset, width: listInsets.left, height: buttonTopInset + colorLayout.contentSize.height + galleryLayout.contentSize.height)
|
||||
self.rightOverlayNode.frame = CGRect(x: layout.size.width - listInsets.right, y: -buttonOffset, width: listInsets.right, height: buttonTopInset + colorLayout.contentSize.height + galleryLayout.contentSize.height)
|
||||
if hasCustomWallpaper {
|
||||
self.descriptionItemNode.isHidden = true
|
||||
self.removeItemNode.isHidden = false
|
||||
|
||||
transition.updateFrame(node: self.removeItemNode, frame: CGRect(origin: CGPoint(x: 0.0, y: originY), size: removeLayout.contentSize))
|
||||
} else {
|
||||
self.descriptionItemNode.isHidden = false
|
||||
self.removeItemNode.isHidden = true
|
||||
transition.updateFrame(node: self.descriptionItemNode, frame: CGRect(origin: CGPoint(x: 0.0, y: originY), size: descriptionLayout.contentSize))
|
||||
}
|
||||
|
||||
self.leftOverlayNode.frame = CGRect(x: 0.0, y: -buttonOffset, width: listInsets.left, height: buttonTopInset + colorLayout.contentSize.height + galleryLayout.contentSize.height + 10000.0)
|
||||
self.rightOverlayNode.frame = CGRect(x: layout.size.width - listInsets.right, y: -buttonOffset, width: listInsets.right, height: buttonTopInset + colorLayout.contentSize.height + galleryLayout.contentSize.height + 10000.0)
|
||||
|
||||
insets.top += spacing + buttonInset
|
||||
listInsets.top = insets.top
|
||||
|
||||
if self.currentState.editing {
|
||||
let panelHeight: CGFloat
|
||||
@@ -748,10 +933,12 @@ final class ThemeGridControllerNode: ASDisplayNode {
|
||||
let makeResetDescriptionLayout = self.resetDescriptionItemNode.asyncLayout()
|
||||
let (resetDescriptionLayout, _) = makeResetDescriptionLayout(self.resetDescriptionItem, params, ItemListNeighbors(top: .none, bottom: .none))
|
||||
|
||||
insets.bottom += buttonHeight + 35.0 + resetDescriptionLayout.contentSize.height + 32.0
|
||||
if !isChannel {
|
||||
insets.bottom += buttonHeight + 35.0 + resetDescriptionLayout.contentSize.height + 32.0
|
||||
}
|
||||
|
||||
self.gridNode.frame = CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: layout.size.height)
|
||||
self.gridNode.transaction(GridNodeTransaction(deleteItems: [], insertItems: [], updateItems: [], scrollToItem: nil, updateLayout: GridNodeUpdateLayout(layout: GridNodeLayout(size: layout.size, insets: insets, scrollIndicatorInsets: scrollIndicatorInsets, preloadSize: 300.0, type: .fixed(itemSize: imageSize, fillWidth: nil, lineSpacing: spacing, itemSpacing: nil)), transition: transition), itemTransition: .immediate, stationaryItems: .none, updateFirstIndexInSectionOffset: nil), completion: { _ in })
|
||||
self.gridNode.transaction(GridNodeTransaction(deleteItems: [], insertItems: [], updateItems: [], scrollToItem: nil, updateLayout: GridNodeUpdateLayout(layout: GridNodeLayout(size: layout.size, insets: listInsets, scrollIndicatorInsets: scrollIndicatorInsets, preloadSize: 300.0, type: .fixed(itemSize: imageSize, fillWidth: nil, lineSpacing: spacing, itemSpacing: nil)), transition: transition), itemTransition: .immediate, stationaryItems: .none, updateFirstIndexInSectionOffset: nil), completion: { _ in })
|
||||
|
||||
if !hadValidLayout {
|
||||
self.dequeueTransitions()
|
||||
+377
@@ -0,0 +1,377 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import SwiftSignalKit
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import MediaResources
|
||||
import LocalMediaResources
|
||||
import TelegramUIPreferences
|
||||
import AccountContext
|
||||
import LegacyComponents
|
||||
import WallpaperGalleryScreen
|
||||
import ImageBlur
|
||||
|
||||
public func uploadCustomWallpaper(context: AccountContext, wallpaper: WallpaperGalleryEntry, mode: WallpaperPresentationOptions, editedImage: UIImage?, cropRect: CGRect?, brightness: CGFloat?, completion: @escaping () -> Void) {
|
||||
var imageSignal: Signal<UIImage, NoError>
|
||||
switch wallpaper {
|
||||
case let .wallpaper(wallpaper, _):
|
||||
switch wallpaper {
|
||||
case let .file(file):
|
||||
if let path = context.account.postbox.mediaBox.completedResourcePath(file.file.resource), let data = try? Data(contentsOf: URL(fileURLWithPath: path), options: .mappedRead) {
|
||||
context.sharedContext.accountManager.mediaBox.storeResourceData(file.file.resource.id, data: data)
|
||||
let _ = context.sharedContext.accountManager.mediaBox.cachedResourceRepresentation(file.file.resource, representation: CachedScaledImageRepresentation(size: CGSize(width: 720.0, height: 720.0), mode: .aspectFit), complete: true, fetch: true).start()
|
||||
let _ = context.sharedContext.accountManager.mediaBox.cachedResourceRepresentation(file.file.resource, representation: CachedBlurredWallpaperRepresentation(), complete: true, fetch: true).start()
|
||||
}
|
||||
case let .image(representations, _):
|
||||
for representation in representations {
|
||||
let resource = representation.resource
|
||||
if let path = context.account.postbox.mediaBox.completedResourcePath(resource), let data = try? Data(contentsOf: URL(fileURLWithPath: path), options: .mappedRead) {
|
||||
context.sharedContext.accountManager.mediaBox.storeResourceData(resource.id, data: data)
|
||||
let _ = context.sharedContext.accountManager.mediaBox.cachedResourceRepresentation(resource, representation: CachedScaledImageRepresentation(size: CGSize(width: 720.0, height: 720.0), mode: .aspectFit), complete: true, fetch: true).start()
|
||||
}
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
imageSignal = .complete()
|
||||
completion()
|
||||
case let .asset(asset):
|
||||
imageSignal = fetchPhotoLibraryImage(localIdentifier: asset.localIdentifier, thumbnail: false)
|
||||
|> filter { value in
|
||||
return !(value?.1 ?? true)
|
||||
}
|
||||
|> mapToSignal { result -> Signal<UIImage, NoError> in
|
||||
if let result = result {
|
||||
return .single(result.0)
|
||||
} else {
|
||||
return .complete()
|
||||
}
|
||||
}
|
||||
case let .contextResult(result):
|
||||
var imageResource: TelegramMediaResource?
|
||||
switch result {
|
||||
case let .externalReference(externalReference):
|
||||
if let content = externalReference.content {
|
||||
imageResource = content.resource
|
||||
}
|
||||
case let .internalReference(internalReference):
|
||||
if let image = internalReference.image {
|
||||
if let imageRepresentation = imageRepresentationLargerThan(image.representations, size: PixelDimensions(width: 1000, height: 800)) {
|
||||
imageResource = imageRepresentation.resource
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let imageResource = imageResource {
|
||||
imageSignal = .single(context.account.postbox.mediaBox.completedResourcePath(imageResource))
|
||||
|> mapToSignal { path -> Signal<UIImage, NoError> in
|
||||
if let path = path, let data = try? Data(contentsOf: URL(fileURLWithPath: path), options: [.mappedIfSafe]), let image = UIImage(data: data) {
|
||||
return .single(image)
|
||||
} else {
|
||||
return .complete()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
imageSignal = .complete()
|
||||
}
|
||||
}
|
||||
|
||||
if let editedImage {
|
||||
imageSignal = .single(editedImage)
|
||||
}
|
||||
|
||||
let _ = (imageSignal
|
||||
|> map { image -> UIImage in
|
||||
var croppedImage = UIImage()
|
||||
|
||||
let finalCropRect: CGRect
|
||||
if let cropRect = cropRect {
|
||||
finalCropRect = cropRect
|
||||
} else {
|
||||
let screenSize = TGScreenSize()
|
||||
let fittedSize = TGScaleToFit(screenSize, image.size)
|
||||
finalCropRect = CGRect(x: (image.size.width - fittedSize.width) / 2.0, y: (image.size.height - fittedSize.height) / 2.0, width: fittedSize.width, height: fittedSize.height)
|
||||
}
|
||||
croppedImage = TGPhotoEditorCrop(image, nil, .up, 0.0, finalCropRect, false, CGSize(width: 1440.0, height: 2960.0), image.size, true)
|
||||
|
||||
let thumbnailDimensions = finalCropRect.size.fitted(CGSize(width: 320.0, height: 320.0))
|
||||
let thumbnailImage = generateScaledImage(image: croppedImage, size: thumbnailDimensions, scale: 1.0)
|
||||
|
||||
if let data = croppedImage.jpegData(compressionQuality: 0.8), let thumbnailImage = thumbnailImage, let thumbnailData = thumbnailImage.jpegData(compressionQuality: 0.4) {
|
||||
let thumbnailResource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max))
|
||||
context.sharedContext.accountManager.mediaBox.storeResourceData(thumbnailResource.id, data: thumbnailData, synchronous: true)
|
||||
context.account.postbox.mediaBox.storeResourceData(thumbnailResource.id, data: thumbnailData, synchronous: true)
|
||||
|
||||
let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max))
|
||||
context.sharedContext.accountManager.mediaBox.storeResourceData(resource.id, data: data, synchronous: true)
|
||||
context.account.postbox.mediaBox.storeResourceData(resource.id, data: data, synchronous: true)
|
||||
|
||||
let autoNightModeTriggered = context.sharedContext.currentPresentationData.with {$0 }.autoNightModeTriggered
|
||||
let accountManager = context.sharedContext.accountManager
|
||||
let account = context.account
|
||||
let updateWallpaper: (TelegramWallpaper) -> Void = { wallpaper in
|
||||
var resource: MediaResource?
|
||||
if case let .image(representations, _) = wallpaper, let representation = largestImageRepresentation(representations) {
|
||||
resource = representation.resource
|
||||
} else if case let .file(file) = wallpaper {
|
||||
resource = file.file.resource
|
||||
}
|
||||
|
||||
if let resource = resource {
|
||||
let _ = accountManager.mediaBox.cachedResourceRepresentation(resource, representation: CachedScaledImageRepresentation(size: CGSize(width: 720.0, height: 720.0), mode: .aspectFit), complete: true, fetch: true).start(completed: {})
|
||||
let _ = account.postbox.mediaBox.cachedResourceRepresentation(resource, representation: CachedScaledImageRepresentation(size: CGSize(width: 720.0, height: 720.0), mode: .aspectFit), complete: true, fetch: true).start(completed: {})
|
||||
}
|
||||
|
||||
let _ = (updatePresentationThemeSettingsInteractively(accountManager: accountManager, { current in
|
||||
var themeSpecificChatWallpapers = current.themeSpecificChatWallpapers
|
||||
let themeReference: PresentationThemeReference
|
||||
if autoNightModeTriggered {
|
||||
themeReference = current.automaticThemeSwitchSetting.theme
|
||||
} else {
|
||||
themeReference = current.theme
|
||||
}
|
||||
let accentColor = current.themeSpecificAccentColors[themeReference.index]
|
||||
if let accentColor = accentColor, accentColor.baseColor == .custom {
|
||||
themeSpecificChatWallpapers[coloredThemeIndex(reference: themeReference, accentColor: accentColor)] = wallpaper
|
||||
} else {
|
||||
themeSpecificChatWallpapers[coloredThemeIndex(reference: themeReference, accentColor: accentColor)] = nil
|
||||
themeSpecificChatWallpapers[themeReference.index] = wallpaper
|
||||
}
|
||||
return current.withUpdatedThemeSpecificChatWallpapers(themeSpecificChatWallpapers)
|
||||
})).start()
|
||||
}
|
||||
|
||||
let apply: () -> Void = {
|
||||
let settings = WallpaperSettings(blur: mode.contains(.blur), motion: mode.contains(.motion), colors: [], intensity: nil)
|
||||
let wallpaper: TelegramWallpaper = .image([TelegramMediaImageRepresentation(dimensions: PixelDimensions(thumbnailDimensions), resource: thumbnailResource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false), TelegramMediaImageRepresentation(dimensions: PixelDimensions(croppedImage.size), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)], settings)
|
||||
updateWallpaper(wallpaper)
|
||||
DispatchQueue.main.async {
|
||||
completion()
|
||||
}
|
||||
}
|
||||
|
||||
if mode.contains(.blur) {
|
||||
let representation = CachedBlurredWallpaperRepresentation()
|
||||
let _ = context.account.postbox.mediaBox.cachedResourceRepresentation(resource, representation: representation, complete: true, fetch: true).start()
|
||||
let _ = context.sharedContext.accountManager.mediaBox.cachedResourceRepresentation(resource, representation: representation, complete: true, fetch: true).start(completed: {
|
||||
apply()
|
||||
})
|
||||
} else {
|
||||
apply()
|
||||
}
|
||||
}
|
||||
return croppedImage
|
||||
}).start()
|
||||
}
|
||||
|
||||
public func getTemporaryCustomPeerWallpaper(context: AccountContext, wallpaper: WallpaperGalleryEntry, mode: WallpaperPresentationOptions, editedImage: UIImage?, cropRect: CGRect?, brightness: CGFloat?) -> Signal<TelegramWallpaper?, NoError> {
|
||||
var imageSignal: Signal<UIImage, NoError>
|
||||
switch wallpaper {
|
||||
case .wallpaper:
|
||||
fatalError()
|
||||
case let .asset(asset):
|
||||
imageSignal = fetchPhotoLibraryImage(localIdentifier: asset.localIdentifier, thumbnail: false)
|
||||
|> filter { value in
|
||||
return !(value?.1 ?? true)
|
||||
}
|
||||
|> mapToSignal { result -> Signal<UIImage, NoError> in
|
||||
if let result = result {
|
||||
return .single(result.0)
|
||||
} else {
|
||||
return .complete()
|
||||
}
|
||||
}
|
||||
case let .contextResult(result):
|
||||
var imageResource: TelegramMediaResource?
|
||||
switch result {
|
||||
case let .externalReference(externalReference):
|
||||
if let content = externalReference.content {
|
||||
imageResource = content.resource
|
||||
}
|
||||
case let .internalReference(internalReference):
|
||||
if let image = internalReference.image {
|
||||
if let imageRepresentation = imageRepresentationLargerThan(image.representations, size: PixelDimensions(width: 1000, height: 800)) {
|
||||
imageResource = imageRepresentation.resource
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let imageResource = imageResource {
|
||||
imageSignal = .single(context.account.postbox.mediaBox.completedResourcePath(imageResource))
|
||||
|> mapToSignal { path -> Signal<UIImage, NoError> in
|
||||
if let path = path, let data = try? Data(contentsOf: URL(fileURLWithPath: path), options: [.mappedIfSafe]), let image = UIImage(data: data) {
|
||||
return .single(image)
|
||||
} else {
|
||||
return .complete()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
imageSignal = .complete()
|
||||
}
|
||||
}
|
||||
|
||||
if let editedImage {
|
||||
imageSignal = .single(editedImage)
|
||||
}
|
||||
|
||||
return imageSignal
|
||||
|> map { image -> TelegramWallpaper? in
|
||||
var croppedImage = UIImage()
|
||||
|
||||
let finalCropRect: CGRect
|
||||
if let cropRect = cropRect {
|
||||
finalCropRect = cropRect
|
||||
} else {
|
||||
let screenSize = TGScreenSize()
|
||||
let fittedSize = TGScaleToFit(screenSize, image.size)
|
||||
finalCropRect = CGRect(x: (image.size.width - fittedSize.width) / 2.0, y: (image.size.height - fittedSize.height) / 2.0, width: fittedSize.width, height: fittedSize.height)
|
||||
}
|
||||
croppedImage = TGPhotoEditorCrop(image, nil, .up, 0.0, finalCropRect, false, CGSize(width: 1440.0, height: 2960.0), image.size, true)
|
||||
|
||||
let thumbnailDimensions = finalCropRect.size.fitted(CGSize(width: 320.0, height: 320.0))
|
||||
let thumbnailImage = generateScaledImage(image: croppedImage, size: thumbnailDimensions, scale: 1.0)
|
||||
|
||||
if let data = croppedImage.jpegData(compressionQuality: 0.8), let thumbnailImage = thumbnailImage, let thumbnailData = thumbnailImage.jpegData(compressionQuality: 0.4) {
|
||||
let thumbnailResource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max))
|
||||
context.sharedContext.accountManager.mediaBox.storeResourceData(thumbnailResource.id, data: thumbnailData, synchronous: true)
|
||||
context.account.postbox.mediaBox.storeResourceData(thumbnailResource.id, data: thumbnailData, synchronous: true)
|
||||
|
||||
let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max))
|
||||
context.sharedContext.accountManager.mediaBox.storeResourceData(resource.id, data: data, synchronous: true)
|
||||
context.account.postbox.mediaBox.storeResourceData(resource.id, data: data, synchronous: true)
|
||||
|
||||
let _ = context.sharedContext.accountManager.mediaBox.cachedResourceRepresentation(resource, representation: CachedBlurredWallpaperRepresentation(), complete: true, fetch: true).start()
|
||||
|
||||
var intensity: Int32?
|
||||
if let brightness {
|
||||
intensity = max(0, min(100, Int32(brightness * 100.0)))
|
||||
}
|
||||
|
||||
let settings = WallpaperSettings(blur: mode.contains(.blur), motion: mode.contains(.motion), colors: [], intensity: intensity)
|
||||
let temporaryWallpaper: TelegramWallpaper = .image([TelegramMediaImageRepresentation(dimensions: PixelDimensions(thumbnailDimensions), resource: thumbnailResource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false), TelegramMediaImageRepresentation(dimensions: PixelDimensions(croppedImage.size), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)], settings)
|
||||
|
||||
return temporaryWallpaper
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
public func uploadCustomPeerWallpaper(context: AccountContext, wallpaper: WallpaperGalleryEntry, mode: WallpaperPresentationOptions, editedImage: UIImage?, cropRect: CGRect?, brightness: CGFloat?, peerId: PeerId, forBoth: Bool, completion: @escaping () -> Void) {
|
||||
var imageSignal: Signal<UIImage, NoError>
|
||||
switch wallpaper {
|
||||
case let .wallpaper(wallpaper, _):
|
||||
imageSignal = .complete()
|
||||
switch wallpaper {
|
||||
case let .file(file):
|
||||
if let path = context.account.postbox.mediaBox.completedResourcePath(file.file.resource), let data = try? Data(contentsOf: URL(fileURLWithPath: path), options: .mappedRead), let image = UIImage(data: data) {
|
||||
context.sharedContext.accountManager.mediaBox.storeResourceData(file.file.resource.id, data: data, synchronous: true)
|
||||
let _ = context.sharedContext.accountManager.mediaBox.cachedResourceRepresentation(file.file.resource, representation: CachedScaledImageRepresentation(size: CGSize(width: 720.0, height: 720.0), mode: .aspectFit), complete: true, fetch: true).start()
|
||||
let _ = context.sharedContext.accountManager.mediaBox.cachedResourceRepresentation(file.file.resource, representation: CachedBlurredWallpaperRepresentation(), complete: true, fetch: true).start()
|
||||
|
||||
imageSignal = .single(image)
|
||||
}
|
||||
case let .image(representations, _):
|
||||
for representation in representations {
|
||||
let resource = representation.resource
|
||||
if let path = context.account.postbox.mediaBox.completedResourcePath(resource), let data = try? Data(contentsOf: URL(fileURLWithPath: path), options: .mappedRead) {
|
||||
context.sharedContext.accountManager.mediaBox.storeResourceData(resource.id, data: data, synchronous: true)
|
||||
let _ = context.sharedContext.accountManager.mediaBox.cachedResourceRepresentation(resource, representation: CachedScaledImageRepresentation(size: CGSize(width: 720.0, height: 720.0), mode: .aspectFit), complete: true, fetch: true).start()
|
||||
}
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
completion()
|
||||
case let .asset(asset):
|
||||
imageSignal = fetchPhotoLibraryImage(localIdentifier: asset.localIdentifier, thumbnail: false)
|
||||
|> filter { value in
|
||||
return !(value?.1 ?? true)
|
||||
}
|
||||
|> mapToSignal { result -> Signal<UIImage, NoError> in
|
||||
if let result = result {
|
||||
return .single(result.0)
|
||||
} else {
|
||||
return .complete()
|
||||
}
|
||||
}
|
||||
case let .contextResult(result):
|
||||
var imageResource: TelegramMediaResource?
|
||||
switch result {
|
||||
case let .externalReference(externalReference):
|
||||
if let content = externalReference.content {
|
||||
imageResource = content.resource
|
||||
}
|
||||
case let .internalReference(internalReference):
|
||||
if let image = internalReference.image {
|
||||
if let imageRepresentation = imageRepresentationLargerThan(image.representations, size: PixelDimensions(width: 1000, height: 800)) {
|
||||
imageResource = imageRepresentation.resource
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let imageResource = imageResource {
|
||||
imageSignal = .single(context.account.postbox.mediaBox.completedResourcePath(imageResource))
|
||||
|> mapToSignal { path -> Signal<UIImage, NoError> in
|
||||
if let path = path, let data = try? Data(contentsOf: URL(fileURLWithPath: path), options: [.mappedIfSafe]), let image = UIImage(data: data) {
|
||||
return .single(image)
|
||||
} else {
|
||||
return .complete()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
imageSignal = .complete()
|
||||
}
|
||||
}
|
||||
|
||||
if let editedImage {
|
||||
imageSignal = .single(editedImage)
|
||||
}
|
||||
|
||||
let _ = (imageSignal
|
||||
|> map { image -> UIImage in
|
||||
var croppedImage = UIImage()
|
||||
|
||||
let finalCropRect: CGRect
|
||||
if let cropRect = cropRect {
|
||||
finalCropRect = cropRect
|
||||
} else {
|
||||
let screenSize = TGScreenSize()
|
||||
let fittedSize = TGScaleToFit(screenSize, image.size)
|
||||
finalCropRect = CGRect(x: (image.size.width - fittedSize.width) / 2.0, y: (image.size.height - fittedSize.height) / 2.0, width: fittedSize.width, height: fittedSize.height)
|
||||
}
|
||||
croppedImage = TGPhotoEditorCrop(image, nil, .up, 0.0, finalCropRect, false, CGSize(width: 1440.0, height: 2960.0), image.size, true)
|
||||
|
||||
let thumbnailDimensions = finalCropRect.size.fitted(CGSize(width: 320.0, height: 320.0))
|
||||
let thumbnailImage = generateScaledImage(image: croppedImage, size: thumbnailDimensions, scale: 1.0)
|
||||
|
||||
if let data = croppedImage.jpegData(compressionQuality: 0.8), let thumbnailImage = thumbnailImage, let thumbnailData = thumbnailImage.jpegData(compressionQuality: 0.4) {
|
||||
let thumbnailResource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max))
|
||||
context.sharedContext.accountManager.mediaBox.storeResourceData(thumbnailResource.id, data: thumbnailData, synchronous: true)
|
||||
context.account.postbox.mediaBox.storeResourceData(thumbnailResource.id, data: thumbnailData, synchronous: true)
|
||||
|
||||
let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max))
|
||||
context.sharedContext.accountManager.mediaBox.storeResourceData(resource.id, data: data, synchronous: true)
|
||||
context.account.postbox.mediaBox.storeResourceData(resource.id, data: data, synchronous: true)
|
||||
|
||||
let _ = context.sharedContext.accountManager.mediaBox.cachedResourceRepresentation(resource, representation: CachedBlurredWallpaperRepresentation(), complete: true, fetch: true).start()
|
||||
|
||||
var intensity: Int32?
|
||||
if let brightness {
|
||||
intensity = max(0, min(100, Int32(brightness * 100.0)))
|
||||
}
|
||||
|
||||
Queue.mainQueue().after(0.05) {
|
||||
let settings = WallpaperSettings(blur: mode.contains(.blur), motion: mode.contains(.motion), colors: [], intensity: intensity)
|
||||
let temporaryWallpaper: TelegramWallpaper = .image([TelegramMediaImageRepresentation(dimensions: PixelDimensions(thumbnailDimensions), resource: thumbnailResource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false), TelegramMediaImageRepresentation(dimensions: PixelDimensions(croppedImage.size), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)], settings)
|
||||
|
||||
context.account.pendingPeerMediaUploadManager.add(peerId: peerId, content: .wallpaper(wallpaper: temporaryWallpaper, forBoth: forBoth))
|
||||
|
||||
Queue.mainQueue().after(0.05) {
|
||||
completion()
|
||||
}
|
||||
}
|
||||
}
|
||||
return croppedImage
|
||||
}).start()
|
||||
}
|
||||
+3
-3
@@ -1066,7 +1066,7 @@ final class ShareWithPeersScreenComponent: Component {
|
||||
rightAccessory: accessory,
|
||||
selectionState: .none,
|
||||
hasNext: i < peers.count - 1,
|
||||
action: { [weak self] peer, _ in
|
||||
action: { [weak self] peer, _, _ in
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
}
|
||||
@@ -1414,7 +1414,7 @@ final class ShareWithPeersScreenComponent: Component {
|
||||
presence: stateValue.presences[peer.id],
|
||||
selectionState: .editing(isSelected: isSelected, isTinted: false),
|
||||
hasNext: true,
|
||||
action: { [weak self] peer, _ in
|
||||
action: { [weak self] peer, _, _ in
|
||||
guard let self, let environment = self.environment, let controller = environment.controller() as? ShareWithPeersScreen else {
|
||||
return
|
||||
}
|
||||
@@ -2143,7 +2143,7 @@ final class ShareWithPeersScreenComponent: Component {
|
||||
presence: nil,
|
||||
selectionState: .editing(isSelected: false, isTinted: false),
|
||||
hasNext: true,
|
||||
action: { _, _ in
|
||||
action: { _, _, _ in
|
||||
}
|
||||
)),
|
||||
environment: {},
|
||||
|
||||
+8
-17
@@ -116,10 +116,9 @@ public final class PeerListItemComponent: Component {
|
||||
let selectionPosition: SelectionPosition
|
||||
let isEnabled: Bool
|
||||
let hasNext: Bool
|
||||
let action: (EnginePeer, EngineMessage.Id?) -> Void
|
||||
let action: (EnginePeer, EngineMessage.Id?, UIView?) -> Void
|
||||
let contextAction: ((EnginePeer, ContextExtractedContentContainingView, ContextGesture) -> Void)?
|
||||
let openStories: ((EnginePeer, AvatarNode) -> Void)?
|
||||
let openStory: ((EnginePeer, Int32, UIView) -> Void)?
|
||||
|
||||
public init(
|
||||
context: AccountContext,
|
||||
@@ -141,10 +140,9 @@ public final class PeerListItemComponent: Component {
|
||||
selectionPosition: SelectionPosition = .left,
|
||||
isEnabled: Bool = true,
|
||||
hasNext: Bool,
|
||||
action: @escaping (EnginePeer, EngineMessage.Id?) -> Void,
|
||||
action: @escaping (EnginePeer, EngineMessage.Id?, UIView?) -> Void,
|
||||
contextAction: ((EnginePeer, ContextExtractedContentContainingView, ContextGesture) -> Void)? = nil,
|
||||
openStories: ((EnginePeer, AvatarNode) -> Void)? = nil,
|
||||
openStory: ((EnginePeer, Int32, UIView) -> Void)? = nil
|
||||
openStories: ((EnginePeer, AvatarNode) -> Void)? = nil
|
||||
) {
|
||||
self.context = context
|
||||
self.theme = theme
|
||||
@@ -168,7 +166,6 @@ public final class PeerListItemComponent: Component {
|
||||
self.action = action
|
||||
self.contextAction = contextAction
|
||||
self.openStories = openStories
|
||||
self.openStory = openStory
|
||||
}
|
||||
|
||||
public static func ==(lhs: PeerListItemComponent, rhs: PeerListItemComponent) -> Bool {
|
||||
@@ -353,7 +350,7 @@ public final class PeerListItemComponent: Component {
|
||||
guard let component = self.component, let peer = component.peer else {
|
||||
return
|
||||
}
|
||||
component.action(peer, component.message?.id)
|
||||
component.action(peer, component.message?.id, self.imageNode?.view)
|
||||
}
|
||||
|
||||
@objc private func avatarButtonPressed() {
|
||||
@@ -363,13 +360,6 @@ public final class PeerListItemComponent: Component {
|
||||
component.openStories?(peer, self.avatarNode)
|
||||
}
|
||||
|
||||
@objc private func imageButtonPressed() {
|
||||
guard let component = self.component, let peer = component.peer, let story = component.story, let imageNode = self.imageNode else {
|
||||
return
|
||||
}
|
||||
component.openStory?(peer, story.id, imageNode.view)
|
||||
}
|
||||
|
||||
private func updateReactionLayer() {
|
||||
guard let component = self.component else {
|
||||
return
|
||||
@@ -506,6 +496,9 @@ public final class PeerListItemComponent: Component {
|
||||
if component.reaction != nil || component.rightAccessory != .none {
|
||||
rightInset += 32.0
|
||||
}
|
||||
if component.story != nil {
|
||||
rightInset += 40.0
|
||||
}
|
||||
|
||||
var avatarLeftInset: CGFloat = component.sideInset + 10.0
|
||||
|
||||
@@ -913,15 +906,13 @@ public final class PeerListItemComponent: Component {
|
||||
self.imageNode = imageNode
|
||||
|
||||
imageButtonView = HighlightTrackingButton()
|
||||
imageButtonView.addTarget(self, action: #selector(self.imageButtonPressed), for: .touchUpInside)
|
||||
imageButtonView.isEnabled = component.message == nil
|
||||
imageButtonView.isEnabled = false
|
||||
self.imageButtonView = imageButtonView
|
||||
|
||||
self.containerButton.addSubview(imageNode.view)
|
||||
self.addSubview(imageButtonView)
|
||||
|
||||
var imageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>?
|
||||
|
||||
if let imageReference = mediaReference.concrete(TelegramMediaImage.self) {
|
||||
imageSignal = mediaGridMessagePhoto(account: component.context.account, userLocation: .peer(peer.id), photoReference: imageReference)
|
||||
} else if let fileReference = mediaReference.concrete(TelegramMediaFile.self) {
|
||||
|
||||
+8
-2
@@ -3162,7 +3162,13 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
|
||||
for (id, views) in preloadViewListIds {
|
||||
if component.sharedViewListsContext.viewLists[StoryId(peerId: component.slice.peer.id, id: id)] == nil {
|
||||
let viewList = component.context.engine.messages.storyViewList(peerId: component.slice.peer.id, id: id, views: views, listMode: .everyone, sortMode: .reactionsFirst)
|
||||
let defaultSortMode: EngineStoryViewListContext.SortMode
|
||||
if component.slice.peer.id.isGroupOrChannel {
|
||||
defaultSortMode = .repostsFirst
|
||||
} else {
|
||||
defaultSortMode = .reactionsFirst
|
||||
}
|
||||
let viewList = component.context.engine.messages.storyViewList(peerId: component.slice.peer.id, id: id, views: views, listMode: .everyone, sortMode: defaultSortMode)
|
||||
component.sharedViewListsContext.viewLists[StoryId(peerId: component.slice.peer.id, id: id)] = viewList
|
||||
}
|
||||
}
|
||||
@@ -3512,7 +3518,7 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
}
|
||||
self.openPeerStories(peer: peer, avatarNode: avatarNode)
|
||||
},
|
||||
openStory: { [weak self] peer, id, stories, sourceView in
|
||||
openReposts: { [weak self] peer, id, sourceView in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
|
||||
+24
-5
@@ -3311,20 +3311,39 @@ final class StoryItemSetContainerSendMessage {
|
||||
action()
|
||||
}))
|
||||
case let .channelMessage(_, messageId):
|
||||
let action = { [weak self, weak view] in
|
||||
let _ = ((context.engine.messages.getMessagesLoadIfNecessary([messageId], strategy: .local)
|
||||
|> mapToSignal { result -> Signal<Message?, NoError> in
|
||||
let action = { [weak self, weak view, weak controller] in
|
||||
let _ = ((context.engine.messages.getMessagesLoadIfNecessary([messageId], strategy: .cloud(skipLocal: true))
|
||||
|> mapToSignal { result -> Signal<Message?, GetMessagesError> in
|
||||
if case let .result(messages) = result {
|
||||
return .single(messages.first)
|
||||
} else {
|
||||
return .complete()
|
||||
}
|
||||
return .single(nil)
|
||||
})
|
||||
|> deliverOnMainQueue).startStandalone(next: { [weak self, weak view] message in
|
||||
|> deliverOnMainQueue).startStandalone(next: { [weak self, weak view, weak controller] message in
|
||||
guard let self, let view else {
|
||||
return
|
||||
}
|
||||
if let message, let peer = message.peers[message.id.peerId] {
|
||||
self.openResolved(view: view, result: .channelMessage(peer: peer, messageId: message.id, timecode: nil))
|
||||
} else {
|
||||
controller?.present(UndoOverlayController(presentationData: updatedPresentationData.initial, content: .info(title: nil, text: updatedPresentationData.initial.strings.Conversation_MessageDoesntExist, timeout: nil, customUndoText: nil), elevatedLayout: false, position: .top, action: { _ in return true }), in: .current)
|
||||
}
|
||||
}, error: { [weak self, weak view] error in
|
||||
guard let self, let view else {
|
||||
return
|
||||
}
|
||||
switch error {
|
||||
case .privateChannel:
|
||||
let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: messageId.peerId))
|
||||
|> deliverOnMainQueue).startStandalone(next: { [weak self, weak view] peer in
|
||||
guard let self, let view else {
|
||||
return
|
||||
}
|
||||
if let peer {
|
||||
self.openResolved(view: view, result: .peer(peer._asPeer(), .default))
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
+9
-20
@@ -72,7 +72,7 @@ final class StoryItemSetViewListComponent: Component {
|
||||
let openMessage: (EnginePeer, EngineMessage.Id) -> Void
|
||||
let peerContextAction: (EnginePeer, ContextExtractedContentContainingView, ContextGesture) -> Void
|
||||
let openPeerStories: (EnginePeer, AvatarNode) -> Void
|
||||
let openStory: (EnginePeer, Int32, [(EnginePeer, EngineStoryItem)], UIView) -> Void
|
||||
let openReposts: (EnginePeer, Int32, UIView) -> Void
|
||||
let openPremiumIntro: () -> Void
|
||||
let setIsSearchActive: (Bool) -> Void
|
||||
let controller: () -> ViewController?
|
||||
@@ -98,7 +98,7 @@ final class StoryItemSetViewListComponent: Component {
|
||||
openMessage: @escaping (EnginePeer, EngineMessage.Id) -> Void,
|
||||
peerContextAction: @escaping (EnginePeer, ContextExtractedContentContainingView, ContextGesture) -> Void,
|
||||
openPeerStories: @escaping (EnginePeer, AvatarNode) -> Void,
|
||||
openStory: @escaping (EnginePeer, Int32, [(EnginePeer, EngineStoryItem)], UIView) -> Void,
|
||||
openReposts: @escaping (EnginePeer, Int32, UIView) -> Void,
|
||||
openPremiumIntro: @escaping () -> Void,
|
||||
setIsSearchActive: @escaping (Bool) -> Void,
|
||||
controller: @escaping () -> ViewController?
|
||||
@@ -123,7 +123,7 @@ final class StoryItemSetViewListComponent: Component {
|
||||
self.openMessage = openMessage
|
||||
self.peerContextAction = peerContextAction
|
||||
self.openPeerStories = openPeerStories
|
||||
self.openStory = openStory
|
||||
self.openReposts = openReposts
|
||||
self.openPremiumIntro = openPremiumIntro
|
||||
self.setIsSearchActive = setIsSearchActive
|
||||
self.controller = controller
|
||||
@@ -580,12 +580,14 @@ final class StoryItemSetViewListComponent: Component {
|
||||
message: item.message,
|
||||
selectionState: .none,
|
||||
hasNext: index != viewListState.totalCount - 1 || itemLayout.premiumFooterSize != nil,
|
||||
action: { [weak self] peer, messageId in
|
||||
action: { [weak self] peer, messageId, sourceView in
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
}
|
||||
if let messageId {
|
||||
component.openMessage(peer, messageId)
|
||||
} else if let storyItem, let sourceView {
|
||||
component.openReposts(peer, storyItem.id, sourceView)
|
||||
} else {
|
||||
component.openPeer(peer)
|
||||
}
|
||||
@@ -598,19 +600,6 @@ final class StoryItemSetViewListComponent: Component {
|
||||
return
|
||||
}
|
||||
component.openPeerStories(peer, avatarNode)
|
||||
},
|
||||
openStory: { [weak self] peer, id, sourceView in
|
||||
guard let self, let component = self.component, let state = self.viewListState else {
|
||||
return
|
||||
}
|
||||
|
||||
var stories: [(EnginePeer, EngineStoryItem)] = []
|
||||
for item in state.items {
|
||||
if let story = item.story {
|
||||
stories.append((item.peer, story))
|
||||
}
|
||||
}
|
||||
component.openStory(peer, id, stories, sourceView)
|
||||
}
|
||||
)),
|
||||
environment: {},
|
||||
@@ -801,7 +790,7 @@ final class StoryItemSetViewListComponent: Component {
|
||||
} else {
|
||||
let defaultSortMode: SortMode
|
||||
if component.peerId.isGroupOrChannel {
|
||||
defaultSortMode = .recentFirst
|
||||
defaultSortMode = .repostsFirst
|
||||
} else {
|
||||
defaultSortMode = .reactionsFirst
|
||||
}
|
||||
@@ -933,7 +922,7 @@ final class StoryItemSetViewListComponent: Component {
|
||||
presence: nil,
|
||||
selectionState: .none,
|
||||
hasNext: true,
|
||||
action: { _, _ in
|
||||
action: { _, _, _ in
|
||||
}
|
||||
)),
|
||||
environment: {},
|
||||
@@ -1494,7 +1483,7 @@ final class StoryItemSetViewListComponent: Component {
|
||||
|
||||
if self.mainViewList == nil {
|
||||
if component.peerId.isGroupOrChannel {
|
||||
self.sortMode = .recentFirst
|
||||
self.sortMode = .repostsFirst
|
||||
} else {
|
||||
self.sortMode = .reactionsFirst
|
||||
}
|
||||
|
||||
+3
@@ -9,6 +9,7 @@ public enum WallpaperPreviewMediaContent: Equatable {
|
||||
case color(UIColor)
|
||||
case gradient([UInt32], Int32?)
|
||||
case themeSettings(TelegramThemeSettings)
|
||||
case emoticon(String)
|
||||
}
|
||||
|
||||
public final class WallpaperPreviewMedia: Media {
|
||||
@@ -64,6 +65,8 @@ public extension WallpaperPreviewMedia {
|
||||
self.init(content: .file(file: file.file, colors: file.settings.colors, rotation: file.settings.rotation, intensity: file.settings.intensity, false, false))
|
||||
case let .image(representations, _):
|
||||
self.init(content: .image(representations: representations))
|
||||
case let .emoticon(emoticon):
|
||||
self.init(content: .emoticon(emoticon))
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -246,6 +246,9 @@ func makeAttachmentFileControllerImpl(context: AccountContext, updatedPresentati
|
||||
},
|
||||
send: { message in
|
||||
let _ = (context.engine.messages.getMessagesLoadIfNecessary([message.id], strategy: .cloud(skipLocal: true))
|
||||
|> `catch` { _ in
|
||||
return .single(.result([]))
|
||||
}
|
||||
|> mapToSignal { result -> Signal<[Message], NoError> in
|
||||
guard case let .result(result) = result else {
|
||||
return .complete()
|
||||
|
||||
@@ -108,6 +108,9 @@ extension ChatControllerImpl {
|
||||
let _ = (combineLatest(
|
||||
self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: messageId.peerId)),
|
||||
self.context.engine.messages.getMessagesLoadIfNecessary([messageId], strategy: .local)
|
||||
|> `catch` { _ in
|
||||
return .single(.result([]))
|
||||
}
|
||||
|> mapToSignal { result -> Signal<[Message], NoError> in
|
||||
guard case let .result(result) = result else {
|
||||
return .complete()
|
||||
|
||||
@@ -119,6 +119,7 @@ import ChatQrCodeScreen
|
||||
import PeerInfoScreen
|
||||
import MediaEditorScreen
|
||||
import WallpaperGalleryScreen
|
||||
import WallpaperGridScreen
|
||||
|
||||
public enum ChatControllerPeekActions {
|
||||
case standard
|
||||
@@ -918,6 +919,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
strongSelf.presentThemeSelection()
|
||||
return true
|
||||
}
|
||||
if peer is TelegramChannel {
|
||||
return true
|
||||
}
|
||||
strongSelf.chatDisplayNode.dismissInput()
|
||||
var options = WallpaperPresentationOptions()
|
||||
var intensity: Int32?
|
||||
@@ -6364,6 +6368,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
var presentationData = presentationData
|
||||
var useDarkAppearance = presentationData.theme.overallDarkAppearance
|
||||
|
||||
if let wallpaper = chatWallpaper, case let .emoticon(wallpaperEmoticon) = wallpaper, let theme = chatThemes.first(where: { $0.emoticon?.strippedEmoji == wallpaperEmoticon.strippedEmoji }) {
|
||||
if let themeWallpaper = theme.settings?.first?.wallpaper {
|
||||
chatWallpaper = themeWallpaper
|
||||
}
|
||||
}
|
||||
if let themeEmoticon = themeEmoticon, let theme = chatThemes.first(where: { $0.emoticon?.strippedEmoji == themeEmoticon.strippedEmoji }) {
|
||||
if let darkAppearancePreview = darkAppearancePreview {
|
||||
useDarkAppearance = darkAppearancePreview
|
||||
@@ -7396,17 +7405,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
|
||||
if let peerId = self.chatLocation.peerId {
|
||||
self.chatThemeEmoticonPromise.set(self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.ThemeEmoticon(id: peerId)))
|
||||
let chatWallpaper = self.context.account.viewTracker.peerView(peerId)
|
||||
let chatWallpaper = self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Wallpaper(id: peerId))
|
||||
|> take(1)
|
||||
|> map { view -> TelegramWallpaper? in
|
||||
if let cachedData = view.cachedData as? CachedUserData {
|
||||
return cachedData.wallpaper
|
||||
} else if let cachedData = view.cachedData as? CachedChannelData {
|
||||
return cachedData.wallpaper
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
self.chatWallpaperPromise.set(chatWallpaper)
|
||||
} else {
|
||||
self.chatThemeEmoticonPromise.set(.single(nil))
|
||||
|
||||
@@ -330,7 +330,8 @@ private func extractAssociatedData(
|
||||
translateToLanguage: String?,
|
||||
maxReadStoryId: Int32?,
|
||||
recommendedChannels: RecommendedChannels?,
|
||||
audioTranscriptionTrial: AudioTranscription.TrialState
|
||||
audioTranscriptionTrial: AudioTranscription.TrialState,
|
||||
chatThemes: [TelegramTheme]
|
||||
) -> ChatMessageItemAssociatedData {
|
||||
var automaticDownloadPeerId: EnginePeer.Id?
|
||||
var automaticMediaDownloadPeerType: MediaAutoDownloadPeerType = .channel
|
||||
@@ -385,7 +386,7 @@ private func extractAssociatedData(
|
||||
automaticDownloadPeerId = message.messageId.peerId
|
||||
}
|
||||
|
||||
return ChatMessageItemAssociatedData(automaticDownloadPeerType: automaticMediaDownloadPeerType, automaticDownloadPeerId: automaticDownloadPeerId, automaticDownloadNetworkType: automaticDownloadNetworkType, isRecentActions: false, subject: subject, contactsPeerIds: contactsPeerIds, channelDiscussionGroup: channelDiscussionGroup, animatedEmojiStickers: animatedEmojiStickers, additionalAnimatedEmojiStickers: additionalAnimatedEmojiStickers, currentlyPlayingMessageId: currentlyPlayingMessageId, isCopyProtectionEnabled: isCopyProtectionEnabled, availableReactions: availableReactions, defaultReaction: defaultReaction, isPremium: isPremium, accountPeer: accountPeer, alwaysDisplayTranscribeButton: alwaysDisplayTranscribeButton, topicAuthorId: topicAuthorId, hasBots: hasBots, translateToLanguage: translateToLanguage, maxReadStoryId: maxReadStoryId, recommendedChannels: recommendedChannels, audioTranscriptionTrial: audioTranscriptionTrial)
|
||||
return ChatMessageItemAssociatedData(automaticDownloadPeerType: automaticMediaDownloadPeerType, automaticDownloadPeerId: automaticDownloadPeerId, automaticDownloadNetworkType: automaticDownloadNetworkType, isRecentActions: false, subject: subject, contactsPeerIds: contactsPeerIds, channelDiscussionGroup: channelDiscussionGroup, animatedEmojiStickers: animatedEmojiStickers, additionalAnimatedEmojiStickers: additionalAnimatedEmojiStickers, currentlyPlayingMessageId: currentlyPlayingMessageId, isCopyProtectionEnabled: isCopyProtectionEnabled, availableReactions: availableReactions, defaultReaction: defaultReaction, isPremium: isPremium, accountPeer: accountPeer, alwaysDisplayTranscribeButton: alwaysDisplayTranscribeButton, topicAuthorId: topicAuthorId, hasBots: hasBots, translateToLanguage: translateToLanguage, maxReadStoryId: maxReadStoryId, recommendedChannels: recommendedChannels, audioTranscriptionTrial: audioTranscriptionTrial, chatThemes: chatThemes)
|
||||
}
|
||||
|
||||
private extension ChatHistoryLocationInput {
|
||||
@@ -950,7 +951,11 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto
|
||||
} |> distinctUntilChanged(isEqual: { $0 == $1 })
|
||||
|> mapToSignal { messageId -> Signal<Void, NoError> in
|
||||
if let messageId = messageId {
|
||||
return context.engine.messages.getMessagesLoadIfNecessary([messageId]) |> map { _ -> Void in return Void() }
|
||||
return context.engine.messages.getMessagesLoadIfNecessary([messageId])
|
||||
|> `catch` { _ in
|
||||
return .single(.result([]))
|
||||
}
|
||||
|> map { _ -> Void in return Void() }
|
||||
} else {
|
||||
return .complete()
|
||||
}
|
||||
@@ -1370,6 +1375,8 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto
|
||||
|
||||
let audioTranscriptionTrial = self.context.engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.AudioTranscriptionTrial())
|
||||
|
||||
let chatThemes = self.context.engine.themes.getChatThemes(accountManager: self.context.sharedContext.accountManager)
|
||||
|
||||
let messageViewQueue = Queue.mainQueue()
|
||||
let historyViewTransitionDisposable = combineLatest(queue: messageViewQueue,
|
||||
historyViewUpdate,
|
||||
@@ -1390,8 +1397,9 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto
|
||||
translationState,
|
||||
maxReadStoryId,
|
||||
recommendedChannels,
|
||||
audioTranscriptionTrial
|
||||
).startStrict(next: { [weak self] update, chatPresentationData, selectedMessages, updatingMedia, networkType, animatedEmojiStickers, additionalAnimatedEmojiStickers, customChannelDiscussionReadState, customThreadOutgoingReadState, availableReactions, defaultReaction, accountPeer, suggestAudioTranscription, promises, topicAuthorId, translationState, maxReadStoryId, recommendedChannels, audioTranscriptionTrial in
|
||||
audioTranscriptionTrial,
|
||||
chatThemes
|
||||
).startStrict(next: { [weak self] update, chatPresentationData, selectedMessages, updatingMedia, networkType, animatedEmojiStickers, additionalAnimatedEmojiStickers, customChannelDiscussionReadState, customThreadOutgoingReadState, availableReactions, defaultReaction, accountPeer, suggestAudioTranscription, promises, topicAuthorId, translationState, maxReadStoryId, recommendedChannels, audioTranscriptionTrial, chatThemes in
|
||||
let (historyAppearsCleared, pendingUnpinnedAllMessages, pendingRemovedMessages, currentlyPlayingMessageIdAndType, scrollToMessageId, chatHasBots, allAdMessages) = promises
|
||||
|
||||
func applyHole() {
|
||||
@@ -1547,7 +1555,7 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto
|
||||
translateToLanguage = languageCode
|
||||
}
|
||||
|
||||
let associatedData = extractAssociatedData(chatLocation: chatLocation, view: view, automaticDownloadNetworkType: networkType, animatedEmojiStickers: animatedEmojiStickers, additionalAnimatedEmojiStickers: additionalAnimatedEmojiStickers, subject: subject, currentlyPlayingMessageId: currentlyPlayingMessageIdAndType?.0, isCopyProtectionEnabled: isCopyProtectionEnabled, availableReactions: availableReactions, defaultReaction: defaultReaction, isPremium: isPremium, alwaysDisplayTranscribeButton: alwaysDisplayTranscribeButton, accountPeer: accountPeer, topicAuthorId: topicAuthorId, hasBots: chatHasBots, translateToLanguage: translateToLanguage, maxReadStoryId: maxReadStoryId, recommendedChannels: recommendedChannels, audioTranscriptionTrial: audioTranscriptionTrial)
|
||||
let associatedData = extractAssociatedData(chatLocation: chatLocation, view: view, automaticDownloadNetworkType: networkType, animatedEmojiStickers: animatedEmojiStickers, additionalAnimatedEmojiStickers: additionalAnimatedEmojiStickers, subject: subject, currentlyPlayingMessageId: currentlyPlayingMessageIdAndType?.0, isCopyProtectionEnabled: isCopyProtectionEnabled, availableReactions: availableReactions, defaultReaction: defaultReaction, isPremium: isPremium, alwaysDisplayTranscribeButton: alwaysDisplayTranscribeButton, accountPeer: accountPeer, topicAuthorId: topicAuthorId, hasBots: chatHasBots, translateToLanguage: translateToLanguage, maxReadStoryId: maxReadStoryId, recommendedChannels: recommendedChannels, audioTranscriptionTrial: audioTranscriptionTrial, chatThemes: chatThemes)
|
||||
|
||||
let filteredEntries = chatHistoryEntriesForView(
|
||||
location: chatLocation,
|
||||
|
||||
@@ -266,6 +266,7 @@ class ChatSearchResultsControllerNode: ViewControllerTracingNode, UIScrollViewDe
|
||||
}, openStorageManagement: {
|
||||
}, openPasswordSetup: {
|
||||
}, openPremiumIntro: {
|
||||
}, openPremiumGift: {
|
||||
}, openActiveSessions: {
|
||||
}, performActiveSessionAction: { _, _ in
|
||||
}, openChatFolderUpdates: {
|
||||
|
||||
@@ -702,6 +702,9 @@ private func resolveInternalUrl(context: AccountContext, url: ParsedInternalUrl)
|
||||
if case let .channel(channel) = peer, channel.flags.contains(.isForum) {
|
||||
let messageId = MessageId(peerId: channel.id, namespace: Namespaces.Message.Cloud, id: id)
|
||||
return context.engine.messages.getMessagesLoadIfNecessary([messageId], strategy: .cloud(skipLocal: false))
|
||||
|> `catch` { _ in
|
||||
return .single(.result([]))
|
||||
}
|
||||
|> take(1)
|
||||
|> mapToSignal { result -> Signal<ResolveInternalUrlResult, NoError> in
|
||||
switch result {
|
||||
@@ -830,6 +833,9 @@ private func resolveInternalUrl(context: AccountContext, url: ParsedInternalUrl)
|
||||
}
|
||||
} else {
|
||||
return context.engine.messages.getMessagesLoadIfNecessary([messageId], strategy: .cloud(skipLocal: false))
|
||||
|> `catch` { _ in
|
||||
return .single(.result([]))
|
||||
}
|
||||
|> mapToSignal { result -> Signal<ResolveInternalUrlResult, NoError> in
|
||||
switch result {
|
||||
case .progress:
|
||||
|
||||
@@ -1452,7 +1452,7 @@ public func themeIconImage(account: Account, accountManager: AccountManager<Tele
|
||||
let outgoingColors = theme.chat.message.outgoing.bubble.withoutWallpaper.fill
|
||||
let wallpaper = wallpaper ?? theme.chat.defaultWallpaper
|
||||
switch wallpaper {
|
||||
case .builtin:
|
||||
case .builtin, .emoticon:
|
||||
backgroundColor = (UIColor(rgb: 0xd6e2ee), nil, [])
|
||||
case let .color(color):
|
||||
backgroundColor = (UIColor(rgb: color), nil, [])
|
||||
@@ -1463,8 +1463,11 @@ public func themeIconImage(account: Account, accountManager: AccountManager<Tele
|
||||
backgroundColor = (.white, nil, [])
|
||||
}
|
||||
rotation = gradient.settings.rotation
|
||||
case .image:
|
||||
case let .image(representations, options):
|
||||
backgroundColor = (.black, nil, [])
|
||||
if let largest = representations.first, let path = account.postbox.mediaBox.completedResourcePath(largest.resource), let image = UIImage(contentsOfFile: path)?.precomposed() {
|
||||
wallpaperSignal = .single((backgroundColor, incomingColors, outgoingColors, image, options.blur, false, 1.0, rotation))
|
||||
}
|
||||
case let .file(file):
|
||||
rotation = file.settings.rotation
|
||||
if file.isPattern, let intensity = file.settings.intensity, intensity < 0 {
|
||||
@@ -1578,12 +1581,16 @@ public func themeIconImage(account: Account, accountManager: AccountManager<Tele
|
||||
let isBlack = UIColor.average(of: colors.0.2.map(UIColor.init(rgb:))).hsb.b <= 0.01
|
||||
var patternIntensity: CGFloat = 0.5
|
||||
var isPattern = false
|
||||
if let wallpaper = wallpaper, case let .file(file) = wallpaper {
|
||||
if !file.settings.colors.isEmpty {
|
||||
isPattern = file.isPattern
|
||||
if let intensity = file.settings.intensity {
|
||||
patternIntensity = CGFloat(intensity) / 100.0
|
||||
if let wallpaper = wallpaper {
|
||||
if case let .file(file) = wallpaper {
|
||||
if !file.settings.colors.isEmpty {
|
||||
isPattern = file.isPattern
|
||||
if let intensity = file.settings.intensity {
|
||||
patternIntensity = CGFloat(intensity) / 100.0
|
||||
}
|
||||
}
|
||||
} else {
|
||||
patternIntensity = 0.0
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user