From 4e1b9943a4ed65a37073cc2bfaa8639bac598e04 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Tue, 26 Aug 2025 15:44:41 +0400 Subject: [PATCH 01/32] Various improvements --- .../Telegram-iOS/en.lproj/Localizable.strings | 8 + .../Sources/AccountContext.swift | 2 +- submodules/Svg/PublicHeaders/Svg/Svg.h | 20 + submodules/Svg/Sources/Svg.m | 1346 ++++++++++------- .../Sources/State/Serialization.swift | 2 +- .../SyncCore/SyncCore_TelegramWallpaper.swift | 31 + .../Sources/MakePresentationTheme.swift | 25 +- .../Sources/PresentationTheme.swift | 1 + .../Sources/ServiceMessageStrings.swift | 21 +- .../Sources/ChatMessageBubbleItemNode.swift | 2 + .../ChatMessageGiftBubbleContentNode/BUILD | 1 + .../ChatMessageGiftBubbleContentNode.swift | 53 +- .../ChatMessageInteractiveMediaNode.swift | 2 +- ...hatMessageWallpaperBubbleContentNode.swift | 2 +- .../Sources/GiftViewScreen.swift | 21 +- .../Sources/SettingsThemeWallpaperNode.swift | 6 +- .../Chat/ChatControllerThemeManagement.swift | 2 +- .../TelegramUI/Sources/ChatController.swift | 38 +- .../Sources/ChatControllerNode.swift | 2 +- .../TelegramUI/Sources/ChatThemeScreen.swift | 29 +- .../TelegramUI/Sources/OpenResolvedUrl.swift | 2 +- .../Sources/SharedAccountContext.swift | 4 +- .../MetalWallpaperBackgroundNode.swift | 1 - .../Sources/WallpaperBackgroundNode.swift | 203 ++- .../Sources/WallpaperResources.swift | 56 +- 25 files changed, 1286 insertions(+), 594 deletions(-) delete mode 100644 submodules/WallpaperBackgroundNode/Sources/MetalWallpaperBackgroundNode.swift diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 763900807a..e3cf59f815 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -14924,3 +14924,11 @@ 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 %@"; 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/Svg/PublicHeaders/Svg/Svg.h b/submodules/Svg/PublicHeaders/Svg/Svg.h index ec08bd75f8..bfbc5c3e9a 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 scale; +@property (nonatomic) CGFloat rotation; + +@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..1e9424f4cb 100755 --- a/submodules/Svg/Sources/Svg.m +++ b/submodules/Svg/Sources/Svg.m @@ -1,255 +1,433 @@ #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 CGAffineTransform SVGParseOneTransform(NSString *one) { + NSString *s = [one stringByTrimmingCharactersInSet:NSCharacterSet.whitespaceAndNewlineCharacterSet]; + if (s.length == 0) return CGAffineTransformIdentity; + + NSRange paren = [s rangeOfString:@"("]; + if (paren.location == NSNotFound) return CGAffineTransformIdentity; + + 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"]) { + CGFloat tx = nums.count > 0 ? nums[0].doubleValue : 0; + CGFloat ty = nums.count > 1 ? nums[1].doubleValue : 0; + return CGAffineTransformMakeTranslation(tx, ty); + } else if ([name isEqualToString:@"scale"]) { + CGFloat sx = nums.count > 0 ? nums[0].doubleValue : 1; + CGFloat sy = nums.count > 1 ? nums[1].doubleValue : sx; + return CGAffineTransformMakeScale(sx, sy); + } else if ([name isEqualToString:@"rotate"]) { + CGFloat a = nums.count > 0 ? deg2rad(nums[0].doubleValue) : 0; + if (nums.count >= 3) { + CGFloat cx = nums[1].doubleValue, cy = nums[2].doubleValue; + CGAffineTransform t = CGAffineTransformIdentity; + t = CGAffineTransformTranslate(t, cx, cy); + t = CGAffineTransformRotate(t, a); + t = CGAffineTransformTranslate(t, -cx, -cy); + return t; + } else { + return CGAffineTransformMakeRotation(a); + } + } else if ([name isEqualToString:@"matrix"] && nums.count >= 6) { + CGFloat a = nums[0].doubleValue, b = nums[1].doubleValue; + CGFloat c = nums[2].doubleValue, d = nums[3].doubleValue; + CGFloat e = nums[4].doubleValue, f = nums[5].doubleValue; + return CGAffineTransformMake(a, b, c, d, e, f); + } + return CGAffineTransformIdentity; } -@property (nonatomic, strong, readonly) NSMutableDictionary *styles; +static CGAffineTransform SVGParseTransformList(NSString *list) { + if (list.length == 0) { + return CGAffineTransformIdentity; + } + + 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:@""]; + } + } + } + } + CGAffineTransform t = CGAffineTransformIdentity; + for (NSString *part in chunks) { + t = CGAffineTransformConcat(t, SVGParseOneTransform(part)); + } + return t; +} + +static inline CGPoint CGPointApplyAffineToPoint(CGPoint p, CGAffineTransform t) { + return CGPointMake(p.x * t.a + p.y * t.c + t.tx, + p.x * t.b + p.y * t.d + t.ty); +} + +static inline void DecomposeScaleRotation(CGAffineTransform t, CGFloat *outScale, CGFloat *outRotation) { + CGFloat scaleX = hypot(t.a, t.b); + CGFloat scaleY = hypot(t.c, t.d); + if (outScale) *outScale = (scaleX + scaleY) * 0.5; + if (outRotation) *outRotation = atan2(t.b, t.a); +} + +@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; + CGAffineTransform _giftGroupTransform; +} - (instancetype)init { self = [super init]; - if (self != nil) { + if (self) { _styles = [[NSMutableDictionary alloc] init]; + _currentStyleString = [[NSMutableString alloc] init]; + _giftRects = [NSMutableArray array]; + _giftGroupTransform = CGAffineTransformIdentity; + _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; + NSString *t = attributeDict[@"transform"]; + _giftGroupTransform = t.length ? SVGParseTransformList(t) : CGAffineTransformIdentity; + } + } 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 = w > 0 ? w : h; + + CGAffineTransform rectT = CGAffineTransformIdentity; + NSString *rt = attributeDict[@"transform"]; + if (rt.length) { + rectT = SVGParseTransformList(rt); + } + + CGAffineTransform total = CGAffineTransformConcat(_giftGroupTransform, rectT); + + CGPoint localCenter = CGPointMake(x + w * 0.5, y + h * 0.5); + CGPoint center = CGPointApplyAffineToPoint(localCenter, total); + + CGFloat scale = 1.0, rotation = 0.0; + DecomposeScaleRotation(total, &scale, &rotation); + + GiftPatternRect *rec = [[GiftPatternRect alloc] init]; + rec.center = center; + rec.side = side; + rec.scale = scale; + rec.rotation = rotation; + [_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; + _giftGroupTransform = CGAffineTransformIdentity; } _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 +435,587 @@ 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.scale, + (float)rect.rotation + }; + [_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); + if (rects) { + GiftPatternRect *rect = [GiftPatternRect new]; + rect.center = CGPointMake(payload[0], payload[1]); + rect.side = payload[2]; + rect.scale = payload[3]; + rect.rotation = 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 new]; + rect.center = CGPointMake(payload[0], payload[1]); + rect.side = payload[2]; + rect.scale = payload[3]; + rect.rotation = payload[4]; + [rects addObject:rect]; + } + } + continue; + } + + if (!processRenderCommand(cmd, data, &ptr, &context, foregroundColor)) { + break; + } + } + + if (symbolImage && rects.count > 0) { + CGFloat symbolWidth = symbolImage.size.width; + CGFloat symbolHeight = symbolImage.size.height; + CGFloat symbolAspectRatio = (symbolHeight > 0.0 ? (symbolWidth / symbolHeight) : 1.0); + + int32_t index = 0; + for (GiftPatternRect *rect in rects) { + if (index == modelRectIndex) { + } else { + CGContextSaveGState(context); + CGContextTranslateCTM(context, rect.center.x, rect.center.y); + CGContextRotateCTM(context, rect.rotation); + + CGFloat side = rect.side * rect.scale; + CGFloat dw = side, dh = side; + if (symbolAspectRatio > 1.0) { + dh = dw / symbolAspectRatio; + } else { + dw = dh * symbolAspectRatio; + } + + CGRect dst = CGRectMake(-dw * 0.5, -dh * 0.5, dw, dh); + [symbolImage drawInRect:dst 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/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/TelegramPresentationData/Sources/MakePresentationTheme.swift b/submodules/TelegramPresentationData/Sources/MakePresentationTheme.swift index 0698eab825..360bbd44b3 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: true, 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..2e3f36e6dc 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): 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..728d37a4be 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 { 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/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift index 80e9d9241e..422cb574c2 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift @@ -3658,7 +3658,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 +3677,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 +3738,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) @@ -4381,6 +4391,7 @@ public class GiftViewScreen: ViewControllerComponentContainer { fileprivate let updateResellStars: ((CurrencyAmount?) -> Signal)? fileprivate let togglePinnedToTop: ((Bool) -> Bool)? fileprivate let shareStory: ((StarGift.UniqueGift) -> Void)? + fileprivate let openChatTheme: (() -> Void)? public var disposed: () -> Void = {} @@ -4397,7 +4408,8 @@ public class GiftViewScreen: ViewControllerComponentContainer { buyGift: ((String, EnginePeer.Id, CurrencyAmount?) -> Signal)? = nil, updateResellStars: ((CurrencyAmount?) -> Signal)? = nil, togglePinnedToTop: ((Bool) -> Bool)? = nil, - shareStory: ((StarGift.UniqueGift) -> Void)? = nil + shareStory: ((StarGift.UniqueGift) -> Void)? = nil, + openChatTheme: (() -> Void)? = nil ) { self.context = context self.subject = subject @@ -4410,6 +4422,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 diff --git a/submodules/TelegramUI/Components/Settings/SettingsThemeWallpaperNode/Sources/SettingsThemeWallpaperNode.swift b/submodules/TelegramUI/Components/Settings/SettingsThemeWallpaperNode/Sources/SettingsThemeWallpaperNode.swift index 3e9a18b768..6af509eee1 100644 --- a/submodules/TelegramUI/Components/Settings/SettingsThemeWallpaperNode/Sources/SettingsThemeWallpaperNode.swift +++ b/submodules/TelegramUI/Components/Settings/SettingsThemeWallpaperNode/Sources/SettingsThemeWallpaperNode.swift @@ -273,9 +273,9 @@ public final class SettingsThemeWallpaperNode: ASDisplayNode { self.arguments = PatternWallpaperArguments(colors: [.clear], rotation: nil, customPatternColor: isLight ? .black : .white) } imageSignal = patternWallpaperImage(account: context.account, accountManager: context.sharedContext.accountManager, representations: convertedRepresentations, mode: .thumbnail, autoFetchFullSize: true) - |> 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/Sources/Chat/ChatControllerThemeManagement.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerThemeManagement.swift index 4d9aa139cf..921c93180e 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerThemeManagement.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerThemeManagement.swift @@ -182,7 +182,7 @@ 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 diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index a6353274f4..e02763bd13 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -1110,8 +1110,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 +5829,15 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G }) } } - case let .gift(gift, wallpaper): - let _ = gift - let _ = wallpaper - //TODO:release + case .gift: + 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..8852a916e9 100644 --- a/submodules/TelegramUI/Sources/ChatControllerNode.swift +++ b/submodules/TelegramUI/Sources/ChatControllerNode.swift @@ -3582,7 +3582,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/ChatThemeScreen.swift b/submodules/TelegramUI/Sources/ChatThemeScreen.swift index 67c96af618..7f7107b815 100644 --- a/submodules/TelegramUI/Sources/ChatThemeScreen.swift +++ b/submodules/TelegramUI/Sources/ChatThemeScreen.swift @@ -743,6 +743,7 @@ private class ChatThemeScreenNode: ViewControllerTracingNode, ASScrollViewDelega private var initialized = false private let uniqueGiftChatThemesContext: UniqueGiftChatThemesContext + private var currentUniqueGiftChatThemesState: UniqueGiftChatThemesContext.State? private let peerName: String @@ -891,10 +892,12 @@ private class ChatThemeScreenNode: ViewControllerTracingNode, ASScrollViewDelega self.uniqueGiftChatThemesContext.state, self.selectedThemePromise.get(), self.isDarkAppearancePromise.get() - ).startStrict(next: { [weak self] themes, uniqueGiftChatThemes, selectedTheme, isDarkAppearance in + ).startStrict(next: { [weak self] themes, uniqueGiftChatThemesState, selectedTheme, isDarkAppearance in guard let strongSelf = self else { return } + + strongSelf.currentUniqueGiftChatThemesState = uniqueGiftChatThemesState let isFirstTime = strongSelf.entries == nil let presentationData = strongSelf.presentationData @@ -927,8 +930,8 @@ private class ChatThemeScreenNode: ViewControllerTracingNode, ASScrollViewDelega wallpaper: nil )) } - for theme in uniqueGiftChatThemes.themes { - guard case let .gift(gift, wallpaperFile) = theme else { + for theme in uniqueGiftChatThemesState.themes { + guard case let .gift(gift, themeSettings) = theme else { continue } var emojiFile: TelegramMediaFile? @@ -939,16 +942,23 @@ private class ChatThemeScreenNode: ViewControllerTracingNode, ASScrollViewDelega } } } + + var wallpaper: TelegramWallpaper? + if isDarkAppearance { + wallpaper = themeSettings.first(where: { $0.baseTheme == .night || $0.baseTheme == .tinted })?.wallpaper + } else { + wallpaper = themeSettings.first(where: { $0.baseTheme == .classic || $0.baseTheme == .day })?.wallpaper + } entries.append(ThemeSettingsThemeEntry( index: entries.count, chatTheme: theme, emojiFile: emojiFile, - themeReference: nil, + themeReference: .builtin(.dayClassic), 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 )) } @@ -1008,6 +1018,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() } 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/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..0010790b54 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,65 @@ 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, let modelRectIndex = self.modelRectIndex, let modelFile { + let rect = validPatternImage.rects[Int(modelRectIndex) % validPatternImage.rects.count] + + 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 +1683,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..6697d7cd1e 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,7 +477,23 @@ 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) -> 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 { @@ -481,7 +504,7 @@ public func patternWallpaperImage(account: Account, accountManager: AccountManag } } -private func patternWallpaperImageInternal(fullSizeData: Data?, fullSizeComplete: Bool, mode: PatternWallpaperDrawMode) -> Signal<((TransformImageArguments) -> DrawingContext?)?, NoError> { +private func patternWallpaperImageInternal(fullSizeData: Data?, fullSizeComplete: Bool, mode: PatternWallpaperDrawMode) -> 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 { @@ -1512,11 +1546,15 @@ public func themeIconImage(account: Account, accountManager: AccountManager mapToSignal { generator -> Signal<((UIColor, UIColor?, [UInt32]), [UIColor], [UIColor], UIImage?, Bool, Bool, CGFloat, Int32?), NoError> in + |> 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 { From 03fce1ed0922e24d05564eee35a72bd7d0dae4b1 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Tue, 26 Aug 2025 20:08:50 +0400 Subject: [PATCH 02/32] Various improvements --- submodules/Svg/PublicHeaders/Svg/Svg.h | 2 +- submodules/Svg/Sources/Svg.m | 152 +++++++----------- .../Chat/ChatControllerOpenWebApp.swift | 68 +++++--- .../Sources/WallpaperBackgroundNode.swift | 7 +- .../Sources/WallpaperResources.swift | 14 +- 5 files changed, 122 insertions(+), 121 deletions(-) diff --git a/submodules/Svg/PublicHeaders/Svg/Svg.h b/submodules/Svg/PublicHeaders/Svg/Svg.h index bfbc5c3e9a..39080185f3 100755 --- a/submodules/Svg/PublicHeaders/Svg/Svg.h +++ b/submodules/Svg/PublicHeaders/Svg/Svg.h @@ -8,8 +8,8 @@ @property (nonatomic) CGPoint center; @property (nonatomic) CGFloat side; -@property (nonatomic) CGFloat scale; @property (nonatomic) CGFloat rotation; +@property (nonatomic) CGFloat scale; @end diff --git a/submodules/Svg/Sources/Svg.m b/submodules/Svg/Sources/Svg.m index 1e9424f4cb..975d102a65 100755 --- a/submodules/Svg/Sources/Svg.m +++ b/submodules/Svg/Sources/Svg.m @@ -18,12 +18,12 @@ static inline CGSize aspectFitSize(CGSize size, CGSize bounds) { static inline CGFloat deg2rad(CGFloat deg) { return (deg * (CGFloat)M_PI) / 180.0; } -static CGAffineTransform SVGParseOneTransform(NSString *one) { +static CGSize SVGParseOneTransform(NSString *one, NSString *requiredName) { NSString *s = [one stringByTrimmingCharactersInSet:NSCharacterSet.whitespaceAndNewlineCharacterSet]; - if (s.length == 0) return CGAffineTransformIdentity; + if (s.length == 0) return CGSizeZero; NSRange paren = [s rangeOfString:@"("]; - if (paren.location == NSNotFound) return CGAffineTransformIdentity; + 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)]; @@ -41,38 +41,24 @@ static CGAffineTransform SVGParseOneTransform(NSString *one) { } } - if ([name isEqualToString:@"translate"]) { + 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 CGAffineTransformMakeTranslation(tx, ty); - } else if ([name isEqualToString:@"scale"]) { + 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 CGAffineTransformMakeScale(sx, sy); - } else if ([name isEqualToString:@"rotate"]) { + return CGSizeMake(sx, sy); + } else if ([name isEqualToString:@"rotate"] && [name isEqualToString:requiredName]) { CGFloat a = nums.count > 0 ? deg2rad(nums[0].doubleValue) : 0; - if (nums.count >= 3) { - CGFloat cx = nums[1].doubleValue, cy = nums[2].doubleValue; - CGAffineTransform t = CGAffineTransformIdentity; - t = CGAffineTransformTranslate(t, cx, cy); - t = CGAffineTransformRotate(t, a); - t = CGAffineTransformTranslate(t, -cx, -cy); - return t; - } else { - return CGAffineTransformMakeRotation(a); - } - } else if ([name isEqualToString:@"matrix"] && nums.count >= 6) { - CGFloat a = nums[0].doubleValue, b = nums[1].doubleValue; - CGFloat c = nums[2].doubleValue, d = nums[3].doubleValue; - CGFloat e = nums[4].doubleValue, f = nums[5].doubleValue; - return CGAffineTransformMake(a, b, c, d, e, f); + return CGSizeMake(a, a); } - return CGAffineTransformIdentity; + return CGSizeZero; } static CGAffineTransform SVGParseTransformList(NSString *list) { if (list.length == 0) { - return CGAffineTransformIdentity; + return CGAffineTransformMake(0.0, 1.0, 1.0, 0.0, 0.0, 0.0); } NSMutableArray *chunks = [NSMutableArray array]; @@ -92,23 +78,19 @@ static CGAffineTransform SVGParseTransformList(NSString *list) { } } } - CGAffineTransform t = CGAffineTransformIdentity; + CGFloat rotation = 0.0; + CGSize scale = CGSizeMake(1.0, 1.0); for (NSString *part in chunks) { - t = CGAffineTransformConcat(t, SVGParseOneTransform(part)); + 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 t; -} - -static inline CGPoint CGPointApplyAffineToPoint(CGPoint p, CGAffineTransform t) { - return CGPointMake(p.x * t.a + p.y * t.c + t.tx, - p.x * t.b + p.y * t.d + t.ty); -} - -static inline void DecomposeScaleRotation(CGAffineTransform t, CGFloat *outScale, CGFloat *outRotation) { - CGFloat scaleX = hypot(t.a, t.b); - CGFloat scaleY = hypot(t.c, t.d); - if (outScale) *outScale = (scaleX + scaleY) * 0.5; - if (outRotation) *outRotation = atan2(t.b, t.a); + return CGAffineTransformMake(rotation, scale.width, scale.height, 0.0, 0.0, 0.0); } @implementation GiftPatternData @@ -130,7 +112,6 @@ static inline void DecomposeScaleRotation(CGAffineTransform t, CGFloat *outScale NSMutableString *_currentStyleString; bool _inGiftPatterns; - CGAffineTransform _giftGroupTransform; } - (instancetype)init { @@ -139,7 +120,6 @@ static inline void DecomposeScaleRotation(CGAffineTransform t, CGFloat *outScale _styles = [[NSMutableDictionary alloc] init]; _currentStyleString = [[NSMutableString alloc] init]; _giftRects = [NSMutableArray array]; - _giftGroupTransform = CGAffineTransformIdentity; _inGiftPatterns = false; } return self; @@ -152,8 +132,6 @@ static inline void DecomposeScaleRotation(CGAffineTransform t, CGFloat *outScale NSString *gid = attributeDict[@"id"]; if ([[gid lowercaseString] isEqualToString:@"giftpatterns"]) { _inGiftPatterns = true; - NSString *t = attributeDict[@"transform"]; - _giftGroupTransform = t.length ? SVGParseTransformList(t) : CGAffineTransformIdentity; } } else if (_inGiftPatterns && [_elementName isEqualToString:@"rect"]) { CGFloat x = attributeDict[@"x"] ? attributeDict[@"x"].doubleValue : 0; @@ -161,27 +139,21 @@ static inline void DecomposeScaleRotation(CGAffineTransform t, CGFloat *outScale CGFloat w = attributeDict[@"width"] ? attributeDict[@"width"].doubleValue : 0; CGFloat h = attributeDict[@"height"] ? attributeDict[@"height"].doubleValue : 0; - CGFloat side = w > 0 ? w : h; + CGFloat side = MAX(w, h); - CGAffineTransform rectT = CGAffineTransformIdentity; + CGAffineTransform fakeTransform = CGAffineTransformMake(0.0, 1.0, 1.0, 0.0, 0.0, 0.0); NSString *rt = attributeDict[@"transform"]; if (rt.length) { - rectT = SVGParseTransformList(rt); + fakeTransform = SVGParseTransformList(rt); } - - CGAffineTransform total = CGAffineTransformConcat(_giftGroupTransform, rectT); - CGPoint localCenter = CGPointMake(x + w * 0.5, y + h * 0.5); - CGPoint center = CGPointApplyAffineToPoint(localCenter, total); - - CGFloat scale = 1.0, rotation = 0.0; - DecomposeScaleRotation(total, &scale, &rotation); - - GiftPatternRect *rec = [[GiftPatternRect alloc] init]; - rec.center = center; + CGPoint rectCenter = CGPointMake(x + w * 0.5, y + h * 0.5); + + GiftPatternRect *rec = [GiftPatternRect new]; + rec.center = rectCenter; rec.side = side; - rec.scale = scale; - rec.rotation = rotation; + rec.rotation = fakeTransform.a; + rec.scale = fakeTransform.b; [_giftRects addObject:rec]; } } @@ -193,7 +165,6 @@ static inline void DecomposeScaleRotation(CGAffineTransform t, CGFloat *outScale } if ([_elementName isEqualToString:@"g"] && _inGiftPatterns) { _inGiftPatterns = false; - _giftGroupTransform = CGAffineTransformIdentity; } _elementName = nil; } @@ -529,11 +500,7 @@ typedef NS_ENUM(uint8_t, SvgRenderCommand) { [_data appendBytes:&item length:sizeof(item)]; float payload[5] = { - (float)rect.center.x, - (float)rect.center.y, - (float)rect.side, - (float)rect.scale, - (float)rect.rotation + (float)rect.center.x, (float)rect.center.y, (float)rect.side, (float)rect.rotation, (float)rect.scale }; [_data appendBytes:payload length:sizeof(payload)]; } @@ -720,14 +687,12 @@ GiftPatternData *getGiftPatternData(NSData * _Nonnull data) { [data getBytes:payload range:NSMakeRange(ptr, sizeof(payload))]; ptr += sizeof(payload); - if (rects) { - GiftPatternRect *rect = [GiftPatternRect new]; - rect.center = CGPointMake(payload[0], payload[1]); - rect.side = payload[2]; - rect.scale = payload[3]; - rect.rotation = payload[4]; - [rects addObject:rect]; - } + 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; } @@ -821,11 +786,11 @@ UIImage * _Nullable renderPreparedImageWithSymbol(NSData * _Nonnull data, CGSize ptr += sizeof(payload); if (rects) { - GiftPatternRect *rect = [GiftPatternRect new]; + GiftPatternRect *rect = [[GiftPatternRect alloc] init]; rect.center = CGPointMake(payload[0], payload[1]); rect.side = payload[2]; - rect.scale = payload[3]; - rect.rotation = payload[4]; + rect.rotation = payload[3]; + rect.scale = payload[4]; [rects addObject:rect]; } } @@ -838,28 +803,35 @@ UIImage * _Nullable renderPreparedImageWithSymbol(NSData * _Nonnull data, CGSize } if (symbolImage && rects.count > 0) { - CGFloat symbolWidth = symbolImage.size.width; - CGFloat symbolHeight = symbolImage.size.height; - CGFloat symbolAspectRatio = (symbolHeight > 0.0 ? (symbolWidth / symbolHeight) : 1.0); - int32_t index = 0; + + NSMutableArray *filteredRects = [[NSMutableArray alloc] init]; for (GiftPatternRect *rect in rects) { - if (index == modelRectIndex) { - } else { + if (rect.center.y > 240.0) { + [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 side = rect.side * rect.scale; - CGFloat dw = side, dh = side; + 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) { - dh = dw / symbolAspectRatio; + drawHeight = drawWidth / symbolAspectRatio; } else { - dw = dh * symbolAspectRatio; + drawWidth = drawHeight * symbolAspectRatio; } - CGRect dst = CGRectMake(-dw * 0.5, -dh * 0.5, dw, dh); - [symbolImage drawInRect:dst blendMode:kCGBlendModeNormal alpha:1.0]; + CGRect symbolRect = CGRectMake(-drawWidth * 0.5, -drawHeight * 0.5, drawWidth, drawHeight); + [symbolImage drawInRect:symbolRect blendMode:kCGBlendModeNormal alpha:1.0]; CGContextRestoreGState(context); } @@ -898,9 +870,7 @@ void processShape(NSVGshape *shape, CGContextCoder *context, bool template) { 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])]; + [context addCurveToPoint:CGPointMake(p[2], p[3]) p2:CGPointMake(p[4], p[5]) p3:CGPointMake(p[6], p[7])]; } if (path->closed && hasStartPoint) { 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/WallpaperBackgroundNode/Sources/WallpaperBackgroundNode.swift b/submodules/WallpaperBackgroundNode/Sources/WallpaperBackgroundNode.swift index 0010790b54..83a23132fa 100644 --- a/submodules/WallpaperBackgroundNode/Sources/WallpaperBackgroundNode.swift +++ b/submodules/WallpaperBackgroundNode/Sources/WallpaperBackgroundNode.swift @@ -1382,8 +1382,11 @@ public final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgrou } } } - if let validPatternImage = self.validPatternImage, !validPatternImage.rects.isEmpty, let modelRectIndex = self.modelRectIndex, let modelFile { - let rect = validPatternImage.rects[Int(modelRectIndex) % validPatternImage.rects.count] + if let validPatternImage = self.validPatternImage, !validPatternImage.rects.isEmpty, var modelRectIndex = self.modelRectIndex, let modelFile { + let filteredRects = validPatternImage.rects.filter { $0.center.y > 240.0 } + modelRectIndex = modelRectIndex % Int32(filteredRects.count); + + let rect = filteredRects[Int(modelRectIndex)] let modelStickerNode: DefaultAnimatedStickerNodeImpl if let current = self.modelStickerNode { diff --git a/submodules/WallpaperResources/Sources/WallpaperResources.swift b/submodules/WallpaperResources/Sources/WallpaperResources.swift index 6697d7cd1e..dc916aa7df 100644 --- a/submodules/WallpaperResources/Sources/WallpaperResources.swift +++ b/submodules/WallpaperResources/Sources/WallpaperResources.swift @@ -493,18 +493,18 @@ public struct WallpaperGiftPatternRect: Equatable { } } -public func patternWallpaperImage(account: Account, accountManager: AccountManager, representations: [ImageRepresentationWithReference], mode: PatternWallpaperDrawMode, autoFetchFullSize: Bool = false) -> Signal<(generator: (TransformImageArguments) -> DrawingContext?, rects: [WallpaperGiftPatternRect])?, NoError> { +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<(generator: (TransformImageArguments) -> DrawingContext?, rects: [WallpaperGiftPatternRect])?, 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 @@ -1513,6 +1513,7 @@ public func themeIconImage(account: Account, accountManager: AccountManager 0.3 arguments = PatternWallpaperArguments(colors: [.clear], rotation: nil, customPatternColor: isLight ? .black : .white) } - - if file.settings.intensity == 100 { - print() - } - - return patternWallpaperImage(account: account, accountManager: accountManager, representations: convertedPreviewRepresentations, mode: .thumbnail, autoFetchFullSize: true) + 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) From 144516eae54beccc0065abd75ea49dcc1b36d4b2 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Tue, 26 Aug 2025 21:14:46 +0400 Subject: [PATCH 03/32] Various improvements --- .../TelegramEngine/Data/PeersData.swift | 33 +++++++++++++++++++ .../OverlayAudioPlayerControllerNode.swift | 23 +++++++++++-- 2 files changed, 54 insertions(+), 2 deletions(-) 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/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift b/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift index cd2a276ac9..1ec0d69465 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, @@ -388,14 +390,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) @@ -638,6 +651,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 } From bbef11084ad6026283b38987b9c47ad3282e6769 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Tue, 26 Aug 2025 23:58:41 +0400 Subject: [PATCH 04/32] Update API --- submodules/TelegramApi/Sources/Api0.swift | 4 +- submodules/TelegramApi/Sources/Api25.swift | 20 +++++--- submodules/TelegramApi/Sources/Api29.swift | 40 ++++++++++++---- .../Sources/State/ManagedRecentStickers.swift | 4 +- .../TelegramEngine/Payments/StarGifts.swift | 46 +++++++++++++++++-- .../TelegramEngine/Themes/ChatThemes.swift | 24 +++++++--- .../Sources/UserApperanceScreen.swift | 4 +- 7 files changed, 110 insertions(+), 32 deletions(-) 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/TelegramEngine/Payments/StarGifts.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift index dc20722766..da4087c1c6 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,9 @@ 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 ) } } @@ -884,7 +916,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 +928,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)) } } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Themes/ChatThemes.swift b/submodules/TelegramCore/Sources/TelegramEngine/Themes/ChatThemes.swift index 35785fd745..cb71197c95 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Themes/ChatThemes.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Themes/ChatThemes.swift @@ -495,6 +495,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 @@ -521,12 +522,23 @@ public final class UniqueGiftChatThemesContext { } 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) + |> 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) + } } } diff --git a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/UserApperanceScreen.swift b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/UserApperanceScreen.swift index c4169c8d8e..1bc64a2a86 100644 --- a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/UserApperanceScreen.swift +++ b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/UserApperanceScreen.swift @@ -465,7 +465,9 @@ final class UserAppearanceScreenComponent: Component { resellForTonOnly: false, releasedBy: nil, valueAmount: nil, - valueCurrency: nil + valueCurrency: nil, + flags: [], + themePeerId: nil ) signal = component.context.engine.accountData.setStarGiftStatus(starGift: gift, expirationDate: emojiStatus.expirationDate) } else { From 11a5e97304671c922b49237b70d04c7ccc9eea38 Mon Sep 17 00:00:00 2001 From: Mikhail Filimonov Date: Wed, 27 Aug 2025 11:17:28 +0100 Subject: [PATCH 05/32] better isEqual for ChatTheme --- .../TelegramCore/Sources/TelegramEngine/Themes/ChatThemes.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Themes/ChatThemes.swift b/submodules/TelegramCore/Sources/TelegramEngine/Themes/ChatThemes.swift index 35785fd745..317562dc59 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Themes/ChatThemes.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Themes/ChatThemes.swift @@ -117,7 +117,7 @@ public enum ChatTheme: PostboxCoding, Codable, Equatable { case .unique(let lhsUnique): switch rhsGift { case .unique(let rhsUnique): - return lhsUnique.id == rhsUnique.id + return lhsUnique.slug == rhsUnique.slug default: return false } From 6ec15695362bc95837ed3fdecdcb641f9babf75f Mon Sep 17 00:00:00 2001 From: Mikhail Filimonov Date: Wed, 27 Aug 2025 12:10:41 +0100 Subject: [PATCH 06/32] fix nextoffset --- .../TelegramCore/Sources/TelegramEngine/Themes/ChatThemes.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Themes/ChatThemes.swift b/submodules/TelegramCore/Sources/TelegramEngine/Themes/ChatThemes.swift index 6dc7ae2d87..d7385cb27a 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Themes/ChatThemes.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Themes/ChatThemes.swift @@ -559,6 +559,7 @@ public final class UniqueGiftChatThemesContext { } self.dataState = .ready(canLoadMore: nextOffset != nil) + self.nextOffset = nextOffset self.pushState() })) } From 4a5e7ebed442007ba07541535c76e4ee845ae56f Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Wed, 27 Aug 2025 17:32:48 +0400 Subject: [PATCH 07/32] Various improvements --- .../Telegram-iOS/en.lproj/Localizable.strings | 41 +++ .../TelegramEngine/Payments/StarGifts.swift | 21 ++ .../TelegramEngine/Themes/ChatThemes.swift | 41 ++- .../Sources/ServiceMessageStrings.swift | 3 +- submodules/TelegramUI/BUILD | 1 + .../Components/ChatThemeScreen/BUILD | 44 +++ .../Sources/ChatThemeScreen.swift | 78 +++-- .../GiftThemeTransferAlertController.swift | 296 ++++++++++++++++++ .../Sources/PeerInfoPaneContainerNode.swift | 5 +- .../Sources/StarsTransactionsScreen.swift | 12 +- .../Chat/ChatControllerThemeManagement.swift | 53 ++-- .../TelegramUI/Sources/ChatController.swift | 1 + .../Sources/ChatControllerNode.swift | 1 + .../OverlayAudioPlayerControllerNode.swift | 29 +- .../Sources/OverlayPlayerControlsNode.swift | 12 +- 15 files changed, 545 insertions(+), 93 deletions(-) create mode 100644 submodules/TelegramUI/Components/ChatThemeScreen/BUILD rename submodules/TelegramUI/{ => Components/ChatThemeScreen}/Sources/ChatThemeScreen.swift (98%) create mode 100644 submodules/TelegramUI/Components/ChatThemeScreen/Sources/GiftThemeTransferAlertController.swift diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index e3cf59f815..1f3ab2992f 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -14932,3 +14932,44 @@ Sorry for the inconvenience."; "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" = "Save to Files"; +"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$@"; diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift index da4087c1c6..8b77355546 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift @@ -809,6 +809,27 @@ public enum StarGift: Equatable, Codable, PostboxCoding { 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 + ) + } } public enum DecodingError: Error { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Themes/ChatThemes.swift b/submodules/TelegramCore/Sources/TelegramEngine/Themes/ChatThemes.swift index cb71197c95..d880c1b20a 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Themes/ChatThemes.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Themes/ChatThemes.swift @@ -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) } @@ -521,7 +552,7 @@ public final class UniqueGiftChatThemesContext { self.pushState() } - let signal = network.request(Api.functions.account.getUniqueGiftChatThemes(offset: offset ?? 0, limit: 32, hash: 0)) + let signal = network.request(Api.functions.account.getUniqueGiftChatThemes(offset: offset, limit: 32, hash: 0)) |> map(Optional.init) |> `catch` { error in return .single(nil) @@ -557,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/TelegramStringFormatting/Sources/ServiceMessageStrings.swift b/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift index 2e3f36e6dc..ae1f5e25ca 100644 --- a/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift +++ b/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift @@ -830,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) } 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/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 98% rename from submodules/TelegramUI/Sources/ChatThemeScreen.swift rename to submodules/TelegramUI/Components/ChatThemeScreen/Sources/ChatThemeScreen.swift index 7f7107b815..517c896dc8 100644 --- a/submodules/TelegramUI/Sources/ChatThemeScreen.swift +++ b/submodules/TelegramUI/Components/ChatThemeScreen/Sources/ChatThemeScreen.swift @@ -21,12 +21,14 @@ 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 @@ -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 @@ -525,9 +543,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 +557,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 +566,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 +576,7 @@ final class ChatThemeScreen: ViewController { } } - init( + public init( context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal), animatedEmojiStickers: [String: [StickerPackItem]], @@ -657,7 +675,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 +701,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() } } @@ -908,28 +926,13 @@ 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 - } - 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 uniqueGiftChatThemesState.themes { guard case let .gift(gift, themeSettings) = theme else { continue @@ -954,6 +957,7 @@ private class ChatThemeScreenNode: ViewControllerTracingNode, ASScrollViewDelega chatTheme: theme, emojiFile: emojiFile, themeReference: .builtin(.dayClassic), + peer: nil, nightMode: isDarkAppearance, selected: selectedTheme?.id == theme.id, theme: presentationData.theme, @@ -961,6 +965,24 @@ private class ChatThemeScreenNode: ViewControllerTracingNode, ASScrollViewDelega wallpaper: wallpaper )) } + 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 { diff --git a/submodules/TelegramUI/Components/ChatThemeScreen/Sources/GiftThemeTransferAlertController.swift b/submodules/TelegramUI/Components/ChatThemeScreen/Sources/GiftThemeTransferAlertController.swift new file mode 100644 index 0000000000..a662a003d1 --- /dev/null +++ b/submodules/TelegramUI/Components/ChatThemeScreen/Sources/GiftThemeTransferAlertController.swift @@ -0,0 +1,296 @@ +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: "Peer Info/AlertArrow"), color: theme.secondaryColor) + + 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) - 44.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) + 44.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 = max(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 * CGFloat(self.actionNodes.count) + + let resultSize = CGSize(width: contentWidth, height: avatarSize.height + titleSize.height + textSize.height + actionsHeight + 24.0 + insets.top + insets.bottom) + transition.updateFrame(node: 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 + var separatorIndex = -1 + var nodeIndex = 0 + for actionNode in self.actionNodes { + if separatorIndex >= 0 { + let separatorNode = self.actionVerticalSeparators[separatorIndex] + do { + transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel))) + } + } + separatorIndex += 1 + + let currentActionWidth: CGFloat + do { + currentActionWidth = resultSize.width + } + + let actionNodeFrame: CGRect + do { + actionNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset), size: CGSize(width: currentActionWidth, height: actionButtonHeight)) + actionOffset += actionButtonHeight + } + + 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: .defaultAction, title: presentationData.strings.Conversation_Theme_GiftTransfer_Proceed, action: { [weak contentNode] in + contentNode?.inProgress = true + commit() + }), TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: { + dismissImpl?(true) + })] + + 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/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/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift index 6f928dde21..fa0006025b 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift @@ -680,7 +680,7 @@ final class StarsTransactionsScreenComponent: Component { 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 +691,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 +725,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 +754,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 +766,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/Sources/Chat/ChatControllerThemeManagement.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerThemeManagement.swift index 921c93180e..b51952d115 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() { @@ -186,11 +187,11 @@ extension ChatControllerImpl { } }, 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,60 +207,58 @@ 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 { @@ -267,8 +266,8 @@ extension ChatControllerImpl { } 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 e02763bd13..70d9760246 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) diff --git a/submodules/TelegramUI/Sources/ChatControllerNode.swift b/submodules/TelegramUI/Sources/ChatControllerNode.swift index 8852a916e9..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 diff --git a/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift b/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift index 1ec0d69465..af6b7bb36d 100644 --- a/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift +++ b/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift @@ -570,19 +570,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 ), @@ -620,14 +619,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 ), @@ -1029,10 +1027,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( @@ -1044,7 +1041,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 { @@ -1056,7 +1053,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 { @@ -1067,7 +1064,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 { @@ -1091,7 +1088,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)))) @@ -1099,7 +1096,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) @@ -1131,7 +1128,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 { @@ -1144,7 +1141,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 { @@ -1181,9 +1178,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) )), From bd915704fa1efc581ce7f00b54919cfc2e574d5e Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Wed, 27 Aug 2025 18:52:13 +0400 Subject: [PATCH 08/32] Fix build --- .../TelegramCore/Sources/TelegramEngine/Themes/ChatThemes.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Themes/ChatThemes.swift b/submodules/TelegramCore/Sources/TelegramEngine/Themes/ChatThemes.swift index 8bbe658947..ac0444050c 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Themes/ChatThemes.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Themes/ChatThemes.swift @@ -592,7 +592,6 @@ public final class UniqueGiftChatThemesContext { self.nextOffset = nextOffset } self.dataState = .ready(canLoadMore: nextOffset != nil) - self.nextOffset = nextOffset self.pushState() })) } From 24e01cc8d47e4027b33023ce9cc5c823aa213e0f Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Wed, 27 Aug 2025 21:51:58 +0400 Subject: [PATCH 09/32] Various improvements --- .../Telegram-iOS/en.lproj/Localizable.strings | 21 ++++ .../Sources/GiftValueScreen.swift | 31 +++--- .../Sources/GiftViewScreen.swift | 105 +++++++++++++++--- 3 files changed, 126 insertions(+), 31 deletions(-) diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 1f3ab2992f..7d1706c435 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -14973,3 +14973,24 @@ Sorry for the inconvenience."; "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"; diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftValueScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftValueScreen.swift index f0b757f8ee..cb1b95c110 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftValueScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftValueScreen.swift @@ -205,8 +205,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 +317,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 +392,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 +408,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 +423,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 +455,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 +465,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 +492,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 +501,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 +528,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 +537,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 +584,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 +636,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) diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift index 422cb574c2..d5a0f3cc06 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift @@ -104,6 +104,7 @@ private final class GiftViewSheetContent: CombinedComponent { var cachedHiddenImage: (UIImage, PresentationTheme)? var inProgress = false + var canSkip = false var testUpgradeAnimation = !"".isEmpty @@ -668,14 +669,32 @@ 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) + 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)) + } }) } @@ -1528,7 +1547,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,10 +1578,14 @@ private final class GiftViewSheetContent: CombinedComponent { } Queue.mainQueue().after(0.5, { + self.canSkip = true + self.updated(transition: .immediate) + self.inUpgradePreview = false self.inProgress = false self.justUpgraded = true + self.revealedNumberDigits = -1 for i in 0 ..< "\(uniqueGift.number)".count { @@ -1552,17 +1596,22 @@ private final class GiftViewSheetContent: CombinedComponent { } 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 { @@ -1619,6 +1668,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 @@ -1638,18 +1690,23 @@ 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 { @@ -3771,7 +3828,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 }) @@ -4091,7 +4166,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 @@ -4100,7 +4177,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( @@ -4108,7 +4185,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( From 276743d0f99042ed9dda2cde1fdbe52d1b63e020 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Wed, 27 Aug 2025 22:22:39 +0400 Subject: [PATCH 10/32] Various improvements --- .../Sources/GiftViewScreen.swift | 18 +++++++++++------- .../Sources/PeerInfoScreen.swift | 8 ++++---- .../Sources/GiftsListView.swift | 8 ++++---- 3 files changed, 19 insertions(+), 15 deletions(-) diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift index d5a0f3cc06..aa68a54267 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift @@ -596,7 +596,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() @@ -1641,8 +1641,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) @@ -1690,7 +1693,8 @@ private final class GiftViewSheetContent: CombinedComponent { } } } - + self.updated(transition: .spring(duration: 0.4)) + Queue.mainQueue().after(firstDuration) { self.revealedAttributes.insert(.backdrop) self.updated(transition: .immediate) @@ -4461,9 +4465,9 @@ 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)? @@ -4479,9 +4483,9 @@ 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, 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..3c5f060d1c 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) From eb8464f687ccea111e14fceb3910380e4d69e2e2 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Wed, 27 Aug 2025 22:30:35 +0400 Subject: [PATCH 11/32] Various improvements --- .../Gifts/GiftViewScreen/Sources/GiftViewScreen.swift | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift index aa68a54267..3aa13d0f39 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift @@ -1581,11 +1581,10 @@ private final class GiftViewSheetContent: CombinedComponent { self.canSkip = true self.updated(transition: .immediate) - self.inUpgradePreview = false self.inProgress = false + self.inUpgradePreview = false self.justUpgraded = true - self.revealedNumberDigits = -1 for i in 0 ..< "\(uniqueGift.number)".count { @@ -1594,7 +1593,6 @@ private final class GiftViewSheetContent: CombinedComponent { self.updated(transition: .immediate) } } - self.updated(transition: .spring(duration: 0.4)) Queue.mainQueue().after(firstDuration) { self.revealedAttributes.insert(.backdrop) @@ -1621,6 +1619,8 @@ private final class GiftViewSheetContent: CombinedComponent { } } } + + self.updated(transition: .spring(duration: 0.4)) }) return } @@ -1693,7 +1693,6 @@ private final class GiftViewSheetContent: CombinedComponent { } } } - self.updated(transition: .spring(duration: 0.4)) Queue.mainQueue().after(firstDuration) { self.revealedAttributes.insert(.backdrop) @@ -1722,7 +1721,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) { From effaa875a6ccc97557e50045cbe366cb6407040d Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Wed, 27 Aug 2025 23:47:26 +0400 Subject: [PATCH 12/32] Various improvements --- .../SheetComponent/Sources/SheetComponent.swift | 16 ++++++++++++++-- .../TelegramEngine/Payments/StarGifts.swift | 13 +++++++++---- .../GiftViewScreen/Sources/GiftValueScreen.swift | 5 +---- .../GiftViewScreen/Sources/GiftViewScreen.swift | 8 +++++--- .../TelegramUI/Sources/ChatHistoryListNode.swift | 2 +- 5 files changed, 30 insertions(+), 14 deletions(-) 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/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift index 8b77355546..9cba0391f7 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift @@ -1379,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? @@ -1402,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 @@ -1410,6 +1412,7 @@ private final class ProfileGiftsContextImpl { self.collectionId = collectionId self.sorting = sorting self.filter = filter + self.limit = limit self.loadMore() } @@ -2420,14 +2423,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) }) } @@ -3009,6 +3013,7 @@ private final class ResaleGiftsContextImpl { let network = self.account.network let postbox = self.account.postbox let sorting = self.sorting + let limit = self.limit let filterAttributes = self.filterAttributes let currentAttributesHash = self.attributesHash @@ -3048,7 +3053,7 @@ private final class ResaleGiftsContextImpl { let attributesHash = currentAttributesHash ?? 0 flags |= (1 << 0) - let signal = network.request(Api.functions.payments.getResaleStarGifts(flags: flags, attributesHash: attributesHash, giftId: giftId, attributes: apiAttributes, offset: initialNextOffset ?? "", limit: 36)) + let signal = network.request(Api.functions.payments.getResaleStarGifts(flags: flags, attributesHash: attributesHash, giftId: giftId, attributes: apiAttributes, offset: initialNextOffset ?? "", limit: limit)) |> map(Optional.init) |> `catch` { _ -> Signal in return .single(nil) diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftValueScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftValueScreen.swift index cb1b95c110..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) { @@ -721,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 3aa13d0f39..49f151d6ce 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift @@ -676,7 +676,9 @@ private final class GiftViewSheetContent: CombinedComponent { guard let self else { return } - self.isOpeningValue = false + Queue.mainQueue().after(0.2) { + self.isOpeningValue = false + } if let valueInfo { let valueController = GiftValueScreen(context: self.context, gift: gift, valueInfo: valueInfo) controller.push(valueController) @@ -4545,8 +4547,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 { diff --git a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift index 27843831b2..d620a2a3c9 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 } From 6405f365a9e2c01771425c60cad42df1ef3100e7 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Thu, 28 Aug 2025 00:23:12 +0400 Subject: [PATCH 13/32] Various improvements --- .../TelegramEngine/Payments/StarGifts.swift | 6 ++--- .../OverlayAudioPlayerControllerNode.swift | 23 +++++++++++++------ 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift index 9cba0391f7..055890e327 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift @@ -1437,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 { @@ -1524,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) @@ -3013,7 +3014,6 @@ private final class ResaleGiftsContextImpl { let network = self.account.network let postbox = self.account.postbox let sorting = self.sorting - let limit = self.limit let filterAttributes = self.filterAttributes let currentAttributesHash = self.attributesHash @@ -3053,7 +3053,7 @@ private final class ResaleGiftsContextImpl { let attributesHash = currentAttributesHash ?? 0 flags |= (1 << 0) - let signal = network.request(Api.functions.payments.getResaleStarGifts(flags: flags, attributesHash: attributesHash, giftId: giftId, attributes: apiAttributes, offset: initialNextOffset ?? "", limit: limit)) + let signal = network.request(Api.functions.payments.getResaleStarGifts(flags: flags, attributesHash: attributesHash, giftId: giftId, attributes: apiAttributes, offset: initialNextOffset ?? "", limit: 36)) |> map(Optional.init) |> `catch` { _ -> Signal in return .single(nil) diff --git a/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift b/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift index af6b7bb36d..b5ea9e80bc 100644 --- a/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift +++ b/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift @@ -277,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 @@ -507,7 +511,6 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu return .single(true) } }) - self.historyNode.useMainQueueTransactions = false self.historyNode.autoScrollWhenReordering = false } @@ -689,7 +692,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 @@ -998,9 +1000,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) From 2713bcee1dc65b12945ccf6639afe1940300131e Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Thu, 28 Aug 2025 01:05:59 +0400 Subject: [PATCH 14/32] Various improvements --- .../Telegram-iOS/en.lproj/Localizable.strings | 2 + .../TelegramEngine/Themes/ChatThemes.swift | 2 +- .../Sources/ChatThemeScreen.swift | 61 ++++++++++++------- .../Sources/GiftViewScreen.swift | 46 ++++++++++++++ 4 files changed, 87 insertions(+), 24 deletions(-) diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 7d1706c435..e26d1f0ab1 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -14994,3 +14994,5 @@ Sorry for the inconvenience."; "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..."; diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Themes/ChatThemes.swift b/submodules/TelegramCore/Sources/TelegramEngine/Themes/ChatThemes.swift index ac0444050c..ee07d49015 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Themes/ChatThemes.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Themes/ChatThemes.swift @@ -552,7 +552,7 @@ public final class UniqueGiftChatThemesContext { self.pushState() } - let signal = network.request(Api.functions.account.getUniqueGiftChatThemes(offset: offset, limit: 32, hash: 0)) + let signal = network.request(Api.functions.account.getUniqueGiftChatThemes(offset: offset, limit: 50, hash: 0)) |> map(Optional.init) |> `catch` { error in return .single(nil) diff --git a/submodules/TelegramUI/Components/ChatThemeScreen/Sources/ChatThemeScreen.swift b/submodules/TelegramUI/Components/ChatThemeScreen/Sources/ChatThemeScreen.swift index 517c896dc8..f97cbe751b 100644 --- a/submodules/TelegramUI/Components/ChatThemeScreen/Sources/ChatThemeScreen.swift +++ b/submodules/TelegramUI/Components/ChatThemeScreen/Sources/ChatThemeScreen.swift @@ -933,8 +933,19 @@ private class ChatThemeScreenNode: ViewControllerTracingNode, ASScrollViewDelega strings: presentationData.strings, wallpaper: nil )) - for theme in uniqueGiftChatThemesState.themes { - guard case let .gift(gift, themeSettings) = theme else { + + 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) + existingIds.insert(initiallySelectedTheme.id) + } + } + + for theme in giftThemes { + guard case let .gift(gift, themeSettings) = theme, !existingIds.contains(theme.id) else { continue } var emojiFile: TelegramMediaFile? @@ -945,18 +956,20 @@ private class ChatThemeScreenNode: ViewControllerTracingNode, ASScrollViewDelega } } } - - var wallpaper: TelegramWallpaper? + 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: .builtin(.dayClassic), + themeReference: themeReference, peer: nil, nightMode: isDarkAppearance, selected: selectedTheme?.id == theme.id, @@ -965,33 +978,35 @@ private class ChatThemeScreenNode: ViewControllerTracingNode, ASScrollViewDelega wallpaper: wallpaper )) } - for theme in themes { - guard let emoticon = theme.emoticon else { - continue + + 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 + )) } - 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 diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift index 49f151d6ce..8cc5417193 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift @@ -803,6 +803,42 @@ 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 peerController = self.context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: self.context, filter: [.excludeRecent, .doNotSearchMessages], requestPeerType: [.user(.init(isBot: false, isPremium: false))], hasContactSelector: false, hasCreation: false)) + peerController.peerSelected = { [weak peerController, weak navigationController] peer, _ in + let _ = context.engine.themes.setChatTheme(peerId: peer.id, chatTheme: .gift(.unique(gift), [])).start() + peerController?.dismiss() + + if let navigationController { + 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 + )) + } + } + 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 @@ -1135,6 +1171,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) { From a7924e7b6f9ec6832bc3043380b73a56eaeb90fa Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Thu, 28 Aug 2025 02:14:32 +0400 Subject: [PATCH 15/32] Various improvements --- .../Sources/ChatThemeScreen.swift | 83 +++++++++++++++++-- .../GiftThemeTransferAlertController.swift | 29 ++++--- .../Sources/GiftViewScreen.swift | 16 ++-- .../Sources/ChatHistoryListNode.swift | 2 +- .../Sources/WallpaperResources.swift | 4 +- 5 files changed, 100 insertions(+), 34 deletions(-) diff --git a/submodules/TelegramUI/Components/ChatThemeScreen/Sources/ChatThemeScreen.swift b/submodules/TelegramUI/Components/ChatThemeScreen/Sources/ChatThemeScreen.swift index f97cbe751b..cc06dc3451 100644 --- a/submodules/TelegramUI/Components/ChatThemeScreen/Sources/ChatThemeScreen.swift +++ b/submodules/TelegramUI/Components/ChatThemeScreen/Sources/ChatThemeScreen.swift @@ -258,6 +258,7 @@ private final class ThemeSettingsThemeItemIconNode : ListViewItemNode { private let emojiImageNode: TransformImageNode private var animatedStickerNode: AnimatedStickerNode? private var placeholderNode: StickerShimmerEffectNode + private var avatarNode: AvatarNode? var snapshotView: UIView? var item: ThemeSettingsThemeIconItem? @@ -507,6 +508,23 @@ private final class ThemeSettingsThemeItemIconNode : ListViewItemNode { animatedStickerNode.frame = emojiFrame animatedStickerNode.updateLayout(size: emojiFrame.size) } + + 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() + } } }) } @@ -896,7 +914,7 @@ private class ChatThemeScreenNode: ViewControllerTracingNode, ASScrollViewDelega if let strongSelf = self { strongSelf.doneButton.isUserInteractionEnabled = false if strongSelf.doneButton.font == .bold { - strongSelf.completion?(strongSelf.selectedTheme) + strongSelf.complete() } else { strongSelf.controller?.changeWallpaper() } @@ -907,14 +925,36 @@ 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, uniqueGiftChatThemesState, 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 @@ -949,12 +989,16 @@ private class ChatThemeScreenNode: ViewControllerTracingNode, ASScrollViewDelega 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 { + peer = peers[themePeerId] + } } let themeReference: PresentationThemeReference let wallpaper: TelegramWallpaper? @@ -970,7 +1014,7 @@ private class ChatThemeScreenNode: ViewControllerTracingNode, ASScrollViewDelega chatTheme: theme, emojiFile: emojiFile, themeReference: themeReference, - peer: nil, + peer: peer, nightMode: isDarkAppearance, selected: selectedTheme?.id == theme.id, theme: presentationData.theme, @@ -1251,13 +1295,38 @@ private class ChatThemeScreenNode: ViewControllerTracingNode, ASScrollViewDelega } } + func complete() { + let proceed = { + 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 index a662a003d1..576a3c5a82 100644 --- a/submodules/TelegramUI/Components/ChatThemeScreen/Sources/GiftThemeTransferAlertController.swift +++ b/submodules/TelegramUI/Components/ChatThemeScreen/Sources/GiftThemeTransferAlertController.swift @@ -202,40 +202,39 @@ private final class GiftThemeTransferAlertContentNode: AlertContentNode { for actionNode in self.actionNodes { let actionTitleSize = actionNode.titleNode.updateLayout(CGSize(width: maxActionWidth, height: actionButtonHeight)) - minActionsWidth = max(minActionsWidth, actionTitleSize.width + actionTitleInsets) + 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 * CGFloat(self.actionNodes.count) + let actionsHeight = actionButtonHeight let resultSize = CGSize(width: contentWidth, height: avatarSize.height + titleSize.height + textSize.height + actionsHeight + 24.0 + insets.top + insets.bottom) - transition.updateFrame(node: self.actionNodesSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel))) + 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] - do { - transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel))) - } + 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 - do { - currentActionWidth = resultSize.width + if nodeIndex == self.actionNodes.count - 1 { + currentActionWidth = resultSize.width - actionOffset + } else { + currentActionWidth = actionWidth } let actionNodeFrame: CGRect - do { - actionNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset), size: CGSize(width: currentActionWidth, height: actionButtonHeight)) - actionOffset += actionButtonHeight - } + actionNodeFrame = CGRect(origin: CGPoint(x: actionOffset, y: resultSize.height - actionsHeight), size: CGSize(width: currentActionWidth, height: actionButtonHeight)) + actionOffset += currentActionWidth transition.updateFrame(node: actionNode, frame: actionNodeFrame) @@ -274,11 +273,11 @@ public func giftThemeTransferAlertController( var contentNode: GiftThemeTransferAlertContentNode? var dismissImpl: ((Bool) -> Void)? - let actions: [TextAlertAction] = [TextAlertAction(type: .defaultAction, title: presentationData.strings.Conversation_Theme_GiftTransfer_Proceed, action: { [weak contentNode] in + 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: { [weak contentNode] in contentNode?.inProgress = true commit() - }), TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: { - dismissImpl?(true) })] let text = strings.Conversation_Theme_GiftTransfer_Text(previousPeer.compactDisplayTitle).string diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift index 8cc5417193..df5bce8ccf 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift @@ -2374,13 +2374,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 { @@ -3736,7 +3736,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( diff --git a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift index d620a2a3c9..efc125f26c 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift @@ -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/WallpaperResources/Sources/WallpaperResources.swift b/submodules/WallpaperResources/Sources/WallpaperResources.swift index dc916aa7df..588c1cc851 100644 --- a/submodules/WallpaperResources/Sources/WallpaperResources.swift +++ b/submodules/WallpaperResources/Sources/WallpaperResources.swift @@ -1496,8 +1496,6 @@ public func themeIconImage(account: Account, accountManager: AccountManager mapToSignal { wallpaper in if let wallpaper = wallpaper, case let .file(file) = wallpaper.wallpaper { @@ -1805,7 +1803,7 @@ public func themeIconImage(account: Account, accountManager: AccountManager Date: Thu, 28 Aug 2025 02:30:08 +0400 Subject: [PATCH 16/32] Various improvements --- .../Sources/GiftThemeTransferAlertController.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/submodules/TelegramUI/Components/ChatThemeScreen/Sources/GiftThemeTransferAlertController.swift b/submodules/TelegramUI/Components/ChatThemeScreen/Sources/GiftThemeTransferAlertController.swift index 576a3c5a82..13ed25a99d 100644 --- a/submodules/TelegramUI/Components/ChatThemeScreen/Sources/GiftThemeTransferAlertController.swift +++ b/submodules/TelegramUI/Components/ChatThemeScreen/Sources/GiftThemeTransferAlertController.swift @@ -127,7 +127,7 @@ private final class GiftThemeTransferAlertContentNode: AlertContentNode { return ("URL", url) } ), textAlignment: .center) - self.arrowNode.image = generateTintedImage(image: UIImage(bundleImageName: "Peer Info/AlertArrow"), color: theme.secondaryColor) + self.arrowNode.image = generateTintedImage(image: UIImage(bundleImageName: "Media Editor/CutoutUndo"), color: theme.controlBorderColor) self.actionNodesSeparator.backgroundColor = theme.separatorColor for actionNode in self.actionNodes { @@ -153,7 +153,7 @@ private final class GiftThemeTransferAlertContentNode: AlertContentNode { 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) - 44.0, y: origin.y), 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, @@ -182,7 +182,7 @@ private final class GiftThemeTransferAlertContentNode: AlertContentNode { transition.updateFrame(node: self.arrowNode, frame: arrowFrame) } - let avatarFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - avatarSize.width) / 2.0) + 44.0, y: origin.y), size: avatarSize) + 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 @@ -276,7 +276,7 @@ public func giftThemeTransferAlertController( 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: { [weak contentNode] in - contentNode?.inProgress = true + dismissImpl?(true) commit() })] From 1802a2868670009bcbea69e9d909623e83197812 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Thu, 28 Aug 2025 02:40:18 +0400 Subject: [PATCH 17/32] Various improvements --- .../Sources/GiftThemeTransferAlertController.swift | 2 +- .../PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/submodules/TelegramUI/Components/ChatThemeScreen/Sources/GiftThemeTransferAlertController.swift b/submodules/TelegramUI/Components/ChatThemeScreen/Sources/GiftThemeTransferAlertController.swift index 13ed25a99d..8153c510b8 100644 --- a/submodules/TelegramUI/Components/ChatThemeScreen/Sources/GiftThemeTransferAlertController.swift +++ b/submodules/TelegramUI/Components/ChatThemeScreen/Sources/GiftThemeTransferAlertController.swift @@ -275,7 +275,7 @@ public func giftThemeTransferAlertController( 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: { [weak contentNode] in + }), TextAlertAction(type: .defaultAction, title: presentationData.strings.Conversation_Theme_GiftTransfer_Proceed, action: { dismissImpl?(true) commit() })] diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift index 34fa05787e..1eaa5836e7 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift @@ -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) From 34ff4d1f16ca107efc256881f2aae34b2c319d61 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Thu, 28 Aug 2025 02:55:00 +0400 Subject: [PATCH 18/32] Various improvements --- Telegram/Telegram-iOS/en.lproj/Localizable.strings | 2 +- .../PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index e26d1f0ab1..7949042e9f 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -14951,7 +14951,7 @@ Sorry for the inconvenience."; "MediaPlayer.ContextMenu.SaveTo.Profile" = "Profile"; "MediaPlayer.ContextMenu.SaveTo.SavedMessages" = "Saved Messages"; "MediaPlayer.ContextMenu.SaveTo.Files" = "Files"; -"MediaPlayer.ContextMenu.SaveTo.Info" = "Save to 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"; diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift index 1eaa5836e7..59df90ccd2 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift @@ -596,7 +596,7 @@ final class PeerInfoHeaderNode: ASDisplayNode { 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 @@ -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) From 1e800125d5d692e12a01d5b3e911ebf1c76a59e2 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Thu, 28 Aug 2025 04:16:09 +0400 Subject: [PATCH 19/32] Various improvements --- submodules/Display/Source/ListView.swift | 6 ++ .../Sources/MakePresentationTheme.swift | 2 +- .../Sources/ChatThemeScreen.swift | 54 +++++++++++++++++- .../GiftThemeTransferAlertController.swift | 2 +- .../Settings/Refresh.imageset/Contents.json | 12 ++++ .../Settings/Refresh.imageset/rotate_18.pdf | Bin 0 -> 4473 bytes .../OverlayAudioPlayerControllerNode.swift | 14 +++-- 7 files changed, 83 insertions(+), 7 deletions(-) create mode 100644 submodules/TelegramUI/Images.xcassets/Settings/Refresh.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Settings/Refresh.imageset/rotate_18.pdf 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/TelegramPresentationData/Sources/MakePresentationTheme.swift b/submodules/TelegramPresentationData/Sources/MakePresentationTheme.swift index 360bbd44b3..3f0398e984 100644 --- a/submodules/TelegramPresentationData/Sources/MakePresentationTheme.swift +++ b/submodules/TelegramPresentationData/Sources/MakePresentationTheme.swift @@ -70,7 +70,7 @@ public func makePresentationTheme(chatTheme: ChatTheme, dark: Bool = false) -> P return nil } let defaultTheme = makeDefaultPresentationTheme(reference: PresentationBuiltinThemeReference(baseTheme: settings.baseTheme), serviceBackgroundColor: nil, preview: false) - let theme = customizePresentationTheme(defaultTheme, editing: true, accentColor: UIColor(rgb: settings.accentColor), outgoingAccentColor: settings.outgoingAccentColor.flatMap { UIColor(rgb: $0) }, backgroundColors: [], bubbleColors: settings.messageColors, animateBubbleColors: settings.animateMessageColors, wallpaper: settings.wallpaper) + 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 } diff --git a/submodules/TelegramUI/Components/ChatThemeScreen/Sources/ChatThemeScreen.swift b/submodules/TelegramUI/Components/ChatThemeScreen/Sources/ChatThemeScreen.swift index cc06dc3451..202d5d05ab 100644 --- a/submodules/TelegramUI/Components/ChatThemeScreen/Sources/ChatThemeScreen.swift +++ b/submodules/TelegramUI/Components/ChatThemeScreen/Sources/ChatThemeScreen.swift @@ -258,7 +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? @@ -509,6 +511,37 @@ private final class ThemeSettingsThemeItemIconNode : ListViewItemNode { 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 }) { + bubbleColor = UIColor(rgb: UInt32(bitPattern: theme.accentColor)) + } + } else { + if let theme = themeSettings.first(where: { $0.baseTheme == .classic || $0.baseTheme == .day }) { + bubbleColor = UIColor(rgb: UInt32(bitPattern: theme.accentColor)) + } + } + } + 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 { @@ -525,6 +558,25 @@ private final class ThemeSettingsThemeItemIconNode : ListViewItemNode { 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() + } } }) } @@ -996,7 +1048,7 @@ private class ChatThemeScreenNode: ViewControllerTracingNode, ASScrollViewDelega emojiFile = file } } - if let themePeerId = uniqueGift.themePeerId { + if let themePeerId = uniqueGift.themePeerId, theme.id != initiallySelectedTheme?.id { peer = peers[themePeerId] } } diff --git a/submodules/TelegramUI/Components/ChatThemeScreen/Sources/GiftThemeTransferAlertController.swift b/submodules/TelegramUI/Components/ChatThemeScreen/Sources/GiftThemeTransferAlertController.swift index 8153c510b8..03ea78002f 100644 --- a/submodules/TelegramUI/Components/ChatThemeScreen/Sources/GiftThemeTransferAlertController.swift +++ b/submodules/TelegramUI/Components/ChatThemeScreen/Sources/GiftThemeTransferAlertController.swift @@ -127,7 +127,7 @@ private final class GiftThemeTransferAlertContentNode: AlertContentNode { return ("URL", url) } ), textAlignment: .center) - self.arrowNode.image = generateTintedImage(image: UIImage(bundleImageName: "Media Editor/CutoutUndo"), color: theme.controlBorderColor) + 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 { 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 0000000000000000000000000000000000000000..34df5f002f48412aa13643f0bd469731770beed1 GIT binary patch literal 4473 zcmai1c|6qX_cxemB88N-kz@&D#x~O;dnQXm_I)rI%M4>ClO=>yC`M1 z?17e`8KEu6Ut7fqcp{pJv4YAeLE&`KKg`JeVMf)Rh#}x;x-|co%UB1D;d!-RrnInh z85pAFP7`Zj9Po~qUlLsm&Y9=}hAAj2{X+D?a({X8CK51c4-h%6G69R?8sKTc`z%LE z0|4et4cGM&7|O3p-@6v%p3ND{fZlt6^QrYggW!Xj8q8KIdo%?+iDr%m4jK&a0RoMz zuCak$!r{00B}gW9O)CU3IIl&2kT z8a5mM#G0v+#KX_mp;j%(a0an`nkS&8j$4R>p~?7op!24cG*sIpRSJ0!+!5ym%8U#4 zRBT!=Lt^biPhz~f{kO5a5o^QDp|uB3IA2)R7Bo;y6g0OtSC&vqpz3k8!FkavI7XH> z;a;%2@`s%?R^CWyX;2y-}xo74n|7wWJ0no8$u1|q`!N40TJpBw+*Uq3AmV>tL2?zib?(LTklDh}2 z2#FX4RReG#09m&Cc5HRbmqrDXg2AjT24Knid$8(zqIn&epu(oD{1%Kr^%xWzH^*M# zy%m7LJ%?0Ypb$pDUe$BzU++b9Ddcn1a73wab=h`_9RbFn`HJ`QHzmk&EeA1;qR58? zIP$1dqgG@kt39UFyiupsWByD+LAi4}WIiD3_+8KimKQ2e11Tf0Bna2`%oE;#GH%wQoBbcPEQ>C!M%R+ z`nB$-N$50LcU|TaLUCbj)VubsP`#w~BtXZJFM>P~#?ScBf{(8DYr7x2e75RF3Mn^JQ<~0{ZOE*S((sKf>Wq84jNVJ|G`J-yMExz|*+7K{9_GhcXEA{OhRByx6?p z>eWr}O*YkFI~C7k%$(%V%FttK>{(Hs5wcoI!jha(H)Nip?2mb!EQ{<{snhk>_E$d^ zLqy)b|M@royRRUUm_m$~S6kOMIhGo0KJHQ;T9@jdHjHezcvU?M#U?RxSNv8|d8*-g zipCY)HSqcP$I%Y$^W%!`#;pqNOYQZsVy*nGeyybmwqmxDfhOAtoA()2pP_N0Q*>ot zf{`Mx=kj*H&NGk7m|yf%cV-| zO5mXA;Nc3}uJ}ozACA7f^{L9aIa#0bM7j%lCVP{+6?&|*Qgep#H?;5Mx4eybbFzD? zC$*>Voo!*GYNO3gFyL7bHrmef>AjZG`m7jq_1JzY57kS=Sfr}^RE}>>cr&+y(kJ34 z*H5)4%(xyPW7~w|h8{dBNXK5ohD}P#^>@72wY%I}+$rEN<02zps(92snpY@NSL>|9 zbF~-tH#Ki-M!Yzw6VNW+UY~F_!M5akacqfe@%57U;;jX%|o3fhbyZ*jha%zt!vu`(*g}7%8{5xmt)6Uo%ip{$>V50u>rBase&?3E zmu{{UtRyc~E?UnHO@`0!n;V)^dj8JQdw}#^B9LV#YI_7Q9uyt45R|8*n|LFkLOLvQ z9WwId#~3UG(}a-@21g54L*@*k4N$&arR`j>_=eFPZjCCfvFxU7*PI8YzNN#B1gSxp zQxIRz`0m2*#(gG5rTR7jSEDxhUZ*^UCMC@!#X&O>!D+6qiBhMlV3%*Te-s*R7^f^= zAanU2z6#yBlBQ$jHB7G4*=$(7$^0a=Clm?LLCKKQClblW=K~DwdgkvBjL(fXbNgS= zeTm45l|}Za7sjWg4<`c zsanM$%HpdPuv2)13E~9;|0E3W*52DO5_`I4#K$in!zx1@?-IUvTjX^2Y2ax;AL&7o zXJ4Pc&Gr&w(Y2myongaGQ|HWfP|MWsGu0#Yn#-EuD|RcaD{M2QA-`<1GP3pjL__XI ziq3@SnkasybRe+gZPkxfz(+tE06P#{{w`#lWkq1qDg&jA8Z+-UD=?rGEo=$D&)TNW zg!A8a5t4u}Oy-YH)JuLS{NhUe*5lCckld2d8`}G0vvsrVp-Fw!z2$bVF|Wy+qg*4T z{Unl@f<8|#bt!iJ=B4gD+|KyYB{!y=tU`m_vQc@sG-B)H*Q89APUf`9Q|)Jl<|3X! z;`QD>*mTOj;<0F?qZU!2P+mEkwnKTS__gYRt@PEi?;pOg@v>R-H()6fdE&6#RI~3* zIyOD-@F&Ra%O#V14cuOnxRr&O_w9hhQk!`Sp`az_Ym;$e(R5$h!}QwMb?*t(ZLLMU z+El&9fvtCM-98NYw~v`jd9)091Vn6{S|3@5yq~Y}{w%XG6BUeM3J5v5XO>-)135cC z6ZPR@!_Kfv6SzjRHT&{~x^vt+`Tm&AZ;9tdcIps^pbET`tRXyexM#LQKJp##f+@`EwYdqYh;#kXn^ve(wU z{14jRa97ZM+R4fKDQ0Wm6~;2KvG!rs$w2-$I}MACQdKNbr8B1F6g*0_4`hNL zJPxyO&~A+(Jx6Ao7K|xwi8P8a10$h%BKkf1gCeUn6}@#-9iNLhWOTf4fHE_fGCobr z9vDb292kJKN};F>#0%TNDAsNfI(PlW2zObm{BQdClRoak3cq1XFa)Wtu7>u;ID+Y1 z)evm`n*uBT)}j+;b-X*CaLE(xfceRv)uCXz6+;@?{U_g5`eV=kOKRbC6Mu49fX7`u z+>i=e*vhFenXbubfmpezjC&IY*|@Fm18)rwx`aW{XX2zy&I zsFzQ@rMUEH=(X{b$T9WJbR-^DStQ%|U{3+_n&t(#I;Xi|jEZq`j@|Vy$InX$=Impw z_r8#Y>9CFpYbF$-U_-hkUBXb0X)qT%B-b}F%c%E+bvg&{k*a=@u_lzW=h5?*2RB^5 zc@rHb;W;|fYswRvxXRsHk9~S@t4Pd)+ z*4Lt5;tw%gVjpc7;{;Y1Yr?f2D{Z-~Ey992hZqE>$kifU*1ae1`fqtu6tS*#Nx~4K zDTqX3Hf9NzpU^Ep`4fjMzJUtY<^#G77Z% z>^YyUxbiw;wqb57;k*|$rum(AC|d&!_#FnRb8xo*(TMbm{@3uSBn$uDVTj)-y~9*- zI6ToCY-#;#%rm;;NgEVVXy@HYP!HpXMXTY-U`w#9jO@?#_YV}l3;q4KYsnDfjVBQt zFy3H#PtzNYKE0~raYPJ`=8@jCyC4cppkZLB>@Vyev+Pgo?+IE5>qrY0Og{tt)!4O3 zqVN9C3A|~@KTZ8PjGJIFJ{SUufN{dmywl^P%XdRU;+*hc`aHf1=%R7XU~vp?cMPYe zh_+=nWcu0u%AGozh<3+2|LL1da|4nELt&u5Qb_-T6%`cZ6u?gGzcDE7b^9s7{f#No zqWTvGm7@jrCkBH^;12-tt@$srK` zI0un{#=2t&Ao^uPYS3yY4waRYhg!)h!Q|u-2ziPB0zc~tF~s9(Mg3h9$R(^VhMquL XMZJk=0`X@`<)KgnNK{nwqSpTb04<;d literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift b/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift index b5ea9e80bc..8311472202 100644 --- a/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift +++ b/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift @@ -456,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 } } @@ -512,6 +512,12 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu } }) self.historyNode.autoScrollWhenReordering = false + self.historyNode.didEndScrollingWithOverscroll = { [weak self] in + guard let self else { + return + } + self.requestDismiss() + } } @@ -751,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) From a4ed31da7420091878c59087cce333a911269be3 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Thu, 28 Aug 2025 05:14:18 +0400 Subject: [PATCH 20/32] Various improvements --- .../ChatThemeScreen/Sources/ChatThemeScreen.swift | 12 +++++++----- .../Sources/GiftsListView.swift | 9 ++++++++- .../Sources/PeerInfoGiftsPaneNode.swift | 1 - 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/submodules/TelegramUI/Components/ChatThemeScreen/Sources/ChatThemeScreen.swift b/submodules/TelegramUI/Components/ChatThemeScreen/Sources/ChatThemeScreen.swift index 202d5d05ab..23fe009606 100644 --- a/submodules/TelegramUI/Components/ChatThemeScreen/Sources/ChatThemeScreen.swift +++ b/submodules/TelegramUI/Components/ChatThemeScreen/Sources/ChatThemeScreen.swift @@ -35,8 +35,8 @@ private struct ThemeSettingsThemeEntry: Comparable, Identifiable { 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 { @@ -524,11 +524,13 @@ private final class ThemeSettingsThemeItemIconNode : ListViewItemNode { 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 }) { - bubbleColor = UIColor(rgb: UInt32(bitPattern: theme.accentColor)) + 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 }) { - bubbleColor = UIColor(rgb: UInt32(bitPattern: theme.accentColor)) + let color = theme.wallpaper?.settings?.colors.first ?? theme.accentColor + bubbleColor = UIColor(rgb: UInt32(bitPattern: color)) } } } @@ -964,7 +966,6 @@ 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.complete() } else { @@ -1349,6 +1350,7 @@ 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 { diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/GiftsListView.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/GiftsListView.swift index 3c5f060d1c..4412a7ea4a 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/GiftsListView.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/GiftsListView.swift @@ -996,7 +996,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 +1024,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? From eac655f5721b6f3828e0f365516b6e7bafbc53d7 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Thu, 28 Aug 2025 05:22:22 +0400 Subject: [PATCH 21/32] Various improvements --- .../ChatThemeScreen/Sources/ChatThemeScreen.swift | 8 ++++++++ .../Gifts/GiftViewScreen/Sources/GiftViewScreen.swift | 7 ++++++- .../Sources/StarsTransactionsScreen.swift | 1 - 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/submodules/TelegramUI/Components/ChatThemeScreen/Sources/ChatThemeScreen.swift b/submodules/TelegramUI/Components/ChatThemeScreen/Sources/ChatThemeScreen.swift index 23fe009606..b7cd11f010 100644 --- a/submodules/TelegramUI/Components/ChatThemeScreen/Sources/ChatThemeScreen.swift +++ b/submodules/TelegramUI/Components/ChatThemeScreen/Sources/ChatThemeScreen.swift @@ -975,6 +975,11 @@ private class ChatThemeScreenNode: ViewControllerTracingNode, ASScrollViewDelega } self.otherButton.addTarget(self, action: #selector(self.otherButtonPressed), forControlEvents: .touchUpInside) + var ignoreGiftThemes = false + if let data = self.context.currentAppConfiguration.with({ $0 }).data, let _ = data["ios_killswitch_disable_gift_themes"] { + ignoreGiftThemes = true + } + self.disposable.set(combineLatest( queue: Queue.mainQueue(), self.context.engine.themes.getChatThemes(accountManager: self.context.sharedContext.accountManager), @@ -1028,6 +1033,9 @@ private class ChatThemeScreenNode: ViewControllerTracingNode, ASScrollViewDelega )) var giftThemes = uniqueGiftChatThemesState.themes + if ignoreGiftThemes { + giftThemes = [] + } var existingIds = Set() if let initiallySelectedTheme, case .gift = initiallySelectedTheme { let initialThemeIndex = giftThemes.firstIndex(where: { $0.id == initiallySelectedTheme.id }) diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift index df5bce8ccf..ae78039072 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift @@ -1171,7 +1171,12 @@ private final class GiftViewSheetContent: CombinedComponent { self?.shareGift() }))) - if gift.flags.contains(.isThemeAvailable) { + var ignoreGiftThemes = false + if let data = self.context.currentAppConfiguration.with({ $0 }).data, let _ = data["ios_killswitch_disable_gift_themes"] { + ignoreGiftThemes = true + } + + if gift.flags.contains(.isThemeAvailable) && !ignoreGiftThemes { 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 diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift index fa0006025b..52d60129f8 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift @@ -673,7 +673,6 @@ 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( From da7fae6566ae62ba2338e0f63da48df0c129416b Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Thu, 28 Aug 2025 22:23:10 +0400 Subject: [PATCH 22/32] Various fixes --- submodules/Svg/Sources/Svg.m | 2 +- .../Sources/ChatMessageGiftBubbleContentNode.swift | 4 +++- .../Sources/WallpaperBackgroundNode.swift | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/submodules/Svg/Sources/Svg.m b/submodules/Svg/Sources/Svg.m index 975d102a65..d3c4edee41 100755 --- a/submodules/Svg/Sources/Svg.m +++ b/submodules/Svg/Sources/Svg.m @@ -807,7 +807,7 @@ UIImage * _Nullable renderPreparedImageWithSymbol(NSData * _Nonnull data, CGSize NSMutableArray *filteredRects = [[NSMutableArray alloc] init]; for (GiftPatternRect *rect in rects) { - if (rect.center.y > 240.0) { + if (rect.center.y > height * 0.1 && rect.center.y < height * 0.9) { [filteredRects addObject:rect]; } } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift index 728d37a4be..838412e9d1 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift @@ -1573,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/WallpaperBackgroundNode/Sources/WallpaperBackgroundNode.swift b/submodules/WallpaperBackgroundNode/Sources/WallpaperBackgroundNode.swift index 83a23132fa..4bfe3ee995 100644 --- a/submodules/WallpaperBackgroundNode/Sources/WallpaperBackgroundNode.swift +++ b/submodules/WallpaperBackgroundNode/Sources/WallpaperBackgroundNode.swift @@ -1383,7 +1383,7 @@ public final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgrou } } if let validPatternImage = self.validPatternImage, !validPatternImage.rects.isEmpty, var modelRectIndex = self.modelRectIndex, let modelFile { - let filteredRects = validPatternImage.rects.filter { $0.center.y > 240.0 } + 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)] From c54e9fbedfd0b94a6a810089ca7af849866e98f3 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Fri, 29 Aug 2025 23:34:46 +0400 Subject: [PATCH 23/32] Various fixes --- .../TelegramEngine/Themes/ChatThemes.swift | 16 ++-- .../Components/Gifts/GiftViewScreen/BUILD | 1 + .../Sources/GiftViewScreen.swift | 82 ++++++++++++++----- .../Sources/Panes/PeerInfoMembersPane.swift | 15 +++- 4 files changed, 83 insertions(+), 31 deletions(-) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Themes/ChatThemes.swift b/submodules/TelegramCore/Sources/TelegramEngine/Themes/ChatThemes.swift index ee07d49015..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.slug == rhsUnique.slug + case let .unique(rhsUnique): + return lhsUnique.slug == rhsUnique.slug && lhsThemeSettings == rhsThemeSettings default: return false } 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/GiftViewScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift index ae78039072..d23dbbed52 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 @@ -809,27 +810,66 @@ private final class GiftViewSheetContent: CombinedComponent { } let context = self.context - let peerController = self.context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: self.context, filter: [.excludeRecent, .doNotSearchMessages], requestPeerType: [.user(.init(isBot: false, isPremium: false))], hasContactSelector: false, hasCreation: false)) + + 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 - let _ = context.engine.themes.setChatTheme(peerId: peer.id, chatTheme: .gift(.unique(gift), [])).start() - peerController?.dismiss() - if let navigationController { - 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 proceed = { + let _ = context.engine.themes.setChatTheme(peerId: peer.id, chatTheme: .gift(.unique(gift), [])).start() + 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) @@ -3278,8 +3318,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, 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, From be54adbfa7687957b0cc4660dc10b32535cd0b86 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Fri, 29 Aug 2025 23:55:43 +0400 Subject: [PATCH 24/32] Various fixes --- submodules/ChatListUI/Sources/Node/ChatListNode.swift | 2 +- .../Sources/ServiceMessageStrings.swift | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) 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/TelegramStringFormatting/Sources/ServiceMessageStrings.swift b/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift index ae1f5e25ca..a13b4239ea 100644 --- a/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift +++ b/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift @@ -1167,7 +1167,7 @@ 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())) @@ -1177,7 +1177,7 @@ public func universalServiceMessageString(presentationData: (PresentationTheme, } } 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)) From 9e099d66124628ed33ed7ded87cd771e7e31adc2 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Sun, 31 Aug 2025 22:36:46 +0400 Subject: [PATCH 25/32] Various fixes --- .../ChatThemeScreen/Sources/ChatThemeScreen.swift | 8 -------- .../GiftViewScreen/Sources/GiftViewScreen.swift | 15 ++++++--------- .../Chat/ChatControllerThemeManagement.swift | 2 +- versions.json | 2 +- 4 files changed, 8 insertions(+), 19 deletions(-) diff --git a/submodules/TelegramUI/Components/ChatThemeScreen/Sources/ChatThemeScreen.swift b/submodules/TelegramUI/Components/ChatThemeScreen/Sources/ChatThemeScreen.swift index b7cd11f010..23fe009606 100644 --- a/submodules/TelegramUI/Components/ChatThemeScreen/Sources/ChatThemeScreen.swift +++ b/submodules/TelegramUI/Components/ChatThemeScreen/Sources/ChatThemeScreen.swift @@ -975,11 +975,6 @@ private class ChatThemeScreenNode: ViewControllerTracingNode, ASScrollViewDelega } self.otherButton.addTarget(self, action: #selector(self.otherButtonPressed), forControlEvents: .touchUpInside) - var ignoreGiftThemes = false - if let data = self.context.currentAppConfiguration.with({ $0 }).data, let _ = data["ios_killswitch_disable_gift_themes"] { - ignoreGiftThemes = true - } - self.disposable.set(combineLatest( queue: Queue.mainQueue(), self.context.engine.themes.getChatThemes(accountManager: self.context.sharedContext.accountManager), @@ -1033,9 +1028,6 @@ private class ChatThemeScreenNode: ViewControllerTracingNode, ASScrollViewDelega )) var giftThemes = uniqueGiftChatThemesState.themes - if ignoreGiftThemes { - giftThemes = [] - } var existingIds = Set() if let initiallySelectedTheme, case .gift = initiallySelectedTheme { let initialThemeIndex = giftThemes.firstIndex(where: { $0.id == initiallySelectedTheme.id }) diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift index d23dbbed52..c56e3120ae 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift @@ -826,7 +826,9 @@ private final class GiftViewSheetContent: CombinedComponent { peerController.peerSelected = { [weak peerController, weak navigationController] peer, _ in if let navigationController { let proceed = { - let _ = context.engine.themes.setChatTheme(peerId: peer.id, chatTheme: .gift(.unique(gift), [])).start() + 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( @@ -1210,13 +1212,8 @@ private final class GiftViewSheetContent: CombinedComponent { self?.shareGift() }))) - - var ignoreGiftThemes = false - if let data = self.context.currentAppConfiguration.with({ $0 }).data, let _ = data["ios_killswitch_disable_gift_themes"] { - ignoreGiftThemes = true - } - - if gift.flags.contains(.isThemeAvailable) && !ignoreGiftThemes { + + 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 @@ -4714,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/Sources/Chat/ChatControllerThemeManagement.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerThemeManagement.swift index b51952d115..8006a42067 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerThemeManagement.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerThemeManagement.swift @@ -262,7 +262,7 @@ extension ChatControllerImpl { 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 diff --git a/versions.json b/versions.json index 6c85ab5077..6b7e5549eb 100644 --- a/versions.json +++ b/versions.json @@ -1,5 +1,5 @@ { - "app": "11.15", + "app": "12.0", "xcode": "16.2", "bazel": "8.3.1:0cac3a67dc5429c68272dc6944104952e9e4cf84b29d126a5ff3fbaa59045217", "macos": "15" From 975430aa85db13d89318db2a23a69f9502b204ce Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Mon, 1 Sep 2025 00:15:29 +0400 Subject: [PATCH 26/32] Various fixes --- .../Sources/GiftStoreScreen.swift | 2 +- .../Sources/GiftViewScreen.swift | 30 ++++++------- .../Sources/GiftsListView.swift | 44 +++++++++---------- 3 files changed, 37 insertions(+), 39 deletions(-) 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/Sources/GiftViewScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift index c56e3120ae..ba017d2150 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift @@ -154,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 } @@ -981,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 @@ -1031,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 @@ -1160,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 { @@ -1168,7 +1168,7 @@ private final class GiftViewSheetContent: CombinedComponent { } let pinnedToTop = !pinnedToTop - if togglePinnedToTop(pinnedToTop) { + if togglePinnedToTop(reference, pinnedToTop) { if pinnedToTop { controller.dismissAnimated() } else { @@ -4457,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)) @@ -4471,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 { @@ -4496,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 { @@ -4512,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: @@ -4557,8 +4557,8 @@ public class GiftViewScreen: ViewControllerComponentContainer { fileprivate let transferGift: ((Bool, EnginePeer.Id) -> 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)? @@ -4575,8 +4575,8 @@ public class GiftViewScreen: ViewControllerComponentContainer { transferGift: ((Bool, EnginePeer.Id) -> Signal)? = nil, upgradeGift: ((Int64?, StarGiftReference, Bool) -> Signal)? = nil, buyGift: ((String, EnginePeer.Id, CurrencyAmount?) -> Signal)? = nil, - updateResellStars: ((CurrencyAmount?) -> Signal)? = nil, - togglePinnedToTop: ((Bool) -> Bool)? = nil, + updateResellStars: ((StarGiftReference, CurrencyAmount?) -> Signal)? = nil, + togglePinnedToTop: ((StarGiftReference, Bool) -> Bool)? = nil, shareStory: ((StarGift.UniqueGift) -> Void)? = nil, openChatTheme: (() -> Void)? = nil ) { diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/GiftsListView.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/GiftsListView.swift index 4412a7ea4a..89396b24d4 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/GiftsListView.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/GiftsListView.swift @@ -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 From 3532aad036aeb115877259fe76a6c9eefdfc5a26 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Mon, 1 Sep 2025 01:04:55 +0400 Subject: [PATCH 27/32] Various fixes --- Telegram/Telegram-iOS/en.lproj/Localizable.strings | 4 ++++ .../Sources/ServiceMessageStrings.swift | 7 ++++++- submodules/TelegramUI/Sources/ChatController.swift | 3 +++ 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 7949042e9f..1bf76e5a2a 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -14996,3 +14996,7 @@ Sorry for the inconvenience."; "Gift.Value.ForSaleOnFragment" = "for sale on Fragment"; "Gift.View.Context.SetAsTheme" = "Set as Theme in..."; + +"Notification.PrepaidGiftUpgrade" = "Gift Upgrade for %@"; +"Notification.PrepaidGiftUpgrade.Stars_1" = "%@ Star"; +"Notification.PrepaidGiftUpgrade.Stars_any" = "%@ Stars"; diff --git a/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift b/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift index a13b4239ea..c375006deb 100644 --- a/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift +++ b/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift @@ -1173,7 +1173,12 @@ public func universalServiceMessageString(presentationData: (PresentationTheme, 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 diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 70d9760246..70c2e024cd 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -5831,6 +5831,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } 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) From d156e552335c98c2d7c432567b8a093457ea10a5 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Mon, 1 Sep 2025 01:28:22 +0400 Subject: [PATCH 28/32] Various fixes --- Telegram/Telegram-iOS/en.lproj/Localizable.strings | 4 ++++ .../Sources/ServiceMessageStrings.swift | 3 +++ 2 files changed, 7 insertions(+) diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 1bf76e5a2a..59dee5c2d8 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -14997,6 +14997,10 @@ Sorry for the inconvenience."; "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"; diff --git a/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift b/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift index c375006deb..093ceb7e98 100644 --- a/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift +++ b/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift @@ -1176,6 +1176,9 @@ public func universalServiceMessageString(presentationData: (PresentationTheme, 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 if case let .generic(gift) = gift { + let starsPrice = strings.Notification_GiftStars_Stars(Int32(clamping: gift.price)) + attributedString = NSAttributedString(string: strings.Notification_GiftStars(starsPrice).string, font: titleFont, textColor: primaryTextColor) } else { attributedString = NSAttributedString(string: strings.Notification_Gift, font: titleFont, textColor: primaryTextColor) } From b41ec120245b26e50e369c6f510e85c4cd29c9ed Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Mon, 1 Sep 2025 02:28:03 +0400 Subject: [PATCH 29/32] Various fixes --- .../Sources/ServiceMessageStrings.swift | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift b/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift index 093ceb7e98..0f9bc6fdf7 100644 --- a/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift +++ b/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift @@ -845,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 { @@ -1176,9 +1177,6 @@ public func universalServiceMessageString(presentationData: (PresentationTheme, 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 if case let .generic(gift) = gift { - let starsPrice = strings.Notification_GiftStars_Stars(Int32(clamping: gift.price)) - attributedString = NSAttributedString(string: strings.Notification_GiftStars(starsPrice).string, font: titleFont, textColor: primaryTextColor) } else { attributedString = NSAttributedString(string: strings.Notification_Gift, font: titleFont, textColor: primaryTextColor) } From 7b3cf927201cfb3b4bf548ac10ee33d2abd01853 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Mon, 1 Sep 2025 03:30:20 +0400 Subject: [PATCH 30/32] Various fixes --- .../Sources/TelegramEngine/Payments/Stars.swift | 2 ++ .../Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift | 7 ++++++- 2 files changed, 8 insertions(+), 1 deletion(-) 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/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 } From b1bcf52a09d4a2359ca7e69ac28a92e8e2261b78 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Mon, 1 Sep 2025 13:14:37 +0400 Subject: [PATCH 31/32] Various fixes --- .../Telegram-iOS/en.lproj/Localizable.strings | 4 ++ .../Sources/PeerInfoHeaderNode.swift | 2 +- .../Sources/ProfileLevelInfoScreen.swift | 46 ++++++++++++++----- 3 files changed, 39 insertions(+), 13 deletions(-) diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 59dee5c2d8..66be514ea9 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -15004,3 +15004,7 @@ Sorry for the inconvenience."; "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/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift index 59df90ccd2..d38cf5f67f 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift @@ -589,7 +589,7 @@ 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 { 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[.. Date: Mon, 1 Sep 2025 19:06:11 +0400 Subject: [PATCH 32/32] Various fixes --- .../Components/ChatThemeScreen/Sources/ChatThemeScreen.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/submodules/TelegramUI/Components/ChatThemeScreen/Sources/ChatThemeScreen.swift b/submodules/TelegramUI/Components/ChatThemeScreen/Sources/ChatThemeScreen.swift index 23fe009606..6ab4a46adc 100644 --- a/submodules/TelegramUI/Components/ChatThemeScreen/Sources/ChatThemeScreen.swift +++ b/submodules/TelegramUI/Components/ChatThemeScreen/Sources/ChatThemeScreen.swift @@ -1033,7 +1033,6 @@ private class ChatThemeScreenNode: ViewControllerTracingNode, ASScrollViewDelega let initialThemeIndex = giftThemes.firstIndex(where: { $0.id == initiallySelectedTheme.id }) if initialThemeIndex == nil || initialThemeIndex! > 50 { giftThemes.insert(initiallySelectedTheme, at: 0) - existingIds.insert(initiallySelectedTheme.id) } } @@ -1074,6 +1073,7 @@ private class ChatThemeScreenNode: ViewControllerTracingNode, ASScrollViewDelega strings: presentationData.strings, wallpaper: wallpaper )) + existingIds.insert(theme.id) } if uniqueGiftChatThemesState.themes.count == 0 || uniqueGiftChatThemesState.dataState == .ready(canLoadMore: false) {