Merge commit 'e6ba681c030590e5fa095f9828610889abf08ef3'

This commit is contained in:
Isaac
2023-12-20 14:06:25 +04:00
99 changed files with 2342 additions and 1244 deletions
@@ -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
//}
-23
View File
@@ -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)
+3
View File
@@ -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
}
}
}
@@ -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 {
}
}
}
@@ -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)
}
@@ -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
+1
View File
@@ -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": [],
@@ -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
@@ -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))
}
}
@@ -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)
@@ -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)
@@ -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
@@ -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
@@ -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()
}
@@ -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
@@ -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
@@ -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",
],
)
@@ -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",
],
)
@@ -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))
@@ -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",
@@ -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",
@@ -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,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)
@@ -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
@@ -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",
@@ -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
@@ -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)
@@ -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",
],
)
@@ -10,6 +10,7 @@ import TelegramUIPreferences
import AccountContext
import AttachmentUI
import WallpaperGalleryScreen
import ThemeAccentColorScreen
private func availableGradients(dark: Bool) -> [[UInt32]] {
if dark {
@@ -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)))
@@ -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 {
@@ -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)
}
}
}
@@ -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()
@@ -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()
}
@@ -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: {},
@@ -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) {
@@ -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
}
@@ -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))
}
})
}
})
}
@@ -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
}
@@ -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
}
}