diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 763900807a..66be514ea9 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -14924,3 +14924,87 @@ Sorry for the inconvenience."; "Gift.Upgrade.GiftUpgrade" = "Pay %@ for Upgrade"; "Gift.View.GiftUpgrade" = "Gift an Upgrade"; + +"Gift.View.OpenChatTheme" = "This gift is the chat's theme. [Change Theme >]()"; + +"Notification.ChatTheme.Text" = "%1$@ set **%2$@** as a new theme for this chat."; +"Notification.ChatTheme.TextYou" = "You set **%@** as a new theme for this chat."; + +"Notification.ChangedThemeGift" = "%1$@ changed chat theme to %2$@"; +"Notification.YouChangedThemeGift" = "You changed chat theme to %@"; + +"Conversation.Theme.GiftTransfer.Text" = "This gift is already your theme in the chat with **%@**. Remove it there and use it here instead?"; +"Conversation.Theme.GiftTransfer.Proceed" = "Yes"; + +"PeerInfo.Tabs.SetMainTab" = "Set as Main Tab"; +"PeerInfo.Tabs.SetMainTab.Succeed" = "Tab order changed."; + +"MediaPlayer.SavedMusic.AddToProfile" = "Add to Profile"; +"MediaPlayer.SavedMusic.RemoveFromProfile" = "This audio is visible on your profile. [Remove >]()"; + +"MediaPlayer.SavedMusic.AddedToProfile.View" = "View"; +"MediaPlayer.SavedMusic.AddedToProfile" = "Audio added to your profile."; +"MediaPlayer.SavedMusic.RemovedFromProfile" = "Audio removed from your profile."; + +"MediaPlayer.ContextMenu.SaveToFiles" = "Save to Files"; +"MediaPlayer.ContextMenu.SaveTo" = "Save to..."; +"MediaPlayer.ContextMenu.SaveTo.Profile" = "Profile"; +"MediaPlayer.ContextMenu.SaveTo.SavedMessages" = "Saved Messages"; +"MediaPlayer.ContextMenu.SaveTo.Files" = "Files"; +"MediaPlayer.ContextMenu.SaveTo.Info" = "Choose where you want this audio to be saved."; +"MediaPlayer.ContextMenu.ShowInChat" = "Show in Chat"; +"MediaPlayer.ContextMenu.Forward" = "Forward"; +"MediaPlayer.ContextMenu.Delete" = "Delete"; +"MediaPlayer.ContextMenu.Remove" = "Remove"; + +"MediaPlayer.Playlist.ThisChat" = "AUDIO IN THIS CHAT"; +"MediaPlayer.Playlist.SavedMusic" = "%@'S PLAYLIST"; +"MediaPlayer.Playlist.SavedMusicYou" = "YOUR PLAYLIST"; + +"Notification.PremiumGift.Stars_1" = "%@ Star"; +"Notification.PremiumGift.Stars_any" = "%@ Stars"; + +"Ton.ProceedsOverview" = "PROCEEDS OVERVIEW"; +"Ton.AvailableBalance" = "Balance Available to Withdraw"; +"Ton.LifetimeProceeds" = "Total Lifetime Proceeds"; + +"Ton.WithdrawViaFragment" = "Withdraw via Fragment"; +"Ton.WithdrawViaFragment.Info" = "Collect your TON using Fragment. [Learn More >]()"; +"Ton.WithdrawViaFragment.Info_URL" = "https://telegram.org/tos/bot-developers#6-2-2-tpa-balance"; + +"MESSAGE_GIFT_THEME" = "%1$@ changed theme to %2$@"; + +"Gift.Upgrade.Skip" = "Skip"; +"Gift.Upgrade.UpgradeNext" = "Upgrade Next Gift"; + +"Gift.Value.DescriptionAveragePrice" = "This is the average sale price of **%@** on Telegram and Fragment over the past month."; +"Gift.Value.DescriptionLastPriceFragment" = "This is the last price at which **%@** was last sold on Fragment."; +"Gift.Value.DescriptionLastPriceTelegram" = "This is the last price at which **%@** was last sold on Telegram."; + +"Gift.Value.LastPriceInfo" = "**%1$@** is the last price for %2$@ gifts listed on Telegram and Fragment."; +"Gift.Value.MinimumPriceInfo" = "**%1$@** is the floor price for %2$@ gifts listed on Telegram and Fragment."; +"Gift.Value.AveragePriceInfo" = "**%1$@** is the average sale price for %2$@ gifts listed on Telegram and Fragment over the past month."; + +"Gift.Value.AveragePrice" = "Last Sale"; +"Gift.Value.InitialSale" = "Initial Sale"; +"Gift.Value.InitialPrice" = "Initial Price"; +"Gift.Value.LastSale" = "Last Sale"; +"Gift.Value.LastPrice" = "Last Price"; +"Gift.Value.MinimumPrice" = "Minimum Price"; +"Gift.Value.AveragePrice" = "Average Price"; +"Gift.Value.ForSaleOnTelegram" = "for sale on Telegram"; +"Gift.Value.ForSaleOnFragment" = "for sale on Fragment"; + +"Gift.View.Context.SetAsTheme" = "Set as Theme in..."; + +"Notification.GiftStars" = "Gift for %@"; +"Notification.GiftStars.Stars_1" = "%@ Star"; +"Notification.GiftStars.Stars_any" = "%@ Stars"; + +"Notification.PrepaidGiftUpgrade" = "Gift Upgrade for %@"; +"Notification.PrepaidGiftUpgrade.Stars_1" = "%@ Star"; +"Notification.PrepaidGiftUpgrade.Stars_any" = "%@ Stars"; + +"ProfileLevelInfo.RatingTitle" = "Rating"; +"ProfileLevelInfo.FutureRatingTitle" = "Future Rating"; + diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index e186981260..6a36eb689d 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -1326,7 +1326,7 @@ public protocol SharedAccountContext: AnyObject { func makeStarsGiveawayBoostScreen(context: AccountContext, peerId: EnginePeer.Id, boost: ChannelBoostersContext.State.Boost) -> ViewController func makeStarsIntroScreen(context: AccountContext) -> ViewController func makeGiftViewScreen(context: AccountContext, message: EngineMessage, shareStory: ((StarGift.UniqueGift) -> Void)?) -> ViewController - func makeGiftViewScreen(context: AccountContext, gift: StarGift.UniqueGift, shareStory: ((StarGift.UniqueGift) -> Void)?, dismissed: (() -> Void)?) -> ViewController + func makeGiftViewScreen(context: AccountContext, gift: StarGift.UniqueGift, shareStory: ((StarGift.UniqueGift) -> Void)?, openChatTheme: (() -> Void)?, dismissed: (() -> Void)?) -> ViewController func makeGiftWearPreviewScreen(context: AccountContext, gift: StarGift.UniqueGift) -> ViewController func makeStorySharingScreen(context: AccountContext, subject: StorySharingSubject, parentController: ViewController) -> ViewController diff --git a/submodules/ChatListUI/Sources/Node/ChatListNode.swift b/submodules/ChatListUI/Sources/Node/ChatListNode.swift index b4bfa0c22b..cf42c530ac 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListNode.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListNode.swift @@ -2503,7 +2503,7 @@ public final class ChatListNode: ListView { case let .user(userType): if case let .user(user) = peer { match = true - if user.id.isVerificationCodes { + if user.id.isVerificationCodes || user.id.isTelegramNotifications { match = false } if let isBot = userType.isBot { diff --git a/submodules/Components/SheetComponent/Sources/SheetComponent.swift b/submodules/Components/SheetComponent/Sources/SheetComponent.swift index ba85ac254c..1d8c0f4788 100644 --- a/submodules/Components/SheetComponent/Sources/SheetComponent.swift +++ b/submodules/Components/SheetComponent/Sources/SheetComponent.swift @@ -65,6 +65,7 @@ public final class SheetComponent: C public let clipsContent: Bool public let isScrollEnabled: Bool public let hasDimView: Bool + public let autoAnimateOut: Bool public let externalState: ExternalState? public let animateOut: ActionSlot> public let onPan: () -> Void @@ -77,6 +78,7 @@ public final class SheetComponent: C clipsContent: Bool = false, isScrollEnabled: Bool = true, hasDimView: Bool = true, + autoAnimateOut: Bool = true, externalState: ExternalState? = nil, animateOut: ActionSlot>, onPan: @escaping () -> Void = {}, @@ -88,6 +90,7 @@ public final class SheetComponent: C self.clipsContent = clipsContent self.isScrollEnabled = isScrollEnabled self.hasDimView = hasDimView + self.autoAnimateOut = autoAnimateOut self.externalState = externalState self.animateOut = animateOut self.onPan = onPan @@ -110,6 +113,9 @@ public final class SheetComponent: C if lhs.hasDimView != rhs.hasDimView { return false } + if lhs.autoAnimateOut != rhs.autoAnimateOut { + return false + } if lhs.animateOut != rhs.animateOut { return false } @@ -430,9 +436,15 @@ public final class SheetComponent: C if environment[SheetComponentEnvironment.self].value.isDisplaying, !self.previousIsDisplaying, let _ = transition.userData(ViewControllerComponentContainer.AnimateInTransition.self) { self.animateIn() } else if !environment[SheetComponentEnvironment.self].value.isDisplaying, self.previousIsDisplaying, let _ = transition.userData(ViewControllerComponentContainer.AnimateOutTransition.self) { - self.animateOut(completion: {}) + if component.autoAnimateOut { + self.animateOut(completion: {}) + } + } + if !sheetEnvironment.isDisplaying && !component.autoAnimateOut { + + } else { + self.previousIsDisplaying = sheetEnvironment.isDisplaying } - self.previousIsDisplaying = sheetEnvironment.isDisplaying self.dismiss = sheetEnvironment.dismiss return availableSize diff --git a/submodules/Display/Source/ListView.swift b/submodules/Display/Source/ListView.swift index 26d7997e92..a800abaed0 100644 --- a/submodules/Display/Source/ListView.swift +++ b/submodules/Display/Source/ListView.swift @@ -349,6 +349,8 @@ open class ListView: ASDisplayNode, ASScrollViewDelegate, ASGestureRecognizerDel public final var beganInteractiveDragging: (CGPoint) -> Void = { _ in } public final var endedInteractiveDragging: (CGPoint) -> Void = { _ in } public final var didEndScrolling: ((Bool) -> Void)? + public final var didEndScrollingWithOverscroll: (() -> Void)? + private var currentGeneralScrollDirection: GeneralScrollDirection? public final var generalScrollDirectionUpdated: (GeneralScrollDirection) -> Void = { _ in } @@ -891,6 +893,10 @@ open class ListView: ASDisplayNode, ASScrollViewDelegate, ASGestureRecognizerDel self.resetScrollIndicatorFlashTimer(start: false) self.isAuxiliaryDisplayLinkEnabled = true + + if scrollView.contentOffset.y < -48.0 { + self.didEndScrollingWithOverscroll?() + } } else { self.isDeceleratingAfterTracking = false self.resetHeaderItemsFlashTimer(start: true) diff --git a/submodules/Svg/PublicHeaders/Svg/Svg.h b/submodules/Svg/PublicHeaders/Svg/Svg.h index ec08bd75f8..39080185f3 100755 --- a/submodules/Svg/PublicHeaders/Svg/Svg.h +++ b/submodules/Svg/PublicHeaders/Svg/Svg.h @@ -4,8 +4,28 @@ #import #import +@interface GiftPatternRect : NSObject + +@property (nonatomic) CGPoint center; +@property (nonatomic) CGFloat side; +@property (nonatomic) CGFloat rotation; +@property (nonatomic) CGFloat scale; + +@end + +@interface GiftPatternData : NSObject + +@property (nonatomic) CGSize size; +@property (nonatomic, strong) NSArray * _Nonnull rects; + +@end + NSData * _Nullable prepareSvgImage(NSData * _Nonnull data, bool pattern); + +GiftPatternData * _Nullable getGiftPatternData(NSData * _Nonnull data); + UIImage * _Nullable renderPreparedImage(NSData * _Nonnull data, CGSize size, UIColor * _Nonnull backgroundColor, CGFloat scale, bool fit); +UIImage * _Nullable renderPreparedImageWithSymbol(NSData * _Nonnull data, CGSize size, UIColor * _Nonnull backgroundColor, CGFloat scale, bool fit, UIImage * _Nullable symbolImage, int32_t modelRectIndex); UIImage * _Nullable drawSvgImage(NSData * _Nonnull data, CGSize size, UIColor * _Nullable backgroundColor, UIColor * _Nullable foregroundColor, CGFloat scale, bool opaque); diff --git a/submodules/Svg/Sources/Svg.m b/submodules/Svg/Sources/Svg.m index f425c9c968..d3c4edee41 100755 --- a/submodules/Svg/Sources/Svg.m +++ b/submodules/Svg/Sources/Svg.m @@ -1,255 +1,404 @@ #import - #import "nanosvg.h" #define UIColorRGBA(rgb,a) ([[UIColor alloc] initWithRed:(((rgb >> 16) & 0xff) / 255.0f) green:(((rgb >> 8) & 0xff) / 255.0f) blue:(((rgb) & 0xff) / 255.0f) alpha:a]) - -CGSize aspectFillSize(CGSize size, CGSize bounds) { - CGFloat scale = MAX(bounds.width / MAX(1.0, size.width), bounds.height / MAX(1.0, size.height)); +#define CLAMP(x, min, max) ((x) < (min) ? (min) : ((x) > (max) ? (max) : (x))) + +static inline CGSize aspectFillSize(CGSize size, CGSize bounds) { + if (size.width <= 0 || size.height <= 0) return CGSizeZero; + CGFloat scale = MAX(bounds.width / size.width, bounds.height / size.height); return CGSizeMake(floor(size.width * scale), floor(size.height * scale)); } -CGSize aspectFitSize(CGSize size, CGSize bounds) { - CGFloat scale = MIN(bounds.width / MAX(1.0, size.width), bounds.height / MAX(1.0, size.height)); +static inline CGSize aspectFitSize(CGSize size, CGSize bounds) { + if (size.width <= 0 || size.height <= 0) return CGSizeZero; + CGFloat scale = MIN(bounds.width / size.width, bounds.height / size.height); return CGSizeMake(floor(size.width * scale), floor(size.height * scale)); } -@interface SvgXMLParsingDelegate : NSObject { - NSString *_elementName; - NSString *_currentStyleString; +static inline CGFloat deg2rad(CGFloat deg) { return (deg * (CGFloat)M_PI) / 180.0; } + +static CGSize SVGParseOneTransform(NSString *one, NSString *requiredName) { + NSString *s = [one stringByTrimmingCharactersInSet:NSCharacterSet.whitespaceAndNewlineCharacterSet]; + if (s.length == 0) return CGSizeZero; + + NSRange paren = [s rangeOfString:@"("]; + if (paren.location == NSNotFound) return CGSizeZero; + + NSString *name = [[s substringToIndex:paren.location] lowercaseString]; + NSString *argsStr = [s substringWithRange:NSMakeRange(paren.location + 1, s.length - paren.location - 2)]; + + NSMutableArray *nums = [NSMutableArray array]; + { + NSScanner *sc = [NSScanner scannerWithString:argsStr]; + while (!sc.isAtEnd) { + double v; + if ([sc scanDouble:&v]) { + [nums addObject:@(v)]; + } else { + sc.scanLocation += 1; + } + } + } + + if ([name isEqualToString:@"translate"] && [name isEqualToString:requiredName]) { + CGFloat tx = nums.count > 0 ? nums[0].doubleValue : 0; + CGFloat ty = nums.count > 1 ? nums[1].doubleValue : 0; + return CGSizeMake(tx, ty); + } else if ([name isEqualToString:@"scale"] && [name isEqualToString:requiredName]) { + CGFloat sx = nums.count > 0 ? nums[0].doubleValue : 1; + CGFloat sy = nums.count > 1 ? nums[1].doubleValue : sx; + return CGSizeMake(sx, sy); + } else if ([name isEqualToString:@"rotate"] && [name isEqualToString:requiredName]) { + CGFloat a = nums.count > 0 ? deg2rad(nums[0].doubleValue) : 0; + return CGSizeMake(a, a); + } + return CGSizeZero; } -@property (nonatomic, strong, readonly) NSMutableDictionary *styles; +static CGAffineTransform SVGParseTransformList(NSString *list) { + if (list.length == 0) { + return CGAffineTransformMake(0.0, 1.0, 1.0, 0.0, 0.0, 0.0); + } + + NSMutableArray *chunks = [NSMutableArray array]; + { + NSMutableString *cur = [NSMutableString string]; + NSInteger depth = 0; + for (NSUInteger i = 0; i < list.length; i++) { + unichar ch = [list characterAtIndex:i]; + [cur appendFormat:@"%C", ch]; + if (ch == '(') depth++; + if (ch == ')') { + depth--; + if (depth == 0) { + [chunks addObject:[cur copy]]; + [cur setString:@""]; + } + } + } + } + CGFloat rotation = 0.0; + CGSize scale = CGSizeMake(1.0, 1.0); + for (NSString *part in chunks) { + CGSize rotationValue = SVGParseOneTransform(part, @"rotate"); + if (ABS(rotationValue.width) > 0.001) { + rotation = rotationValue.width; + } + CGSize scaleValue = SVGParseOneTransform(part, @"scale"); + if (ABS(scaleValue.width) > 0.001 && (ABS(scaleValue.width - 1.0) > 0.001 || ABS(scaleValue.height - 1.0) > 0.001)) { + scale = scaleValue; + } + } + return CGAffineTransformMake(rotation, scale.width, scale.height, 0.0, 0.0, 0.0); +} + +@implementation GiftPatternData @end -@implementation SvgXMLParsingDelegate +@implementation GiftPatternRect + +@end + + +@interface SvgXMLParsingDelegate : NSObject +@property (nonatomic, strong, readonly) NSMutableDictionary *styles; +@property (nonatomic, strong) NSMutableArray *giftRects; +@end + +@implementation SvgXMLParsingDelegate { + NSString *_elementName; + NSMutableString *_currentStyleString; + + bool _inGiftPatterns; +} - (instancetype)init { self = [super init]; - if (self != nil) { + if (self) { _styles = [[NSMutableDictionary alloc] init]; + _currentStyleString = [[NSMutableString alloc] init]; + _giftRects = [NSMutableArray array]; + _inGiftPatterns = false; } return self; } - (void)parser:(NSXMLParser *)parser didStartElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName attributes:(NSDictionary *)attributeDict { - _elementName = elementName; + _elementName = [elementName copy]; + + if ([_elementName isEqualToString:@"g"]) { + NSString *gid = attributeDict[@"id"]; + if ([[gid lowercaseString] isEqualToString:@"giftpatterns"]) { + _inGiftPatterns = true; + } + } else if (_inGiftPatterns && [_elementName isEqualToString:@"rect"]) { + CGFloat x = attributeDict[@"x"] ? attributeDict[@"x"].doubleValue : 0; + CGFloat y = attributeDict[@"y"] ? attributeDict[@"y"].doubleValue : 0; + CGFloat w = attributeDict[@"width"] ? attributeDict[@"width"].doubleValue : 0; + CGFloat h = attributeDict[@"height"] ? attributeDict[@"height"].doubleValue : 0; + + CGFloat side = MAX(w, h); + + CGAffineTransform fakeTransform = CGAffineTransformMake(0.0, 1.0, 1.0, 0.0, 0.0, 0.0); + NSString *rt = attributeDict[@"transform"]; + if (rt.length) { + fakeTransform = SVGParseTransformList(rt); + } + + CGPoint rectCenter = CGPointMake(x + w * 0.5, y + h * 0.5); + + GiftPatternRect *rec = [GiftPatternRect new]; + rec.center = rectCenter; + rec.side = side; + rec.rotation = fakeTransform.a; + rec.scale = fakeTransform.b; + [_giftRects addObject:rec]; + } } - (void)parser:(NSXMLParser *)parser didEndElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName { - if ([_elementName isEqualToString:@"style"]) { - int currentClassNameStartIndex = -1; - int currentClassContentsStartIndex = -1; - - NSString *currentClassName = nil; - - NSCharacterSet *alphanumeric = [NSCharacterSet alphanumericCharacterSet]; - - for (int i = 0; i < _currentStyleString.length; i++) { - unichar c = [_currentStyleString characterAtIndex:i]; - if (currentClassNameStartIndex != -1) { - if (![alphanumeric characterIsMember:c]) { - currentClassName = [_currentStyleString substringWithRange:NSMakeRange(currentClassNameStartIndex, i - currentClassNameStartIndex)]; - currentClassNameStartIndex = -1; - } - } else if (currentClassContentsStartIndex != -1) { - if (c == '}') { - NSString *classContents = [_currentStyleString substringWithRange:NSMakeRange(currentClassContentsStartIndex, i - currentClassContentsStartIndex)]; - if (currentClassName != nil && classContents != nil) { - _styles[currentClassName] = classContents; - currentClassName = nil; - } - currentClassContentsStartIndex = -1; - } - } - - if (currentClassNameStartIndex == -1 && currentClassContentsStartIndex == -1) { - if (c == '.') { - currentClassNameStartIndex = i + 1; - } else if (c == '{') { - currentClassContentsStartIndex = i + 1; - } - } - } + if ([_elementName isEqualToString:@"style"] && _currentStyleString.length > 0) { + [self parseStyleString:_currentStyleString]; + [_currentStyleString setString:@""]; + } + if ([_elementName isEqualToString:@"g"] && _inGiftPatterns) { + _inGiftPatterns = false; } _elementName = nil; } - (void)parser:(NSXMLParser *)parser foundCharacters:(NSString *)string { if ([_elementName isEqualToString:@"style"]) { - if (_currentStyleString == nil) { - _currentStyleString = string; - } else { - _currentStyleString = [_currentStyleString stringByAppendingString:string]; + [_currentStyleString appendString:string]; + } +} + +- (void)parseStyleString:(NSString *)styleString { + NSError *error = nil; + NSRegularExpression *regex = [NSRegularExpression + regularExpressionWithPattern:@"\\.([a-zA-Z0-9_-]+)\\s*\\{([^}]+)\\}" + options:0 error:&error]; + + if (error) { + [self parseStyleStringLegacy:styleString]; + return; + } + + [regex enumerateMatchesInString:styleString options:0 range:NSMakeRange(0, styleString.length) usingBlock:^(NSTextCheckingResult *match, NSMatchingFlags flags, BOOL *stop) { + if (match.numberOfRanges >= 3) { + NSString *className = [styleString substringWithRange:[match rangeAtIndex:1]]; + NSString *classContents = [styleString substringWithRange:[match rangeAtIndex:2]]; + self.styles[className] = [classContents stringByTrimmingCharactersInSet: [NSCharacterSet whitespaceAndNewlineCharacterSet]]; + } + }]; +} + +- (void)parseStyleStringLegacy:(NSString *)styleString { + NSInteger currentClassNameStartIndex = -1; + NSInteger currentClassContentsStartIndex = -1; + NSString *currentClassName = nil; + NSCharacterSet *classNameChars = [NSCharacterSet characterSetWithCharactersInString:@"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-"]; + + for (NSInteger i = 0; i < styleString.length; i++) { + unichar c = [styleString characterAtIndex:i]; + + if (currentClassNameStartIndex != -1) { + if (![classNameChars characterIsMember:c]) { + currentClassName = [styleString substringWithRange:NSMakeRange(currentClassNameStartIndex, i - currentClassNameStartIndex)]; + currentClassNameStartIndex = -1; + } + } else if (currentClassContentsStartIndex != -1) { + if (c == '}') { + NSString *classContents = [styleString substringWithRange:NSMakeRange(currentClassContentsStartIndex, i - currentClassContentsStartIndex)]; + if (currentClassName.length > 0 && classContents.length > 0) { + self.styles[currentClassName] = [classContents stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; + } + currentClassName = nil; + currentClassContentsStartIndex = -1; + } + } + + if (currentClassNameStartIndex == -1 && currentClassContentsStartIndex == -1) { + if (c == '.') { + currentClassNameStartIndex = i + 1; + } else if (c == '{') { + currentClassContentsStartIndex = i + 1; + } } } } @end +void renderShape(NSVGshape *shape, CGContextRef context, UIColor *foregroundColor) { + if (shape->fill.type != NSVG_PAINT_NONE) { + CGContextSetFillColorWithColor(context, [foregroundColor colorWithAlphaComponent:shape->opacity].CGColor); + + CGContextBeginPath(context); + bool isFirstPath = true; + + for (NSVGpath *path = shape->paths; path; path = path->next) { + if (!isFirstPath && path->closed) { + CGContextBeginPath(context); + } + + CGContextMoveToPoint(context, path->pts[0], path->pts[1]); + + for (int i = 0; i < path->npts - 1; i += 3) { + float *p = &path->pts[i * 2]; + CGContextAddCurveToPoint(context, p[2], p[3], p[4], p[5], p[6], p[7]); + } + + if (path->closed) { + CGContextClosePath(context); + } + + isFirstPath = NO; + } + + switch (shape->fillRule) { + case NSVG_FILLRULE_EVENODD: + CGContextEOFillPath(context); + break; + default: + CGContextFillPath(context); + break; + } + } + + if (shape->stroke.type != NSVG_PAINT_NONE && shape->strokeWidth > 0) { + CGContextSetStrokeColorWithColor(context, [foregroundColor colorWithAlphaComponent:shape->opacity].CGColor); + CGContextSetLineWidth(context, shape->strokeWidth); + CGContextSetMiterLimit(context, shape->miterLimit); + + switch (shape->strokeLineCap) { + case NSVG_CAP_ROUND: CGContextSetLineCap(context, kCGLineCapRound); break; + case NSVG_CAP_SQUARE: CGContextSetLineCap(context, kCGLineCapSquare); break; + default: CGContextSetLineCap(context, kCGLineCapButt); break; + } + + switch (shape->strokeLineJoin) { + case NSVG_JOIN_BEVEL: CGContextSetLineJoin(context, kCGLineJoinBevel); break; + case NSVG_JOIN_ROUND: CGContextSetLineJoin(context, kCGLineJoinRound); break; + default: CGContextSetLineJoin(context, kCGLineJoinMiter); break; + } + + for (NSVGpath *path = shape->paths; path; path = path->next) { + CGContextBeginPath(context); + CGContextMoveToPoint(context, path->pts[0], path->pts[1]); + + for (int i = 0; i < path->npts - 1; i += 3) { + float *p = &path->pts[i * 2]; + CGContextAddCurveToPoint(context, p[2], p[3], p[4], p[5], p[6], p[7]); + } + + if (path->closed) { + CGContextClosePath(context); + } + CGContextStrokePath(context); + } + } +} + UIImage * _Nullable drawSvgImage(NSData * _Nonnull data, CGSize size, UIColor *backgroundColor, UIColor *foregroundColor, CGFloat canvasScale, bool opaque) { + if (!data || data.length == 0) return nil; NSXMLParser *parser = [[NSXMLParser alloc] initWithData:data]; - if (parser == nil) { - return nil; - } + if (!parser) return nil; + SvgXMLParsingDelegate *delegate = [[SvgXMLParsingDelegate alloc] init]; parser.delegate = delegate; - [parser parse]; + if (![parser parse]) return nil; NSMutableString *xmlString = [[NSMutableString alloc] initWithData:data encoding:NSUTF8StringEncoding]; - if (xmlString == nil) { - return nil; - } + if (!xmlString) return nil; for (NSString *styleName in delegate.styles) { NSString *styleValue = delegate.styles[styleName]; - [xmlString replaceOccurrencesOfString:[NSString stringWithFormat:@"class=\"%@\"", styleName] withString:[NSString stringWithFormat:@"style=\"%@\"", styleValue] options:0 range:NSMakeRange(0, xmlString.length)]; + NSString *searchPattern = [NSString stringWithFormat:@"class=\"%@\"", styleName]; + NSString *replacement = [NSString stringWithFormat:@"style=\"%@\"", styleValue]; + [xmlString replaceOccurrencesOfString:searchPattern withString:replacement + options:NSLiteralSearch range:NSMakeRange(0, xmlString.length)]; } - const char *zeroTerminatedData = xmlString.UTF8String; - - NSVGimage *image = nsvgParse((char *)zeroTerminatedData, "px", 96); - if (image == nil || image->width < 1.0f || image->height < 1.0f) { + const char *svgString = xmlString.UTF8String; + NSVGimage *image = nsvgParse((char *)svgString, "px", 96); + if (!image || image->width < 1.0f || image->height < 1.0f) { + if (image) nsvgDelete(image); return nil; } + CGSize originalSize = CGSizeMake(image->width, image->height); if (CGSizeEqualToSize(size, CGSizeZero)) { - size = CGSizeMake(image->width, image->height); + size = originalSize; } - + UIGraphicsBeginImageContextWithOptions(size, opaque, canvasScale); CGContextRef context = UIGraphicsGetCurrentContext(); - if (backgroundColor != nil) { - CGContextSetFillColorWithColor(context, backgroundColor.CGColor); - CGContextFillRect(context, CGRectMake(0.0f, 0.0f, size.width, size.height)); + if (!context) { + nsvgDelete(image); + return nil; } - CGSize svgSize = CGSizeMake(image->width, image->height); - CGSize drawingSize = aspectFillSize(svgSize, size); + if (backgroundColor) { + CGContextSetFillColorWithColor(context, backgroundColor.CGColor); + CGContextFillRect(context, CGRectMake(0, 0, size.width, size.height)); + } - CGFloat scale = MAX(size.width / MAX(1.0, svgSize.width), size.height / MAX(1.0, svgSize.height)); + CGSize drawingSize = aspectFillSize(originalSize, size); + CGFloat scale = MAX(size.width / originalSize.width, size.height / originalSize.height); + CGFloat offsetX = (size.width - drawingSize.width) / (2.0 * scale); + CGFloat offsetY = (size.height - drawingSize.height) / (2.0 * scale); CGContextScaleCTM(context, scale, scale); - CGContextTranslateCTM(context, (size.width - drawingSize.width) / 2.0, (size.height - drawingSize.height) / 2.0); + CGContextTranslateCTM(context, offsetX, offsetY); - for (NSVGshape *shape = image->shapes; shape != NULL; shape = shape->next) { - if (!(shape->flags & NSVG_FLAGS_VISIBLE)) { - continue; - } + for (NSVGshape *shape = image->shapes; shape; shape = shape->next) { + if (!(shape->flags & NSVG_FLAGS_VISIBLE) || shape->opacity <= 0) continue; - if (shape->fill.type != NSVG_PAINT_NONE) { - CGContextSetFillColorWithColor(context, [foregroundColor colorWithAlphaComponent:shape->opacity].CGColor); - - bool isFirst = true; - bool hasStartPoint = false; - CGPoint startPoint; - for (NSVGpath *path = shape->paths; path != NULL; path = path->next) { - if (isFirst) { - CGContextBeginPath(context); - isFirst = false; - hasStartPoint = true; - startPoint.x = path->pts[0]; - startPoint.y = path->pts[1]; - } - CGContextMoveToPoint(context, path->pts[0], path->pts[1]); - for (int i = 0; i < path->npts - 1; i += 3) { - float *p = &path->pts[i * 2]; - CGContextAddCurveToPoint(context, p[2], p[3], p[4], p[5], p[6], p[7]); - } - - if (path->closed) { - if (hasStartPoint) { - hasStartPoint = false; - CGContextAddLineToPoint(context, startPoint.x, startPoint.y); - } - } - } - switch (shape->fillRule) { - case NSVG_FILLRULE_EVENODD: - CGContextEOFillPath(context); - break; - default: - CGContextFillPath(context); - break; - } - } - - if (shape->stroke.type != NSVG_PAINT_NONE) { - CGContextSetStrokeColorWithColor(context, [foregroundColor colorWithAlphaComponent:shape->opacity].CGColor); - CGContextSetMiterLimit(context, shape->miterLimit); - - CGContextSetLineWidth(context, shape->strokeWidth); - switch (shape->strokeLineCap) { - case NSVG_CAP_BUTT: - CGContextSetLineCap(context, kCGLineCapButt); - break; - case NSVG_CAP_ROUND: - CGContextSetLineCap(context, kCGLineCapRound); - break; - case NSVG_CAP_SQUARE: - CGContextSetLineCap(context, kCGLineCapSquare); - break; - default: - break; - } - switch (shape->strokeLineJoin) { - case NSVG_JOIN_BEVEL: - CGContextSetLineJoin(context, kCGLineJoinBevel); - break; - case NSVG_JOIN_MITER: - CGContextSetLineJoin(context, kCGLineJoinMiter); - break; - case NSVG_JOIN_ROUND: - CGContextSetLineJoin(context, kCGLineJoinRound); - break; - default: - break; - } - - for (NSVGpath *path = shape->paths; path != NULL; path = path->next) { - CGContextBeginPath(context); - CGContextMoveToPoint(context, path->pts[0], path->pts[1]); - for (int i = 0; i < path->npts - 1; i += 3) { - float *p = &path->pts[i * 2]; - CGContextAddCurveToPoint(context, p[2], p[3], p[4], p[5], p[6], p[7]); - } - - if (path->closed) { - CGContextClosePath(context); - } - CGContextStrokePath(context); - } - } + renderShape(shape, context, foregroundColor); } UIImage *resultImage = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); - nsvgDelete(image); return resultImage; } +typedef NS_ENUM(uint8_t, SvgRenderCommand) { + SvgRenderCommandSetFillColorWithOpacity = 1, + SvgRenderCommandSetupStroke = 2, + SvgRenderCommandBeginPath = 3, + SvgRenderCommandMoveTo = 4, + SvgRenderCommandLineTo = 5, + SvgRenderCommandCurveTo = 6, + SvgRenderCommandClosePath = 7, + SvgRenderCommandEOFillPath = 8, + SvgRenderCommandFillPath = 9, + SvgRenderCommandStrokePath = 10, + SvgRenderCommandSetFillColorRGBA = 11, + SvgRenderCommandRectsHeader = 100, + SvgRenderCommandRectsItem = 101 +}; -@interface CGContextCoder : NSObject { +@interface CGContextCoder : NSObject +@property (nonatomic, readonly) NSData *data; +@end + +@implementation CGContextCoder { NSMutableData *_data; } -@property (nonatomic, readonly) NSData *data; - -@end - -@implementation CGContextCoder - - (instancetype)initWithSize:(CGSize)size { self = [super init]; - if (self != nil) { + if (self) { _data = [[NSMutableData alloc] init]; - int32_t intWidth = size.width; - int32_t intHeight = size.height; + int32_t intWidth = (int32_t)size.width; + int32_t intHeight = (int32_t)size.height; [_data appendBytes:&intWidth length:sizeof(intWidth)]; [_data appendBytes:&intHeight length:sizeof(intHeight)]; } @@ -257,451 +406,586 @@ UIImage * _Nullable drawSvgImage(NSData * _Nonnull data, CGSize size, UIColor *b } - (void)setFillColorWithOpacity:(CGFloat)opacity { - uint8_t command = 1; - [_data appendBytes:&command length:sizeof(command)]; + uint8_t command = SvgRenderCommandSetFillColorWithOpacity; + uint8_t intOpacity = (uint8_t)(CLAMP(opacity, 0.0, 1.0) * 255.0); - uint8_t intOpacity = opacity * 255.0; + [_data appendBytes:&command length:sizeof(command)]; [_data appendBytes:&intOpacity length:sizeof(intOpacity)]; } -- (void)setupStrokeOpacity:(CGFloat)opacity mitterLimit:(CGFloat)mitterLimit lineWidth:(CGFloat)lineWidth lineCap:(CGLineCap)lineCap lineJoin:(CGLineJoin)lineJoin { - uint8_t command = 2; +- (void)setupStrokeOpacity:(CGFloat)opacity miterLimit:(CGFloat)miterLimit lineWidth:(CGFloat)lineWidth lineCap:(CGLineCap)lineCap lineJoin:(CGLineJoin)lineJoin { + uint8_t command = SvgRenderCommandSetupStroke; + uint8_t intOpacity = (uint8_t)(CLAMP(opacity, 0.0, 1.0) * 255.0); + float floatMiterLimit = (float)miterLimit; + float floatLineWidth = (float)lineWidth; + uint8_t intLineCap = (uint8_t)lineCap; + uint8_t intLineJoin = (uint8_t)lineJoin; + [_data appendBytes:&command length:sizeof(command)]; - - uint8_t intOpacity = opacity * 255.0; [_data appendBytes:&intOpacity length:sizeof(intOpacity)]; - - float floatMitterLimit = mitterLimit; - [_data appendBytes:&floatMitterLimit length:sizeof(floatMitterLimit)]; - - float floatLineWidth = lineWidth; + [_data appendBytes:&floatMiterLimit length:sizeof(floatMiterLimit)]; [_data appendBytes:&floatLineWidth length:sizeof(floatLineWidth)]; - - uint8_t intLineCap = lineCap; [_data appendBytes:&intLineCap length:sizeof(intLineCap)]; - - uint8_t intLineJoin = lineJoin; [_data appendBytes:&intLineJoin length:sizeof(intLineJoin)]; } - (void)beginPath { - uint8_t command = 3; + uint8_t command = SvgRenderCommandBeginPath; [_data appendBytes:&command length:sizeof(command)]; } - (void)moveToPoint:(CGPoint)point { - uint8_t command = 4; - [_data appendBytes:&command length:sizeof(command)]; + uint8_t command = SvgRenderCommandMoveTo; + float x = (float)point.x, y = (float)point.y; - float floatX = point.x; - [_data appendBytes:&floatX length:sizeof(floatX)]; - - float floatY = point.y; - [_data appendBytes:&floatY length:sizeof(floatY)]; + [_data appendBytes:&command length:sizeof(command)]; + [_data appendBytes:&x length:sizeof(x)]; + [_data appendBytes:&y length:sizeof(y)]; } - (void)addLineToPoint:(CGPoint)point { - uint8_t command = 5; - [_data appendBytes:&command length:sizeof(command)]; + uint8_t command = SvgRenderCommandLineTo; + float x = (float)point.x, y = (float)point.y; - float floatX = point.x; - [_data appendBytes:&floatX length:sizeof(floatX)]; - - float floatY = point.y; - [_data appendBytes:&floatY length:sizeof(floatY)]; + [_data appendBytes:&command length:sizeof(command)]; + [_data appendBytes:&x length:sizeof(x)]; + [_data appendBytes:&y length:sizeof(y)]; } - (void)addCurveToPoint:(CGPoint)p1 p2:(CGPoint)p2 p3:(CGPoint)p3 { - uint8_t command = 6; + uint8_t command = SvgRenderCommandCurveTo; + float coords[6] = {(float)p1.x, (float)p1.y, (float)p2.x, (float)p2.y, (float)p3.x, (float)p3.y}; + [_data appendBytes:&command length:sizeof(command)]; - - float floatX1 = p1.x; - [_data appendBytes:&floatX1 length:sizeof(floatX1)]; - - float floatY1 = p1.y; - [_data appendBytes:&floatY1 length:sizeof(floatY1)]; - - float floatX2 = p2.x; - [_data appendBytes:&floatX2 length:sizeof(floatX2)]; - - float floatY2 = p2.y; - [_data appendBytes:&floatY2 length:sizeof(floatY2)]; - - float floatX3 = p3.x; - [_data appendBytes:&floatX3 length:sizeof(floatX3)]; - - float floatY3 = p3.y; - [_data appendBytes:&floatY3 length:sizeof(floatY3)]; + [_data appendBytes:coords length:sizeof(coords)]; } - (void)closePath { - uint8_t command = 7; + uint8_t command = SvgRenderCommandClosePath; [_data appendBytes:&command length:sizeof(command)]; } - (void)eoFillPath { - uint8_t command = 8; + uint8_t command = SvgRenderCommandEOFillPath; [_data appendBytes:&command length:sizeof(command)]; } - (void)fillPath { - uint8_t command = 9; + uint8_t command = SvgRenderCommandFillPath; [_data appendBytes:&command length:sizeof(command)]; } - (void)strokePath { - uint8_t command = 10; + uint8_t command = SvgRenderCommandStrokePath; [_data appendBytes:&command length:sizeof(command)]; } - (void)setFillColor:(uint32_t)color opacity:(CGFloat)opacity { - uint8_t command = 11; + uint8_t command = SvgRenderCommandSetFillColorRGBA; + uint32_t colorWithAlpha = ((uint32_t)(CLAMP(opacity, 0.0, 1.0) * 255.0) << 24) | color; + + [_data appendBytes:&command length:sizeof(command)]; + [_data appendBytes:&colorWithAlpha length:sizeof(colorWithAlpha)]; +} + +- (void)storeGiftPatternRects:(NSArray *)rects { + uint8_t command = SvgRenderCommandRectsHeader; [_data appendBytes:&command length:sizeof(command)]; - color = ((uint32_t)(opacity * 255.0) << 24) | color; - [_data appendBytes:&color length:sizeof(color)]; + uint32_t count = (uint32_t)rects.count; + [_data appendBytes:&count length:sizeof(count)]; + + for (GiftPatternRect *rect in rects) { + uint8_t item = SvgRenderCommandRectsItem; + [_data appendBytes:&item length:sizeof(item)]; + + float payload[5] = { + (float)rect.center.x, (float)rect.center.y, (float)rect.side, (float)rect.rotation, (float)rect.scale + }; + [_data appendBytes:payload length:sizeof(payload)]; + } } @end -UIColor *colorWithBGRA(uint32_t bgra) -{ - return [[UIColor alloc] initWithRed:(((bgra) & 0xff) / 255.0f) green:(((bgra >> 8) & 0xff) / 255.0f) blue:(((bgra >> 16) & 0xff) / 255.0f) alpha:(((bgra >> 24) & 0xff) / 255.0f)]; +static inline UIColor *colorWithBGRA(uint32_t bgra) { + return [[UIColor alloc] initWithRed:(((bgra) & 0xff) / 255.0f) + green:(((bgra >> 8) & 0xff) / 255.0f) + blue:(((bgra >> 16) & 0xff) / 255.0f) + alpha:(((bgra >> 24) & 0xff) / 255.0f)]; } -UIImage * _Nullable renderPreparedImage(NSData * _Nonnull data, CGSize size, UIColor *backgroundColor, CGFloat scale, bool fit) { - NSDate *startTime = [NSDate date]; - - UIColor *foregroundColor = [UIColor whiteColor]; - - int32_t ptr = 0; - int32_t width; - int32_t height; - - if (data.length < 4 * 2) { +bool processRenderCommand(uint8_t cmd, NSData *data, NSUInteger *ptr, CGContextRef *context, UIColor *foregroundColor) { + switch (cmd) { + case SvgRenderCommandSetFillColorWithOpacity: { + if (*ptr + 1 > data.length) return NO; + uint8_t opacity; + [data getBytes:&opacity range:NSMakeRange(*ptr, sizeof(opacity))]; + *ptr += sizeof(opacity); + if (context != nil) { + CGContextSetFillColorWithColor(*context, [foregroundColor colorWithAlphaComponent:opacity / 255.0].CGColor); + } + break; + } + + case SvgRenderCommandSetupStroke: { + if (*ptr + 10 > data.length) return NO; + uint8_t opacity; + float miterLimit, lineWidth; + uint8_t lineCap, lineJoin; + + [data getBytes:&opacity range:NSMakeRange(*ptr, sizeof(opacity))]; + *ptr += sizeof(opacity); + [data getBytes:&miterLimit range:NSMakeRange(*ptr, sizeof(miterLimit))]; + *ptr += sizeof(miterLimit); + [data getBytes:&lineWidth range:NSMakeRange(*ptr, sizeof(lineWidth))]; + *ptr += sizeof(lineWidth); + [data getBytes:&lineCap range:NSMakeRange(*ptr, sizeof(lineCap))]; + *ptr += sizeof(lineCap); + [data getBytes:&lineJoin range:NSMakeRange(*ptr, sizeof(lineJoin))]; + *ptr += sizeof(lineJoin); + + if (context != nil) { + CGContextSetStrokeColorWithColor(*context, [foregroundColor colorWithAlphaComponent:opacity / 255.0].CGColor); + CGContextSetMiterLimit(*context, miterLimit); + CGContextSetLineWidth(*context, lineWidth); + CGContextSetLineCap(*context, (CGLineCap)lineCap); + CGContextSetLineJoin(*context, (CGLineJoin)lineJoin); + } + break; + } + + case SvgRenderCommandBeginPath: + if (context != nil) { + CGContextBeginPath(*context); + } + break; + + case SvgRenderCommandMoveTo: { + if (*ptr + 8 > data.length) return NO; + float x, y; + [data getBytes:&x range:NSMakeRange(*ptr, sizeof(x))]; + *ptr += sizeof(x); + [data getBytes:&y range:NSMakeRange(*ptr, sizeof(y))]; + *ptr += sizeof(y); + if (context != nil) { + CGContextMoveToPoint(*context, x, y); + } + break; + } + + case SvgRenderCommandLineTo: { + if (*ptr + 8 > data.length) return NO; + float x, y; + [data getBytes:&x range:NSMakeRange(*ptr, sizeof(x))]; + *ptr += sizeof(x); + [data getBytes:&y range:NSMakeRange(*ptr, sizeof(y))]; + *ptr += sizeof(y); + if (context != nil) { + CGContextAddLineToPoint(*context, x, y); + } + break; + } + + case SvgRenderCommandCurveTo: { + if (*ptr + 24 > data.length) return NO; + float coords[6]; + [data getBytes:coords range:NSMakeRange(*ptr, sizeof(coords))]; + *ptr += sizeof(coords); + if (context != nil) { + CGContextAddCurveToPoint(*context, coords[0], coords[1], coords[2], coords[3], coords[4], coords[5]); + } + break; + } + + case SvgRenderCommandClosePath: + if (context != nil) { + CGContextClosePath(*context); + } + break; + + case SvgRenderCommandEOFillPath: + if (context != nil) { + CGContextEOFillPath(*context); + } + break; + + case SvgRenderCommandFillPath: + if (context != nil) { + CGContextFillPath(*context); + } + break; + + case SvgRenderCommandStrokePath: + if (context != nil) { + CGContextStrokePath(*context); + } + break; + + case SvgRenderCommandSetFillColorRGBA: { + if (*ptr + 4 > data.length) return NO; + uint32_t bgra; + [data getBytes:&bgra range:NSMakeRange(*ptr, sizeof(bgra))]; + *ptr += sizeof(bgra); + if (context != nil) { + CGContextSetFillColorWithColor(*context, colorWithBGRA(bgra).CGColor); + } + break; + } + case SvgRenderCommandRectsHeader: + break; + case SvgRenderCommandRectsItem: + break; + default: + return false; + } + return true; +} + +GiftPatternData *getGiftPatternData(NSData * _Nonnull data) { + if (!data || data.length < 8) { return nil; } - + + NSUInteger ptr = 0; + int32_t width, height; [data getBytes:&width range:NSMakeRange(ptr, sizeof(width))]; ptr += sizeof(width); [data getBytes:&height range:NSMakeRange(ptr, sizeof(height))]; ptr += sizeof(height); - if (CGSizeEqualToSize(size, CGSizeZero)) { - size = CGSizeMake(width, height); - } - - bool isTransparent = [backgroundColor isEqual:[UIColor clearColor]]; - - CGSize svgSize = CGSizeMake(width, height); - CGSize drawingSize; - if (fit) { - drawingSize = aspectFitSize(svgSize, size); - size = drawingSize; - } else { - drawingSize = aspectFillSize(svgSize, size); - } - - UIGraphicsBeginImageContextWithOptions(size, !isTransparent, scale); - CGContextRef context = UIGraphicsGetCurrentContext(); - if (isTransparent) { - CGContextClearRect(context, CGRectMake(0.0f, 0.0f, size.width, size.height)); - } else { - CGContextSetFillColorWithColor(context, backgroundColor.CGColor); - CGContextFillRect(context, CGRectMake(0.0f, 0.0f, size.width, size.height)); - } - - - - CGFloat renderScale = MAX(size.width / MAX(1.0, svgSize.width), size.height / MAX(1.0, svgSize.height)); - - CGContextScaleCTM(context, renderScale, renderScale); - CGContextTranslateCTM(context, (size.width - drawingSize.width) / 2.0, (size.height - drawingSize.height) / 2.0); + NSMutableArray *rects = [[NSMutableArray alloc] init]; while (ptr < data.length) { + if (ptr + 1 > data.length) break; + uint8_t cmd; [data getBytes:&cmd range:NSMakeRange(ptr, sizeof(cmd))]; ptr += sizeof(cmd); - switch (cmd) { - case 1: - { - uint8_t opacity; - [data getBytes:&opacity range:NSMakeRange(ptr, sizeof(opacity))]; - ptr += sizeof(opacity); - CGContextSetFillColorWithColor(context, [foregroundColor colorWithAlphaComponent:opacity / 255.0].CGColor); - } - break; - - case 2: - { - uint8_t opacity; - [data getBytes:&opacity range:NSMakeRange(ptr, sizeof(opacity))]; - ptr += sizeof(opacity); - CGContextSetStrokeColorWithColor(context, [foregroundColor colorWithAlphaComponent:opacity / 255.0].CGColor); - - float mitterLimit; - [data getBytes:&mitterLimit range:NSMakeRange(ptr, sizeof(mitterLimit))]; - ptr += sizeof(mitterLimit); - CGContextSetMiterLimit(context, mitterLimit); - - float lineWidth; - [data getBytes:&lineWidth range:NSMakeRange(ptr, sizeof(lineWidth))]; - ptr += sizeof(lineWidth); - CGContextSetLineWidth(context, lineWidth); - - uint8_t lineCap; - [data getBytes:&lineCap range:NSMakeRange(ptr, sizeof(lineCap))]; - ptr += sizeof(lineCap); - CGContextSetLineCap(context, lineCap); - - uint8_t lineJoin; - [data getBytes:&lineJoin range:NSMakeRange(ptr, sizeof(lineJoin))]; - ptr += sizeof(lineJoin); - CGContextSetLineCap(context, lineJoin); - } - break; - - case 3: - { - CGContextBeginPath(context); - } - break; - - case 4: - { - float x; - [data getBytes:&x range:NSMakeRange(ptr, sizeof(x))]; - ptr += sizeof(x); - - float y; - [data getBytes:&y range:NSMakeRange(ptr, sizeof(y))]; - ptr += sizeof(y); - - CGContextMoveToPoint(context, x, y); - } - break; - - case 5: - { - float x; - [data getBytes:&x range:NSMakeRange(ptr, sizeof(x))]; - ptr += sizeof(x); - - float y; - [data getBytes:&y range:NSMakeRange(ptr, sizeof(y))]; - ptr += sizeof(y); - - CGContextAddLineToPoint(context, x, y); - } - break; - - case 6: - { - float x1; - [data getBytes:&x1 range:NSMakeRange(ptr, sizeof(x1))]; - ptr += sizeof(x1); - - float y1; - [data getBytes:&y1 range:NSMakeRange(ptr, sizeof(y1))]; - ptr += sizeof(y1); - - float x2; - [data getBytes:&x2 range:NSMakeRange(ptr, sizeof(x2))]; - ptr += sizeof(x2); - - float y2; - [data getBytes:&y2 range:NSMakeRange(ptr, sizeof(y2))]; - ptr += sizeof(y2); - - float x3; - [data getBytes:&x3 range:NSMakeRange(ptr, sizeof(x3))]; - ptr += sizeof(x3); - - float y3; - [data getBytes:&y3 range:NSMakeRange(ptr, sizeof(y3))]; - ptr += sizeof(y3); - - CGContextAddCurveToPoint(context, x1, y1, x2, y2, x3, y3); - } - break; - - case 7: - { - CGContextClosePath(context); - } - break; - - case 8: - { - CGContextEOFillPath(context); - } - break; + if (cmd == SvgRenderCommandRectsHeader) { + if (ptr + sizeof(uint32_t) > data.length) break; + uint32_t count = 0; + [data getBytes:&count range:NSMakeRange(ptr, sizeof(count))]; + ptr += sizeof(count); - case 9: - { - CGContextFillPath(context); - } - break; + for (uint32_t i = 0; i < count; i++) { + if (ptr + 1 > data.length) { ptr = data.length; break; } + uint8_t itemCmd = 0; + [data getBytes:&itemCmd range:NSMakeRange(ptr, sizeof(itemCmd))]; + ptr += sizeof(itemCmd); + if (itemCmd != SvgRenderCommandRectsItem) { + ptr = data.length; + break; + } - case 10: - { - CGContextStrokePath(context); - } - break; - case 11: - { - uint32_t bgra; - [data getBytes:&bgra range:NSMakeRange(ptr, sizeof(bgra))]; - ptr += sizeof(bgra); + if (ptr + sizeof(float) * 5 > data.length) { + ptr = data.length; + break; + } + float payload[5]; + [data getBytes:payload range:NSMakeRange(ptr, sizeof(payload))]; + ptr += sizeof(payload); - CGContextSetFillColorWithColor(context, colorWithBGRA(bgra).CGColor); - CGContextStrokePath(context); + GiftPatternRect *rect = [[GiftPatternRect alloc] init]; + rect.center = CGPointMake(payload[0], payload[1]); + rect.side = payload[2]; + rect.rotation = payload[3]; + rect.scale = payload[4]; + [rects addObject:rect]; } - default: - break; + continue; + } + + if (!processRenderCommand(cmd, data, &ptr, nil, [UIColor whiteColor])) { + break; } } + + GiftPatternData *patternData = [[GiftPatternData alloc] init]; + patternData.size = CGSizeMake(width, height); + patternData.rects = rects; + return patternData; +} + +UIImage * _Nullable renderPreparedImage(NSData * _Nonnull data, CGSize size, UIColor *backgroundColor, CGFloat scale, bool fit) { + return renderPreparedImageWithSymbol(data, size, backgroundColor, scale, fit, nil, -1); +} + +UIImage * _Nullable renderPreparedImageWithSymbol(NSData * _Nonnull data, CGSize size, UIColor *backgroundColor, CGFloat scale, bool fit, UIImage * _Nullable symbolImage, int32_t modelRectIndex) { + if (!data || data.length < 8) { + return nil; + } + + CFTimeInterval startTime = CACurrentMediaTime(); + UIColor *foregroundColor = [UIColor whiteColor]; + + NSUInteger ptr = 0; + int32_t width, height; + [data getBytes:&width range:NSMakeRange(ptr, sizeof(width))]; + ptr += sizeof(width); + [data getBytes:&height range:NSMakeRange(ptr, sizeof(height))]; + ptr += sizeof(height); + + if (width <= 0 || height <= 0) return nil; + + CGSize svgSize = CGSizeMake(width, height); + if (CGSizeEqualToSize(size, CGSizeZero)) { + size = svgSize; + } + + bool isTransparent = [backgroundColor isEqual:[UIColor clearColor]]; + CGSize drawingSize = fit ? aspectFitSize(svgSize, size) : aspectFillSize(svgSize, size); + + if (fit) size = drawingSize; + + UIGraphicsBeginImageContextWithOptions(size, !isTransparent, scale); + CGContextRef context = UIGraphicsGetCurrentContext(); + if (!context) { + UIGraphicsEndImageContext(); + return nil; + } + + if (isTransparent) { + CGContextClearRect(context, CGRectMake(0, 0, size.width, size.height)); + } else { + CGContextSetFillColorWithColor(context, backgroundColor.CGColor); + CGContextFillRect(context, CGRectMake(0, 0, size.width, size.height)); + } + + CGFloat renderScale = fit ? MIN(size.width / svgSize.width, size.height / svgSize.height) : MAX(size.width / svgSize.width, size.height / svgSize.height); + + CGContextScaleCTM(context, renderScale, renderScale); + CGContextTranslateCTM(context, (size.width - drawingSize.width) / (2.0 * renderScale), (size.height - drawingSize.height) / (2.0 * renderScale)); + + NSMutableArray *rects = symbolImage ? [[NSMutableArray alloc] init] : nil; + + while (ptr < data.length) { + if (ptr + 1 > data.length) break; + + uint8_t cmd; + [data getBytes:&cmd range:NSMakeRange(ptr, sizeof(cmd))]; + ptr += sizeof(cmd); + + if (cmd == SvgRenderCommandRectsHeader) { + if (ptr + sizeof(uint32_t) > data.length) break; + uint32_t count = 0; + [data getBytes:&count range:NSMakeRange(ptr, sizeof(count))]; + ptr += sizeof(count); + for (uint32_t i = 0; i < count; i++) { + if (ptr + 1 > data.length) { ptr = data.length; break; } + uint8_t itemCmd = 0; + [data getBytes:&itemCmd range:NSMakeRange(ptr, sizeof(itemCmd))]; + ptr += sizeof(itemCmd); + if (itemCmd != SvgRenderCommandRectsItem) { ptr = data.length; break; } + + if (ptr + sizeof(float) * 5 > data.length) { ptr = data.length; break; } + float payload[5]; + [data getBytes:payload range:NSMakeRange(ptr, sizeof(payload))]; + ptr += sizeof(payload); + + if (rects) { + GiftPatternRect *rect = [[GiftPatternRect alloc] init]; + rect.center = CGPointMake(payload[0], payload[1]); + rect.side = payload[2]; + rect.rotation = payload[3]; + rect.scale = payload[4]; + [rects addObject:rect]; + } + } + continue; + } + + if (!processRenderCommand(cmd, data, &ptr, &context, foregroundColor)) { + break; + } + } + + if (symbolImage && rects.count > 0) { + int32_t index = 0; + + NSMutableArray *filteredRects = [[NSMutableArray alloc] init]; + for (GiftPatternRect *rect in rects) { + if (rect.center.y > height * 0.1 && rect.center.y < height * 0.9) { + [filteredRects addObject:rect]; + } + } + modelRectIndex = modelRectIndex % (int32_t)filteredRects.count; + + for (GiftPatternRect *rect in filteredRects) { + if (index != modelRectIndex) { + CGContextSaveGState(context); + + CGContextTranslateCTM(context, rect.center.x, rect.center.y); + CGContextRotateCTM(context, rect.rotation); + + CGFloat symbolAspectRatio = (symbolImage.size.height > 0.0) ? (symbolImage.size.width / symbolImage.size.height) : 1.0; + CGFloat drawWidth = rect.side; + CGFloat drawHeight = rect.side; + + if (symbolAspectRatio > 1.0) { + drawHeight = drawWidth / symbolAspectRatio; + } else { + drawWidth = drawHeight * symbolAspectRatio; + } + + CGRect symbolRect = CGRectMake(-drawWidth * 0.5, -drawHeight * 0.5, drawWidth, drawHeight); + [symbolImage drawInRect:symbolRect blendMode:kCGBlendModeNormal alpha:1.0]; + + CGContextRestoreGState(context); + } + index += 1; + } + } + UIImage *resultImage = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); - double deltaTime = -1.0f * [startTime timeIntervalSinceNow]; - printf("drawingTime %fx%f = %f\n", size.width, size.height, deltaTime); + CFTimeInterval deltaTime = CACurrentMediaTime() - startTime; + NSLog(@"Render time %.0fx%.0f = %.4f seconds", size.width, size.height, deltaTime); return resultImage; } -NSData * _Nullable prepareSvgImage(NSData * _Nonnull data, bool template) { - NSDate *startTime = [NSDate date]; +void processShape(NSVGshape *shape, CGContextCoder *context, bool template) { + if (shape->fill.type != NSVG_PAINT_NONE) { + if (template) { + [context setFillColorWithOpacity:shape->opacity]; + } else { + [context setFillColor:shape->fill.color opacity:shape->opacity]; + } + + [context beginPath]; + BOOL hasStartPoint = false; + CGPoint startPoint = CGPointZero; + + for (NSVGpath *path = shape->paths; path; path = path->next) { + if (!hasStartPoint) { + hasStartPoint = true; + startPoint = CGPointMake(path->pts[0], path->pts[1]); + } + + [context moveToPoint:CGPointMake(path->pts[0], path->pts[1])]; + + for (int i = 0; i < path->npts - 1; i += 3) { + float *p = &path->pts[i * 2]; + [context addCurveToPoint:CGPointMake(p[2], p[3]) p2:CGPointMake(p[4], p[5]) p3:CGPointMake(p[6], p[7])]; + } + + if (path->closed && hasStartPoint) { + [context addLineToPoint:startPoint]; + hasStartPoint = NO; + } + } + + switch (shape->fillRule) { + case NSVG_FILLRULE_EVENODD: + [context eoFillPath]; + break; + default: + [context fillPath]; + break; + } + } + + if (shape->stroke.type != NSVG_PAINT_NONE && shape->strokeWidth > 0) { + CGLineCap lineCap = kCGLineCapButt; + CGLineJoin lineJoin = kCGLineJoinMiter; + + switch (shape->strokeLineCap) { + case NSVG_CAP_ROUND: lineCap = kCGLineCapRound; break; + case NSVG_CAP_SQUARE: lineCap = kCGLineCapSquare; break; + default: break; + } + + switch (shape->strokeLineJoin) { + case NSVG_JOIN_BEVEL: lineJoin = kCGLineJoinBevel; break; + case NSVG_JOIN_ROUND: lineJoin = kCGLineJoinRound; break; + default: break; + } + + [context setupStrokeOpacity:shape->opacity + miterLimit:shape->miterLimit + lineWidth:shape->strokeWidth + lineCap:lineCap + lineJoin:lineJoin]; + + for (NSVGpath *path = shape->paths; path; path = path->next) { + [context beginPath]; + [context moveToPoint:CGPointMake(path->pts[0], path->pts[1])]; + + for (int i = 0; i < path->npts - 1; i += 3) { + float *p = &path->pts[i * 2]; + [context addCurveToPoint:CGPointMake(p[2], p[3]) + p2:CGPointMake(p[4], p[5]) + p3:CGPointMake(p[6], p[7])]; + } + + if (path->closed) { + [context closePath]; + } + [context strokePath]; + } + } +} + +NSData * _Nullable prepareSvgImage(NSData * _Nonnull data, BOOL template) { + if (!data || data.length == 0) return nil; + + CFTimeInterval startTime = CACurrentMediaTime(); NSXMLParser *parser = [[NSXMLParser alloc] initWithData:data]; - if (parser == nil) { - return nil; - } + if (!parser) return nil; + SvgXMLParsingDelegate *delegate = [[SvgXMLParsingDelegate alloc] init]; parser.delegate = delegate; - [parser parse]; + if (![parser parse]) return nil; NSMutableString *xmlString = [[NSMutableString alloc] initWithData:data encoding:NSUTF8StringEncoding]; - if (xmlString == nil) { - return nil; - } + if (!xmlString) return nil; for (NSString *styleName in delegate.styles) { NSString *styleValue = delegate.styles[styleName]; - [xmlString replaceOccurrencesOfString:[NSString stringWithFormat:@"class=\"%@\"", styleName] withString:[NSString stringWithFormat:@"style=\"%@\"", styleValue] options:0 range:NSMakeRange(0, xmlString.length)]; + NSString *searchPattern = [NSString stringWithFormat:@"class=\"%@\"", styleName]; + NSString *replacement = [NSString stringWithFormat:@"style=\"%@\"", styleValue]; + [xmlString replaceOccurrencesOfString:searchPattern withString:replacement + options:NSLiteralSearch range:NSMakeRange(0, xmlString.length)]; } - const char *zeroTerminatedData = xmlString.UTF8String; + { + NSError *err = nil; + NSRegularExpression *gx = [NSRegularExpression regularExpressionWithPattern:@"]*\\bid\\s*=\\s*[\"']GiftPatterns[\"'][^>]*>[\\s\\S]*?" options:NSRegularExpressionCaseInsensitive error:&err]; + if (gx) { + xmlString = [[gx stringByReplacingMatchesInString:xmlString options:0 range:NSMakeRange(0, xmlString.length) withTemplate:@""] mutableCopy]; + } + } - NSVGimage *image = nsvgParse((char *)zeroTerminatedData, "px", 96); - if (image == nil || image->width < 1.0f || image->height < 1.0f) { + const char *svgString = xmlString.UTF8String; + NSVGimage *image = nsvgParse((char *)svgString, "px", 96); + if (!image || image->width < 1.0f || image->height < 1.0f) { + if (image) nsvgDelete(image); return nil; } - double deltaTime = -1.0f * [startTime timeIntervalSinceNow]; - printf("parseTime = %f\n", deltaTime); + CFTimeInterval parseTime = CACurrentMediaTime() - startTime; + NSLog(@"Parse time: %.4f seconds", parseTime); - startTime = [NSDate date]; - + startTime = CACurrentMediaTime(); CGContextCoder *context = [[CGContextCoder alloc] initWithSize:CGSizeMake(image->width, image->height)]; - - for (NSVGshape *shape = image->shapes; shape != NULL; shape = shape->next) { - if (!(shape->flags & NSVG_FLAGS_VISIBLE)) { - continue; - } + + for (NSVGshape *shape = image->shapes; shape; shape = shape->next) { + if (!(shape->flags & NSVG_FLAGS_VISIBLE) || shape->opacity <= 0) continue; - if (shape->fill.type != NSVG_PAINT_NONE) { - if (template) { - [context setFillColorWithOpacity:shape->opacity]; - } else { - [context setFillColor:shape->fill.color opacity:shape->opacity]; - } - - bool isFirst = true; - bool hasStartPoint = false; - CGPoint startPoint; - for (NSVGpath *path = shape->paths; path != NULL; path = path->next) { - if (isFirst) { - [context beginPath]; - - isFirst = false; - hasStartPoint = true; - startPoint.x = path->pts[0]; - startPoint.y = path->pts[1]; - } - [context moveToPoint:CGPointMake(path->pts[0], path->pts[1])]; - for (int i = 0; i < path->npts - 1; i += 3) { - float *p = &path->pts[i * 2]; - [context addCurveToPoint:CGPointMake(p[2], p[3]) p2:CGPointMake(p[4], p[5]) p3:CGPointMake(p[6], p[7])]; - } - - if (path->closed) { - if (hasStartPoint) { - hasStartPoint = false; - [context addLineToPoint:startPoint]; - } - } - } - switch (shape->fillRule) { - case NSVG_FILLRULE_EVENODD: - [context eoFillPath]; - break; - default: - [context fillPath]; - break; - } - } - - if (shape->stroke.type != NSVG_PAINT_NONE) { - CGLineCap lineCap = kCGLineCapButt; - CGLineJoin lineJoin = kCGLineJoinMiter; - switch (shape->strokeLineCap) { - case NSVG_CAP_BUTT: - lineCap = kCGLineCapButt; - break; - case NSVG_CAP_ROUND: - lineCap = kCGLineCapRound; - break; - case NSVG_CAP_SQUARE: - lineCap = kCGLineCapSquare; - break; - default: - break; - } - switch (shape->strokeLineJoin) { - case NSVG_JOIN_BEVEL: - lineJoin = kCGLineJoinBevel; - break; - case NSVG_JOIN_MITER: - lineJoin = kCGLineJoinMiter; - break; - case NSVG_JOIN_ROUND: - lineJoin = kCGLineJoinRound; - break; - default: - break; - } - - [context setupStrokeOpacity:shape->opacity mitterLimit:shape->miterLimit lineWidth:shape->strokeWidth lineCap:lineCap lineJoin:lineJoin]; - - for (NSVGpath *path = shape->paths; path != NULL; path = path->next) { - [context beginPath]; - [context moveToPoint:CGPointMake(path->pts[0], path->pts[1])]; - for (int i = 0; i < path->npts - 1; i += 3) { - float *p = &path->pts[i * 2]; - [context addCurveToPoint:CGPointMake(p[2], p[3]) p2:CGPointMake(p[4], p[5]) p3:CGPointMake(p[6], p[7])]; - } - - if (path->closed) { - [context closePath]; - } - [context strokePath]; - } - } + processShape(shape, context, template); } nsvgDelete(image); + + [context storeGiftPatternRects:delegate.giftRects]; + + CFTimeInterval prepTime = CACurrentMediaTime() - startTime; + NSLog(@"Preparation time: %.4f seconds", prepTime); + return context.data; } diff --git a/submodules/TelegramApi/Sources/Api0.swift b/submodules/TelegramApi/Sources/Api0.swift index 9016e7156b..ad00f2f471 100644 --- a/submodules/TelegramApi/Sources/Api0.swift +++ b/submodules/TelegramApi/Sources/Api0.swift @@ -959,7 +959,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[1124938064] = { return Api.SponsoredMessageReportOption.parse_sponsoredMessageReportOption($0) } dict[-963180333] = { return Api.SponsoredPeer.parse_sponsoredPeer($0) } dict[-2136190013] = { return Api.StarGift.parse_starGift($0) } - dict[648369470] = { return Api.StarGift.parse_starGiftUnique($0) } + dict[468707429] = { return Api.StarGift.parse_starGiftUnique($0) } dict[-650279524] = { return Api.StarGiftAttribute.parse_starGiftAttributeBackdrop($0) } dict[970559507] = { return Api.StarGiftAttribute.parse_starGiftAttributeModel($0) } dict[-524291476] = { return Api.StarGiftAttribute.parse_starGiftAttributeOriginalDetails($0) } @@ -1230,7 +1230,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[1674235686] = { return Api.account.AutoDownloadSettings.parse_autoDownloadSettings($0) } dict[1279133341] = { return Api.account.AutoSaveSettings.parse_autoSaveSettings($0) } dict[-331111727] = { return Api.account.BusinessChatLinks.parse_businessChatLinks($0) } - dict[1271855483] = { return Api.account.ChatThemes.parse_chatThemes($0) } + dict[373835863] = { return Api.account.ChatThemes.parse_chatThemes($0) } dict[-535699004] = { return Api.account.ChatThemes.parse_chatThemesNotModified($0) } dict[400029819] = { return Api.account.ConnectedBots.parse_connectedBots($0) } dict[1474462241] = { return Api.account.ContentSettings.parse_contentSettings($0) } diff --git a/submodules/TelegramApi/Sources/Api25.swift b/submodules/TelegramApi/Sources/Api25.swift index c896ec76d6..25f6c4b10d 100644 --- a/submodules/TelegramApi/Sources/Api25.swift +++ b/submodules/TelegramApi/Sources/Api25.swift @@ -289,7 +289,7 @@ public extension Api { public extension Api { enum StarGift: TypeConstructorDescription { case starGift(flags: Int32, id: Int64, sticker: Api.Document, stars: Int64, availabilityRemains: Int32?, availabilityTotal: Int32?, availabilityResale: Int64?, convertStars: Int64, firstSaleDate: Int32?, lastSaleDate: Int32?, upgradeStars: Int64?, resellMinStars: Int64?, title: String?, releasedBy: Api.Peer?, perUserTotal: Int32?, perUserRemains: Int32?, lockedUntilDate: Int32?) - case starGiftUnique(flags: Int32, id: Int64, giftId: Int64, title: String, slug: String, num: Int32, ownerId: Api.Peer?, ownerName: String?, ownerAddress: String?, attributes: [Api.StarGiftAttribute], availabilityIssued: Int32, availabilityTotal: Int32, giftAddress: String?, resellAmount: [Api.StarsAmount]?, releasedBy: Api.Peer?, valueAmount: Int64?, valueCurrency: String?) + case starGiftUnique(flags: Int32, id: Int64, giftId: Int64, title: String, slug: String, num: Int32, ownerId: Api.Peer?, ownerName: String?, ownerAddress: String?, attributes: [Api.StarGiftAttribute], availabilityIssued: Int32, availabilityTotal: Int32, giftAddress: String?, resellAmount: [Api.StarsAmount]?, releasedBy: Api.Peer?, valueAmount: Int64?, valueCurrency: String?, themePeer: Api.Peer?) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { @@ -315,9 +315,9 @@ public extension Api { if Int(flags) & Int(1 << 8) != 0 {serializeInt32(perUserRemains!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 9) != 0 {serializeInt32(lockedUntilDate!, buffer: buffer, boxed: false)} break - case .starGiftUnique(let flags, let id, let giftId, let title, let slug, let num, let ownerId, let ownerName, let ownerAddress, let attributes, let availabilityIssued, let availabilityTotal, let giftAddress, let resellAmount, let releasedBy, let valueAmount, let valueCurrency): + case .starGiftUnique(let flags, let id, let giftId, let title, let slug, let num, let ownerId, let ownerName, let ownerAddress, let attributes, let availabilityIssued, let availabilityTotal, let giftAddress, let resellAmount, let releasedBy, let valueAmount, let valueCurrency, let themePeer): if boxed { - buffer.appendInt32(648369470) + buffer.appendInt32(468707429) } serializeInt32(flags, buffer: buffer, boxed: false) serializeInt64(id, buffer: buffer, boxed: false) @@ -344,6 +344,7 @@ public extension Api { if Int(flags) & Int(1 << 5) != 0 {releasedBy!.serialize(buffer, true)} if Int(flags) & Int(1 << 8) != 0 {serializeInt64(valueAmount!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 8) != 0 {serializeString(valueCurrency!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 10) != 0 {themePeer!.serialize(buffer, true)} break } } @@ -352,8 +353,8 @@ public extension Api { switch self { case .starGift(let flags, let id, let sticker, let stars, let availabilityRemains, let availabilityTotal, let availabilityResale, let convertStars, let firstSaleDate, let lastSaleDate, let upgradeStars, let resellMinStars, let title, let releasedBy, let perUserTotal, let perUserRemains, let lockedUntilDate): return ("starGift", [("flags", flags as Any), ("id", id as Any), ("sticker", sticker as Any), ("stars", stars as Any), ("availabilityRemains", availabilityRemains as Any), ("availabilityTotal", availabilityTotal as Any), ("availabilityResale", availabilityResale as Any), ("convertStars", convertStars as Any), ("firstSaleDate", firstSaleDate as Any), ("lastSaleDate", lastSaleDate as Any), ("upgradeStars", upgradeStars as Any), ("resellMinStars", resellMinStars as Any), ("title", title as Any), ("releasedBy", releasedBy as Any), ("perUserTotal", perUserTotal as Any), ("perUserRemains", perUserRemains as Any), ("lockedUntilDate", lockedUntilDate as Any)]) - case .starGiftUnique(let flags, let id, let giftId, let title, let slug, let num, let ownerId, let ownerName, let ownerAddress, let attributes, let availabilityIssued, let availabilityTotal, let giftAddress, let resellAmount, let releasedBy, let valueAmount, let valueCurrency): - return ("starGiftUnique", [("flags", flags as Any), ("id", id as Any), ("giftId", giftId as Any), ("title", title as Any), ("slug", slug as Any), ("num", num as Any), ("ownerId", ownerId as Any), ("ownerName", ownerName as Any), ("ownerAddress", ownerAddress as Any), ("attributes", attributes as Any), ("availabilityIssued", availabilityIssued as Any), ("availabilityTotal", availabilityTotal as Any), ("giftAddress", giftAddress as Any), ("resellAmount", resellAmount as Any), ("releasedBy", releasedBy as Any), ("valueAmount", valueAmount as Any), ("valueCurrency", valueCurrency as Any)]) + case .starGiftUnique(let flags, let id, let giftId, let title, let slug, let num, let ownerId, let ownerName, let ownerAddress, let attributes, let availabilityIssued, let availabilityTotal, let giftAddress, let resellAmount, let releasedBy, let valueAmount, let valueCurrency, let themePeer): + return ("starGiftUnique", [("flags", flags as Any), ("id", id as Any), ("giftId", giftId as Any), ("title", title as Any), ("slug", slug as Any), ("num", num as Any), ("ownerId", ownerId as Any), ("ownerName", ownerName as Any), ("ownerAddress", ownerAddress as Any), ("attributes", attributes as Any), ("availabilityIssued", availabilityIssued as Any), ("availabilityTotal", availabilityTotal as Any), ("giftAddress", giftAddress as Any), ("resellAmount", resellAmount as Any), ("releasedBy", releasedBy as Any), ("valueAmount", valueAmount as Any), ("valueCurrency", valueCurrency as Any), ("themePeer", themePeer as Any)]) } } @@ -463,6 +464,10 @@ public extension Api { if Int(_1!) & Int(1 << 8) != 0 {_16 = reader.readInt64() } var _17: String? if Int(_1!) & Int(1 << 8) != 0 {_17 = parseString(reader) } + var _18: Api.Peer? + if Int(_1!) & Int(1 << 10) != 0 {if let signature = reader.readInt32() { + _18 = Api.parse(reader, signature: signature) as? Api.Peer + } } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil @@ -480,8 +485,9 @@ public extension Api { let _c15 = (Int(_1!) & Int(1 << 5) == 0) || _15 != nil let _c16 = (Int(_1!) & Int(1 << 8) == 0) || _16 != nil let _c17 = (Int(_1!) & Int(1 << 8) == 0) || _17 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 && _c15 && _c16 && _c17 { - return Api.StarGift.starGiftUnique(flags: _1!, id: _2!, giftId: _3!, title: _4!, slug: _5!, num: _6!, ownerId: _7, ownerName: _8, ownerAddress: _9, attributes: _10!, availabilityIssued: _11!, availabilityTotal: _12!, giftAddress: _13, resellAmount: _14, releasedBy: _15, valueAmount: _16, valueCurrency: _17) + let _c18 = (Int(_1!) & Int(1 << 10) == 0) || _18 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 && _c15 && _c16 && _c17 && _c18 { + return Api.StarGift.starGiftUnique(flags: _1!, id: _2!, giftId: _3!, title: _4!, slug: _5!, num: _6!, ownerId: _7, ownerName: _8, ownerAddress: _9, attributes: _10!, availabilityIssued: _11!, availabilityTotal: _12!, giftAddress: _13, resellAmount: _14, releasedBy: _15, valueAmount: _16, valueCurrency: _17, themePeer: _18) } else { return nil diff --git a/submodules/TelegramApi/Sources/Api29.swift b/submodules/TelegramApi/Sources/Api29.swift index ee37df45c0..b2a68ca7b1 100644 --- a/submodules/TelegramApi/Sources/Api29.swift +++ b/submodules/TelegramApi/Sources/Api29.swift @@ -570,14 +570,14 @@ public extension Api.account { } public extension Api.account { enum ChatThemes: TypeConstructorDescription { - case chatThemes(flags: Int32, hash: Int64, themes: [Api.ChatTheme], nextOffset: Int32?) + case chatThemes(flags: Int32, hash: Int64, themes: [Api.ChatTheme], chats: [Api.Chat], users: [Api.User], nextOffset: Int32?) case chatThemesNotModified public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .chatThemes(let flags, let hash, let themes, let nextOffset): + case .chatThemes(let flags, let hash, let themes, let chats, let users, let nextOffset): if boxed { - buffer.appendInt32(1271855483) + buffer.appendInt32(373835863) } serializeInt32(flags, buffer: buffer, boxed: false) serializeInt64(hash, buffer: buffer, boxed: false) @@ -586,6 +586,16 @@ public extension Api.account { for item in themes { item.serialize(buffer, true) } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(chats.count)) + for item in chats { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(users.count)) + for item in users { + item.serialize(buffer, true) + } if Int(flags) & Int(1 << 0) != 0 {serializeInt32(nextOffset!, buffer: buffer, boxed: false)} break case .chatThemesNotModified: @@ -599,8 +609,8 @@ public extension Api.account { public func descriptionFields() -> (String, [(String, Any)]) { switch self { - case .chatThemes(let flags, let hash, let themes, let nextOffset): - return ("chatThemes", [("flags", flags as Any), ("hash", hash as Any), ("themes", themes as Any), ("nextOffset", nextOffset as Any)]) + case .chatThemes(let flags, let hash, let themes, let chats, let users, let nextOffset): + return ("chatThemes", [("flags", flags as Any), ("hash", hash as Any), ("themes", themes as Any), ("chats", chats as Any), ("users", users as Any), ("nextOffset", nextOffset as Any)]) case .chatThemesNotModified: return ("chatThemesNotModified", []) } @@ -615,14 +625,24 @@ public extension Api.account { if let _ = reader.readInt32() { _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.ChatTheme.self) } - var _4: Int32? - if Int(_1!) & Int(1 << 0) != 0 {_4 = reader.readInt32() } + var _4: [Api.Chat]? + if let _ = reader.readInt32() { + _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) + } + var _5: [Api.User]? + if let _ = reader.readInt32() { + _5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + } + var _6: Int32? + if Int(_1!) & Int(1 << 0) != 0 {_6 = reader.readInt32() } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil - let _c4 = (Int(_1!) & Int(1 << 0) == 0) || _4 != nil - if _c1 && _c2 && _c3 && _c4 { - return Api.account.ChatThemes.chatThemes(flags: _1!, hash: _2!, themes: _3!, nextOffset: _4) + let _c4 = _4 != nil + let _c5 = _5 != nil + let _c6 = (Int(_1!) & Int(1 << 0) == 0) || _6 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { + return Api.account.ChatThemes.chatThemes(flags: _1!, hash: _2!, themes: _3!, chats: _4!, users: _5!, nextOffset: _6) } else { return nil diff --git a/submodules/TelegramCore/Sources/State/ManagedRecentStickers.swift b/submodules/TelegramCore/Sources/State/ManagedRecentStickers.swift index 778fd37a4c..f3400dbef0 100644 --- a/submodules/TelegramCore/Sources/State/ManagedRecentStickers.swift +++ b/submodules/TelegramCore/Sources/State/ManagedRecentStickers.swift @@ -359,7 +359,9 @@ func managedUniqueStarGifts(accountPeerId: PeerId, postbox: Postbox, network: Ne resellForTonOnly: false, releasedBy: nil, valueAmount: nil, - valueCurrency: nil + valueCurrency: nil, + flags: [], + themePeerId: nil ) if let entry = CodableEntry(RecentStarGiftItem(gift)) { items.append(OrderedItemListEntry(id: RecentStarGiftItemId(id).rawValue, contents: entry)) diff --git a/submodules/TelegramCore/Sources/State/Serialization.swift b/submodules/TelegramCore/Sources/State/Serialization.swift index 51d3094bf3..1de180df61 100644 --- a/submodules/TelegramCore/Sources/State/Serialization.swift +++ b/submodules/TelegramCore/Sources/State/Serialization.swift @@ -210,7 +210,7 @@ public class BoxedMessage: NSObject { public class Serialization: NSObject, MTSerialization { public func currentLayer() -> UInt { - return 213 + return 214 } public func parseMessage(_ data: Data!) -> Any! { diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramWallpaper.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramWallpaper.swift index 0872978f07..b96ac65480 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramWallpaper.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramWallpaper.swift @@ -230,6 +230,37 @@ public enum TelegramWallpaper: Equatable { self.file = file self.settings = settings } + + public static func ==(lhs: File, rhs: File) -> Bool { + if lhs.id != rhs.id { + return false + } + if lhs.accessHash != rhs.accessHash { + return false + } + if lhs.isCreator != rhs.isCreator { + return false + } + if lhs.isDefault != rhs.isDefault { + return false + } + if lhs.isPattern != rhs.isPattern { + return false + } + if lhs.isDark != rhs.isDark { + return false + } + if lhs.slug != rhs.slug { + return false + } + if lhs.file.fileId != rhs.file.fileId { + return false + } + if lhs.settings != rhs.settings { + return false + } + return true + } } case builtin(WallpaperSettings) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift b/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift index eceaab249d..305f4882f0 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift @@ -2303,6 +2303,39 @@ public extension TelegramEngine.EngineData.Item { } } + public struct CopyProtectionEnabled: TelegramEngineDataItem, TelegramEngineMapKeyDataItem, PostboxViewDataItem { + public typealias Result = Bool + + fileprivate var id: EnginePeer.Id + public var mapKey: EnginePeer.Id { + return self.id + } + + public init(id: EnginePeer.Id) { + self.id = id + } + + var key: PostboxViewKey { + return .basicPeer(self.id) + } + + func extract(view: PostboxView) -> Result { + guard let view = view as? BasicPeerView else { + preconditionFailure() + } + guard let peer = view.peer else { + return false + } + if let group = peer as? TelegramGroup { + return group.flags.contains(.copyProtectionEnabled) + } else if let channel = peer as? TelegramChannel { + return channel.flags.contains(.copyProtectionEnabled) + } else { + return false + } + } + } + public struct BotPreview: TelegramEngineDataItem, TelegramEngineMapKeyDataItem, PostboxViewDataItem { public typealias Result = CachedUserData.BotPreview? diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift index dc20722766..055890e327 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift @@ -318,6 +318,18 @@ public enum StarGift: Equatable, Codable, PostboxCoding { case releasedBy case valueAmount case valueCurrency + case flags + case themePeerId + } + + public struct Flags: OptionSet { + public var rawValue: Int32 + + public init(rawValue: Int32) { + self.rawValue = rawValue + } + + public static let isThemeAvailable = Flags(rawValue: 1 << 0) } public enum Attribute: Equatable, Codable, PostboxCoding { @@ -593,8 +605,10 @@ public enum StarGift: Equatable, Codable, PostboxCoding { public let releasedBy: EnginePeer.Id? public let valueAmount: Int64? public let valueCurrency: String? + public let flags: Flags + public let themePeerId: EnginePeer.Id? - public init(id: Int64, giftId: Int64, title: String, number: Int32, slug: String, owner: Owner, attributes: [Attribute], availability: Availability, giftAddress: String?, resellAmounts: [CurrencyAmount]?, resellForTonOnly: Bool, releasedBy: EnginePeer.Id?, valueAmount: Int64?, valueCurrency: String?) { + public init(id: Int64, giftId: Int64, title: String, number: Int32, slug: String, owner: Owner, attributes: [Attribute], availability: Availability, giftAddress: String?, resellAmounts: [CurrencyAmount]?, resellForTonOnly: Bool, releasedBy: EnginePeer.Id?, valueAmount: Int64?, valueCurrency: String?, flags: Flags, themePeerId: EnginePeer.Id?) { self.id = id self.giftId = giftId self.title = title @@ -609,6 +623,8 @@ public enum StarGift: Equatable, Codable, PostboxCoding { self.releasedBy = releasedBy self.valueAmount = valueAmount self.valueCurrency = valueCurrency + self.flags = flags + self.themePeerId = themePeerId } public init(from decoder: Decoder) throws { @@ -641,6 +657,8 @@ public enum StarGift: Equatable, Codable, PostboxCoding { self.releasedBy = try container.decodeIfPresent(EnginePeer.Id.self, forKey: .releasedBy) self.valueAmount = try container.decodeIfPresent(Int64.self, forKey: .valueAmount) self.valueCurrency = try container.decodeIfPresent(String.self, forKey: .valueCurrency) + self.flags = try container.decodeIfPresent(Int32.self, forKey: .flags).flatMap { Flags(rawValue: $0) } ?? [] + self.themePeerId = try container.decodeIfPresent(Int64.self, forKey: .themePeerId).flatMap { EnginePeer.Id($0) } } public init(decoder: PostboxDecoder) { @@ -672,6 +690,8 @@ public enum StarGift: Equatable, Codable, PostboxCoding { self.releasedBy = decoder.decodeOptionalInt64ForKey(CodingKeys.releasedBy.rawValue).flatMap { EnginePeer.Id($0) } self.valueAmount = decoder.decodeOptionalInt64ForKey(CodingKeys.valueAmount.rawValue) self.valueCurrency = decoder.decodeOptionalStringForKey(CodingKeys.valueCurrency.rawValue) + self.flags = decoder.decodeOptionalInt32ForKey(CodingKeys.flags.rawValue).flatMap { Flags(rawValue: $0) } ?? [] + self.themePeerId = decoder.decodeOptionalInt64ForKey(CodingKeys.themePeerId.rawValue).flatMap { EnginePeer.Id($0) } } public func encode(to encoder: Encoder) throws { @@ -697,6 +717,8 @@ public enum StarGift: Equatable, Codable, PostboxCoding { try container.encodeIfPresent(self.releasedBy, forKey: .releasedBy) try container.encodeIfPresent(self.valueAmount, forKey: .valueAmount) try container.encodeIfPresent(self.valueCurrency, forKey: .valueCurrency) + try container.encode(self.flags.rawValue, forKey: .flags) + try container.encodeIfPresent(self.themePeerId?.toInt64(), forKey: .themePeerId) } public func encode(_ encoder: PostboxEncoder) { @@ -738,6 +760,12 @@ public enum StarGift: Equatable, Codable, PostboxCoding { encoder.encodeNil(forKey: CodingKeys.valueAmount.rawValue) encoder.encodeNil(forKey: CodingKeys.valueCurrency.rawValue) } + encoder.encodeInt32(self.flags.rawValue, forKey: CodingKeys.flags.rawValue) + if let themePeerId = self.themePeerId { + encoder.encodeInt64(themePeerId.toInt64(), forKey: CodingKeys.themePeerId.rawValue) + } else { + encoder.encodeNil(forKey: CodingKeys.themePeerId.rawValue) + } } public func withResellAmounts(_ resellAmounts: [CurrencyAmount]?) -> UniqueGift { @@ -755,7 +783,9 @@ public enum StarGift: Equatable, Codable, PostboxCoding { resellForTonOnly: self.resellForTonOnly, releasedBy: self.releasedBy, valueAmount: self.valueAmount, - valueCurrency: self.valueCurrency + valueCurrency: self.valueCurrency, + flags: self.flags, + themePeerId: self.themePeerId ) } @@ -774,7 +804,30 @@ public enum StarGift: Equatable, Codable, PostboxCoding { resellForTonOnly: resellForTonOnly, releasedBy: self.releasedBy, valueAmount: self.valueAmount, - valueCurrency: self.valueCurrency + valueCurrency: self.valueCurrency, + flags: self.flags, + themePeerId: self.themePeerId + ) + } + + public func withThemePeerId(_ themePeerId: EnginePeer.Id?) -> UniqueGift { + return UniqueGift( + id: self.id, + giftId: self.giftId, + title: self.title, + number: self.number, + slug: self.slug, + owner: self.owner, + attributes: self.attributes, + availability: self.availability, + giftAddress: self.giftAddress, + resellAmounts: self.resellAmounts, + resellForTonOnly: self.resellForTonOnly, + releasedBy: self.releasedBy, + valueAmount: self.valueAmount, + valueCurrency: self.valueCurrency, + flags: self.flags, + themePeerId: themePeerId ) } } @@ -884,7 +937,7 @@ extension StarGift { return nil } self = .generic(StarGift.Gift(id: id, title: title, file: file, price: stars, convertStars: convertStars, availability: availability, soldOut: soldOut, flags: flags, upgradeStars: upgradeStars, releasedBy: releasedBy?.peerId, perUserLimit: perUserLimit, lockedUntilDate: lockedUntilDate)) - case let .starGiftUnique(flags, id, giftId, title, slug, num, ownerPeerId, ownerName, ownerAddress, attributes, availabilityIssued, availabilityTotal, giftAddress, resellAmounts, releasedBy, valueAmount, valueCurrency): + case let .starGiftUnique(apiFlags, id, giftId, title, slug, num, ownerPeerId, ownerName, ownerAddress, attributes, availabilityIssued, availabilityTotal, giftAddress, resellAmounts, releasedBy, valueAmount, valueCurrency, themePeer): let owner: StarGift.UniqueGift.Owner if let ownerAddress { owner = .address(ownerAddress) @@ -896,7 +949,11 @@ extension StarGift { return nil } let resellAmounts = resellAmounts?.compactMap { CurrencyAmount(apiAmount: $0) } - self = .unique(StarGift.UniqueGift(id: id, giftId: giftId, title: title, number: num, slug: slug, owner: owner, attributes: attributes.compactMap { UniqueGift.Attribute(apiAttribute: $0) }, availability: UniqueGift.Availability(issued: availabilityIssued, total: availabilityTotal), giftAddress: giftAddress, resellAmounts: resellAmounts, resellForTonOnly: (flags & (1 << 7)) != 0, releasedBy: releasedBy?.peerId, valueAmount: valueAmount, valueCurrency: valueCurrency)) + var flags = StarGift.UniqueGift.Flags() + if (apiFlags & (1 << 9)) != 0 { + flags.insert(.isThemeAvailable) + } + self = .unique(StarGift.UniqueGift(id: id, giftId: giftId, title: title, number: num, slug: slug, owner: owner, attributes: attributes.compactMap { UniqueGift.Attribute(apiAttribute: $0) }, availability: UniqueGift.Availability(issued: availabilityIssued, total: availabilityTotal), giftAddress: giftAddress, resellAmounts: resellAmounts, resellForTonOnly: (apiFlags & (1 << 7)) != 0, releasedBy: releasedBy?.peerId, valueAmount: valueAmount, valueCurrency: valueCurrency, flags: flags, themePeerId: themePeer?.peerId)) } } } @@ -1322,6 +1379,7 @@ private final class ProfileGiftsContextImpl { private var sorting: ProfileGiftsContext.Sorting private var filter: ProfileGiftsContext.Filters + private var limit: Int32 private var gifts: [ProfileGiftsContext.State.StarGift] = [] private var count: Int32? @@ -1345,7 +1403,8 @@ private final class ProfileGiftsContextImpl { peerId: EnginePeer.Id, collectionId: Int32?, sorting: ProfileGiftsContext.Sorting, - filter: ProfileGiftsContext.Filters + filter: ProfileGiftsContext.Filters, + limit: Int32 ) { self.queue = queue self.account = account @@ -1353,6 +1412,7 @@ private final class ProfileGiftsContextImpl { self.collectionId = collectionId self.sorting = sorting self.filter = filter + self.limit = limit self.loadMore() } @@ -1377,6 +1437,7 @@ private final class ProfileGiftsContextImpl { let postbox = self.account.postbox let filter = self.filter let sorting = self.sorting + let limit = self.limit let isFiltered = self.filter != .All || self.sorting != .date if !isFiltered { @@ -1464,7 +1525,7 @@ private final class ProfileGiftsContextImpl { if !filter.contains(.unique) { flags |= (1 << 4) } - return network.request(Api.functions.payments.getSavedStarGifts(flags: flags, peer: inputPeer, collectionId: collectionId, offset: initialNextOffset ?? "", limit: 36)) + return network.request(Api.functions.payments.getSavedStarGifts(flags: flags, peer: inputPeer, collectionId: collectionId, offset: initialNextOffset ?? "", limit: limit)) |> map(Optional.init) |> `catch` { _ -> Signal in return .single(nil) @@ -2363,14 +2424,15 @@ public final class ProfileGiftsContext { peerId: EnginePeer.Id, collectionId: Int32? = nil, sorting: ProfileGiftsContext.Sorting = .date, - filter: ProfileGiftsContext.Filters = .All + filter: ProfileGiftsContext.Filters = .All, + limit: Int32 = 36 ) { self.peerId = peerId self.collectionId = collectionId let queue = self.queue self.impl = QueueLocalObject(queue: queue, generate: { - return ProfileGiftsContextImpl(queue: queue, account: account, peerId: peerId, collectionId: collectionId, sorting: sorting, filter: filter) + return ProfileGiftsContextImpl(queue: queue, account: account, peerId: peerId, collectionId: collectionId, sorting: sorting, filter: filter, limit: limit) }) } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift index f2c19cb801..1b1e7ba771 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift @@ -1678,6 +1678,8 @@ func _internal_sendStarsPaymentForm(account: Account, formId: Int64, source: Bot return .fail(.alreadyPaid) } else if error.errorDescription == "STARGIFT_USAGE_LIMITED" { return .fail(.starGiftOutOfStock) + } else if error.errorDescription == "STARGIFT_USER_USAGE_LIMITED" { + return .fail(.starGiftUserLimit) } return .fail(.generic) } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Themes/ChatThemes.swift b/submodules/TelegramCore/Sources/TelegramEngine/Themes/ChatThemes.swift index 35785fd745..d946ce72ca 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Themes/ChatThemes.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Themes/ChatThemes.swift @@ -104,20 +104,20 @@ public enum ChatTheme: PostboxCoding, Codable, Equatable { } else { return false } - case let .gift(lhsGift, _): - if case let .gift(rhsGift, _) = rhs { + case let .gift(lhsGift, lhsThemeSettings): + if case let .gift(rhsGift, rhsThemeSettings) = rhs { switch lhsGift { - case .generic(let lhsGeneric): + case let .generic(lhsGeneric): switch rhsGift { - case .generic(let rhsGeneric): - return lhsGeneric == rhsGeneric + case let .generic(rhsGeneric): + return lhsGeneric == rhsGeneric && lhsThemeSettings == rhsThemeSettings default: return false } - case .unique(let lhsUnique): + case let .unique(lhsUnique): switch rhsGift { - case .unique(let rhsUnique): - return lhsUnique.id == rhsUnique.id + case let .unique(rhsUnique): + return lhsUnique.slug == rhsUnique.slug && lhsThemeSettings == rhsThemeSettings default: return false } @@ -148,6 +148,20 @@ public enum ChatTheme: PostboxCoding, Codable, Equatable { } } } + + public func withThemePeerId(_ themePeerId: EnginePeer.Id?) -> ChatTheme { + switch self { + case .emoticon: + return self + case let .gift(gift, themeSettings): + switch gift { + case let .unique(uniqueGift): + return .gift(.unique(uniqueGift.withThemePeerId(themePeerId)), themeSettings) + case .generic: + return self + } + } + } } extension ChatTheme { @@ -235,6 +249,22 @@ func _internal_setChatTheme(account: Account, peerId: PeerId, chatTheme: ChatThe return .complete() } return account.postbox.transaction { transaction -> Signal in + var chatTheme = chatTheme + if case let .gift(gift, _) = chatTheme, case let .unique(uniqueGift) = gift, let previousThemePeerId = uniqueGift.themePeerId { + transaction.updatePeerCachedData(peerIds: Set([previousThemePeerId]), update: { _, current in + if let current = current as? CachedUserData { + return current.withUpdatedChatTheme(nil) + } else if let current = current as? CachedGroupData { + return current.withUpdatedChatTheme(nil) + } else if let current = current as? CachedChannelData { + return current.withUpdatedChatTheme(nil) + } else { + return current + } + }) + } + chatTheme = chatTheme?.withThemePeerId(peerId) + transaction.updatePeerCachedData(peerIds: Set([peerId]), update: { _, current in if let current = current as? CachedUserData { return current.withUpdatedChatTheme(chatTheme) @@ -246,6 +276,7 @@ func _internal_setChatTheme(account: Account, peerId: PeerId, chatTheme: ChatThe return current } }) + let inputTheme: Api.InputChatTheme if let chatTheme { inputTheme = chatTheme.apiChatTheme @@ -466,7 +497,7 @@ public final class UniqueGiftChatThemesContext { private let cacheDisposable = MetaDisposable() private var themes: [ChatTheme] = [] - private var nextOffset: Int32? + private var nextOffset: Int32 = 0 private var dataState: UniqueGiftChatThemesContext.State.DataState = .ready(canLoadMore: true) private let stateValue = Promise() @@ -487,7 +518,7 @@ public final class UniqueGiftChatThemesContext { public func reload() { self.themes = [] - self.nextOffset = nil + self.nextOffset = 0 self.dataState = .ready(canLoadMore: true) self.loadMore(reload: true) } @@ -495,6 +526,7 @@ public final class UniqueGiftChatThemesContext { public func loadMore(reload: Bool = false) { let network = self.account.network let postbox = self.account.postbox + let accountPeerId = self.account.peerId let dataState = self.dataState let offset = self.nextOffset @@ -520,13 +552,24 @@ public final class UniqueGiftChatThemesContext { self.pushState() } - let signal = network.request(Api.functions.account.getUniqueGiftChatThemes(offset: offset ?? 0, limit: 32, hash: 0)) - |> map { result -> ([ChatTheme], Int32?) in - switch result { - case let .chatThemes(_, _, themes, nextOffset): - return (themes.compactMap { ChatTheme(apiChatTheme: $0) }, nextOffset) - case .chatThemesNotModified: - return ([], nil) + let signal = network.request(Api.functions.account.getUniqueGiftChatThemes(offset: offset, limit: 50, hash: 0)) + |> map(Optional.init) + |> `catch` { error in + return .single(nil) + } + |> mapToSignal { result -> Signal<([ChatTheme], Int32?), NoError> in + guard let result else { + return .single(([], nil)) + } + return postbox.transaction { transaction -> ([ChatTheme], Int32?) in + switch result { + case let .chatThemes(_, _, themes, chats, users, nextOffset): + let parsedPeers = AccumulatedPeers(transaction: transaction, chats: chats, users: users) + updatePeers(transaction: transaction, accountPeerId: accountPeerId, peers: parsedPeers) + return (themes.compactMap { ChatTheme(apiChatTheme: $0) }, nextOffset) + case .chatThemesNotModified: + return ([], nil) + } } } @@ -545,7 +588,9 @@ public final class UniqueGiftChatThemesContext { } else { self.themes.append(contentsOf: themes) } - + if let nextOffset { + self.nextOffset = nextOffset + } self.dataState = .ready(canLoadMore: nextOffset != nil) self.pushState() })) diff --git a/submodules/TelegramPresentationData/Sources/MakePresentationTheme.swift b/submodules/TelegramPresentationData/Sources/MakePresentationTheme.swift index 0698eab825..3f0398e984 100644 --- a/submodules/TelegramPresentationData/Sources/MakePresentationTheme.swift +++ b/submodules/TelegramPresentationData/Sources/MakePresentationTheme.swift @@ -47,13 +47,36 @@ public func makePresentationTheme(cloudTheme: TelegramTheme, dark: Bool = false) } else { settings = nil } - guard let settings = settings else { + guard let settings else { return nil } let defaultTheme = makeDefaultPresentationTheme(reference: PresentationBuiltinThemeReference(baseTheme: settings.baseTheme), extendingThemeReference: .cloud(PresentationCloudTheme(theme: cloudTheme, resolvedWallpaper: nil, creatorAccountId: nil)), serviceBackgroundColor: nil, preview: false) return customizePresentationTheme(defaultTheme, editing: true, accentColor: UIColor(argb: settings.accentColor), outgoingAccentColor: settings.outgoingAccentColor.flatMap { UIColor(argb: $0) }, backgroundColors: [], bubbleColors: settings.messageColors, animateBubbleColors: settings.animateMessageColors, wallpaper: settings.wallpaper) } +public func makePresentationTheme(chatTheme: ChatTheme, dark: Bool = false) -> PresentationTheme? { + guard case let .gift(_, themeSettings) = chatTheme else { + return nil + } + let settings: TelegramThemeSettings? + if let exactSettings = themeSettings.first(where: { dark ? ($0.baseTheme == .night || $0.baseTheme == .tinted) : ($0.baseTheme == .classic || $0.baseTheme == .day) }) { + settings = exactSettings + } else if let firstSettings = themeSettings.first { + settings = firstSettings + } else { + settings = nil + } + guard let settings else { + return nil + } + let defaultTheme = makeDefaultPresentationTheme(reference: PresentationBuiltinThemeReference(baseTheme: settings.baseTheme), serviceBackgroundColor: nil, preview: false) + let theme = customizePresentationTheme(defaultTheme, editing: false, accentColor: UIColor(rgb: settings.accentColor), outgoingAccentColor: settings.outgoingAccentColor.flatMap { UIColor(rgb: $0) }, backgroundColors: [], bubbleColors: settings.messageColors, animateBubbleColors: settings.animateMessageColors, wallpaper: settings.wallpaper) + if case let .gift(starGiftValue, _) = chatTheme { + theme.starGift = starGiftValue + } + return theme +} + public func makePresentationTheme(cloudTheme: TelegramTheme, baseTheme: TelegramBaseTheme? = nil) -> PresentationTheme? { let settings: TelegramThemeSettings? if let exactSettings = cloudTheme.settings?.first(where: { $0.baseTheme == baseTheme }) { diff --git a/submodules/TelegramPresentationData/Sources/PresentationTheme.swift b/submodules/TelegramPresentationData/Sources/PresentationTheme.swift index 2f71522c77..f7cdcc3d64 100644 --- a/submodules/TelegramPresentationData/Sources/PresentationTheme.swift +++ b/submodules/TelegramPresentationData/Sources/PresentationTheme.swift @@ -1574,6 +1574,7 @@ public final class PresentationTheme: Equatable { public let chart: PresentationThemeChart public let preview: Bool public var forceSync: Bool = false + public var starGift: StarGift? public let resourceCache: PresentationsResourceCache = PresentationsResourceCache() diff --git a/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift b/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift index 32f5110a72..0f9bc6fdf7 100644 --- a/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift +++ b/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift @@ -781,12 +781,15 @@ public func universalServiceMessageString(presentationData: (PresentationTheme, } else { var emoji = "" var additionalAttributes: [String: Any] = [:] + var giftTitle: String? switch chatTheme { case let .emoticon(emoticon): emoji = emoticon case let .gift(starGift, _): var file: TelegramMediaFile? + if case let .unique(uniqueGift) = starGift { + giftTitle = "\(uniqueGift.title) #\(formatCollectibleNumber(uniqueGift.number, dateTimeFormat: dateTimeFormat))" for attribute in uniqueGift.attributes { if case let .model(_, fileValue, _) = attribute { file = fileValue @@ -802,11 +805,21 @@ public func universalServiceMessageString(presentationData: (PresentationTheme, if message.author?.id.namespace == Namespaces.Peer.CloudChannel { attributedString = NSAttributedString(string: strings.Notification_ChannelChangedTheme(emoji).string, font: titleFont, textColor: primaryTextColor) } else if message.author?.id == accountPeerId { - let resultTitleString = strings.Notification_YouChangedTheme(emoji) - attributedString = addAttributesToStringWithRanges(resultTitleString._tuple, body: bodyAttributes, argumentAttributes: [0: emojiAttributes]) + if let giftTitle { + let resultTitleString = strings.Notification_YouChangedThemeGift(giftTitle) + attributedString = addAttributesToStringWithRanges(resultTitleString._tuple, body: bodyAttributes, argumentAttributes: [:]) + } else { + let resultTitleString = strings.Notification_YouChangedTheme(emoji) + attributedString = addAttributesToStringWithRanges(resultTitleString._tuple, body: bodyAttributes, argumentAttributes: [0: emojiAttributes]) + } } else { - let resultTitleString = strings.Notification_ChangedTheme(compactAuthorName, emoji) - attributedString = addAttributesToStringWithRanges(resultTitleString._tuple, body: bodyAttributes, argumentAttributes: [0: boldAttributes, 1: emojiAttributes]) + if let giftTitle { + let resultTitleString = strings.Notification_ChangedThemeGift(compactAuthorName, giftTitle) + attributedString = addAttributesToStringWithRanges(resultTitleString._tuple, body: bodyAttributes, argumentAttributes: [0: boldAttributes, 1: boldAttributes]) + } else { + let resultTitleString = strings.Notification_ChangedTheme(compactAuthorName, emoji) + attributedString = addAttributesToStringWithRanges(resultTitleString._tuple, body: bodyAttributes, argumentAttributes: [0: boldAttributes, 1: emojiAttributes]) + } } } case let .webViewData(text): @@ -817,8 +830,7 @@ public func universalServiceMessageString(presentationData: (PresentationTheme, } else { let price: String if currency == "XTR" { - //TODO:localize - price = "\(amount) Stars" + price = strings.Notification_PremiumGift_Stars(Int32(clamping: amount)) } else { price = formatCurrencyAmount(amount, currency: currency) } @@ -833,7 +845,8 @@ public func universalServiceMessageString(presentationData: (PresentationTheme, case let .giftStars(currency, amount, count, _, _, _): let _ = count if !forAdditionalServiceMessage { - attributedString = NSAttributedString(string: strings.Notification_Gift, font: titleFont, textColor: primaryTextColor) + let starsPrice = strings.Notification_GiftStars_Stars(Int32(clamping: count)) + attributedString = NSAttributedString(string: strings.Notification_GiftStars(starsPrice).string, font: titleFont, textColor: primaryTextColor) } else { let price = formatCurrencyAmount(amount, currency: currency) if message.author?.id == accountPeerId { @@ -1155,17 +1168,22 @@ public func universalServiceMessageString(presentationData: (PresentationTheme, attributedString = mutableString case .prizeStars: attributedString = NSAttributedString(string: strings.Notification_StarsPrize, font: titleFont, textColor: primaryTextColor) - case let .starGift(gift, _, text, entities, _, _, _, _, _, upgradeStars, _, isPrepaidUpgrade, _, peerId, senderId, _, _, _, _): + case let .starGift(gift, _, text, entities, _, _, _, _, _, upgradeStars, _, isPrepaidUpgrade, _, peerId, senderId, _, _, _, upgradeSeparate): if !forAdditionalServiceMessage { if let text { let mutableAttributedString = NSMutableAttributedString(attributedString: stringWithAppliedEntities(text, entities: entities ?? [], baseColor: primaryTextColor, linkColor: primaryTextColor, baseFont: titleFont, linkFont: titleBoldFont, boldFont: titleBoldFont, italicFont: titleFont, boldItalicFont: titleBoldFont, fixedFont: titleFont, blockQuoteFont: titleFont, underlineLinks: false, message: message._asMessage())) attributedString = mutableAttributedString } else { - attributedString = NSAttributedString(string: strings.Notification_Gift, font: titleFont, textColor: primaryTextColor) + if isPrepaidUpgrade { + let starsPrice = strings.Notification_PrepaidGiftUpgrade_Stars(Int32(clamping: upgradeStars ?? 0)) + attributedString = NSAttributedString(string: strings.Notification_PrepaidGiftUpgrade(starsPrice).string, font: titleFont, textColor: primaryTextColor) + } else { + attributedString = NSAttributedString(string: strings.Notification_Gift, font: titleFont, textColor: primaryTextColor) + } } } else if case let .generic(gift) = gift { var finalPrice = gift.price - if let upgradeStars { + if let upgradeStars, !upgradeSeparate { finalPrice += upgradeStars } let starsPrice = strings.Notification_StarsGift_Stars(Int32(clamping: finalPrice)) diff --git a/submodules/TelegramUI/BUILD b/submodules/TelegramUI/BUILD index 2f5be92e7b..3003487c92 100644 --- a/submodules/TelegramUI/BUILD +++ b/submodules/TelegramUI/BUILD @@ -485,6 +485,7 @@ swift_library( "//submodules/TelegramUI/Components/Stars/BalanceNeededScreen", "//submodules/TelegramUI/Components/FaceScanScreen", "//submodules/TelegramUI/Components/MediaManager/PeerMessagesMediaPlaylist", + "//submodules/TelegramUI/Components/ChatThemeScreen", "//submodules/ContactsHelper", ] + select({ "@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets, diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift index 482cc2b1a3..57dcfda475 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift @@ -241,6 +241,8 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([ } else if case .joinedChannel = action.action { result.append((message, ChatMessageJoinedChannelBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default))) needReactions = false + } else if case let .setChatTheme(chatTheme) = action.action, case .gift = chatTheme { + result.append((message, ChatMessageGiftBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default))) } else { if !canAddMessageReactions(message: message) { needReactions = false diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/BUILD index b5039cdd81..81bce800e8 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/BUILD +++ b/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/BUILD @@ -33,6 +33,7 @@ swift_library( "//submodules/TelegramUI/Components/TextNodeWithEntities", "//submodules/InvisibleInkDustNode", "//submodules/TelegramUI/Components/PeerInfo/PeerInfoCoverComponent", + "//submodules/TelegramUI/Components/Gifts/GiftItemComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift index 228acde43e..838412e9d1 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift @@ -24,6 +24,7 @@ import ChatMessageItemCommon import TextNodeWithEntities import InvisibleInkDustNode import PeerInfoCoverComponent +import GiftItemComponent private func attributedServiceMessageString(theme: ChatPresentationThemeData, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat, message: EngineMessage, accountPeerId: EnginePeer.Id) -> NSAttributedString? { return universalServiceMessageString(presentationData: (theme.theme, theme.wallpaper), strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat, message: message, accountPeerId: accountPeerId, forChatList: false, forForumOverview: false, forAdditionalServiceMessage: true) @@ -45,6 +46,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { private var dustNode: InvisibleInkDustNode? private let placeholderNode: StickerShimmerEffectNode private let animationNode: AnimatedStickerNode + private let giftIcon = ComponentView() private let modelTitleTextNode: TextNode private let modelValueTextNode: TextNode @@ -416,6 +418,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { var months: Int32 = 3 var animationName: String = "" var animationFile: TelegramMediaFile? + var uniqueGift: StarGift.UniqueGift? var title = item.presentationData.strings.Notification_PremiumGift_Title var text = "" var subtitleColor = primaryTextColor @@ -708,6 +711,20 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { text = item.presentationData.strings.Notification_StarGift_Subtitle_Refunded animationFile = gift.file } + case let .setChatTheme(chatTheme): + title = "" + var giftTitle = "" + if case let .gift(gift, _) = chatTheme, case let .unique(uniqueGiftValue) = gift { + giftTitle = "\(uniqueGiftValue.title) #\(formatCollectibleNumber(uniqueGiftValue.number, dateTimeFormat: item.presentationData.dateTimeFormat))" + uniqueGift = uniqueGiftValue + } + if incoming { + let authorName = item.message.author.flatMap { EnginePeer($0) }?.compactDisplayTitle ?? "" + text = item.presentationData.strings.Notification_ChatTheme_Text(authorName, giftTitle).string + } else { + text = item.presentationData.strings.Notification_ChatTheme_TextYou(giftTitle).string + } + hasServiceMessage = false default: break } @@ -866,6 +883,10 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { giftSize.height += 12.0 } + if let _ = uniqueGift { + giftSize.height -= 31.0 + } + var labelRects = labelLayout.linesRects() if labelRects.count > 1 { let sortedIndices = (0 ..< labelRects.count).sorted(by: { labelRects[$0].width > labelRects[$1].width }) @@ -944,7 +965,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { strongSelf.creatorButtonNode.isUserInteractionEnabled = !item.presentationData.isPreview strongSelf.creatorButtonTitleNode.isHidden = creatorButtonTitle.isEmpty - if strongSelf.item == nil && !isStoryEntity { + if strongSelf.item == nil && !isStoryEntity && uniqueGift == nil { strongSelf.animationNode.started = { [weak self] in if let strongSelf = self { let current = CACurrentMediaTime() @@ -1009,7 +1030,10 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { let titleFrame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - titleLayout.size.width) / 2.0) , y: mediaBackgroundFrame.minY + 151.0), size: titleLayout.size) strongSelf.titleNode.frame = titleFrame - let clippingTextFrame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - subtitleLayout.size.width) / 2.0), y: titleFrame.maxY + textSpacing), size: CGSize(width: subtitleLayout.size.width, height: clippedTextHeight)) + var clippingTextFrame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - subtitleLayout.size.width) / 2.0), y: titleFrame.maxY + textSpacing), size: CGSize(width: subtitleLayout.size.width, height: clippedTextHeight)) + if let _ = uniqueGift { + clippingTextFrame.origin.y -= 23.0 + } var attributesOffsetY: CGFloat = 0.0 @@ -1315,6 +1339,31 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { } } + if let uniqueGift { + let iconSize = CGSize(width: 94.0, height: 94.0) + let _ = strongSelf.giftIcon.update( + transition: .immediate, + component: AnyComponent(GiftItemComponent( + context: item.context, + theme: item.presentationData.theme.theme, + strings: item.presentationData.strings, + peer: nil, + subject: .uniqueGift(gift: uniqueGift, price: nil), + mode: .thumbnail + )), + environment: {}, + containerSize: iconSize + ) + if let giftIconView = strongSelf.giftIcon.view { + if giftIconView.superview == nil { + // backgroundView.layer.cornerRadius = 20.0 + //backgroundView.clipsToBounds = true + strongSelf.view.addSubview(giftIconView) + } + giftIconView.frame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - iconSize.width) / 2.0), y: mediaBackgroundFrame.minY + 17.0), size: iconSize) + } + } + let baseBackgroundFrame = labelFrame.offsetBy(dx: 0.0, dy: -11.0) if let (offset, image) = backgroundMaskImage { if strongSelf.backgroundNode == nil { @@ -1524,7 +1573,9 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { if isPlaying { var alreadySeen = true - if item.message.flags.contains(.Incoming) { + if let action = item.message.media.first(where: { $0 is TelegramMediaAction }) as? TelegramMediaAction, case .setChatTheme = action.action { + + } else if item.message.flags.contains(.Incoming) { if let unreadRange = item.controllerInteraction.unreadMessageRange[UnreadMessageRangeKey(peerId: item.message.id.peerId, namespace: item.message.id.namespace)] { if unreadRange.contains(item.message.id.id) { alreadySeen = false diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift index ef67f9f5ec..37ae63c442 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift @@ -1656,7 +1656,7 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr return patternWallpaperImage(account: context.account, accountManager: context.sharedContext.accountManager, representations: representations, mode: .screen) |> mapToSignal { value -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> in if let value = value { - return .single(value) + return .single(value.generator) } else { return .complete() } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageWallpaperBubbleContentNode/Sources/ChatMessageWallpaperBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageWallpaperBubbleContentNode/Sources/ChatMessageWallpaperBubbleContentNode.swift index 290a9c7f39..ec9e6380ea 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageWallpaperBubbleContentNode/Sources/ChatMessageWallpaperBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageWallpaperBubbleContentNode/Sources/ChatMessageWallpaperBubbleContentNode.swift @@ -387,7 +387,7 @@ public class ChatMessageWallpaperBubbleContentNode: ChatMessageBubbleContentNode updateImageSignal = patternWallpaperImage(account: item.context.account, accountManager: item.context.sharedContext.accountManager, representations: representations, mode: .thumbnail) |> mapToSignal { value -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> in if let value { - return .single(value) + return .single(value.generator) } else { return .complete() } diff --git a/submodules/TelegramUI/Components/ChatThemeScreen/BUILD b/submodules/TelegramUI/Components/ChatThemeScreen/BUILD new file mode 100644 index 0000000000..783bf6048c --- /dev/null +++ b/submodules/TelegramUI/Components/ChatThemeScreen/BUILD @@ -0,0 +1,44 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ChatThemeScreen", + module_name = "ChatThemeScreen", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/AsyncDisplayKit", + "//submodules/Display", + "//submodules/Postbox", + "//submodules/TelegramCore", + "//submodules/AccountContext", + "//submodules/ComponentFlow", + "//submodules/TelegramPresentationData", + "//submodules/TelegramUIPreferences", + "//submodules/PresentationDataUtils", + "//submodules/TelegramNotices", + "//submodules/AnimationUI", + "//submodules/MergeLists", + "//submodules/MediaResources", + "//submodules/StickerResources", + "//submodules/WallpaperResources", + "//submodules/TooltipUI", + "//submodules/SolidRoundedButtonNode", + "//submodules/AnimatedStickerNode", + "//submodules/TelegramAnimatedStickerNode", + "//submodules/ShimmerEffect", + "//submodules/AttachmentUI", + "//submodules/AvatarNode", + "//submodules/Markdown", + "//submodules/AppBundle", + "//submodules/ActivityIndicator", + "//submodules/TelegramUI/Components/Gifts/GiftItemComponent", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Sources/ChatThemeScreen.swift b/submodules/TelegramUI/Components/ChatThemeScreen/Sources/ChatThemeScreen.swift similarity index 85% rename from submodules/TelegramUI/Sources/ChatThemeScreen.swift rename to submodules/TelegramUI/Components/ChatThemeScreen/Sources/ChatThemeScreen.swift index 67c96af618..6ab4a46adc 100644 --- a/submodules/TelegramUI/Sources/ChatThemeScreen.swift +++ b/submodules/TelegramUI/Components/ChatThemeScreen/Sources/ChatThemeScreen.swift @@ -21,20 +21,22 @@ import AnimatedStickerNode import TelegramAnimatedStickerNode import ShimmerEffect import AttachmentUI +import AvatarNode private struct ThemeSettingsThemeEntry: Comparable, Identifiable { let index: Int let chatTheme: ChatTheme? let emojiFile: TelegramMediaFile? let themeReference: PresentationThemeReference? + let peer: EnginePeer? let nightMode: Bool var selected: Bool let theme: PresentationTheme let strings: PresentationStrings let wallpaper: TelegramWallpaper? - var stableId: Int { - return index + var stableId: String { + return self.chatTheme?.id ?? "\(self.index)" } static func ==(lhs: ThemeSettingsThemeEntry, rhs: ThemeSettingsThemeEntry) -> Bool { @@ -47,6 +49,9 @@ private struct ThemeSettingsThemeEntry: Comparable, Identifiable { if lhs.themeReference?.index != rhs.themeReference?.index { return false } + if lhs.peer != rhs.peer { + return false + } if lhs.nightMode != rhs.nightMode { return false } @@ -70,16 +75,16 @@ private struct ThemeSettingsThemeEntry: Comparable, Identifiable { } func item(context: AccountContext, action: @escaping (ChatTheme?) -> Void) -> ListViewItem { - return ThemeSettingsThemeIconItem(context: context, chatTheme: self.chatTheme, emojiFile: self.emojiFile, themeReference: self.themeReference, nightMode: self.nightMode, selected: self.selected, theme: self.theme, strings: self.strings, wallpaper: self.wallpaper, action: action) + return ThemeSettingsThemeIconItem(context: context, chatTheme: self.chatTheme, emojiFile: self.emojiFile, themeReference: self.themeReference, peer: self.peer, nightMode: self.nightMode, selected: self.selected, theme: self.theme, strings: self.strings, wallpaper: self.wallpaper, action: action) } } - private class ThemeSettingsThemeIconItem: ListViewItem { let context: AccountContext let chatTheme: ChatTheme? let emojiFile: TelegramMediaFile? let themeReference: PresentationThemeReference? + let peer: EnginePeer? let nightMode: Bool let selected: Bool let theme: PresentationTheme @@ -87,11 +92,24 @@ private class ThemeSettingsThemeIconItem: ListViewItem { let wallpaper: TelegramWallpaper? let action: (ChatTheme?) -> Void - public init(context: AccountContext, chatTheme: ChatTheme?, emojiFile: TelegramMediaFile?, themeReference: PresentationThemeReference?, nightMode: Bool, selected: Bool, theme: PresentationTheme, strings: PresentationStrings, wallpaper: TelegramWallpaper?, action: @escaping (ChatTheme?) -> Void) { + public init( + context: AccountContext, + chatTheme: ChatTheme?, + emojiFile: TelegramMediaFile?, + themeReference: PresentationThemeReference?, + peer: EnginePeer?, + nightMode: Bool, + selected: Bool, + theme: PresentationTheme, + strings: PresentationStrings, + wallpaper: TelegramWallpaper?, + action: @escaping (ChatTheme?) -> Void + ) { self.context = context self.chatTheme = chatTheme self.emojiFile = emojiFile self.themeReference = themeReference + self.peer = peer self.nightMode = nightMode self.selected = selected self.theme = theme @@ -240,6 +258,9 @@ private final class ThemeSettingsThemeItemIconNode : ListViewItemNode { private let emojiImageNode: TransformImageNode private var animatedStickerNode: AnimatedStickerNode? private var placeholderNode: StickerShimmerEffectNode + private var bubbleNode: ASImageNode? + private var avatarNode: AvatarNode? + private var replaceNode: ASImageNode? var snapshotView: UIView? var item: ThemeSettingsThemeIconItem? @@ -489,6 +510,75 @@ private final class ThemeSettingsThemeItemIconNode : ListViewItemNode { animatedStickerNode.frame = emojiFrame animatedStickerNode.updateLayout(size: emojiFrame.size) } + + if let _ = item.peer { + let bubbleNode: ASImageNode + if let current = strongSelf.bubbleNode { + bubbleNode = current + } else { + bubbleNode = ASImageNode() + strongSelf.insertSubnode(bubbleNode, belowSubnode: strongSelf.emojiContainerNode) + strongSelf.bubbleNode = bubbleNode + + var bubbleColor: UIColor? + if let theme = item.chatTheme, case let .gift(_, themeSettings) = theme { + if item.nightMode { + if let theme = themeSettings.first(where: { $0.baseTheme == .night || $0.baseTheme == .tinted }) { + let color = theme.wallpaper?.settings?.colors.first ?? theme.accentColor + bubbleColor = UIColor(rgb: UInt32(bitPattern: color)) + } + } else { + if let theme = themeSettings.first(where: { $0.baseTheme == .classic || $0.baseTheme == .day }) { + let color = theme.wallpaper?.settings?.colors.first ?? theme.accentColor + bubbleColor = UIColor(rgb: UInt32(bitPattern: color)) + } + } + } + if let bubbleColor { + bubbleNode.image = generateFilledRoundedRectImage(size: CGSize(width: 24.0, height: 48.0), cornerRadius: 12.0, color: bubbleColor) + } + } + bubbleNode.frame = CGRect(origin: CGPoint(x: 50.0, y: 12.0), size: CGSize(width: 24.0, height: 48.0)) + } else if let bubbleNode = strongSelf.bubbleNode { + strongSelf.bubbleNode = nil + bubbleNode.removeFromSupernode() + } + + if let peer = item.peer { + let avatarNode: AvatarNode + if let current = strongSelf.avatarNode { + avatarNode = current + } else { + avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 8.0)) + strongSelf.insertSubnode(avatarNode, belowSubnode: strongSelf.emojiContainerNode) + strongSelf.avatarNode = avatarNode + avatarNode.setPeer(context: item.context, theme: item.theme, peer: peer, displayDimensions: CGSize(width: 20.0, height: 20.0)) + } + avatarNode.transform = CATransform3DMakeRotation(.pi / 2.0, 0.0, 0.0, 1.0) + avatarNode.frame = CGRect(origin: CGPoint(x: 52.0, y: 14.0), size: CGSize(width: 20.0, height: 20.0)) + } else if let avatarNode = strongSelf.avatarNode { + strongSelf.avatarNode = nil + avatarNode.removeFromSupernode() + } + + if let _ = item.peer { + let replaceNode: ASImageNode + if let current = strongSelf.replaceNode { + replaceNode = current + } else { + replaceNode = ASImageNode() + strongSelf.insertSubnode(replaceNode, belowSubnode: strongSelf.emojiContainerNode) + strongSelf.replaceNode = replaceNode + replaceNode.image = generateTintedImage(image: UIImage(bundleImageName: "Settings/Refresh"), color: .white) + } + replaceNode.transform = CATransform3DMakeRotation(.pi / 2.0, 0.0, 0.0, 1.0) + if let image = replaceNode.image { + replaceNode.frame = CGRect(origin: CGPoint(x: 53.0, y: 37.0), size: image.size) + } + } else if let replaceNode = strongSelf.replaceNode { + strongSelf.replaceNode = nil + replaceNode.removeFromSupernode() + } } }) } @@ -525,9 +615,9 @@ private final class ThemeSettingsThemeItemIconNode : ListViewItemNode { } } -final class ChatThemeScreen: ViewController { - static let themeCrossfadeDuration: Double = 0.3 - static let themeCrossfadeDelay: Double = 0.25 +public final class ChatThemeScreen: ViewController { + public static let themeCrossfadeDuration: Double = 0.3 + public static let themeCrossfadeDelay: Double = 0.25 private var controllerNode: ChatThemeScreenNode { return self.displayNode as! ChatThemeScreenNode @@ -539,7 +629,7 @@ final class ChatThemeScreen: ViewController { private let animatedEmojiStickers: [String: [StickerPackItem]] private let initiallySelectedTheme: ChatTheme? private let peerName: String - let canResetWallpaper: Bool + fileprivate let canResetWallpaper: Bool private let previewTheme: (ChatTheme?, Bool?) -> Void fileprivate let changeWallpaper: () -> Void fileprivate let resetWallpaper: () -> Void @@ -548,9 +638,9 @@ final class ChatThemeScreen: ViewController { private var presentationData: PresentationData private var presentationDataDisposable: Disposable? - var dismissed: (() -> Void)? + public var dismissed: (() -> Void)? - var passthroughHitTestImpl: ((CGPoint) -> UIView?)? { + public var passthroughHitTestImpl: ((CGPoint) -> UIView?)? { didSet { if self.isNodeLoaded { self.controllerNode.passthroughHitTestImpl = self.passthroughHitTestImpl @@ -558,7 +648,7 @@ final class ChatThemeScreen: ViewController { } } - init( + public init( context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal), animatedEmojiStickers: [String: [StickerPackItem]], @@ -657,7 +747,7 @@ final class ChatThemeScreen: ViewController { } } - override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) { + override public func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) { self.forEachController({ controller in if let controller = controller as? TooltipScreen { controller.dismiss() @@ -683,7 +773,7 @@ final class ChatThemeScreen: ViewController { self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition) } - func dimTapped() { + public func dimTapped() { self.controllerNode.dimTapped() } } @@ -743,6 +833,7 @@ private class ChatThemeScreenNode: ViewControllerTracingNode, ASScrollViewDelega private var initialized = false private let uniqueGiftChatThemesContext: UniqueGiftChatThemesContext + private var currentUniqueGiftChatThemesState: UniqueGiftChatThemesContext.State? private let peerName: String @@ -875,9 +966,8 @@ private class ChatThemeScreenNode: ViewControllerTracingNode, ASScrollViewDelega self.cancelButtonNode.buttonNode.addTarget(self, action: #selector(self.cancelButtonPressed), forControlEvents: .touchUpInside) self.doneButton.pressed = { [weak self] in if let strongSelf = self { - strongSelf.doneButton.isUserInteractionEnabled = false if strongSelf.doneButton.font == .bold { - strongSelf.completion?(strongSelf.selectedTheme) + strongSelf.complete() } else { strongSelf.controller?.changeWallpaper() } @@ -888,13 +978,37 @@ private class ChatThemeScreenNode: ViewControllerTracingNode, ASScrollViewDelega self.disposable.set(combineLatest( queue: Queue.mainQueue(), self.context.engine.themes.getChatThemes(accountManager: self.context.sharedContext.accountManager), - self.uniqueGiftChatThemesContext.state, + self.uniqueGiftChatThemesContext.state + |> mapToSignal { state -> Signal<(UniqueGiftChatThemesContext.State, [EnginePeer.Id: EnginePeer]), NoError> in + var peerIds: [EnginePeer.Id] = [] + for theme in state.themes { + if case let .gift(gift, _) = theme, case let .unique(uniqueGift) = gift, let themePeerId = uniqueGift.themePeerId { + peerIds.append(themePeerId) + } + } + return combineLatest( + .single(state), + context.engine.data.get( + EngineDataMap(peerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init)) + ) |> map { peers in + var result: [EnginePeer.Id: EnginePeer] = [:] + for peerId in peerIds { + if let maybePeer = peers[peerId], let peer = maybePeer { + result[peerId] = peer + } + } + return result + } + ) + }, self.selectedThemePromise.get(), self.isDarkAppearancePromise.get() - ).startStrict(next: { [weak self] themes, uniqueGiftChatThemes, selectedTheme, isDarkAppearance in + ).startStrict(next: { [weak self] themes, uniqueGiftChatThemesStateAndPeers, selectedTheme, isDarkAppearance in guard let strongSelf = self else { return } + let (uniqueGiftChatThemesState, peers) = uniqueGiftChatThemesStateAndPeers + strongSelf.currentUniqueGiftChatThemesState = uniqueGiftChatThemesState let isFirstTime = strongSelf.entries == nil let presentationData = strongSelf.presentationData @@ -905,61 +1019,91 @@ private class ChatThemeScreenNode: ViewControllerTracingNode, ASScrollViewDelega chatTheme: nil, emojiFile: nil, themeReference: nil, + peer: nil, nightMode: false, selected: selectedTheme == nil, theme: presentationData.theme, strings: presentationData.strings, wallpaper: nil )) - for theme in themes { - guard let emoticon = theme.emoticon else { - continue + + var giftThemes = uniqueGiftChatThemesState.themes + var existingIds = Set() + if let initiallySelectedTheme, case .gift = initiallySelectedTheme { + let initialThemeIndex = giftThemes.firstIndex(where: { $0.id == initiallySelectedTheme.id }) + if initialThemeIndex == nil || initialThemeIndex! > 50 { + giftThemes.insert(initiallySelectedTheme, at: 0) } - entries.append(ThemeSettingsThemeEntry( - index: entries.count, - chatTheme: .emoticon(emoticon), - emojiFile: animatedEmojiStickers[emoticon]?.first?.file._parse(), - themeReference: .cloud(PresentationCloudTheme(theme: theme, resolvedWallpaper: nil, creatorAccountId: nil)), - nightMode: isDarkAppearance, - selected: selectedTheme?.id == ChatTheme.emoticon(emoticon).id, - theme: presentationData.theme, - strings: presentationData.strings, - wallpaper: nil - )) } - for theme in uniqueGiftChatThemes.themes { - guard case let .gift(gift, wallpaperFile) = theme else { + + for theme in giftThemes { + guard case let .gift(gift, themeSettings) = theme, !existingIds.contains(theme.id) else { continue } var emojiFile: TelegramMediaFile? + var peer: EnginePeer? if case let .unique(uniqueGift) = gift { for attribute in uniqueGift.attributes { if case let .model(_, file, _) = attribute { emojiFile = file } } + if let themePeerId = uniqueGift.themePeerId, theme.id != initiallySelectedTheme?.id { + peer = peers[themePeerId] + } + } + let themeReference: PresentationThemeReference + let wallpaper: TelegramWallpaper? + if isDarkAppearance { + wallpaper = themeSettings.first(where: { $0.baseTheme == .night || $0.baseTheme == .tinted })?.wallpaper + themeReference = .builtin(.night) + } else { + wallpaper = themeSettings.first(where: { $0.baseTheme == .classic || $0.baseTheme == .day })?.wallpaper + themeReference = .builtin(.dayClassic) } entries.append(ThemeSettingsThemeEntry( index: entries.count, chatTheme: theme, emojiFile: emojiFile, - themeReference: nil, + themeReference: themeReference, + peer: peer, nightMode: isDarkAppearance, selected: selectedTheme?.id == theme.id, theme: presentationData.theme, strings: presentationData.strings, - wallpaper: .file(TelegramWallpaper.File(id: wallpaperFile.fileId.id, accessHash: 0, isCreator: false, isDefault: false, isPattern: true, isDark: false, slug: "", file: wallpaperFile, settings: WallpaperSettings(blur: false, motion: false, colors: [], intensity: 100, rotation: 0))) + wallpaper: wallpaper )) + existingIds.insert(theme.id) } + if uniqueGiftChatThemesState.themes.count == 0 || uniqueGiftChatThemesState.dataState == .ready(canLoadMore: false) { + for theme in themes { + guard let emoticon = theme.emoticon else { + continue + } + entries.append(ThemeSettingsThemeEntry( + index: entries.count, + chatTheme: .emoticon(emoticon), + emojiFile: animatedEmojiStickers[emoticon]?.first?.file._parse(), + themeReference: .cloud(PresentationCloudTheme(theme: theme, resolvedWallpaper: nil, creatorAccountId: nil)), + peer: nil, + nightMode: isDarkAppearance, + selected: selectedTheme?.id == ChatTheme.emoticon(emoticon).id, + theme: presentationData.theme, + strings: presentationData.strings, + wallpaper: nil + )) + } + } + let action: (ChatTheme?) -> Void = { [weak self] chatTheme in if let self, self.selectedTheme != chatTheme { self.setChatTheme(chatTheme) } } let previousEntries = strongSelf.entries ?? [] - let crossfade = previousEntries.count != entries.count - let transition = preparedTransition(context: strongSelf.context, action: action, from: previousEntries, to: entries, crossfade: crossfade) + //let crossfade = previousEntries.count != entries.count + let transition = preparedTransition(context: strongSelf.context, action: action, from: previousEntries, to: entries, crossfade: false) strongSelf.enqueueTransition(transition) strongSelf.entries = entries @@ -1008,6 +1152,15 @@ private class ChatThemeScreenNode: ViewControllerTracingNode, ASScrollViewDelega } } + self.listNode.visibleBottomContentOffsetChanged = { [weak self] offset in + guard let self, let state = self.currentUniqueGiftChatThemesState, case .ready(true) = state.dataState else { + return + } + if case let .known(value) = offset, value < 100.0 { + self.uniqueGiftChatThemesContext.loadMore() + } + } + self.updateCancelButton() } @@ -1195,13 +1348,39 @@ private class ChatThemeScreenNode: ViewControllerTracingNode, ASScrollViewDelega } } + func complete() { + let proceed = { + self.doneButton.isUserInteractionEnabled = false + self.completion?(self.selectedTheme) + } + if case let .gift(gift, _) = self.selectedTheme, case let .unique(uniqueGift) = gift, let themePeerId = uniqueGift.themePeerId { + let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: themePeerId)) + |> deliverOnMainQueue).start(next: { [weak self] peer in + guard let self, let peer else { + return + } + let controller = giftThemeTransferAlertController( + context: self.context, + gift: uniqueGift, + previousPeer: peer, + commit: { + proceed() + } + ) + self.controller?.present(controller, in: .window(.root)) + }) + } else { + proceed() + } + } + func dimTapped() { if self.selectedTheme?.id == self.initiallySelectedTheme?.id { self.cancelButtonPressed() } else { let alertController = textAlertController(context: self.context, updatedPresentationData: (self.presentationData, .single(self.presentationData)), title: nil, text: self.presentationData.strings.Conversation_Theme_DismissAlert, actions: [TextAlertAction(type: .genericAction, title: self.presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Conversation_Theme_DismissAlertApply, action: { [weak self] in - if let strongSelf = self { - strongSelf.completion?(strongSelf.selectedTheme) + if let self { + self.complete() } })], actionLayout: .horizontal, dismissOnOutsideTap: true) self.present?(alertController) diff --git a/submodules/TelegramUI/Components/ChatThemeScreen/Sources/GiftThemeTransferAlertController.swift b/submodules/TelegramUI/Components/ChatThemeScreen/Sources/GiftThemeTransferAlertController.swift new file mode 100644 index 0000000000..03ea78002f --- /dev/null +++ b/submodules/TelegramUI/Components/ChatThemeScreen/Sources/GiftThemeTransferAlertController.swift @@ -0,0 +1,295 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display +import ComponentFlow +import Postbox +import TelegramCore +import TelegramPresentationData +import TelegramUIPreferences +import AccountContext +import AppBundle +import AvatarNode +import Markdown +import GiftItemComponent +import ActivityIndicator + +private final class GiftThemeTransferAlertContentNode: AlertContentNode { + private let context: AccountContext + private let strings: PresentationStrings + private var presentationTheme: PresentationTheme + private let title: String + private let text: String + private let gift: StarGift.UniqueGift + + private let titleNode: ASTextNode + private let giftView = ComponentView() + private let textNode: ASTextNode + private let arrowNode: ASImageNode + private let avatarNode: AvatarNode + + private let actionNodesSeparator: ASDisplayNode + private let actionNodes: [TextAlertContentActionNode] + private let actionVerticalSeparators: [ASDisplayNode] + + private var activityIndicator: ActivityIndicator? + + private var validLayout: CGSize? + + var inProgress = false { + didSet { + if let size = self.validLayout { + let _ = self.updateLayout(size: size, transition: .immediate) + } + } + } + + override var dismissOnOutsideTap: Bool { + return self.isUserInteractionEnabled + } + + init( + context: AccountContext, + theme: AlertControllerTheme, + ptheme: PresentationTheme, + strings: PresentationStrings, + gift: StarGift.UniqueGift, + peer: EnginePeer, + title: String, + text: String, + actions: [TextAlertAction] + ) { + self.context = context + self.strings = strings + self.presentationTheme = ptheme + self.title = title + self.text = text + self.gift = gift + + self.titleNode = ASTextNode() + self.titleNode.maximumNumberOfLines = 0 + + self.textNode = ASTextNode() + self.textNode.maximumNumberOfLines = 0 + + self.arrowNode = ASImageNode() + self.arrowNode.displaysAsynchronously = false + self.arrowNode.displayWithoutProcessing = true + + self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 26.0)) + + self.actionNodesSeparator = ASDisplayNode() + self.actionNodesSeparator.isLayerBacked = true + + self.actionNodes = actions.map { action -> TextAlertContentActionNode in + return TextAlertContentActionNode(theme: theme, action: action) + } + + var actionVerticalSeparators: [ASDisplayNode] = [] + if actions.count > 1 { + for _ in 0 ..< actions.count - 1 { + let separatorNode = ASDisplayNode() + separatorNode.isLayerBacked = true + actionVerticalSeparators.append(separatorNode) + } + } + self.actionVerticalSeparators = actionVerticalSeparators + + super.init() + + self.addSubnode(self.titleNode) + self.addSubnode(self.textNode) + self.addSubnode(self.arrowNode) + self.addSubnode(self.avatarNode) + + self.addSubnode(self.actionNodesSeparator) + + for actionNode in self.actionNodes { + self.addSubnode(actionNode) + } + + for separatorNode in self.actionVerticalSeparators { + self.addSubnode(separatorNode) + } + + self.updateTheme(theme) + + self.avatarNode.setPeer(context: context, theme: ptheme, peer: peer) + } + + override func updateTheme(_ theme: AlertControllerTheme) { + self.titleNode.attributedText = NSAttributedString(string: self.title, font: Font.semibold(17.0), textColor: theme.primaryColor) + self.textNode.attributedText = parseMarkdownIntoAttributedString(self.text, attributes: MarkdownAttributes( + body: MarkdownAttributeSet(font: Font.regular(13.0), textColor: theme.primaryColor), + bold: MarkdownAttributeSet(font: Font.semibold(13.0), textColor: theme.primaryColor), + link: MarkdownAttributeSet(font: Font.regular(13.0), textColor: theme.primaryColor), + linkAttribute: { url in + return ("URL", url) + } + ), textAlignment: .center) + self.arrowNode.image = generateTintedImage(image: UIImage(bundleImageName: "Media Editor/CutoutUndo"), color: theme.secondaryColor.withAlphaComponent(0.9)) + + self.actionNodesSeparator.backgroundColor = theme.separatorColor + for actionNode in self.actionNodes { + actionNode.updateTheme(theme) + } + for separatorNode in self.actionVerticalSeparators { + separatorNode.backgroundColor = theme.separatorColor + } + + if let size = self.validLayout { + _ = self.updateLayout(size: size, transition: .immediate) + } + } + + override func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { + var size = size + size.width = min(size.width, 270.0) + + self.validLayout = size + + var origin: CGPoint = CGPoint(x: 0.0, y: 20.0) + + let avatarSize = CGSize(width: 60.0, height: 60.0) + self.avatarNode.updateSize(size: avatarSize) + + let giftFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - avatarSize.width) / 2.0) - 52.0, y: origin.y), size: avatarSize) + + let _ = self.giftView.update( + transition: .immediate, + component: AnyComponent( + GiftItemComponent( + context: self.context, + theme: self.presentationTheme, + strings: self.strings, + peer: nil, + subject: .uniqueGift(gift: self.gift, price: nil), + mode: .thumbnail + ) + ), + environment: {}, + containerSize: avatarSize + ) + if let view = self.giftView.view { + if view.superview == nil { + self.view.addSubview(view) + } + view.frame = giftFrame + } + + if let arrowImage = self.arrowNode.image { + let arrowFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - arrowImage.size.width) / 2.0), y: origin.y + floorToScreenPixels((avatarSize.height - arrowImage.size.height) / 2.0)), size: arrowImage.size) + transition.updateFrame(node: self.arrowNode, frame: arrowFrame) + } + + let avatarFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - avatarSize.width) / 2.0) + 52.0, y: origin.y), size: avatarSize) + transition.updateFrame(node: self.avatarNode, frame: avatarFrame) + + origin.y += avatarSize.height + 17.0 + + let titleSize = self.titleNode.measure(CGSize(width: size.width - 32.0, height: size.height)) + transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - titleSize.width) / 2.0), y: origin.y), size: titleSize)) + origin.y += titleSize.height + 5.0 + + let textSize = self.textNode.measure(CGSize(width: size.width - 32.0, height: size.height)) + transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - textSize.width) / 2.0), y: origin.y), size: textSize)) + origin.y += textSize.height + 10.0 + + let actionButtonHeight: CGFloat = 44.0 + var minActionsWidth: CGFloat = 0.0 + let maxActionWidth: CGFloat = floor(size.width / CGFloat(self.actionNodes.count)) + let actionTitleInsets: CGFloat = 8.0 + + for actionNode in self.actionNodes { + let actionTitleSize = actionNode.titleNode.updateLayout(CGSize(width: maxActionWidth, height: actionButtonHeight)) + minActionsWidth += actionTitleSize.width + actionTitleInsets + } + + let insets = UIEdgeInsets(top: 18.0, left: 18.0, bottom: 18.0, right: 18.0) + + let contentWidth = max(size.width, minActionsWidth) + + let actionsHeight = actionButtonHeight + + let resultSize = CGSize(width: contentWidth, height: avatarSize.height + titleSize.height + textSize.height + actionsHeight + 24.0 + insets.top + insets.bottom) + self.actionNodesSeparator.frame = CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel)) + + var actionOffset: CGFloat = 0.0 + let actionWidth: CGFloat = floor(resultSize.width / CGFloat(self.actionNodes.count)) + var separatorIndex = -1 + var nodeIndex = 0 + for actionNode in self.actionNodes { + if separatorIndex >= 0 { + let separatorNode = self.actionVerticalSeparators[separatorIndex] + transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: actionOffset - UIScreenPixel, y: resultSize.height - actionsHeight), size: CGSize(width: UIScreenPixel, height: actionsHeight - UIScreenPixel))) + } + separatorIndex += 1 + + let currentActionWidth: CGFloat + if nodeIndex == self.actionNodes.count - 1 { + currentActionWidth = resultSize.width - actionOffset + } else { + currentActionWidth = actionWidth + } + + let actionNodeFrame: CGRect + actionNodeFrame = CGRect(origin: CGPoint(x: actionOffset, y: resultSize.height - actionsHeight), size: CGSize(width: currentActionWidth, height: actionButtonHeight)) + actionOffset += currentActionWidth + + transition.updateFrame(node: actionNode, frame: actionNodeFrame) + + nodeIndex += 1 + } + + if self.inProgress { + let activityIndicator: ActivityIndicator + if let current = self.activityIndicator { + activityIndicator = current + } else { + activityIndicator = ActivityIndicator(type: .custom(self.presentationTheme.list.freeInputField.controlColor, 18.0, 1.5, false)) + self.addSubnode(activityIndicator) + } + + if let actionNode = self.actionNodes.first { + actionNode.isHidden = true + + let indicatorSize = CGSize(width: 22.0, height: 22.0) + transition.updateFrame(node: activityIndicator, frame: CGRect(origin: CGPoint(x: actionNode.frame.minX + floor((actionNode.frame.width - indicatorSize.width) / 2.0), y: actionNode.frame.minY + floor((actionNode.frame.height - indicatorSize.height) / 2.0)), size: indicatorSize)) + } + } + + return resultSize + } +} + +public func giftThemeTransferAlertController( + context: AccountContext, + gift: StarGift.UniqueGift, + previousPeer: EnginePeer, + commit: @escaping () -> Void +) -> AlertController { + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let strings = presentationData.strings + + var contentNode: GiftThemeTransferAlertContentNode? + var dismissImpl: ((Bool) -> Void)? + let actions: [TextAlertAction] = [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: { + dismissImpl?(true) + }), TextAlertAction(type: .defaultAction, title: presentationData.strings.Conversation_Theme_GiftTransfer_Proceed, action: { + dismissImpl?(true) + commit() + })] + + let text = strings.Conversation_Theme_GiftTransfer_Text(previousPeer.compactDisplayTitle).string + contentNode = GiftThemeTransferAlertContentNode(context: context, theme: AlertControllerTheme(presentationData: presentationData), ptheme: presentationData.theme, strings: strings, gift: gift, peer: previousPeer, title: "", text: text, actions: actions) + + let controller = AlertController(theme: AlertControllerTheme(presentationData: presentationData), contentNode: contentNode!) + dismissImpl = { [weak controller] animated in + if animated { + controller?.dismissAnimated() + } else { + controller?.dismiss() + } + } + return controller +} diff --git a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift index b7c8c81f79..25bee3463c 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift @@ -477,7 +477,12 @@ final class GiftSetupScreenComponent: Component { case .starGiftUserLimit: if let perUserLimit, let giftFile { let text = presentationData.strings.Gift_Options_Gift_BuyLimitReached(perUserLimit) - let undoController = UndoOverlayController(presentationData: presentationData, content: .sticker(context: component.context, file: giftFile, loop: true, title: nil, text: text, undoText: nil, customAction: nil), action: { _ in return false }) + let undoController = UndoOverlayController( + presentationData: presentationData, + content: .sticker(context: component.context, file: giftFile, loop: true, title: nil, text: text, undoText: nil, customAction: nil), + elevatedLayout: true, + action: { _ in return false } + ) controller.present(undoController, in: .current) return } diff --git a/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/GiftStoreScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/GiftStoreScreen.swift index 726b43a459..dd1e8eb2ca 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/GiftStoreScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/GiftStoreScreen.swift @@ -277,7 +277,7 @@ final class GiftStoreScreenComponent: Component { buyGift: { slug, peerId, price in return self.state?.starGiftsContext.buyStarGift(slug: slug, peerId: peerId, price: price) ?? .complete() }, - updateResellStars: { price in + updateResellStars: { _, price in return self.state?.starGiftsContext.updateStarGiftResellPrice(slug: uniqueGift.slug, price: price) ?? .complete() } ) diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/BUILD b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/BUILD index 9e0aca1b52..47ded4318a 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/BUILD +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/BUILD @@ -53,6 +53,7 @@ swift_library( "//submodules/ActivityIndicator", "//submodules/TelegramUI/Components/TabSelectorComponent", "//submodules/TelegramUI/Components/Stars/BalanceNeededScreen", + "//submodules/TelegramUI/Components/ChatThemeScreen", "//submodules/ImageBlur", ], visibility = [ diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftValueScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftValueScreen.swift index f0b757f8ee..88a12392aa 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftValueScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftValueScreen.swift @@ -149,10 +149,6 @@ private final class GiftValueSheetContent: CombinedComponent { gift: gift ) controller.push(storeController) - - Queue.mainQueue().after(2.0, { - controller.dismiss(animated: false) - }) } func openGiftFragmentResale(url: String) { @@ -205,8 +201,6 @@ private final class GiftValueSheetContent: CombinedComponent { let theme = environment.theme let strings = environment.strings let dateTimeFormat = environment.dateTimeFormat - //let nameDisplayOrder = component.context.sharedContext.currentPresentationData.with { $0 }.nameDisplayOrder - //let controller = environment.controller let state = context.state @@ -319,12 +313,12 @@ private final class GiftValueSheetContent: CombinedComponent { var descriptionText: String if component.valueInfo.valueIsAverage { - descriptionText = "This is the average sale price of **\(giftCollectionTitle)** on Telegram and Fragment over the past month." + descriptionText = strings.Gift_Value_DescriptionAveragePrice(giftCollectionTitle).string } else { if component.valueInfo.isLastSaleOnFragment { - descriptionText = "This is the last price at which **\(giftTitle)** was last sold on Fragment." + descriptionText = strings.Gift_Value_DescriptionLastPriceFragment(giftTitle).string } else { - descriptionText = "This is the last price at which **\(giftTitle)** was last sold on Telegram." + descriptionText = strings.Gift_Value_DescriptionLastPriceTelegram(giftTitle).string } } if !descriptionText.isEmpty { @@ -394,7 +388,7 @@ private final class GiftValueSheetContent: CombinedComponent { tableItems.append(.init( id: "initialDate", - title: "Initial Sale", + title: strings.Gift_Value_InitialSale, component: AnyComponent( MultilineTextComponent(text: .plain(NSAttributedString(string: stringForMediumDate(timestamp: component.valueInfo.initialSaleDate, strings: strings, dateTimeFormat: dateTimeFormat), font: tableFont, textColor: tableTextColor))) ) @@ -410,7 +404,7 @@ private final class GiftValueSheetContent: CombinedComponent { tableItems.append(.init( id: "initialPrice", - title: "Initial Price", + title: strings.Gift_Value_InitialPrice, component: AnyComponent(MultilineTextWithEntitiesComponent( context: component.context, animationCache: component.context.animationCache, @@ -425,7 +419,7 @@ private final class GiftValueSheetContent: CombinedComponent { if let lastSaleDate = component.valueInfo.lastSaleDate { tableItems.append(.init( id: "lastDate", - title: "Last Sale", + title: strings.Gift_Value_LastSale, component: AnyComponent( MultilineTextComponent(text: .plain(NSAttributedString(string: stringForMediumDate(timestamp: lastSaleDate, strings: strings, dateTimeFormat: dateTimeFormat), font: tableFont, textColor: tableTextColor))) ) @@ -457,7 +451,7 @@ private final class GiftValueSheetContent: CombinedComponent { color: theme.list.itemAccentColor )), action: { [weak state] in - state?.showAttributeInfo(tag: tag, text: "**\(lastSalePriceString)** is the last price for \(giftCollectionTitle) gifts listed on Telegram and Fragment.") + state?.showAttributeInfo(tag: tag, text: strings.Gift_Value_LastPriceInfo(lastSalePriceString, giftCollectionTitle).string) } ).tagged(tag)) @@ -467,7 +461,7 @@ private final class GiftValueSheetContent: CombinedComponent { ) tableItems.append(.init( id: "lastPrice", - title: "Last Price", + title: strings.Gift_Value_LastPrice, hasBackground: false, component: itemComponent )) @@ -494,8 +488,7 @@ private final class GiftValueSheetContent: CombinedComponent { color: theme.list.itemAccentColor )), action: { [weak state] in - state?.showAttributeInfo(tag: tag, text: "**\(floorPriceString)** is the floor price for \(giftCollectionTitle) gifts listed on Telegram and Fragment.") - + state?.showAttributeInfo(tag: tag, text: strings.Gift_Value_MinimumPriceInfo(floorPriceString, giftCollectionTitle).string) } ).tagged(tag)) )) @@ -504,7 +497,7 @@ private final class GiftValueSheetContent: CombinedComponent { ) tableItems.append(.init( id: "floorPrice", - title: "Minumum Price", + title: strings.Gift_Value_MinimumPrice, hasBackground: false, component: itemComponent )) @@ -531,7 +524,7 @@ private final class GiftValueSheetContent: CombinedComponent { color: theme.list.itemAccentColor )), action: { [weak state] in - state?.showAttributeInfo(tag: tag, text: "**\(averagePriceString)** is the average sale price of \(giftCollectionTitle) on Telegram and Fragment over the past month.") + state?.showAttributeInfo(tag: tag, text: strings.Gift_Value_AveragePriceInfo(averagePriceString, giftCollectionTitle).string) } ).tagged(tag)) )) @@ -540,7 +533,7 @@ private final class GiftValueSheetContent: CombinedComponent { ) tableItems.append(.init( id: "averagePrice", - title: "Average Price", + title: strings.Gift_Value_AveragePrice, hasBackground: false, component: itemComponent )) @@ -587,7 +580,7 @@ private final class GiftValueSheetContent: CombinedComponent { ) )), AnyComponentWithIdentity(id: "label", component: AnyComponent( - MultilineTextComponent(text: .plain(NSAttributedString(string: " for sale on Telegram", font: Font.regular(17.0), textColor: theme.actionSheet.controlAccentColor))) + MultilineTextComponent(text: .plain(NSAttributedString(string: " \(strings.Gift_Value_ForSaleOnTelegram)", font: Font.regular(17.0), textColor: theme.actionSheet.controlAccentColor))) )), AnyComponentWithIdentity(id: "arrow", component: AnyComponent( BundleIconComponent(name: "Chat/Context Menu/Arrow", tintColor: theme.actionSheet.controlAccentColor) @@ -639,7 +632,7 @@ private final class GiftValueSheetContent: CombinedComponent { ) )), AnyComponentWithIdentity(id: "label", component: AnyComponent( - MultilineTextComponent(text: .plain(NSAttributedString(string: " for sale on Fragment", font: Font.regular(17.0), textColor: theme.actionSheet.controlAccentColor))) + MultilineTextComponent(text: .plain(NSAttributedString(string: " \(strings.Gift_Value_ForSaleOnFragment)", font: Font.regular(17.0), textColor: theme.actionSheet.controlAccentColor))) )), AnyComponentWithIdentity(id: "arrow", component: AnyComponent( BundleIconComponent(name: "Chat/Context Menu/Arrow", tintColor: theme.actionSheet.controlAccentColor) @@ -724,6 +717,7 @@ final class GiftValueSheetComponent: CombinedComponent { backgroundColor: .color(environment.theme.actionSheet.opaqueItemBackgroundColor), followContentSizeChanges: true, clipsContent: true, + autoAnimateOut: false, externalState: sheetExternalState, animateOut: animateOut, onPan: { diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift index 80e9d9241e..ba017d2150 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift @@ -36,6 +36,7 @@ import StarsBalanceOverlayComponent import BalanceNeededScreen import GiftItemComponent import GiftAnimationComponent +import ChatThemeScreen private final class GiftViewSheetContent: CombinedComponent { typealias EnvironmentType = ViewControllerComponentContainer.Environment @@ -104,6 +105,7 @@ private final class GiftViewSheetContent: CombinedComponent { var cachedHiddenImage: (UIImage, PresentationTheme)? var inProgress = false + var canSkip = false var testUpgradeAnimation = !"".isEmpty @@ -152,7 +154,7 @@ private final class GiftViewSheetContent: CombinedComponent { super.init() if let arguments = subject.arguments { - if let upgradeStars = arguments.upgradeStars, upgradeStars > 0, !arguments.nameHidden { + if let upgradeStars = arguments.upgradeStars, upgradeStars > 0, !arguments.nameHidden && !arguments.upgradeSeparate { self.keepOriginalInfo = true } @@ -595,7 +597,7 @@ private final class GiftViewSheetContent: CombinedComponent { } if let convertToStars = controller?.convertToStars { - convertToStars() + convertToStars(reference) } else { let _ = (self.context.engine.payments.convertStarGift(reference: reference) |> deliverOnMainQueue).startStandalone() @@ -668,14 +670,34 @@ private final class GiftViewSheetContent: CombinedComponent { return } self.isOpeningValue = true + + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } let _ = (self.context.engine.payments.getUniqueStarGiftValueInfo(slug: uniqueGift.slug) |> deliverOnMainQueue).start(next: { [weak self] valueInfo in - guard let self, let valueInfo else { + guard let self else { return } - self.isOpeningValue = false - let valueController = GiftValueScreen(context: self.context, gift: gift, valueInfo: valueInfo) - controller.push(valueController) + Queue.mainQueue().after(0.2) { + self.isOpeningValue = false + } + if let valueInfo { + let valueController = GiftValueScreen(context: self.context, gift: gift, valueInfo: valueInfo) + controller.push(valueController) + } else { + guard let controller = self.getController() as? GiftViewScreen else { + return + } + let alertController = textAlertController( + context: self.context, + title: nil, + text: presentationData.strings.Login_UnknownError, + actions: [ + TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {}) + ], + parseMarkdown: true + ) + controller.present(alertController, in: .window(.root)) + } }) } @@ -782,6 +804,83 @@ private final class GiftViewSheetContent: CombinedComponent { controller.present(shareController, in: .window(.root)) } + func setAsGiftTheme() { + guard let arguments = self.subject.arguments, let controller = self.getController() as? GiftViewScreen, let navigationController = controller.navigationController as? NavigationController, case let .unique(gift) = arguments.gift else { + return + } + + let context = self.context + + let themePeerId = Promise() + themePeerId.set( + .single(gift.themePeerId) + |> then( + context.engine.payments.getUniqueStarGift(slug: gift.slug) + |> map { gift in + return gift?.themePeerId + } + ) + ) + + let peerController = context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: context, filter: [.excludeRecent, .doNotSearchMessages], requestPeerType: [.user(.init(isBot: false, isPremium: nil))], hasContactSelector: false, hasCreation: false)) + peerController.peerSelected = { [weak peerController, weak navigationController] peer, _ in + if let navigationController { + let proceed = { + let _ = context.engine.themes.setChatWallpaper(peerId: peer.id, wallpaper: nil, forBoth: true).startStandalone() + let _ = context.engine.themes.setChatTheme(peerId: peer.id, chatTheme: .gift(.unique(gift), [])).startStandalone() + + peerController?.dismiss() + + context.sharedContext.navigateToChatController(NavigateToChatControllerParams( + navigationController: navigationController, + chatController: nil, + context: context, + chatLocation: .peer(peer), + subject: nil, + botStart: nil, + updateTextInputState: nil, + keepStack: .always, + useExisting: true, + purposefulAction: nil, + scrollToEndIfExists: false, + activateMessageSearch: nil, + animated: true + )) + } + + let _ = (themePeerId.get() + |> deliverOnMainQueue + |> take(1)).start(next: { [weak navigationController] themePeerId in + if let themePeerId, themePeerId != peer.id { + let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: themePeerId)) + |> deliverOnMainQueue).start(next: { [weak navigationController] peer in + guard let peer else { + proceed() + return + } + let controller = giftThemeTransferAlertController( + context: context, + gift: gift, + previousPeer: peer, + commit: { + proceed() + } + ) + (navigationController?.viewControllers.last as? ViewController)?.present(controller, in: .window(.root)) + }) + } else { + proceed() + } + }) + } + } + self.dismiss(animated: true) + + Queue.mainQueue().after(0.4) { + navigationController.pushViewController(peerController) + } + } + func transferGift() { guard let arguments = self.subject.arguments, let controller = self.getController() as? GiftViewScreen, case let .unique(gift) = arguments.gift, let reference = arguments.reference, let transferStars = arguments.transferStars else { return @@ -882,7 +981,7 @@ private final class GiftViewSheetContent: CombinedComponent { guard let self, let controller else { return } - let _ = ((controller.updateResellStars?(nil) ?? context.engine.payments.updateStarGiftResalePrice(reference: reference, price: nil)) + let _ = ((controller.updateResellStars?(reference, nil) ?? context.engine.payments.updateStarGiftResalePrice(reference: reference, price: nil)) |> deliverOnMainQueue).startStandalone(error: { error in }, completed: { [weak self, weak controller] in @@ -932,7 +1031,7 @@ private final class GiftViewSheetContent: CombinedComponent { return } - let _ = ((controller.updateResellStars?(price) ?? context.engine.payments.updateStarGiftResalePrice(reference: reference, price: price)) + let _ = ((controller.updateResellStars?(reference, price) ?? context.engine.payments.updateStarGiftResalePrice(reference: reference, price: price)) |> deliverOnMainQueue).startStandalone(error: { [weak self, weak controller] error in guard let self else { return @@ -1061,7 +1160,7 @@ private final class GiftViewSheetContent: CombinedComponent { var items: [ContextMenuItem] = [] let strings = presentationData.strings - if let _ = arguments.reference, case .unique = arguments.gift, let togglePinnedToTop = controller.togglePinnedToTop, let pinnedToTop = arguments.pinnedToTop { + if let reference = arguments.reference, case .unique = arguments.gift, let togglePinnedToTop = controller.togglePinnedToTop, let pinnedToTop = arguments.pinnedToTop { items.append(.action(ContextMenuActionItem(text: pinnedToTop ? strings.PeerInfo_Gifts_Context_Unpin : strings.PeerInfo_Gifts_Context_Pin , icon: { theme in generateTintedImage(image: UIImage(bundleImageName: pinnedToTop ? "Chat/Context Menu/Unpin" : "Chat/Context Menu/Pin"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, f in c?.dismiss(completion: { [weak self, weak controller] in guard let self, let controller else { @@ -1069,7 +1168,7 @@ private final class GiftViewSheetContent: CombinedComponent { } let pinnedToTop = !pinnedToTop - if togglePinnedToTop(pinnedToTop) { + if togglePinnedToTop(reference, pinnedToTop) { if pinnedToTop { controller.dismissAnimated() } else { @@ -1113,6 +1212,16 @@ private final class GiftViewSheetContent: CombinedComponent { self?.shareGift() }))) + + if gift.flags.contains(.isThemeAvailable) { + items.append(.action(ContextMenuActionItem(text: presentationData.strings.Gift_View_Context_SetAsTheme, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/ApplyTheme"), color: theme.contextMenu.primaryColor) + }, action: { [weak self] c, _ in + c?.dismiss(completion: nil) + + self?.setAsGiftTheme() + }))) + } if let _ = arguments.transferStars { if case let .channel(channel) = peer, !channel.flags.contains(.isCreator) { @@ -1528,7 +1637,28 @@ private final class GiftViewSheetContent: CombinedComponent { } } + func skipAnimation() { + guard let arguments = self.subject.arguments, case let .unique(uniqueGift) = arguments.gift else { + return + } + self.canSkip = false + self.revealedNumberDigits = "\(uniqueGift.number)".count + self.revealedAttributes.insert(.backdrop) + self.revealedAttributes.insert(.pattern) + self.revealedAttributes.insert(.model) + + self.updated(transition: .easeInOut(duration: 0.2)) + } + func commitUpgrade() { + let duration = Double.random(in: 0.85 ..< 2.25) + let firstFraction = Double.random(in: 0.2 ..< 0.4) + let secondFraction = Double.random(in: 0.2 ..< 0.4) + let thirdFraction = 1.0 - firstFraction - secondFraction + let firstDuration = duration * firstFraction + let secondDuration = duration * secondFraction + let thirdDuration = duration * thirdFraction + if self.testUpgradeAnimation, let arguments = self.subject.arguments, case let .unique(uniqueGift) = arguments.gift { self.inProgress = true self.updated() @@ -1538,8 +1668,11 @@ private final class GiftViewSheetContent: CombinedComponent { } Queue.mainQueue().after(0.5, { - self.inUpgradePreview = false + self.canSkip = true + self.updated(transition: .immediate) + self.inProgress = false + self.inUpgradePreview = false self.justUpgraded = true self.revealedNumberDigits = -1 @@ -1550,19 +1683,23 @@ private final class GiftViewSheetContent: CombinedComponent { self.updated(transition: .immediate) } } - self.updated(transition: .spring(duration: 0.4)) - Queue.mainQueue().after(1.2) { + Queue.mainQueue().after(firstDuration) { self.revealedAttributes.insert(.backdrop) self.updated(transition: .immediate) - Queue.mainQueue().after(0.7) { + Queue.mainQueue().after(secondDuration) { self.revealedAttributes.insert(.pattern) self.updated(transition: .immediate) - Queue.mainQueue().after(0.7) { + Queue.mainQueue().after(thirdDuration) { self.revealedAttributes.insert(.model) self.updated(transition: .immediate) + + Queue.mainQueue().after(0.55) { + self.canSkip = false + self.updated(transition: .easeInOut(duration: 0.2)) + } Queue.mainQueue().after(0.6) { if let controller = self.getController() as? GiftViewScreen { @@ -1572,6 +1709,8 @@ private final class GiftViewSheetContent: CombinedComponent { } } } + + self.updated(transition: .spring(duration: 0.4)) }) return } @@ -1592,8 +1731,11 @@ private final class GiftViewSheetContent: CombinedComponent { let context = self.context let upgradeGiftImpl: ((Int64?, Bool) -> Signal) if let upgradeGift = controller.upgradeGift { + guard let reference = arguments.reference else { + return + } upgradeGiftImpl = { formId, keepOriginalInfo in - return upgradeGift(formId, keepOriginalInfo) + return upgradeGift(formId, reference, keepOriginalInfo) |> afterCompleted { if formId != nil { context.starsContext?.load(force: true) @@ -1619,6 +1761,9 @@ private final class GiftViewSheetContent: CombinedComponent { guard let self, let controller = self.getController() as? GiftViewScreen else { return } + self.canSkip = true + self.updated(transition: .immediate) + self.inProgress = false self.inUpgradePreview = false @@ -1639,17 +1784,22 @@ private final class GiftViewSheetContent: CombinedComponent { } } - Queue.mainQueue().after(1.2) { + Queue.mainQueue().after(firstDuration) { self.revealedAttributes.insert(.backdrop) self.updated(transition: .immediate) - Queue.mainQueue().after(0.7) { + Queue.mainQueue().after(secondDuration) { self.revealedAttributes.insert(.pattern) self.updated(transition: .immediate) - Queue.mainQueue().after(0.7) { + Queue.mainQueue().after(thirdDuration) { self.revealedAttributes.insert(.model) self.updated(transition: .immediate) + + Queue.mainQueue().after(0.55) { + self.canSkip = false + self.updated(transition: .easeInOut(duration: 0.2)) + } Queue.mainQueue().after(0.6) { if let controller = self.getController() as? GiftViewScreen { @@ -1661,7 +1811,6 @@ private final class GiftViewSheetContent: CombinedComponent { } self.subject = .profileGift(peerId, result) - controller.animateSuccess() self.updated(transition: .spring(duration: 0.4)) Queue.mainQueue().after(0.5) { @@ -2267,13 +2416,13 @@ private final class GiftViewSheetContent: CombinedComponent { availableSize: CGSize(width: context.availableSize.width - perksSideInset * 2.0, height: 10000.0), transition: context.transition ) - headerComponents.append({ - context.add(wearPerks - .position(CGPoint(x: context.availableSize.width / 2.0, y: originY + wearPerks.size.height / 2.0)) - .appear(.default(alpha: true)) - .disappear(.default(alpha: true)) - ) - }) + + context.add(wearPerks + .position(CGPoint(x: context.availableSize.width / 2.0, y: originY + wearPerks.size.height / 2.0)) + .appear(.default(alpha: true)) + .disappear(.default(alpha: true)) + ) + originY += wearPerks.size.height originY += 16.0 } else if showUpgradePreview { @@ -3166,8 +3315,8 @@ private final class GiftViewSheetContent: CombinedComponent { component: PlainButtonComponent( content: AnyComponent( HeaderButtonComponent( - title: uniqueGift.resellAmounts == nil ? strings.Gift_View_Sell : strings.Gift_View_Unlist, - iconName: uniqueGift.resellAmounts == nil ? "Premium/Collectible/Sell" : "Premium/Collectible/Unlist" + title: (uniqueGift.resellAmounts ?? []).isEmpty ? strings.Gift_View_Sell : strings.Gift_View_Unlist, + iconName: (uniqueGift.resellAmounts ?? []).isEmpty ? "Premium/Collectible/Sell" : "Premium/Collectible/Unlist" ) ), effectAlignment: .center, @@ -3629,7 +3778,7 @@ private final class GiftViewSheetContent: CombinedComponent { } else { resellAmount = uniqueGift.resellAmounts?.first(where: { $0.currency == .stars }) } - if let resellAmount { + if let resellAmount, wearPeerNameChild == nil { if incoming || ownerPeerId == component.context.account.peerId { let priceButton = priceButton.update( component: PlainButtonComponent( @@ -3658,7 +3807,12 @@ private final class GiftViewSheetContent: CombinedComponent { } } - if ((incoming && !converted && !upgraded) || exported || selling) && (!showUpgradePreview && !showWearPreview) { + + var isChatTheme = false + if let controller = controller() as? GiftViewScreen, controller.openChatTheme != nil { + isChatTheme = true + } + if ((incoming && !converted && !upgraded) || exported || selling || isChatTheme) && (!showUpgradePreview && !showWearPreview) { let textFont = Font.regular(13.0) let textColor = theme.list.itemSecondaryTextColor let linkColor = theme.actionSheet.controlAccentColor @@ -3672,7 +3826,9 @@ private final class GiftViewSheetContent: CombinedComponent { var addressToOpen: String? var descriptionText: String - if let uniqueGift, selling { + if isChatTheme { + descriptionText = strings.Gift_View_OpenChatTheme + } else if let uniqueGift, selling { let ownerName: String if case let .peerId(peerId) = uniqueGift.owner { ownerName = state.peerMap[peerId]?.compactDisplayTitle ?? "" @@ -3731,7 +3887,10 @@ private final class GiftViewSheetContent: CombinedComponent { }, tapAction: { [weak state] attributes, _ in if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String { - if let addressToOpen { + if isChatTheme, let controller = controller() as? GiftViewScreen { + state?.dismiss(animated: true) + controller.openChatTheme?() + } else if let addressToOpen { state?.openAddress(addressToOpen) } else { state?.updateSavedToProfile(!savedToProfile) @@ -3761,7 +3920,25 @@ private final class GiftViewSheetContent: CombinedComponent { pressedColor: theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9) ) let buttonChild: _UpdatedChildComponent - if showWearPreview, let uniqueGift { + if state.canSkip { + buttonChild = button.update( + component: ButtonComponent( + background: buttonBackground, + content: AnyComponentWithIdentity( + id: AnyHashable("skip"), + component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: strings.Gift_Upgrade_Skip, font: Font.semibold(17.0), textColor: theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center)))) + ), + isEnabled: true, + displaysProgress: state.inProgress, + action: { [weak state] in + if let state { + state.skipAnimation() + } + }), + availableSize: buttonSize, + transition: context.transition + ) + } else if showWearPreview, let uniqueGift { let buttonContent: AnyComponentWithIdentity let premiumConfiguration = PremiumConfiguration.with(appConfiguration: component.context.currentAppConfiguration.with { $0 }) @@ -4081,7 +4258,9 @@ private final class GiftViewSheetContent: CombinedComponent { isEnabled: true, displaysProgress: state.inProgress, action: { [weak state] in - state?.dismiss(animated: true) + if let state { + state.dismiss(animated: true) + } }), availableSize: buttonSize, transition: context.transition @@ -4090,7 +4269,7 @@ private final class GiftViewSheetContent: CombinedComponent { let buttonFrame = CGRect(origin: CGPoint(x: sideInset, y: originY), size: buttonChild.size) var buttonAlpha: CGFloat = 1.0 - if let nextGiftToUpgrade = state.nextGiftToUpgrade, case let .generic(gift) = nextGiftToUpgrade.gift { + if let nextGiftToUpgrade = state.nextGiftToUpgrade, case let .generic(gift) = nextGiftToUpgrade.gift, !state.canSkip { buttonAlpha = 0.0 let upgradeNextButton = upgradeNextButton.update( @@ -4098,7 +4277,7 @@ private final class GiftViewSheetContent: CombinedComponent { content: AnyComponent( HStack([ AnyComponentWithIdentity(id: "label", component: AnyComponent( - MultilineTextComponent(text: .plain(NSAttributedString(string: "Upgrade Next Gift", font: Font.regular(17.0), textColor: theme.actionSheet.controlAccentColor))) + MultilineTextComponent(text: .plain(NSAttributedString(string: strings.Gift_Upgrade_UpgradeNext, font: Font.regular(17.0), textColor: theme.actionSheet.controlAccentColor))) )), AnyComponentWithIdentity(id: "icon", component: AnyComponent( GiftItemComponent( @@ -4278,12 +4457,12 @@ public class GiftViewScreen: ViewControllerComponentContainer { case upgradePreview([StarGift.UniqueGift.Attribute], String) case wearPreview(StarGift.UniqueGift) - var arguments: (peerId: EnginePeer.Id?, fromPeerId: EnginePeer.Id?, fromPeerName: String?, messageId: EngineMessage.Id?, reference: StarGiftReference?, incoming: Bool, gift: StarGift, date: Int32, convertStars: Int64?, text: String?, entities: [MessageTextEntity]?, nameHidden: Bool, savedToProfile: Bool, pinnedToTop: Bool?, converted: Bool, upgraded: Bool, refunded: Bool, canUpgrade: Bool, upgradeStars: Int64?, transferStars: Int64?, resellAmounts: [CurrencyAmount]?, canExportDate: Int32?, upgradeMessageId: Int32?, canTransferDate: Int32?, canResaleDate: Int32?, prepaidUpgradeHash: String?)? { + var arguments: (peerId: EnginePeer.Id?, fromPeerId: EnginePeer.Id?, fromPeerName: String?, messageId: EngineMessage.Id?, reference: StarGiftReference?, incoming: Bool, gift: StarGift, date: Int32, convertStars: Int64?, text: String?, entities: [MessageTextEntity]?, nameHidden: Bool, savedToProfile: Bool, pinnedToTop: Bool?, converted: Bool, upgraded: Bool, refunded: Bool, canUpgrade: Bool, upgradeStars: Int64?, transferStars: Int64?, resellAmounts: [CurrencyAmount]?, canExportDate: Int32?, upgradeMessageId: Int32?, canTransferDate: Int32?, canResaleDate: Int32?, prepaidUpgradeHash: String?, upgradeSeparate: Bool)? { switch self { case let .message(message): if let action = message.media.first(where: { $0 is TelegramMediaAction }) as? TelegramMediaAction { switch action.action { - case let .starGift(gift, convertStars, text, entities, nameHidden, savedToProfile, converted, upgraded, canUpgrade, upgradeStars, isRefunded, _, upgradeMessageId, peerId, senderId, savedId, prepaidUpgradeHash, giftMessageId, _): + case let .starGift(gift, convertStars, text, entities, nameHidden, savedToProfile, converted, upgraded, canUpgrade, upgradeStars, isRefunded, _, upgradeMessageId, peerId, senderId, savedId, prepaidUpgradeHash, giftMessageId, upgradeSeparate): var reference: StarGiftReference if let peerId, let giftMessageId { reference = .message(messageId: EngineMessage.Id(peerId: peerId, namespace: Namespaces.Message.Cloud, id: giftMessageId)) @@ -4292,7 +4471,7 @@ public class GiftViewScreen: ViewControllerComponentContainer { } else { reference = .message(messageId: message.id) } - return (message.id.peerId, senderId ?? message.author?.id, message.author?.compactDisplayTitle, message.id, reference, message.flags.contains(.Incoming), gift, message.timestamp, convertStars, text, entities, nameHidden, savedToProfile, nil, converted, upgraded, isRefunded, canUpgrade, upgradeStars, nil, nil, nil, upgradeMessageId, nil, nil, prepaidUpgradeHash) + return (message.id.peerId, senderId ?? message.author?.id, message.author?.compactDisplayTitle, message.id, reference, message.flags.contains(.Incoming), gift, message.timestamp, convertStars, text, entities, nameHidden, savedToProfile, nil, converted, upgraded, isRefunded, canUpgrade, upgradeStars, nil, nil, nil, upgradeMessageId, nil, nil, prepaidUpgradeHash, upgradeSeparate) case let .starGiftUnique(gift, isUpgrade, isTransferred, savedToProfile, canExportDate, transferStars, _, _, peerId, senderId, savedId, _, canTransferDate, canResaleDate): var reference: StarGiftReference if let peerId, let savedId { @@ -4317,13 +4496,13 @@ public class GiftViewScreen: ViewControllerComponentContainer { if case let .unique(uniqueGift) = gift { resellAmounts = uniqueGift.resellAmounts } - return (message.id.peerId, senderId ?? message.author?.id, message.author?.compactDisplayTitle, message.id, reference, incoming, gift, message.timestamp, nil, nil, nil, false, savedToProfile, nil, false, false, false, false, nil, transferStars, resellAmounts, canExportDate, nil, canTransferDate, canResaleDate, nil) + return (message.id.peerId, senderId ?? message.author?.id, message.author?.compactDisplayTitle, message.id, reference, incoming, gift, message.timestamp, nil, nil, nil, false, savedToProfile, nil, false, false, false, false, nil, transferStars, resellAmounts, canExportDate, nil, canTransferDate, canResaleDate, nil, false) default: return nil } } case let .uniqueGift(gift, _), let .wearPreview(gift): - return (nil, nil, nil, nil, nil, false, .unique(gift), 0, nil, nil, nil, false, false, nil, false, false, false, false, nil, nil, gift.resellAmounts, nil, nil, nil, nil, nil) + return (nil, nil, nil, nil, nil, false, .unique(gift), 0, nil, nil, nil, false, false, nil, false, false, false, false, nil, nil, gift.resellAmounts, nil, nil, nil, nil, nil, false) case let .profileGift(peerId, gift): var messageId: EngineMessage.Id? if case let .message(messageIdValue) = gift.reference { @@ -4333,7 +4512,7 @@ public class GiftViewScreen: ViewControllerComponentContainer { if case let .unique(uniqueGift) = gift.gift { resellAmounts = uniqueGift.resellAmounts } - return (peerId, gift.fromPeer?.id, gift.fromPeer?.compactDisplayTitle, messageId, gift.reference, false, gift.gift, gift.date, gift.convertStars, gift.text, gift.entities, gift.nameHidden, gift.savedToProfile, gift.pinnedToTop, false, false, false, gift.canUpgrade, gift.upgradeStars, gift.transferStars, resellAmounts, gift.canExportDate, nil, gift.canTransferDate, gift.canResaleDate, gift.prepaidUpgradeHash) + return (peerId, gift.fromPeer?.id, gift.fromPeer?.compactDisplayTitle, messageId, gift.reference, false, gift.gift, gift.date, gift.convertStars, gift.text, gift.entities, gift.nameHidden, gift.savedToProfile, gift.pinnedToTop, false, false, false, gift.canUpgrade, gift.upgradeStars, gift.transferStars, resellAmounts, gift.canExportDate, nil, gift.canTransferDate, gift.canResaleDate, gift.prepaidUpgradeHash, gift.upgradeSeparate) case .soldOutGift: return nil case .upgradePreview: @@ -4374,13 +4553,14 @@ public class GiftViewScreen: ViewControllerComponentContainer { fileprivate let balanceOverlay = ComponentView() fileprivate let updateSavedToProfile: ((StarGiftReference, Bool) -> Void)? - fileprivate let convertToStars: (() -> Void)? + fileprivate let convertToStars: ((StarGiftReference) -> Void)? fileprivate let transferGift: ((Bool, EnginePeer.Id) -> Signal)? - fileprivate let upgradeGift: ((Int64?, Bool) -> Signal)? + fileprivate let upgradeGift: ((Int64?, StarGiftReference, Bool) -> Signal)? fileprivate let buyGift: ((String, EnginePeer.Id, CurrencyAmount?) -> Signal)? - fileprivate let updateResellStars: ((CurrencyAmount?) -> Signal)? - fileprivate let togglePinnedToTop: ((Bool) -> Bool)? + fileprivate let updateResellStars: ((StarGiftReference, CurrencyAmount?) -> Signal)? + fileprivate let togglePinnedToTop: ((StarGiftReference, Bool) -> Bool)? fileprivate let shareStory: ((StarGift.UniqueGift) -> Void)? + fileprivate let openChatTheme: (() -> Void)? public var disposed: () -> Void = {} @@ -4391,13 +4571,14 @@ public class GiftViewScreen: ViewControllerComponentContainer { index: Int? = nil, forceDark: Bool = false, updateSavedToProfile: ((StarGiftReference, Bool) -> Void)? = nil, - convertToStars: (() -> Void)? = nil, + convertToStars: ((StarGiftReference) -> Void)? = nil, transferGift: ((Bool, EnginePeer.Id) -> Signal)? = nil, - upgradeGift: ((Int64?, Bool) -> Signal)? = nil, + upgradeGift: ((Int64?, StarGiftReference, Bool) -> Signal)? = nil, buyGift: ((String, EnginePeer.Id, CurrencyAmount?) -> Signal)? = nil, - updateResellStars: ((CurrencyAmount?) -> Signal)? = nil, - togglePinnedToTop: ((Bool) -> Bool)? = nil, - shareStory: ((StarGift.UniqueGift) -> Void)? = nil + updateResellStars: ((StarGiftReference, CurrencyAmount?) -> Signal)? = nil, + togglePinnedToTop: ((StarGiftReference, Bool) -> Bool)? = nil, + shareStory: ((StarGift.UniqueGift) -> Void)? = nil, + openChatTheme: (() -> Void)? = nil ) { self.context = context self.subject = subject @@ -4410,6 +4591,7 @@ public class GiftViewScreen: ViewControllerComponentContainer { self.updateResellStars = updateResellStars self.togglePinnedToTop = togglePinnedToTop self.shareStory = shareStory + self.openChatTheme = openChatTheme if case let .unique(gift) = subject.arguments?.gift, gift.resellForTonOnly { self.balanceCurrency = .ton @@ -4453,8 +4635,8 @@ public class GiftViewScreen: ViewControllerComponentContainer { self.navigationPresentation = .flatModal self.automaticallyControlPresentationContextLayout = false - if "".isEmpty { - let upgradableGiftsContext = ProfileGiftsContext(account: context.account, peerId: context.account.peerId, collectionId: nil, sorting: .date, filter: [.displayed, .hidden, .limitedUpgradable]) + if let gift = subject.arguments?.gift, case .generic = gift { + let upgradableGiftsContext = ProfileGiftsContext(account: context.account, peerId: context.account.peerId, collectionId: nil, sorting: .date, filter: [.displayed, .hidden, .limitedUpgradable], limit: 50) self.upgradableDisposable = (upgradableGiftsContext.state |> deliverOnMainQueue).start(next: { [weak self] state in guard let self else { @@ -4529,7 +4711,7 @@ public class GiftViewScreen: ViewControllerComponentContainer { self.view.disablesInteractiveModalDismiss = true - if let arguments = self.subject.arguments, let _ = self.subject.arguments?.resellAmounts { + if let arguments = self.subject.arguments, let resellAmounts = self.subject.arguments?.resellAmounts, !resellAmounts.isEmpty { if case let .unique(uniqueGift) = arguments.gift, case .peerId(self.context.account.peerId) = uniqueGift.owner { } else { self.showBalance = true diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoMembersPane.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoMembersPane.swift index ba9e3802b9..e7ce0f314a 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoMembersPane.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoMembersPane.swift @@ -127,7 +127,7 @@ private enum PeerMembersListEntry: Comparable, Identifiable { })) } - let presence: EnginePeer.Presence + var presence: EnginePeer.Presence if member.peer.id == context.account.peerId { presence = EnginePeer.Presence(status: .present(until: Int32.max), lastActivity: 0) } else if let value = member.presence { @@ -136,6 +136,17 @@ private enum PeerMembersListEntry: Comparable, Identifiable { presence = EnginePeer.Presence(status: .longTimeAgo, lastActivity: 0) } + var status: ContactsPeerItemStatus = .presence(presence, presentationData.dateTimeFormat) + if let user = member.peer as? TelegramUser, let botInfo = user.botInfo { + let botStatus: String + if botInfo.flags.contains(.hasAccessToChatHistory) { + botStatus = presentationData.strings.Bot_GroupStatusReadsHistory + } else { + botStatus = presentationData.strings.Bot_GroupStatusDoesNotReadHistory + } + status = .custom(string: NSAttributedString(string: botStatus, font: Font.regular(floor(presentationData.listsFontSize.itemListBaseFontSize * 14.0 / 17.0)), textColor: presentationData.theme.list.itemSecondaryTextColor), multiline: false, isActive: false, icon: nil) + } + return ContactsPeerItem( presentationData: ItemListPresentationData(presentationData), style: .plain, @@ -145,7 +156,7 @@ private enum PeerMembersListEntry: Comparable, Identifiable { context: context, peerMode: .memberList, peer: .peer(peer: EnginePeer(member.peer), chatPeer: EnginePeer(member.peer)), - status: .presence(presence, presentationData.dateTimeFormat), + status: status, rightLabelText: label, enabled: true, selection: .none, diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift index 34fa05787e..d38cf5f67f 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift @@ -589,14 +589,14 @@ final class PeerInfoHeaderNode: ASDisplayNode { } var currentSavedMusic: TelegramMediaFile? - if !self.isSettings, let screenData { + if let peer, peer.id != self.context.account.peerId || self.isMyProfile, let screenData { if let savedMusicState = screenData.savedMusicState { currentSavedMusic = savedMusicState.files.first } else if let cachedUserData = screenData.cachedData as? CachedUserData { currentSavedMusic = cachedUserData.savedMusic } } - let musicHeight: CGFloat = hasBackground ? 24.0 : 16.0 + let musicHeight: CGFloat = hasBackground || self.isAvatarExpanded ? 24.0 : 16.0 let bottomInset: CGFloat = currentSavedMusic != nil ? musicHeight : 0.0 let isLandscape = containerInset > 16.0 @@ -2640,7 +2640,7 @@ final class PeerInfoHeaderNode: ASDisplayNode { } return musicBackground }() - musicTransition.updateFrame(view: musicBackground, frame: CGRect(origin: CGPoint(x: 0.0, y: backgroundHeight - musicHeight - buttonRightOrigin.y), size: CGSize(width: backgroundFrame.width, height: musicHeight))) + musicTransition.updateFrame(view: musicBackground, frame: CGRect(origin: CGPoint(x: 0.0, y: backgroundHeight - 24.0 - buttonRightOrigin.y), size: CGSize(width: backgroundFrame.width, height: 24.0))) if let _ = self.navigationTransition { transition.updateAlpha(layer: musicBackground.layer, alpha: 1.0 - transitionFraction) @@ -2698,7 +2698,7 @@ final class PeerInfoHeaderNode: ASDisplayNode { environment: {}, containerSize: CGSize(width: backgroundFrame.width, height: musicHeight) ) - let musicFrame = CGRect(origin: CGPoint(x: 0.0, y: (apparentBackgroundHeight - backgroundHeight) + backgroundHeight - musicHeight - (hasBackground ? 0.0 : 4.0)), size: musicSize) + let musicFrame = CGRect(origin: CGPoint(x: 0.0, y: (apparentBackgroundHeight - backgroundHeight) + backgroundHeight - musicHeight - (hasBackground || self.isAvatarExpanded ? 0.0 : 4.0)), size: musicSize) if let musicView = music.view { if musicView.superview == nil { self.regularContentNode.view.addSubview(musicView) diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoPaneContainerNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoPaneContainerNode.swift index 4a7afc830c..47d81e1bcd 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoPaneContainerNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoPaneContainerNode.swift @@ -1163,9 +1163,8 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, ASGestureRecognizerDelegat return } - //TODO:localize var items: [ContextMenuItem] = [] - items.append(.action(ContextMenuActionItem(text: "Set as Main Tab", icon: { theme in + items.append(.action(ContextMenuActionItem(text: params.presentationData.strings.PeerInfo_Tabs_SetMainTab, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/ReorderItems"), color: theme.actionSheet.primaryTextColor) }, action: { [weak self] _, f in guard let self else { @@ -1183,7 +1182,7 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, ASGestureRecognizerDelegat guard let self else { return } - let controller = UndoOverlayController(presentationData: params.presentationData, content: .actionSucceeded(title: nil, text: "Tab order changed.", cancel: nil, destructive: false), action: { _ in return true }) + let controller = UndoOverlayController(presentationData: params.presentationData, content: .actionSucceeded(title: nil, text: params.presentationData.strings.PeerInfo_Tabs_SetMainTab_Succeed, cancel: nil, destructive: false), action: { _ in return true }) self.parentController?.present(controller, in: .current) }) } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift index 21c72e8719..5627e06ed1 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift @@ -5012,8 +5012,8 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro } profileGifts.updateStarGiftAddedToProfile(reference: reference, added: added) }, - convertToStars: { [weak profileGifts] in - guard let profileGifts, let reference = gift.reference else { + convertToStars: { [weak profileGifts] reference in + guard let profileGifts else { return } profileGifts.convertStarGift(reference: reference) @@ -5024,8 +5024,8 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro } return profileGifts.transferStarGift(prepaid: prepaid, reference: reference, peerId: peerId) }, - upgradeGift: { [weak profileGifts] formId, keepOriginalInfo in - guard let profileGifts, let reference = gift.reference else { + upgradeGift: { [weak profileGifts] formId, reference, keepOriginalInfo in + guard let profileGifts else { return .never() } return profileGifts.upgradeStarGift(formId: formId, reference: reference, keepOriginalInfo: keepOriginalInfo) diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/GiftsListView.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/GiftsListView.swift index 92eefe5c98..89396b24d4 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/GiftsListView.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/GiftsListView.swift @@ -611,8 +611,8 @@ final class GiftsListView: UIView { } self.profileGifts.updateStarGiftAddedToProfile(reference: reference, added: added) }, - convertToStars: { [weak self] in - guard let self, let reference = product.reference else { + convertToStars: { [weak self] reference in + guard let self else { return } self.profileGifts.convertStarGift(reference: reference) @@ -623,8 +623,8 @@ final class GiftsListView: UIView { } return self.profileGifts.transferStarGift(prepaid: prepaid, reference: reference, peerId: peerId) }, - upgradeGift: { [weak self] formId, keepOriginalInfo in - guard let self, let reference = product.reference else { + upgradeGift: { [weak self] formId, reference, keepOriginalInfo in + guard let self else { return .never() } return self.profileGifts.upgradeStarGift(formId: formId, reference: reference, keepOriginalInfo: keepOriginalInfo) @@ -635,36 +635,34 @@ final class GiftsListView: UIView { } return self.profileGifts.buyStarGift(slug: slug, peerId: peerId, price: price) }, - updateResellStars: { [weak self] price in - guard let self, let reference = product.reference else { + updateResellStars: { [weak self] reference, price in + guard let self else { return .never() } return self.profileGifts.updateStarGiftResellPrice(reference: reference, price: price) }, - togglePinnedToTop: { [weak self] pinnedToTop in + togglePinnedToTop: { [weak self] reference, pinnedToTop in guard let self else { return false } - if let reference = product.reference { - if pinnedToTop && self.pinnedReferences.count >= self.maxPinnedCount { - self.displayUnpinScreen?(product, { - dismissImpl?() - }) - return false - } - self.profileGifts.updateStarGiftPinnedToTop(reference: reference, pinnedToTop: pinnedToTop) - - var title = "" - if case let .unique(uniqueGift) = product.gift { - title = "\(uniqueGift.title) #\(formatCollectibleNumber(uniqueGift.number, dateTimeFormat: params.presentationData.dateTimeFormat))" - } - - if pinnedToTop { - Queue.mainQueue().after(0.35) { - let toastTitle = params.presentationData.strings.PeerInfo_Gifts_ToastPinned_TitleNew(title).string - let toastText = params.presentationData.strings.PeerInfo_Gifts_ToastPinned_Text - self.parentController?.present(UndoOverlayController(presentationData: params.presentationData, content: .universal(animation: "anim_toastpin", scale: 0.06, colors: [:], title: toastTitle, text: toastText, customUndoText: nil, timeout: 5), elevatedLayout: true, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root)) - } + if pinnedToTop && self.pinnedReferences.count >= self.maxPinnedCount { + self.displayUnpinScreen?(product, { + dismissImpl?() + }) + return false + } + self.profileGifts.updateStarGiftPinnedToTop(reference: reference, pinnedToTop: pinnedToTop) + + var title = "" + if case let .unique(uniqueGift) = product.gift { + title = "\(uniqueGift.title) #\(formatCollectibleNumber(uniqueGift.number, dateTimeFormat: params.presentationData.dateTimeFormat))" + } + + if pinnedToTop { + Queue.mainQueue().after(0.35) { + let toastTitle = params.presentationData.strings.PeerInfo_Gifts_ToastPinned_TitleNew(title).string + let toastText = params.presentationData.strings.PeerInfo_Gifts_ToastPinned_Text + self.parentController?.present(UndoOverlayController(presentationData: params.presentationData, content: .universal(animation: "anim_toastpin", scale: 0.06, colors: [:], title: toastTitle, text: toastText, customUndoText: nil, timeout: 5), elevatedLayout: true, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root)) } } return true @@ -996,7 +994,7 @@ final class GiftsListView: UIView { fadeTransition.setAlpha(view: self.emptyResultsClippingView, alpha: visibleHeight < 300.0 ? 0.0 : 1.0) - if self.peerId == self.context.account.peerId, !self.canSelect && !self.filteredResultsAreEmpty && self.profileGifts.collectionId == nil { + if self.peerId == self.context.account.peerId, !self.canSelect && !self.filteredResultsAreEmpty && self.profileGifts.collectionId == nil && self.emptyResultsClippingView.isHidden { let footerText: ComponentView if let current = self.footerText { footerText = current @@ -1024,6 +1022,13 @@ final class GiftsListView: UIView { transition.setFrame(view: view, frame: CGRect(origin: CGPoint(x: floor((size.width - footerTextSize.width) / 2.0), y: contentHeight), size: footerTextSize)) } contentHeight += footerTextSize.height + } else if let footerText = self.footerText { + self.footerText = nil + if let view = footerText.view { + fadeTransition.setAlpha(view: view, alpha: 0.0, completion: { _ in + view.removeFromSuperview() + }) + } } return contentHeight diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift index b24eaa07c4..35ebc2693d 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift @@ -90,7 +90,6 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr private let tabSelector = ComponentView() public private(set) var currentCollection: GiftCollection = .all - private var footerText: ComponentView? private var panelBackground: NavigationBackgroundNode? private var panelSeparator: ASDisplayNode? private var panelButton: ComponentView? diff --git a/submodules/TelegramUI/Components/PeerInfo/ProfileLevelInfoScreen/Sources/ProfileLevelInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/ProfileLevelInfoScreen/Sources/ProfileLevelInfoScreen.swift index fd2a593c0f..992c109dfd 100644 --- a/submodules/TelegramUI/Components/PeerInfo/ProfileLevelInfoScreen/Sources/ProfileLevelInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/ProfileLevelInfoScreen/Sources/ProfileLevelInfoScreen.swift @@ -377,24 +377,46 @@ private final class ProfileLevelInfoScreenComponent: Component { descriptionTextString = environment.strings.ProfileLevelInfo_OtherDescription(component.peer.compactDisplayTitle).string } - //TODO:localize var titleItems: [AnimatedTextComponent.Item] = [] + + let ratingTitle = environment.strings.ProfileLevelInfo_RatingTitle + let futureTitle = environment.strings.ProfileLevelInfo_FutureRatingTitle + if self.isPreviewingPendingRating { - titleItems.append(AnimatedTextComponent.Item( - id: AnyHashable(0), - isUnbreakable: false, - content: .text("Future ") - )) - titleItems.append(AnimatedTextComponent.Item( - id: AnyHashable(1), - isUnbreakable: true, - content: .text("Rating") - )) + if let range = futureTitle.range(of: ratingTitle) { + if !futureTitle[.. mapToSignal { value -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> in - if let value = value { - return .single(value) + |> mapToSignal { generatorAndRects -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> in + if let (generator, _) = generatorAndRects { + return .single(generator) } else { return .complete() } diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift index 6f928dde21..52d60129f8 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift @@ -673,14 +673,13 @@ final class StarsTransactionsScreenComponent: Component { let withdrawAvailable = (self.revenueState?.balances.overallRevenue.amount.value ?? 0) > 0 if component.starsContext.ton { - //TODO:localize let proceedsSize = self.proceedsView.update( transition: .immediate, component: AnyComponent(ListSectionComponent( theme: environment.theme, header: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString( - string: "Proceeds Overview".uppercased(), + string: environment.strings.Ton_ProceedsOverview.uppercased(), font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), textColor: environment.theme.list.freeTextColor )), @@ -691,14 +690,14 @@ final class StarsTransactionsScreenComponent: Component { AnyComponentWithIdentity(id: 0, component: AnyComponent(StarsOverviewItemComponent( theme: environment.theme, dateTimeFormat: environment.dateTimeFormat, - title: "Balance Available to Withdraw", + title: environment.strings.Ton_AvailableBalance, value: self.revenueState?.balances.availableBalance ?? CurrencyAmount(amount: .zero, currency: .ton), rate: self.revenueState?.usdRate ?? 0.0 ))), AnyComponentWithIdentity(id: 1, component: AnyComponent(StarsOverviewItemComponent( theme: environment.theme, dateTimeFormat: environment.dateTimeFormat, - title: "Total Lifetime Proceeds", + title: environment.strings.Ton_LifetimeProceeds, value: self.revenueState?.balances.overallRevenue ?? CurrencyAmount(amount: .zero, currency: .ton), rate: self.revenueState?.usdRate ?? 0.0 ))) @@ -725,7 +724,7 @@ final class StarsTransactionsScreenComponent: Component { return (TelegramTextAttributes.URL, contents) }) - let balanceInfoRawString = "Collect your TON using Fragment. [Learn More >]()" + let balanceInfoRawString = environment.strings.Ton_WithdrawViaFragment_Info let balanceInfoString = NSMutableAttributedString(attributedString: parseMarkdownIntoAttributedString(balanceInfoRawString, attributes: termsMarkdownAttributes, textAlignment: .natural)) if self.cachedChevronImage == nil || self.cachedChevronImage?.1 !== environment.theme { self.cachedChevronImage = (generateTintedImage(image: UIImage(bundleImageName: "Contact List/SubtitleArrow"), color: environment.theme.list.itemAccentColor)!, environment.theme) @@ -754,7 +753,7 @@ final class StarsTransactionsScreenComponent: Component { }, tapAction: { [weak self] attributes, _ in if let controller = self?.controller?() as? StarsTransactionsScreen, let navigationController = controller.navigationController as? NavigationController { - component.context.sharedContext.openExternalUrl(context: component.context, urlContext: .generic, url: environment.strings.Stars_BotRevenue_Withdraw_Info_URL, forceExternal: false, presentationData: presentationData, navigationController: navigationController, dismissInput: {}) + component.context.sharedContext.openExternalUrl(context: component.context, urlContext: .generic, url: component.starsContext.ton ? environment.strings.Ton_WithdrawViaFragment_Info_URL : environment.strings.Stars_BotRevenue_Withdraw_Info_URL, forceExternal: false, presentationData: presentationData, navigationController: navigationController, dismissInput: {}) } } )) : nil, @@ -766,7 +765,7 @@ final class StarsTransactionsScreenComponent: Component { count: self.starsState?.balance ?? StarsAmount.zero, currency: component.starsContext.ton ? .ton : .stars, rate: nil, - actionTitle: component.starsContext.ton ? "Withdraw via Fragment" : (withdrawAvailable ? environment.strings.Stars_Intro_BuyShort : environment.strings.Stars_Intro_Buy), + actionTitle: component.starsContext.ton ? environment.strings.Ton_WithdrawViaFragment : (withdrawAvailable ? environment.strings.Stars_Intro_BuyShort : environment.strings.Stars_Intro_Buy), actionAvailable: (!premiumConfiguration.areStarsDisabled && !premiumConfiguration.isPremiumDisabled), actionIsEnabled: true, actionIcon: component.starsContext.ton ? nil : PresentationResourcesItemList.itemListRoundTopupIcon(environment.theme), diff --git a/submodules/TelegramUI/Images.xcassets/Settings/Refresh.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Settings/Refresh.imageset/Contents.json new file mode 100644 index 0000000000..3bd65d39c2 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Settings/Refresh.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "rotate_18.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Settings/Refresh.imageset/rotate_18.pdf b/submodules/TelegramUI/Images.xcassets/Settings/Refresh.imageset/rotate_18.pdf new file mode 100644 index 0000000000..34df5f002f Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Settings/Refresh.imageset/rotate_18.pdf differ diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenWebApp.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenWebApp.swift index 083f47e8d5..adbc9b3266 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenWebApp.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenWebApp.swift @@ -100,10 +100,23 @@ func openWebAppImpl( } } } + + var botPeer = botPeer + if case let .inline(bot) = source { + botPeer = bot + } - let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.BotAppSettings(id: botPeer.id)) - |> deliverOnMainQueue).start(next: { appSettings in - let openWebView = { [weak parentController] in + let _ = combineLatest(queue: Queue.mainQueue(), + context.engine.data.get(TelegramEngine.EngineData.Item.Peer.BotAppSettings(id: botPeer.id)), + ApplicationSpecificNotice.getBotGameNotice(accountManager: context.sharedContext.accountManager, peerId: botPeer.id), + context.engine.messages.attachMenuBots(), + context.engine.messages.getAttachMenuBot(botId: botPeer.id, cached: true) + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) + } + ).start(next: { appSettings, noticed, attachMenuBots, attachMenuBot in + let openWebView: (Bool) -> Void = { [weak parentController] justInstalled in guard let parentController else { return } @@ -305,6 +318,11 @@ func openWebAppImpl( presentImpl = { [weak controller] c, a in controller?.present(c, in: .window(.root), with: a) } + + if justInstalled { + let content: UndoOverlayContent = .succeed(text: presentationData.strings.WebApp_ShortcutsSettingsAdded(botPeer.compactDisplayTitle).string, timeout: 5.0, customUndoText: nil) + controller.present(UndoOverlayController(presentationData: presentationData, content: content, elevatedLayout: false, position: .top, action: { _ in return false }), in: .current) + } }, error: { [weak parentController] error in if let parentController { parentController.present(textAlertController(context: context, updatedPresentationData: updatedPresentationData, title: nil, text: presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: { @@ -314,25 +332,37 @@ func openWebAppImpl( } } - if skipTermsOfService { - openWebView() - } else { - var botPeer = botPeer - if case let .inline(bot) = source { - botPeer = bot + var isAttachMenuBotInstalled: Bool? + if let _ = attachMenuBot { + if let _ = attachMenuBots.first(where: { $0.peer.id == botPeer.id && !$0.flags.contains(.notActivated) }) { + isAttachMenuBotInstalled = true + } else { + isAttachMenuBotInstalled = false } - let _ = (ApplicationSpecificNotice.getBotGameNotice(accountManager: context.sharedContext.accountManager, peerId: botPeer.id) - |> deliverOnMainQueue).startStandalone(next: { [weak parentController] value in - guard let parentController else { - return + } + + if !noticed || attachMenuBot?.flags.contains(.notActivated) == true || isAttachMenuBotInstalled == false { + if let isAttachMenuBotInstalled, let attachMenuBot { + if !isAttachMenuBotInstalled { + let controller = webAppTermsAlertController(context: context, updatedPresentationData: updatedPresentationData, bot: attachMenuBot, completion: { allowWrite in + let _ = ApplicationSpecificNotice.setBotGameNotice(accountManager: context.sharedContext.accountManager, peerId: botPeer.id).startStandalone() + let _ = (context.engine.messages.addBotToAttachMenu(botId: botPeer.id, allowWrite: allowWrite) + |> deliverOnMainQueue).startStandalone(error: { _ in + }, completed: { + openWebView(true) + }) + }) + parentController.present(controller, in: .window(.root)) + } else { + openWebView(false) } - - if value { - openWebView() + } else { + if skipTermsOfService { + openWebView(false) } else { let controller = webAppLaunchConfirmationController(context: context, updatedPresentationData: updatedPresentationData, peer: botPeer, completion: { _ in let _ = ApplicationSpecificNotice.setBotGameNotice(accountManager: context.sharedContext.accountManager, peerId: botPeer.id).startStandalone() - openWebView() + openWebView(false) }, showMore: nil, openTerms: { if let navigationController = parentController.navigationController as? NavigationController { context.sharedContext.openExternalUrl(context: context, urlContext: .generic, url: presentationData.strings.WebApp_LaunchTermsConfirmation_URL, forceExternal: false, presentationData: presentationData, navigationController: navigationController, dismissInput: {}) @@ -340,7 +370,9 @@ func openWebAppImpl( }) parentController.present(controller, in: .window(.root)) } - }) + } + } else { + openWebView(false) } }) } diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerThemeManagement.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerThemeManagement.swift index 4d9aa139cf..8006a42067 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerThemeManagement.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerThemeManagement.swift @@ -123,6 +123,7 @@ import ChatEmptyNode import ChatMediaInputStickerGridItem import AdsInfoScreen import Photos +import ChatThemeScreen extension ChatControllerImpl { public func presentThemeSelection() { @@ -182,15 +183,15 @@ extension ChatControllerImpl { previewTheme: { [weak self] chatTheme, dark in if let strongSelf = self { strongSelf.presentCrossfadeSnapshot() - strongSelf.chatThemeAndDarkAppearancePreviewPromise.set(.single((chatTheme ?? .emoticon(""), dark))) + strongSelf.chatThemeAndDarkAppearancePreviewPromise.set(.single((chatTheme, dark))) } }, changeWallpaper: { [weak self] in - guard let strongSelf = self, let peerId else { + guard let self, let peerId else { return } - if let themeController = strongSelf.themeScreen { - strongSelf.themeScreen = nil + if let themeController = self.themeScreen { + self.themeScreen = nil themeController.dimTapped() } let dismissControllers = { [weak self] in @@ -206,69 +207,67 @@ extension ChatControllerImpl { } var openWallpaperPickerImpl: ((Bool) -> Void)? let openWallpaperPicker = { [weak self] animateAppearance in - guard let strongSelf = self else { + guard let self else { return } let controller = wallpaperMediaPickerController( - context: strongSelf.context, - updatedPresentationData: strongSelf.updatedPresentationData, + context: context, + updatedPresentationData: self.updatedPresentationData, peer: EnginePeer(peer), animateAppearance: animateAppearance, completion: { [weak self] _, result in - guard let strongSelf = self, let asset = result as? PHAsset else { + guard let self, let asset = result as? PHAsset else { return } - let controller = WallpaperGalleryController(context: strongSelf.context, source: .asset(asset), mode: .peer(EnginePeer(peer), false)) + let controller = WallpaperGalleryController(context: context, source: .asset(asset), mode: .peer(EnginePeer(peer), false)) controller.navigationPresentation = .modal - controller.apply = { [weak self] 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: peerId, forBoth: forBoth, completion: { - Queue.mainQueue().after(0.3, { - dismissControllers() - }) + controller.apply = { wallpaper, options, editedImage, cropRect, brightness, forBoth in + uploadCustomPeerWallpaper(context: context, wallpaper: wallpaper, mode: options, editedImage: editedImage, cropRect: cropRect, brightness: brightness, peerId: peerId, forBoth: forBoth, completion: { + Queue.mainQueue().after(0.3, { + dismissControllers() }) - } + }) } - strongSelf.push(controller) + self.push(controller) }, openColors: { [weak self] in - guard let strongSelf = self else { + guard let self else { return } - let controller = standaloneColorPickerController(context: strongSelf.context, peer: EnginePeer(peer), push: { [weak self] controller in - if let strongSelf = self { - strongSelf.push(controller) + let controller = standaloneColorPickerController(context: context, peer: EnginePeer(peer), push: { [weak self] controller in + if let self { + self.push(controller) } }, openGallery: { openWallpaperPickerImpl?(false) }) controller.navigationPresentation = .flatModal - strongSelf.push(controller) + self.push(controller) } ) controller.navigationPresentation = .flatModal - strongSelf.push(controller) + self.push(controller) } openWallpaperPickerImpl = openWallpaperPicker openWallpaperPicker(true) }, resetWallpaper: { [weak self] in - guard let strongSelf = self, let peerId else { + guard let self, let peerId else { return } - let _ = strongSelf.context.engine.themes.setChatWallpaper(peerId: peerId, wallpaper: nil, forBoth: false).startStandalone() + let _ = self.context.engine.themes.setChatWallpaper(peerId: peerId, wallpaper: nil, forBoth: false).startStandalone() }, completion: { [weak self] chatTheme in - guard let strongSelf = self, let peerId else { + guard let self, let peerId else { return } if canResetWallpaper && chatTheme != nil { - let _ = context.engine.themes.setChatWallpaper(peerId: peerId, wallpaper: nil, forBoth: false).startStandalone() + let _ = context.engine.themes.setChatWallpaper(peerId: peerId, wallpaper: nil, forBoth: true).startStandalone() } strongSelf.chatThemeAndDarkAppearancePreviewPromise.set(.single((chatTheme ?? .emoticon(""), nil))) let _ = context.engine.themes.setChatTheme(peerId: peerId, chatTheme: chatTheme ?? .emoticon("")).startStandalone(completed: { [weak self] in - if let strongSelf = self { - strongSelf.chatThemeAndDarkAppearancePreviewPromise.set(.single((nil, nil))) + if let self { + self.chatThemeAndDarkAppearancePreviewPromise.set(.single((nil, nil))) } }) } diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index a6353274f4..70c2e024cd 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -141,6 +141,7 @@ import SuggestedPostApproveAlert import AVFoundation import BalanceNeededScreen import FaceScanScreen +import ChatThemeScreen public final class ChatControllerOverlayPresentationData { public let expandData: (ASDisplayNode?, () -> Void) @@ -1110,8 +1111,29 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G self.present(BotReceiptController(context: self.context, messageId: message.id), in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) } return true - case .setChatTheme: - self.presentThemeSelection() + case let .setChatTheme(chatTheme): + switch chatTheme { + case .emoticon: + self.presentThemeSelection() + case let .gift(gift, _): + if case let .unique(uniqueGift) = gift { + let controller = self.context.sharedContext.makeGiftViewScreen(context: self.context, gift: uniqueGift, shareStory: { [weak self] uniqueGift in + Queue.mainQueue().after(0.15) { + if let self { + let controller = self.context.sharedContext.makeStorySharingScreen(context: self.context, subject: .gift(uniqueGift), parentController: self) + self.push(controller) + } + } + }, openChatTheme: { [weak self] in + if let self { + Queue.mainQueue().after(0.15) { + self.presentThemeSelection() + } + } + }, dismissed: nil) + self.push(controller) + } + } return true case let .setChatWallpaper(wallpaper, _): guard let peer = self.presentationInterfaceState.renderedPeer?.peer else { @@ -5808,10 +5830,18 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G }) } } - case let .gift(gift, wallpaper): - let _ = gift - let _ = wallpaper - //TODO:release + case .gift: + if let darkAppearancePreview = darkAppearancePreview { + useDarkAppearance = darkAppearancePreview + } + if let theme = makePresentationTheme(chatTheme: chatTheme, dark: useDarkAppearance) { + theme.forceSync = true + presentationData = presentationData.withUpdated(theme: theme).withUpdated(chatWallpaper: theme.chat.defaultWallpaper) + + Queue.mainQueue().after(1.0, { + theme.forceSync = false + }) + } } } else if let darkAppearancePreview = darkAppearancePreview { useDarkAppearance = darkAppearancePreview diff --git a/submodules/TelegramUI/Sources/ChatControllerNode.swift b/submodules/TelegramUI/Sources/ChatControllerNode.swift index bcd4b9a5a6..4666344780 100644 --- a/submodules/TelegramUI/Sources/ChatControllerNode.swift +++ b/submodules/TelegramUI/Sources/ChatControllerNode.swift @@ -46,6 +46,7 @@ import ComponentFlow import ChatEmptyNode import SpaceWarpView import ChatSideTopicsPanel +import ChatThemeScreen final class VideoNavigationControllerDropContentItem: NavigationControllerDropContentItem { let itemNode: OverlayMediaItemNode @@ -3582,7 +3583,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { let themeUpdated = presentationReadyUpdated || (self.chatPresentationInterfaceState.theme !== chatPresentationInterfaceState.theme) - self.backgroundNode.update(wallpaper: chatPresentationInterfaceState.chatWallpaper, animated: true) + self.backgroundNode.update(wallpaper: chatPresentationInterfaceState.chatWallpaper, starGift: chatPresentationInterfaceState.theme.starGift, animated: true) self.historyNode.verticalScrollIndicatorColor = UIColor(white: 0.5, alpha: 0.8) if self.pendingSwitchToChatLocation == nil { diff --git a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift index 27843831b2..efc125f26c 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift @@ -2053,7 +2053,7 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto } var isSuspiciousPeer = false - if let cachedUserData = data.cachedData as? CachedUserData, let peerStatusSettings = cachedUserData.peerStatusSettings, peerStatusSettings.flags.contains(.canBlock) { + if let cachedUserData = data.cachedData as? CachedUserData, let peerStatusSettings = cachedUserData.peerStatusSettings, peerStatusSettings.flags.contains(.canBlock) || peerStatusSettings.flags.contains(.canReport) { isSuspiciousPeer = true } @@ -2309,7 +2309,7 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto } let rawTransition = preparedChatHistoryViewTransition(from: previous, to: processedView, reason: reason, reverse: reverse, chatLocation: chatLocation, source: source, controllerInteraction: controllerInteraction, scrollPosition: updatedScrollPosition, scrollAnimationCurve: scrollAnimationCurve, initialData: initialData?.initialData, keyboardButtonsMessage: keyboardButtonsMessage, cachedData: initialData?.cachedData, cachedDataMessages: initialData?.cachedDataMessages, readStateData: initialData?.readStateData, flashIndicators: flashIndicators, updatedMessageSelection: previousSelectedMessages != selectedMessages, messageTransitionNode: messageTransitionNode(), allUpdated: !isSavedMusic || forceUpdateAll) - var mappedTransition = mappedChatHistoryViewListTransition(context: context, chatLocation: chatLocation, associatedData: associatedData, controllerInteraction: controllerInteraction, mode: mode, lastHeaderId: lastHeaderId, isSavedMusic: isSavedMusic, canReorder: canReorder, animateFromPreviousFilter: resetScrolling, transition: rawTransition) + var mappedTransition = mappedChatHistoryViewListTransition(context: context, chatLocation: chatLocation, associatedData: associatedData, controllerInteraction: controllerInteraction, mode: mode, lastHeaderId: lastHeaderId, isSavedMusic: isSavedMusic, canReorder: processedView.filteredEntries.count > 1 && canReorder, animateFromPreviousFilter: resetScrolling, transition: rawTransition) if disableAnimations { mappedTransition.options.remove(.AnimateInsertion) diff --git a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift index faa42dd134..ae30ec10c5 100644 --- a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift +++ b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift @@ -1481,7 +1481,7 @@ func openResolvedUrlImpl( navigationController?.pushViewController(controller) } } - }, dismissed: { + }, openChatTheme: nil, dismissed: { dismissedImpl?() }) navigationController?.pushViewController(controller) diff --git a/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift b/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift index cd2a276ac9..8311472202 100644 --- a/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift +++ b/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift @@ -57,6 +57,8 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu private var savedIdsPromise = Promise?>() private var savedIds: Set? + private var copyProtectionEnabled = false + init( context: AccountContext, chatLocation: ChatLocation, @@ -275,13 +277,17 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu } self.historyNode.endedInteractiveDragging = { [weak self] _ in - guard let strongSelf = self else { + guard let self else { return } - switch strongSelf.historyNode.visibleContentOffset() { + switch self.historyNode.visibleContentOffset() { case let .known(value): - if value <= -10.0 { - strongSelf.requestDismiss() + if let playlistLocation = self.playlistLocation as? PeerMessagesPlaylistLocation, case let .savedMusic(_, _, canReorder) = playlistLocation, canReorder { + + } else { + if value <= -10.0 { + self.requestDismiss() + } } default: break @@ -388,14 +394,25 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu } }) - self.savedIdsDisposable = (context.engine.peers.savedMusicIds() - |> deliverOnMainQueue).start(next: { [weak self] savedIds in + let copyProtectionEnabled: Signal + if case let .peer(peerId) = self.chatLocation { + copyProtectionEnabled = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.CopyProtectionEnabled(id: peerId)) + } else { + copyProtectionEnabled = .single(false) + } + + self.savedIdsDisposable = combineLatest( + queue: Queue.mainQueue(), + context.engine.peers.savedMusicIds(), + copyProtectionEnabled + ).start(next: { [weak self] savedIds, copyProtectionEnabled in guard let self else { return } let isFirstTime = self.savedIds == nil self.savedIds = savedIds self.savedIdsPromise.set(.single(savedIds)) + self.copyProtectionEnabled = copyProtectionEnabled let transition: ContainedViewLayoutTransition = isFirstTime ? .immediate : .animated(duration: 0.5, curve: .spring) self.updateFloatingHeaderOffset(offset: self.floatingHeaderOffset ?? 0.0, transition: transition) @@ -439,11 +456,11 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu panRecognizer.delaysTouchesBegan = false panRecognizer.cancelsTouchesInView = true panRecognizer.shouldBegin = { [weak self] point in - guard let strongSelf = self else { + guard let self else { return false } - if strongSelf.controlsNode.bounds.contains(strongSelf.view.convert(point, to: strongSelf.controlsNode.view)) { - if strongSelf.controlsNode.frame.maxY <= strongSelf.historyNode.frame.minY { + if self.controlsNode.bounds.contains(self.view.convert(point, to: self.controlsNode.view)) { + if self.controlsNode.frame.maxY <= self.historyNode.frame.minY { return true } } @@ -494,8 +511,13 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu return .single(true) } }) - self.historyNode.useMainQueueTransactions = false self.historyNode.autoScrollWhenReordering = false + self.historyNode.didEndScrollingWithOverscroll = { [weak self] in + guard let self else { + return + } + self.requestDismiss() + } } @@ -557,19 +579,18 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu func addToSavedMusic(file: FileMediaReference) { self.dismissAllTooltips() - var actionText: String? = "View" + var actionText: String? = self.presentationData.strings.MediaPlayer_SavedMusic_AddedToProfile_View if let itemId = self.controlsNode.currentItemId as? PeerMessagesMediaPlaylistItemId, itemId.messageId.namespace == Namespaces.Message.Local && itemId.messageId.peerId == self.context.account.peerId { actionText = nil } - //TODO:localize let controller = UndoOverlayController( presentationData: self.presentationData, content: .universalImage( image: generateTintedImage(image: UIImage(bundleImageName: "Peer Info/SavedMusic"), color: .white)!, size: nil, title: nil, - text: "Audio added to your profile.", + text: self.presentationData.strings.MediaPlayer_SavedMusic_AddedToProfile, customUndoText: actionText, timeout: 3.0 ), @@ -607,14 +628,13 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu func removeFromSavedMusic(file: FileMediaReference) { self.dismissAllTooltips() - //TODO:localize let controller = UndoOverlayController( presentationData: self.presentationData, content: .universalImage( image: generateTintedImage(image: UIImage(bundleImageName: "Peer Info/SavedMusic"), color: .white)!, size: nil, title: nil, - text: "Audio removed from your profile.", + text: self.presentationData.strings.MediaPlayer_SavedMusic_RemovedFromProfile, customUndoText: nil, timeout: 3.0 ), @@ -638,6 +658,12 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu } private var isSaved: Bool? { + if self .copyProtectionEnabled { + return nil + } + if case let .peer(peerId) = self.chatLocation, peerId.namespace == Namespaces.Peer.SecretChat { + return nil + } guard let fileReference = self.controlsNode.currentFileReference else { return nil } @@ -672,7 +698,6 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu insets.top = max(0.0, listNodeSize.height - floor(56.0 * 3.5)) var itemOffsetInsets = insets - if let playlistLocation = self.playlistLocation as? PeerMessagesPlaylistLocation, case let .savedMusic(_, _, canReorder) = playlistLocation, canReorder { itemOffsetInsets.top = 0.0 itemOffsetInsets.bottom = 0.0 @@ -732,7 +757,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu self.requestDismiss() } } - + override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { if let recognizer = gestureRecognizer as? UIPanGestureRecognizer { let location = recognizer.location(in: self.view) @@ -981,9 +1006,16 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu insets.top = max(0.0, listNodeSize.height - floor(56.0 * 3.5)) + var itemOffsetInsets = insets + if let playlistLocation = self.playlistLocation as? PeerMessagesPlaylistLocation, case let .savedMusic(_, _, canReorder) = playlistLocation, canReorder { + itemOffsetInsets.top = 0.0 + itemOffsetInsets.bottom = 0.0 + insets = itemOffsetInsets + } + self.historyNode.frame = CGRect(origin: CGPoint(x: 0.0, y: listTopInset), size: listNodeSize) - let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: listNodeSize, insets: insets, duration: 0.0, curve: .Default(duration: nil)) + let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: listNodeSize, insets: insets, itemOffsetInsets: itemOffsetInsets, duration: 0.0, curve: .Default(duration: nil)) self.historyNode.updateLayout(transition: .immediate, updateSizeAndInsets: updateSizeAndInsets) self.historyNode.recursivelyEnsureDisplaySynchronously(true) @@ -1010,10 +1042,9 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu } var items: [ContextMenuItem] = [] - //TODO:localize if canSaveToProfile || canSaveToSavedMessages { items.append( - .action(ContextMenuActionItem(text: "Save to...", icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/DownloadTone"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, _ in + .action(ContextMenuActionItem(text: presentationData.strings.MediaPlayer_ContextMenu_SaveTo, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/DownloadTone"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, _ in if let self { var subActions: [ContextMenuItem] = [] subActions.append( @@ -1025,7 +1056,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu if canSaveToProfile { subActions.append( - .action(ContextMenuActionItem(text: "Profile", icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/User"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in + .action(ContextMenuActionItem(text: presentationData.strings.MediaPlayer_ContextMenu_SaveTo_Profile, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/User"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in f(.default) if let self { @@ -1037,7 +1068,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu if canSaveToSavedMessages { subActions.append( - .action(ContextMenuActionItem(text: "Saved Messages", icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Fave"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in + .action(ContextMenuActionItem(text: presentationData.strings.MediaPlayer_ContextMenu_SaveTo_SavedMessages, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Fave"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in f(.default) if let self { @@ -1048,7 +1079,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu } subActions.append( - .action(ContextMenuActionItem(text: "Files", icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Save"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in + .action(ContextMenuActionItem(text: presentationData.strings.MediaPlayer_ContextMenu_SaveTo_Files, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Save"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in f(.default) if let self { @@ -1072,7 +1103,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu let noAction: ((ContextMenuActionItem.Action) -> Void)? = nil subActions.append( - .action(ContextMenuActionItem(text: "Choose where you want this audio to be saved.", textLayout: .multiline, textFont: .small, icon: { _ in return nil }, action: noAction)) + .action(ContextMenuActionItem(text: presentationData.strings.MediaPlayer_ContextMenu_SaveTo_Info, textLayout: .multiline, textFont: .small, icon: { _ in return nil }, action: noAction)) ) c?.pushItems(items: .single(ContextController.Items(content: .list(subActions)))) @@ -1080,7 +1111,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu })) ) } else { - items.append(.action(ContextMenuActionItem(text: "Save to Files", icon: { theme in + items.append(.action(ContextMenuActionItem(text: presentationData.strings.MediaPlayer_ContextMenu_SaveToFiles, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Save"), color: theme.actionSheet.primaryTextColor) }, action: { [weak self] _, f in f(.default) @@ -1112,7 +1143,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu addedSeparator = true } items.append( - .action(ContextMenuActionItem(text: "Show in Chat", icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/GoToMessage"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in + .action(ContextMenuActionItem(text: presentationData.strings.MediaPlayer_ContextMenu_ShowInChat, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/GoToMessage"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in f(.dismissWithoutContent) guard let self else { @@ -1125,7 +1156,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu } // items.append( - // .action(ContextMenuActionItem(text: "Forward", icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Forward"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in + // .action(ContextMenuActionItem(text: presentationData.strings.MediaPlayer_ContextMenu_Forward, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Forward"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in // f(.default) // // if let _ = self { @@ -1162,9 +1193,9 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu items.append(.separator) addedSeparator = true } - var actionTitle = "Delete" + var actionTitle = presentationData.strings.MediaPlayer_ContextMenu_Delete if case .custom = self.source { - actionTitle = "Remove" + actionTitle = presentationData.strings.MediaPlayer_ContextMenu_Remove } items.append( .action(ContextMenuActionItem(text: actionTitle, textColor: .destructive, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor) }, action: { [weak self] c, f in diff --git a/submodules/TelegramUI/Sources/OverlayPlayerControlsNode.swift b/submodules/TelegramUI/Sources/OverlayPlayerControlsNode.swift index 3132575d1e..6f8141ddb2 100644 --- a/submodules/TelegramUI/Sources/OverlayPlayerControlsNode.swift +++ b/submodules/TelegramUI/Sources/OverlayPlayerControlsNode.swift @@ -1026,13 +1026,12 @@ final class OverlayPlayerControlsNode: ASDisplayNode { self.separatorNode.isHidden = hasSectionHeader if hasSectionHeader { - //TODO:localize let sideInset: CGFloat = 16.0 - var sectionTitle = "AUDIO IN THIS CHAT" + var sectionTitle = self.presentationData.strings.MediaPlayer_Playlist_ThisChat if let peerName = self.peerName { - sectionTitle = "\(peerName.uppercased())'S PLAYLIST" + sectionTitle = self.presentationData.strings.MediaPlayer_Playlist_SavedMusic(peerName.uppercased()).string } else if case .custom = self.source { - sectionTitle = "YOUR PLAYLIST" + sectionTitle = self.presentationData.strings.MediaPlayer_Playlist_SavedMusicYou } let sectionTitleSize = self.sectionTitle.update( transition: .immediate, @@ -1096,7 +1095,7 @@ final class OverlayPlayerControlsNode: ASDisplayNode { return (TelegramTextAttributes.URL, contents) }) - let attributedString = parseMarkdownIntoAttributedString("This audio is visible on your profile. [Remove >]()", attributes: markdownAttributes, textAlignment: .center).mutableCopy() as! NSMutableAttributedString + let attributedString = parseMarkdownIntoAttributedString(self.presentationData.strings.MediaPlayer_SavedMusic_RemoveFromProfile, attributes: markdownAttributes, textAlignment: .center).mutableCopy() as! NSMutableAttributedString if let range = attributedString.string.range(of: ">"), let chevronImage = self.cachedChevronImage?.0 { attributedString.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: attributedString.string)) attributedString.addAttribute(.baselineOffset, value: 1.0, range: NSRange(range, in: attributedString.string)) @@ -1125,7 +1124,6 @@ final class OverlayPlayerControlsNode: ASDisplayNode { )) profileAudioOffset = 18.0 } else { - //TODO:localize profileAudioComponent = AnyComponent(ButtonComponent( background: ButtonComponent.Background( color: self.presentationData.theme.list.itemCheckColors.fillColor, @@ -1139,7 +1137,7 @@ final class OverlayPlayerControlsNode: ASDisplayNode { BundleIconComponent(name: "Peer Info/SaveMusic", tintColor: self.presentationData.theme.list.itemCheckColors.foregroundColor) )), AnyComponentWithIdentity(id: "label", component: AnyComponent( - MultilineTextComponent(text: .plain(NSAttributedString(string: "Add to Profile", font: Font.semibold(17.0), textColor: self.presentationData.theme.list.itemCheckColors.foregroundColor))) + MultilineTextComponent(text: .plain(NSAttributedString(string: self.presentationData.strings.MediaPlayer_SavedMusic_AddToProfile, font: Font.semibold(17.0), textColor: self.presentationData.theme.list.itemCheckColors.foregroundColor))) )) ], spacing: 8.0) )), diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index 693d64930e..59b425118f 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -3768,8 +3768,8 @@ public final class SharedAccountContextImpl: SharedAccountContext { return GiftViewScreen(context: context, subject: .message(message), shareStory: shareStory) } - public func makeGiftViewScreen(context: AccountContext, gift: StarGift.UniqueGift, shareStory: ((StarGift.UniqueGift) -> Void)?, dismissed: (() -> Void)?) -> ViewController { - let controller = GiftViewScreen(context: context, subject: .uniqueGift(gift, nil), shareStory: shareStory) + public func makeGiftViewScreen(context: AccountContext, gift: StarGift.UniqueGift, shareStory: ((StarGift.UniqueGift) -> Void)?, openChatTheme: (() -> Void)?, dismissed: (() -> Void)?) -> ViewController { + let controller = GiftViewScreen(context: context, subject: .uniqueGift(gift, nil), shareStory: shareStory, openChatTheme: openChatTheme) controller.disposed = { dismissed?() } diff --git a/submodules/WallpaperBackgroundNode/Sources/MetalWallpaperBackgroundNode.swift b/submodules/WallpaperBackgroundNode/Sources/MetalWallpaperBackgroundNode.swift deleted file mode 100644 index 8b13789179..0000000000 --- a/submodules/WallpaperBackgroundNode/Sources/MetalWallpaperBackgroundNode.swift +++ /dev/null @@ -1 +0,0 @@ - diff --git a/submodules/WallpaperBackgroundNode/Sources/WallpaperBackgroundNode.swift b/submodules/WallpaperBackgroundNode/Sources/WallpaperBackgroundNode.swift index 0e69daa3c4..4bfe3ee995 100644 --- a/submodules/WallpaperBackgroundNode/Sources/WallpaperBackgroundNode.swift +++ b/submodules/WallpaperBackgroundNode/Sources/WallpaperBackgroundNode.swift @@ -8,6 +8,7 @@ import TelegramCore import AccountContext import SwiftSignalKit import WallpaperResources +import StickerResources import FastBlur import Svg import GZip @@ -85,6 +86,7 @@ public protocol WallpaperBackgroundNode: ASDisplayNode { var rotation: CGFloat { get set } func update(wallpaper: TelegramWallpaper, animated: Bool) + func update(wallpaper: TelegramWallpaper, starGift: StarGift?, animated: Bool) func _internalUpdateIsSettingUpWallpaper() func updateLayout(size: CGSize, displayMode: WallpaperDisplayMode, transition: ContainedViewLayoutTransition) func updateIsLooping(_ isLooping: Bool) @@ -758,11 +760,20 @@ public final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgrou private var validLayout: (CGSize, WallpaperDisplayMode)? private var wallpaper: TelegramWallpaper? + private var starGift: StarGift? + private var modelRectIndex: Int32? + + private var modelStickerNode: DefaultAnimatedStickerNodeImpl? + private var isSettingUpWallpaper: Bool = false private struct CachedValidPatternImage { let generate: (TransformImageArguments) -> DrawingContext? let generated: ValidPatternGeneratedImage + let rects: [WallpaperGiftPatternRect] + let starGift: StarGift? + let symbolImage: UIImage? + let modelRectIndex: Int32? let image: UIImage } @@ -771,6 +782,10 @@ public final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgrou private struct ValidPatternImage { let wallpaper: TelegramWallpaper let invertPattern: Bool + let rects: [WallpaperGiftPatternRect] + let starGift: StarGift? + let symbolImage: UIImage? + let modelRectIndex: Int32? let generate: (TransformImageArguments) -> DrawingContext? } private var validPatternImage: ValidPatternImage? @@ -781,10 +796,38 @@ public final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgrou let patternColor: UInt32 let backgroundColor: UInt32 let invertPattern: Bool + let starGift: StarGift? + let modelRectIndex: Int32? + + public static func ==(lhs: ValidPatternGeneratedImage, rhs: ValidPatternGeneratedImage) -> Bool { + if lhs.wallpaper != rhs.wallpaper { + return false + } + if lhs.size != rhs.size { + return false + } + if lhs.patternColor != rhs.patternColor { + return false + } + if lhs.backgroundColor != rhs.backgroundColor { + return false + } + if lhs.invertPattern != rhs.invertPattern { + return false + } + if lhs.starGift?.slug != rhs.starGift?.slug { + return false + } + if lhs.modelRectIndex != rhs.modelRectIndex { + return false + } + return true + } } private var validPatternGeneratedImage: ValidPatternGeneratedImage? private let patternImageDisposable = MetaDisposable() + private let symbolImageDisposable = MetaDisposable() private var bubbleTheme: PresentationTheme? private var bubbleCorners: PresentationChatBubbleCorners? @@ -930,11 +973,26 @@ public final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgrou } public func update(wallpaper: TelegramWallpaper, animated: Bool) { - if self.wallpaper == wallpaper { + self.update(wallpaper: wallpaper, starGift: nil, animated: animated) + } + + public func update(wallpaper: TelegramWallpaper, starGift: StarGift?, animated: Bool) { + if self.wallpaper == wallpaper && self.starGift == starGift { return } let previousWallpaper = self.wallpaper + let previousStarGift = self.starGift + self.wallpaper = wallpaper + self.starGift = starGift + + if previousWallpaper != wallpaper || previousStarGift?.slug != starGift?.slug { + if let _ = starGift { + self.modelRectIndex = Int32.random(in: 0 ..< 10) + } else { + self.modelRectIndex = nil + } + } if let _ = previousWallpaper, animated { if let snapshotView = self.view.snapshotView(afterScreenUpdates: false) { @@ -1132,6 +1190,7 @@ public final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgrou } default: self.patternImageDisposable.set(nil) + self.symbolImageDisposable.set(nil) self.validPatternImage = nil self.patternImageLayer.isHidden = true self.patternImageLayer.fillWithColorUntilLoaded = nil @@ -1146,6 +1205,9 @@ public final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgrou guard let wallpaper = self.wallpaper else { return } + + let starGift = self.starGift + let modelRectIndex = self.modelRectIndex var invertPattern: Bool = false var patternIsLight: Bool = false @@ -1169,13 +1231,20 @@ public final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgrou break } } + + if let previousStarGift = self.validPatternImage?.starGift, !updated { + updated = true + if previousStarGift.slug == starGift?.slug { + updated = false + } + } if updated { self.validPatternGeneratedImage = nil self.validPatternImage = nil - if let cachedValidPatternImage = WallpaperBackgroundNodeImpl.cachedValidPatternImage, cachedValidPatternImage.generated.wallpaper == wallpaper && cachedValidPatternImage.generated.invertPattern == invertPattern { - self.validPatternImage = ValidPatternImage(wallpaper: cachedValidPatternImage.generated.wallpaper, invertPattern: invertPattern, generate: cachedValidPatternImage.generate) + if let cachedValidPatternImage = WallpaperBackgroundNodeImpl.cachedValidPatternImage, cachedValidPatternImage.generated.wallpaper == wallpaper && cachedValidPatternImage.generated.invertPattern == invertPattern && cachedValidPatternImage.starGift == starGift && cachedValidPatternImage.modelRectIndex == modelRectIndex { + self.validPatternImage = ValidPatternImage(wallpaper: cachedValidPatternImage.generated.wallpaper, invertPattern: invertPattern, rects: cachedValidPatternImage.rects, starGift: cachedValidPatternImage.starGift, symbolImage: cachedValidPatternImage.symbolImage, modelRectIndex: cachedValidPatternImage.modelRectIndex, generate: cachedValidPatternImage.generate) } else { func reference(for resource: EngineMediaResource, media: EngineMedia) -> MediaResourceReference { return .wallpaper(wallpaper: .slug(file.slug), resource: resource._asResource()) @@ -1189,37 +1258,33 @@ public final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgrou convertedRepresentations.append(ImageRepresentationWithReference(representation: .init(dimensions: dimensions, resource: file.file.resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false), reference: reference(for: EngineMediaResource(file.file.resource), media: EngineMedia(file.file)))) let signal = patternWallpaperImage(account: self.context.account, accountManager: self.context.sharedContext.accountManager, representations: convertedRepresentations, mode: .screen, autoFetchFullSize: true) - self.patternImageDisposable.set((signal - |> deliverOnMainQueue).start(next: { [weak self] generator in - guard let strongSelf = self else { + var symbolImage: Signal = .single(nil) + if let starGift = self.starGift, case let .unique(uniqueGift) = starGift { + for attribute in uniqueGift.attributes { + if case let .pattern(_, file, _) = attribute, let dimensions = file.dimensions { + let size = dimensions.cgSize.aspectFitted(CGSize(width: 160.0, height: 160.0)) + symbolImage = chatMessageAnimatedSticker(postbox: self.context.account.postbox, userLocation: .other, file: file, small: false, size: size) + |> map { generator -> UIImage? in + return generator(TransformImageArguments(corners: ImageCorners(), imageSize: size, boundingSize: size, intrinsicInsets: .zero))?.generateImage() + } + break + } + } + } + self.patternImageDisposable.set(combineLatest(queue: Queue.mainQueue(), signal, symbolImage).start(next: { [weak self] generator, symbolImage in + guard let self else { return } - - if let generator = generator { - /*generator = { arguments in - let scale = arguments.scale ?? UIScreenScale - let context = DrawingContext(size: arguments.drawingSize, scale: scale, clear: true) - - context.withFlippedContext { c in - if let path = getAppBundle().path(forResource: "PATTERN_static", ofType: "svg"), let data = try? Data(contentsOf: URL(fileURLWithPath: path)) { - if let image = drawSvgImage(data, CGSize(width: arguments.drawingSize.width * scale, height: arguments.drawingSize.height * scale), .clear, .black, false) { - c.draw(image.cgImage!, in: CGRect(origin: CGPoint(), size: arguments.drawingSize)) - } - } - } - - return context - }*/ - - strongSelf.validPatternImage = ValidPatternImage(wallpaper: wallpaper, invertPattern: invertPattern, generate: generator) - strongSelf.validPatternGeneratedImage = nil - if let (size, displayMode) = strongSelf.validLayout { - strongSelf.loadPatternForSizeIfNeeded(size: size, displayMode: displayMode, transition: .immediate) + if let (generator, rects) = generator { + self.validPatternImage = ValidPatternImage(wallpaper: wallpaper, invertPattern: invertPattern, rects: rects, starGift: starGift, symbolImage: symbolImage, modelRectIndex: modelRectIndex, generate: generator) + self.validPatternGeneratedImage = nil + if let (size, displayMode) = self.validLayout { + self.loadPatternForSizeIfNeeded(size: size, displayMode: displayMode, transition: .immediate) } else { - strongSelf._isReady.set(true) + self._isReady.set(true) } } else { - strongSelf._isReady.set(true) + self._isReady.set(true) } })) } @@ -1244,8 +1309,8 @@ public final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgrou self.patternImageLayer.backgroundColor = nil } - let updatedGeneratedImage = ValidPatternGeneratedImage(wallpaper: validPatternImage.wallpaper, size: size, patternColor: patternColor.rgb, backgroundColor: patternBackgroundColor.rgb, invertPattern: invertPattern) - + let updatedGeneratedImage = ValidPatternGeneratedImage(wallpaper: validPatternImage.wallpaper, size: size, patternColor: patternColor.rgb, backgroundColor: patternBackgroundColor.rgb, invertPattern: invertPattern, starGift: starGift, modelRectIndex: modelRectIndex) + if self.validPatternGeneratedImage != updatedGeneratedImage { self.validPatternGeneratedImage = updatedGeneratedImage @@ -1256,7 +1321,7 @@ public final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgrou self.patternImageLayer.suspendCompositionUpdates = false self.patternImageLayer.updateCompositionIfNeeded() } else { - let patternArguments = TransformImageArguments(corners: ImageCorners(), imageSize: size, boundingSize: size, intrinsicInsets: UIEdgeInsets(), custom: PatternWallpaperArguments(colors: [patternBackgroundColor], rotation: nil, customPatternColor: patternColor, preview: false, displayMode: displayMode.argumentsDisplayMode), scale: min(2.0, UIScreenScale)) + let patternArguments = TransformImageArguments(corners: ImageCorners(), imageSize: size, boundingSize: size, intrinsicInsets: UIEdgeInsets(), custom: PatternWallpaperArguments(colors: [patternBackgroundColor], rotation: nil, customPatternColor: patternColor, preview: false, displayMode: displayMode.argumentsDisplayMode, symbolImage: generateTintedImage(image: validPatternImage.symbolImage, color: .white), modelRectIndex: self.modelRectIndex), scale: min(2.0, UIScreenScale)) if self.useSharedAnimationPhase || self.patternImageLayer.contents == nil { if let drawingContext = validPatternImage.generate(patternArguments) { if let image = drawingContext.generateImage() { @@ -1267,7 +1332,7 @@ public final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgrou self.patternImageLayer.updateCompositionIfNeeded() if self.useSharedAnimationPhase { - WallpaperBackgroundNodeImpl.cachedValidPatternImage = CachedValidPatternImage(generate: validPatternImage.generate, generated: updatedGeneratedImage, image: image) + WallpaperBackgroundNodeImpl.cachedValidPatternImage = CachedValidPatternImage(generate: validPatternImage.generate, generated: updatedGeneratedImage, rects: validPatternImage.rects, starGift: validPatternImage.starGift, symbolImage: validPatternImage.symbolImage, modelRectIndex: validPatternImage.modelRectIndex, image: image) } } else { self.updatePatternPresentation() @@ -1288,7 +1353,7 @@ public final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgrou strongSelf.updatePatternPresentation() if let image = image, strongSelf.useSharedAnimationPhase { - WallpaperBackgroundNodeImpl.cachedValidPatternImage = CachedValidPatternImage(generate: validPatternImage.generate, generated: updatedGeneratedImage, image: image) + WallpaperBackgroundNodeImpl.cachedValidPatternImage = CachedValidPatternImage(generate: validPatternImage.generate, generated: updatedGeneratedImage, rects: validPatternImage.rects, starGift: validPatternImage.starGift, symbolImage: validPatternImage.symbolImage, modelRectIndex: validPatternImage.modelRectIndex, image: image) } } } @@ -1306,6 +1371,68 @@ public final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgrou self.updatePatternPresentation() } } + + var modelFile: TelegramMediaFile? + if let validPatternImage = self.validPatternImage, !validPatternImage.rects.isEmpty, let starGift = validPatternImage.starGift { + if case let .unique(uniqueGift) = starGift { + for attribute in uniqueGift.attributes { + if case let .model(_, file, _) = attribute { + modelFile = file + } + } + } + } + if let validPatternImage = self.validPatternImage, !validPatternImage.rects.isEmpty, var modelRectIndex = self.modelRectIndex, let modelFile { + let filteredRects = validPatternImage.rects.filter { $0.center.y > $0.containerSize.height * 0.1 && $0.center.y < $0.containerSize.height * 0.9 } + modelRectIndex = modelRectIndex % Int32(filteredRects.count); + + let rect = filteredRects[Int(modelRectIndex)] + + let modelStickerNode: DefaultAnimatedStickerNodeImpl + if let current = self.modelStickerNode { + modelStickerNode = current + } else { + modelStickerNode = DefaultAnimatedStickerNodeImpl() + modelStickerNode.setup(source: AnimatedStickerResourceSource(account: self.context.account, resource: modelFile.resource, isVideo: false), width: 96, height: 96, playbackMode: .once, mode: .direct(cachePathPrefix: nil)) + modelStickerNode.visibility = true + self.modelStickerNode = modelStickerNode + self.addSubnode(modelStickerNode) + } + + let targetSize: CGSize = self.bounds.size + let containerSize: CGSize = rect.containerSize + let useAspectFit: Bool = false + + let renderScale: CGFloat = useAspectFit + ? min(targetSize.width / containerSize.width, targetSize.height / containerSize.height) + : max(targetSize.width / containerSize.width, targetSize.height / containerSize.height) + + let drawingSize = CGSize(width: containerSize.width * renderScale, height: containerSize.height * renderScale) + + let offsetX = (targetSize.width - drawingSize.width) * 0.5 + let offsetY = (targetSize.height - drawingSize.height) * 0.5 + + let onScreenCenter = CGPoint(x: offsetX + rect.center.x * renderScale, y: offsetY + rect.center.y * renderScale) + + let side = rect.side * rect.scale * renderScale + modelStickerNode.bounds = CGRect(origin: .zero, size: CGSize(width: side, height: side)) + modelStickerNode.position = onScreenCenter + modelStickerNode.updateLayout(size: modelStickerNode.bounds.size) + modelStickerNode.alpha = 0.5 + + modelStickerNode.layer.transform = CATransform3DMakeRotation(rect.rotation, 0, 0, 1) + } else { + if let modelStickerNode = self.modelStickerNode { + self.modelStickerNode = nil + if transition.isAnimated { + modelStickerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { [weak modelStickerNode] _ in + modelStickerNode?.removeFromSupernode() + }) + } else { + modelStickerNode.removeFromSupernode() + } + } + } transition.updateFrame(layer: self.patternImageLayer, frame: CGRect(origin: CGPoint(), size: size)) } @@ -1559,3 +1686,14 @@ private protocol WallpaperComponentView: AnyObject { public func createWallpaperBackgroundNode(context: AccountContext, forChatDisplay: Bool, useSharedAnimationPhase: Bool = false) -> WallpaperBackgroundNode { return WallpaperBackgroundNodeImpl(context: context, useSharedAnimationPhase: useSharedAnimationPhase) } + +private extension StarGift { + var slug: String? { + switch self { + case let .unique(uniqueGift): + return uniqueGift.slug + default: + return nil + } + } +} diff --git a/submodules/WallpaperResources/Sources/WallpaperResources.swift b/submodules/WallpaperResources/Sources/WallpaperResources.swift index 41f5be0378..588c1cc851 100644 --- a/submodules/WallpaperResources/Sources/WallpaperResources.swift +++ b/submodules/WallpaperResources/Sources/WallpaperResources.swift @@ -353,14 +353,18 @@ public struct PatternWallpaperArguments: TransformImageCustomArguments { let customPatternColor: UIColor? let bakePatternAlpha: CGFloat let displayMode: DisplayMode + let symbolImage: UIImage? + let modelRectIndex: Int32? - public init(colors: [UIColor], rotation: Int32?, customPatternColor: UIColor? = nil, preview: Bool = false, bakePatternAlpha: CGFloat = 1.0, displayMode: DisplayMode = .aspectFill) { + public init(colors: [UIColor], rotation: Int32?, customPatternColor: UIColor? = nil, preview: Bool = false, bakePatternAlpha: CGFloat = 1.0, displayMode: DisplayMode = .aspectFill, symbolImage: UIImage? = nil, modelRectIndex: Int32? = nil) { self.colors = colors self.rotation = rotation self.customPatternColor = customPatternColor self.preview = preview self.bakePatternAlpha = bakePatternAlpha self.displayMode = displayMode + self.symbolImage = symbolImage + self.modelRectIndex = modelRectIndex } public func serialized() -> NSArray { @@ -373,6 +377,9 @@ public struct PatternWallpaperArguments: TransformImageCustomArguments { array.add(NSNumber(value: self.preview)) array.add(NSNumber(value: Double(self.bakePatternAlpha))) array.add(NSNumber(value: self.displayMode.rawValue)) + if let symbolImage { + array.add(symbolImage) + } return array } } @@ -470,18 +477,34 @@ private func patternWallpaperDatas(account: Account, accountManager: AccountMana } } -public func patternWallpaperImage(account: Account, accountManager: AccountManager, representations: [ImageRepresentationWithReference], mode: PatternWallpaperDrawMode, autoFetchFullSize: Bool = false) -> Signal<((TransformImageArguments) -> DrawingContext?)?, NoError> { +public struct WallpaperGiftPatternRect: Equatable { + public let containerSize: CGSize + public let center: CGPoint + public let side: CGFloat + public let scale: CGFloat + public let rotation: CGFloat + + fileprivate init(containerSize: CGSize, rect: GiftPatternRect) { + self.containerSize = containerSize + self.center = rect.center + self.side = rect.side + self.scale = rect.scale + self.rotation = rect.rotation + } +} + +public func patternWallpaperImage(account: Account, accountManager: AccountManager, representations: [ImageRepresentationWithReference], mode: PatternWallpaperDrawMode, autoFetchFullSize: Bool = false, forcePrepared: Bool = false) -> Signal<(generator: (TransformImageArguments) -> DrawingContext?, rects: [WallpaperGiftPatternRect])?, NoError> { return patternWallpaperDatas(account: account, accountManager: accountManager, representations: representations, mode: mode, autoFetchFullSize: autoFetchFullSize) |> mapToSignal { fullSizeData, fullSizeComplete in if !autoFetchFullSize || fullSizeComplete { - return patternWallpaperImageInternal(fullSizeData: fullSizeData, fullSizeComplete: fullSizeComplete, mode: mode) + return patternWallpaperImageInternal(fullSizeData: fullSizeData, fullSizeComplete: fullSizeComplete, mode: mode, forcePrepared: forcePrepared) } else { return .single(nil) } } } -private func patternWallpaperImageInternal(fullSizeData: Data?, fullSizeComplete: Bool, mode: PatternWallpaperDrawMode) -> Signal<((TransformImageArguments) -> DrawingContext?)?, NoError> { +private func patternWallpaperImageInternal(fullSizeData: Data?, fullSizeComplete: Bool, mode: PatternWallpaperDrawMode, forcePrepared: Bool = false) -> Signal<(generator: (TransformImageArguments) -> DrawingContext?, rects: [WallpaperGiftPatternRect])?, NoError> { var prominent = false if case .thumbnail = mode { prominent = true @@ -491,7 +514,11 @@ private func patternWallpaperImageInternal(fullSizeData: Data?, fullSizeComplete return .single((fullSizeData, fullSizeComplete)) |> map { fullSizeData, fullSizeComplete in - return { arguments in + var rects: [WallpaperGiftPatternRect] = [] + if let fullSizeData, let patternData = getGiftPatternData(fullSizeData) { + rects = patternData.rects.map { WallpaperGiftPatternRect(containerSize: patternData.size, rect: $0) } + } + return ({ arguments in var scale = scale if scale.isZero { scale = arguments.scale ?? UIScreenScale @@ -561,12 +588,12 @@ private func patternWallpaperImageInternal(fullSizeData: Data?, fullSizeComplete var image: UIImage? if let fullSizeData = fullSizeData { if mode == .screen { - image = renderPreparedImage(fullSizeData, CGSize(width: size.width * context.scale, height: size.height * context.scale), .black, 1.0, displayMode == .aspectFit) + image = renderPreparedImageWithSymbol(fullSizeData, CGSize(width: size.width * context.scale, height: size.height * context.scale), .black, 1.0, displayMode == .aspectFit, customArguments.symbolImage, customArguments.modelRectIndex ?? -1) } else { image = UIImage(data: fullSizeData) } } - + if let customPatternColor = customArguments.customPatternColor, customPatternColor.alpha < 1.0 { patternIsInverted = true c.setBlendMode(.copy) @@ -674,7 +701,7 @@ private func patternWallpaperImageInternal(fullSizeData: Data?, fullSizeComplete } else { return nil } - } + }, rects) } } @@ -1452,6 +1479,10 @@ public func themeIconImage(account: Account, accountManager: AccountManager mapToSignal { wallpaper in if let wallpaper = wallpaper, case let .file(file) = wallpaper.wallpaper { @@ -1479,6 +1511,7 @@ public func themeIconImage(account: Account, accountManager: AccountManager 0.3 arguments = PatternWallpaperArguments(colors: [.clear], rotation: nil, customPatternColor: isLight ? .black : .white) } - - return patternWallpaperImage(account: account, accountManager: accountManager, representations: convertedPreviewRepresentations, mode: .thumbnail, autoFetchFullSize: true) - |> mapToSignal { generator -> Signal<((UIColor, UIColor?, [UInt32]), [UIColor], [UIColor], UIImage?, Bool, Bool, CGFloat, Int32?), NoError> in + return patternWallpaperImage(account: account, accountManager: accountManager, representations: useFallback ? convertedRepresentations : convertedPreviewRepresentations, mode: useFallback ? .screen : .thumbnail, autoFetchFullSize: true) + |> mapToSignal { generatorAndRects -> Signal<((UIColor, UIColor?, [UInt32]), [UIColor], [UIColor], UIImage?, Bool, Bool, CGFloat, Int32?), NoError> in let imageSize = CGSize(width: 148.0, height: 320.0) let imageArguments = TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets(), emptyColor: nil, custom: arguments) - let context = generator?(imageArguments) + let context = generatorAndRects?.generator(imageArguments) let image = context?.generateImage() if !file.settings.colors.isEmpty { @@ -1771,7 +1803,7 @@ public func themeIconImage(account: Account, accountManager: AccountManager