From 0186f1297264ed06f7ae9b2d2797a2a6c24ae074 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Mon, 31 Mar 2025 15:24:55 +0400 Subject: [PATCH] Various improvements --- .../Telegram-iOS/en.lproj/Localizable.strings | 8 + .../Sources/CameraRoundVideoFilter.swift | 124 +++++++++++-- .../Sources/ChatMessageBubbleItemNode.swift | 6 +- .../RoundVideoCorner.imageset/Contents.json | 21 +++ .../RoundVideoCorner.imageset/vlog.png | Bin 0 -> 1897 bytes submodules/WebUI/BUILD | 1 + .../WebUI/Sources/WebAppController.swift | 59 ++++--- .../WebAppSecureStorageTransferScreen.swift | 164 +++++++++++++++--- 8 files changed, 325 insertions(+), 58 deletions(-) create mode 100644 submodules/TelegramUI/Images.xcassets/Components/RoundVideoCorner.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Components/RoundVideoCorner.imageset/vlog.png diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 6a20dc96be..2857455b4d 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -14107,3 +14107,11 @@ Sorry for the inconvenience."; "Login.Fee.Support.Text" = "Sign up for a 1-week Telegram Premium subscription to help cover the SMS costs."; "Login.Fee.SignUp" = "Sign Up for %@"; "Login.Fee.GetPremiumForAWeek" = "Get Telegram Premium for 1 week"; + +"StoryFeed.AddStory" = "Add Story"; + +"WebApp.ImportData.Title" = "Import Data"; +"WebApp.ImportData.Description" = "**%@** is requesting permission to import data from a previous Telegram account used on this device."; +"WebApp.ImportData.AccountHeader" = "ACCOUNT TO IMPORT DATA FROM"; +"WebApp.ImportData.CreatedOn" = "created on %@"; +"WebApp.ImportData.Import" = "Import"; diff --git a/submodules/Camera/Sources/CameraRoundVideoFilter.swift b/submodules/Camera/Sources/CameraRoundVideoFilter.swift index 3ce2d80c05..e8f5c1c617 100644 --- a/submodules/Camera/Sources/CameraRoundVideoFilter.swift +++ b/submodules/Camera/Sources/CameraRoundVideoFilter.swift @@ -7,6 +7,9 @@ import CoreVideo import Metal import Display import TelegramCore +import RLottieBinding +import GZip +import AppBundle let videoMessageDimensions = PixelDimensions(width: 400, height: 400) @@ -98,7 +101,17 @@ final class CameraRoundVideoFilter { private var resizeFilter: CIFilter? private var overlayFilter: CIFilter? private var compositeFilter: CIFilter? - private var borderFilter: CIFilter? + private var maskFilter: CIFilter? + private var blurFilter: CIFilter? + private var darkenFilter: CIFilter? + + private var logoImageFilter: CIFilter? + private var logoImage: CIImage? + + private var animationImageFilter: CIFilter? + private var animationImage: CIImage? + private var animation: LottieInstance? + private var animationFrameIndex: Int32 = 0 private var outputColorSpace: CGColorSpace? private var outputPixelBufferPool: CVPixelBufferPool? @@ -122,21 +135,45 @@ final class CameraRoundVideoFilter { } self.inputFormatDescription = formatDescription - let circleImage = generateImage(videoMessageDimensions.cgSize, opaque: false, scale: 1.0, rotatedContext: { size, context in + if let logoImage = UIImage(bundleImageName: "Components/RoundVideoCorner") { + self.logoImage = CIImage(image: logoImage) + } + + if let path = getAppBundle().path(forResource: "PlaneLogoPlain", ofType: "tgs"), var data = try? Data(contentsOf: URL(fileURLWithPath: path)) { + if let unpackedData = TGGUnzipData(data, 5 * 1024 * 1024) { + data = unpackedData + self.animation = LottieInstance(data: data, fitzModifier: .none, colorReplacements: [:], cacheKey: "") + } + } + + let circleMaskImage = generateImage(videoMessageDimensions.cgSize, opaque: false, scale: 1.0, rotatedContext: { size, context in let bounds = CGRect(origin: .zero, size: size) context.clear(bounds) - context.setFillColor(UIColor.white.cgColor) + context.setFillColor(UIColor.black.cgColor) context.fill(bounds) - context.setBlendMode(.clear) + context.setBlendMode(.normal) + context.setFillColor(UIColor.white.cgColor) context.fillEllipse(in: bounds.insetBy(dx: -2.0, dy: -2.0)) })! - + self.resizeFilter = CIFilter(name: "CILanczosScaleTransform") self.overlayFilter = CIFilter(name: "CIColorMatrix") self.compositeFilter = CIFilter(name: "CISourceOverCompositing") - self.borderFilter = CIFilter(name: "CISourceOverCompositing") - self.borderFilter?.setValue(CIImage(image: circleImage), forKey: kCIInputImageKey) + self.maskFilter = CIFilter(name: "CIBlendWithMask") + self.maskFilter?.setValue(CIImage(image: circleMaskImage), forKey: kCIInputMaskImageKey) + + self.blurFilter = CIFilter(name: "CIGaussianBlur") + self.blurFilter?.setValue(30.0, forKey: kCIInputRadiusKey) + + self.darkenFilter = CIFilter(name: "CIColorMatrix") + let darkenVector = CIVector(x: 0.25, y: 0, z: 0, w: 0) + self.darkenFilter?.setValue(darkenVector, forKey: "inputRVector") + self.darkenFilter?.setValue(darkenVector, forKey: "inputGVector") + self.darkenFilter?.setValue(darkenVector, forKey: "inputBVector") + + self.logoImageFilter = CIFilter(name: "CISourceOverCompositing") + self.animationImageFilter = CIFilter(name: "CISourceOverCompositing") self.isPrepared = true } @@ -145,7 +182,11 @@ final class CameraRoundVideoFilter { self.resizeFilter = nil self.overlayFilter = nil self.compositeFilter = nil - self.borderFilter = nil + self.maskFilter = nil + self.blurFilter = nil + self.darkenFilter = nil + self.logoImageFilter = nil + self.animationImageFilter = nil self.outputColorSpace = nil self.outputPixelBufferPool = nil self.outputFormatDescription = nil @@ -159,7 +200,15 @@ final class CameraRoundVideoFilter { private var lastAdditionalSourceImage: CIImage? func render(pixelBuffer: CVPixelBuffer, additional: Bool, captureOrientation: AVCaptureVideoOrientation, transitionFactor: CGFloat) -> CVPixelBuffer? { - guard let resizeFilter = self.resizeFilter, let overlayFilter = self.overlayFilter, let compositeFilter = self.compositeFilter, let borderFilter = self.borderFilter, self.isPrepared else { + guard let resizeFilter = self.resizeFilter, + let overlayFilter = self.overlayFilter, + let compositeFilter = self.compositeFilter, + let maskFilter = self.maskFilter, + let blurFilter = self.blurFilter, + let darkenFilter = self.darkenFilter, + let logoImageFilter = self.logoImageFilter, + let animationImageFilter = self.animationImageFilter, + self.isPrepared else { return nil } @@ -230,9 +279,62 @@ final class CameraRoundVideoFilter { } } - borderFilter.setValue(effectiveSourceImage, forKey: kCIInputBackgroundImageKey) + let extendedImage = effectiveSourceImage.clampedToExtent() + + blurFilter.setValue(extendedImage, forKey: kCIInputImageKey) + let blurredImage = blurFilter.outputImage ?? effectiveSourceImage + + let blurredAndCropped = blurredImage.cropped(to: effectiveSourceImage.extent) + + darkenFilter.setValue(blurredAndCropped, forKey: kCIInputImageKey) + let darkenedBlurredBackground = darkenFilter.outputImage ?? blurredAndCropped + + maskFilter.setValue(effectiveSourceImage, forKey: kCIInputImageKey) + maskFilter.setValue(darkenedBlurredBackground, forKey: kCIInputBackgroundImageKey) + + var finalImage = maskFilter.outputImage + guard let maskedImage = finalImage else { + return nil + } + + if let logoImage = self.logoImage { + let overlayWidth: CGFloat = 100.0 + let xPosition = maskedImage.extent.width - overlayWidth + let yPosition = 0.0 + + let transformedOverlay = logoImage.transformed(by: CGAffineTransform(translationX: xPosition, y: yPosition)) + logoImageFilter.setValue(transformedOverlay, forKey: kCIInputImageKey) + logoImageFilter.setValue(maskedImage, forKey: kCIInputBackgroundImageKey) + + finalImage = logoImageFilter.outputImage ?? maskedImage + } else { + finalImage = maskedImage + } + + if let animation = self.animation, let renderContext = DrawingContext(size: CGSize(width: 68.0, height: 68.0), scale: 1.0, clear: true) { + animation.renderFrame(with: self.animationFrameIndex, into: renderContext.bytes.assumingMemoryBound(to: UInt8.self), width: Int32(renderContext.size.width * renderContext.scale), height: Int32(renderContext.size.height * renderContext.scale), bytesPerRow: Int32(renderContext.bytesPerRow)) + + self.animationFrameIndex += 2 + if self.animationFrameIndex >= animation.frameCount { + self.animationFrameIndex = 0 + } + + if let image = renderContext.generateImage(), let animationImage = CIImage(image: image) { + let xPosition = 0.0 + let yPosition = 0.0 + + let transformedOverlay = animationImage.transformed(by: CGAffineTransform(translationX: xPosition, y: yPosition)) + animationImageFilter.setValue(transformedOverlay, forKey: kCIInputImageKey) + animationImageFilter.setValue(finalImage, forKey: kCIInputBackgroundImageKey) + + finalImage = animationImageFilter.outputImage ?? maskedImage + } else { + finalImage = maskedImage + } + } else { + finalImage = maskedImage + } - let finalImage = borderFilter.outputImage guard let finalImage else { return nil } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift index 77cfc909ef..181569f730 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift @@ -2291,7 +2291,11 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI dateReplies = Int(attribute.count) } } else if let attribute = attribute as? PaidStarsMessageAttribute, item.message.id.peerId.namespace == Namespaces.Peer.CloudChannel { - starsCount = attribute.stars.value + var messageCount: Int = 1 + if case let .group(messages) = item.content { + messageCount = messages.count + } + starsCount = attribute.stars.value * Int64(messageCount) } } diff --git a/submodules/TelegramUI/Images.xcassets/Components/RoundVideoCorner.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Components/RoundVideoCorner.imageset/Contents.json new file mode 100644 index 0000000000..c3ad42ab48 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Components/RoundVideoCorner.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "vlog.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Components/RoundVideoCorner.imageset/vlog.png b/submodules/TelegramUI/Images.xcassets/Components/RoundVideoCorner.imageset/vlog.png new file mode 100644 index 0000000000000000000000000000000000000000..9aa09ec85004efbf6615f084e45cd570a2432769 GIT binary patch literal 1897 zcma)-Sv(X98^%W=do+wPiJ6cslkH$E=d+AuNEwZ-k<%=M>=D!89BPcoT9#}h%93R= zwq$9t aV+0Kc<*qMyAlEbI_@8Y|7p8xN8-<$X5`Mt?ko$VyW6vO}kfTV-H4eE&a zpAr#1^3d7rYex_bwfBqw06?lgCE$Qk;R66Czz#N+*P;t~llN*cXu1B4D?u`j;LSiT6>_rDxwHbHs|nGo^7zyrr5SqcS0? z-`52-tv#p`T(I`(f}e!ZFC?bodf%W2B)_9zLj~ltbMy6m1NNyJZHvRybnZLy7Q3H= z(5eZlSFVjv(~&=BmYP28F9~U2!T~5mo@=veeJG6@#ar=u}!|3&z zwmJ~@LWZ6aO=u!J{pQ14D>sr{!~O%4WTD-P68RO-e?vDq`qHV~#TM`(FGuF}{ZA8Q zL&Tog2l;`2E^s zZnv^QR-ef@>Q>V9*GXbD2v4#|gn^cm>O@S58`Bsn7LAlKj?5s8Pp1AYz<_;vJ|KOYImpqkq$}Opvyk-}PV&&nU_vR%tuhOZW=d*ts z7xYX7G8EVph9ow;YoR_R0RP0dC`ClqB3+!yp`B`w^3fC@a?Rml_FcRspuPxWp83X8 zX|-h9J@%O#x6!rPh(9Xn0n2iTJ-lwVpZPhLV#H7c`Ol<6>N@rG2oD-%-%79^btxmtHKhNd~WwU@?T9Aj4C8v zwJPU|nz8ArSq;6!w|-Q!K>h;l`WQ4(ndLOo;+i?-xo@Ev@Vl#x9UpL3oj99Kkk0Qg zq zNZvL1Jn_8}B|Ud1TMzYFXRz9z@gZT`s^}1cB7N$(j}CDPhn*kloX;CEuhM3bToj~a z^+nx5Or`W6eBPtA6-_h<@6_>~*eboGzF%j2_l84MWOiKW3J0h!QY@|5*7g9Wr1e)Z zq^jvM;q;jQo~3lx3ucKuTc*~KP9)$fScdI}FzZ8yr>f&Vqzf>NXL=I>C$E+`QP*V$aboG@LpA1&BLqml`oSJ=8mXP?|MF#+uEF zKc4e;pKCKLyW`t!+-IWEXNgp*SQ;6u1s9L;QLxJz0|hFFQyOJKl4E9hD0u5yuD(b@ zJG`us5&dG-75VURz3j>zo=k5f7=?^@AE!%DEZT((j~G=e$uw9}Zt0d zokjCB7KVa*uB0;OdrUvcGcnrvZwz(mas{@#-O+LD{)5Zy7s|Giy1q~OkcyiN+TT-2 zCqXe%&Kl-d_nS@18r>(P8mV^rd9(r)T&K(+f2AgHQy}o+{&fYqIdkpT)^|rcH-q*M zBfX2GSwj|=F>OBGP0Y(09p!7kwyfCcTg*9=96w~n%G2c`TdM^vySj|Z&2Sm1GeOli z6rq3n6L?x`I>1p*ghKz#@HB>mUDSoGH!vAY-~>;2KNFe>zFqi|=9j zD@XlKY;V0{yo?&QTzj9FBe`zyaZGL$~@Z;lH|AlZ5~P literal 0 HcmV?d00001 diff --git a/submodules/WebUI/BUILD b/submodules/WebUI/BUILD index 353903800b..3c807f17c0 100644 --- a/submodules/WebUI/BUILD +++ b/submodules/WebUI/BUILD @@ -53,6 +53,7 @@ swift_library( "//submodules/DeviceLocationManager", "//submodules/DeviceAccess", "//submodules/TelegramUI/Components/Utils/GenerateStickerPlaceholderImage", + "//submodules/AvatarNode", ], visibility = [ "//visibility:public", diff --git a/submodules/WebUI/Sources/WebAppController.swift b/submodules/WebUI/Sources/WebAppController.swift index 5e852102c7..f11573857d 100644 --- a/submodules/WebUI/Sources/WebAppController.swift +++ b/submodules/WebUI/Sources/WebAppController.swift @@ -2992,39 +2992,46 @@ public final class WebAppController: ViewController, AttachmentContainable { return } - let transferController = WebAppSecureStorageTransferScreen( - context: self.context, - existingKeys: storedKeys, - completion: { [weak self] uuid in - guard let self else { - return - } - guard let uuid else { - let data: JSON = [ - "req_id": requestId, - "error": "RESTORE_CANCELLED" - ] - self.webView?.sendEvent(name: "secure_storage_failed", data: data.string) - return - } - - let _ = (WebAppSecureStorage.transferAllValues(context: self.context, fromUuid: uuid, botId: controller.botId) - |> deliverOnMainQueue).start(completed: { [weak self] in + let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: controller.botId)) + |> deliverOnMainQueue).start(next: { [weak self] botPeer in + guard let self, let botPeer, let controller = self.controller else { + return + } + let transferController = WebAppSecureStorageTransferScreen( + context: self.context, + peer: botPeer, + existingKeys: storedKeys, + completion: { [weak self] uuid in guard let self else { return } - let _ = (WebAppSecureStorage.getValue(context: self.context, botId: controller.botId, key: key) - |> deliverOnMainQueue).start(next: { [weak self] value in + guard let uuid else { let data: JSON = [ "req_id": requestId, - "value": value ?? NSNull() + "error": "RESTORE_CANCELLED" ] - self?.webView?.sendEvent(name: "secure_storage_key_restored", data: data.string) + self.webView?.sendEvent(name: "secure_storage_failed", data: data.string) + return + } + + let _ = (WebAppSecureStorage.transferAllValues(context: self.context, fromUuid: uuid, botId: controller.botId) + |> deliverOnMainQueue).start(completed: { [weak self] in + guard let self else { + return + } + let _ = (WebAppSecureStorage.getValue(context: self.context, botId: controller.botId, key: key) + |> deliverOnMainQueue).start(next: { [weak self] value in + let data: JSON = [ + "req_id": requestId, + "value": value ?? NSNull() + ] + self?.webView?.sendEvent(name: "secure_storage_key_restored", data: data.string) + }) }) - }) - } - ) - controller.parentController()?.push(transferController) + } + ) + controller.parentController()?.push(transferController) + }) } fileprivate func openLocationSettings() { diff --git a/submodules/WebUI/Sources/WebAppSecureStorageTransferScreen.swift b/submodules/WebUI/Sources/WebAppSecureStorageTransferScreen.swift index 6bdff49327..e64a909355 100644 --- a/submodules/WebUI/Sources/WebAppSecureStorageTransferScreen.swift +++ b/submodules/WebUI/Sources/WebAppSecureStorageTransferScreen.swift @@ -11,28 +11,31 @@ import TelegramStringFormatting import ViewControllerComponent import SheetComponent import BundleIconComponent -import BalancedTextComponent import MultilineTextComponent import ButtonComponent import ListSectionComponent import ListActionItemComponent import AccountContext +import AvatarNode private final class SheetContent: CombinedComponent { typealias EnvironmentType = ViewControllerComponentContainer.Environment let context: AccountContext + let peer: EnginePeer let existingKeys: [WebAppSecureStorage.ExistingKey] let completion: (String) -> Void let dismiss: () -> Void init( context: AccountContext, + peer: EnginePeer, existingKeys: [WebAppSecureStorage.ExistingKey], completion: @escaping (String) -> Void, dismiss: @escaping () -> Void ) { self.context = context + self.peer = peer self.existingKeys = existingKeys self.completion = completion self.dismiss = dismiss @@ -42,6 +45,9 @@ private final class SheetContent: CombinedComponent { if lhs.context !== rhs.context { return false } + if lhs.peer != rhs.peer { + return false + } if lhs.existingKeys != rhs.existingKeys { return false } @@ -59,8 +65,9 @@ private final class SheetContent: CombinedComponent { static var body: Body { let closeButton = Child(Button.self) - let title = Child(BalancedTextComponent.self) - let text = Child(BalancedTextComponent.self) + let title = Child(MultilineTextComponent.self) + let avatar = Child(AvatarComponent.self) + let text = Child(MultilineTextComponent.self) let keys = Child(ListSectionComponent.self) let button = Child(ButtonComponent.self) @@ -76,11 +83,11 @@ private final class SheetContent: CombinedComponent { let textSideInset: CGFloat = 32.0 + environment.safeInsets.left let titleFont = Font.semibold(17.0) - let subtitleFont = Font.regular(12.0) + let textFont = Font.regular(13.0) + let boldTextFont = Font.semibold(13.0) let textColor = theme.actionSheet.primaryTextColor - let secondaryTextColor = theme.actionSheet.secondaryTextColor - var contentSize = CGSize(width: context.availableSize.width, height: 10.0) + var contentSize = CGSize(width: context.availableSize.width, height: 18.0) let closeButton = closeButton.update( component: Button( @@ -98,8 +105,8 @@ private final class SheetContent: CombinedComponent { //TODO:localize let title = title.update( - component: BalancedTextComponent( - text: .plain(NSAttributedString(string: "Data Transfer Requested", font: titleFont, textColor: textColor)), + component: MultilineTextComponent( + text: .plain(NSAttributedString(string: strings.WebApp_ImportData_Title, font: titleFont, textColor: textColor)), horizontalAlignment: .center, maximumNumberOfLines: 1, lineSpacing: 0.1 @@ -111,12 +118,36 @@ private final class SheetContent: CombinedComponent { .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + title.size.height / 2.0)) ) contentSize.height += title.size.height + contentSize.height += 24.0 + + let avatar = avatar.update( + component: AvatarComponent( + context: component.context, + peer: component.peer, + size: CGSize(width: 80.0, height: 80.0) + ), + availableSize: CGSize(width: 80.0, height: 80.0), + transition: .immediate + ) + context.add(avatar + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + avatar.size.height / 2.0)) + ) + contentSize.height += avatar.size.height + contentSize.height += 22.0 let text = text.update( - component: BalancedTextComponent( - text: .plain(NSAttributedString(string: "Choose account to transfer data from:", font: subtitleFont, textColor: secondaryTextColor)), + component: MultilineTextComponent( + text: .markdown( + text: strings.WebApp_ImportData_Description(component.peer.compactDisplayTitle).string, + attributes: MarkdownAttributes( + body: MarkdownAttributeSet(font: textFont, textColor: textColor), + bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor), + link: MarkdownAttributeSet(font: textFont, textColor: textColor), + linkAttribute: { _ in return nil } + ) + ), horizontalAlignment: .center, - maximumNumberOfLines: 1, + maximumNumberOfLines: 0, lineSpacing: 0.2 ), availableSize: CGSize(width: context.availableSize.width - textSideInset * 2.0, height: context.availableSize.height), @@ -126,7 +157,7 @@ private final class SheetContent: CombinedComponent { .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + text.size.height / 2.0)) ) contentSize.height += text.size.height - contentSize.height += 17.0 + contentSize.height += 29.0 let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } @@ -146,8 +177,8 @@ private final class SheetContent: CombinedComponent { titleComponents.append( AnyComponentWithIdentity(id: AnyHashable(1), component: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString( - string: "Created on \(stringForMediumCompactDate(timestamp: key.timestamp, strings: strings, dateTimeFormat: environment.dateTimeFormat))", - font: Font.regular(floor(presentationData.listsFontSize.itemListBaseFontSize * 14.0 / 17.0)), + string: strings.WebApp_ImportData_CreatedOn(stringForMediumCompactDate(timestamp: key.timestamp, strings: strings, dateTimeFormat: environment.dateTimeFormat)).string, + font: Font.regular(floor(presentationData.listsFontSize.itemListBaseFontSize * 15.0 / 17.0)), textColor: environment.theme.list.itemSecondaryTextColor )), maximumNumberOfLines: 1 @@ -155,7 +186,8 @@ private final class SheetContent: CombinedComponent { ) items.append(AnyComponentWithIdentity(id: key.uuid, component: AnyComponent(ListActionItemComponent( theme: theme, - title: AnyComponent(VStack(titleComponents, alignment: .left, spacing: 2.0)), + title: AnyComponent(VStack(titleComponents, alignment: .left, spacing: 3.0)), + contentInsets: UIEdgeInsets(top: 10.0, left: 0.0, bottom: 10.0, right: 0.0), leftIcon: .check(ListActionItemComponent.LeftIcon.Check(isSelected: key.uuid == state.selectedUuid, isEnabled: true, toggle: nil)), accessory: nil, action: { [weak state] _ in @@ -170,7 +202,14 @@ private final class SheetContent: CombinedComponent { let keys = keys.update( component: ListSectionComponent( theme: environment.theme, - header: nil, + header: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: strings.WebApp_ImportData_AccountHeader.uppercased(), + font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), + textColor: environment.theme.list.freeTextColor + )), + maximumNumberOfLines: 0 + )), footer: nil, items: items ), @@ -181,9 +220,8 @@ private final class SheetContent: CombinedComponent { .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + keys.size.height / 2.0)) ) contentSize.height += keys.size.height - contentSize.height += 17.0 + contentSize.height += 24.0 - //TODO:localize let button = button.update( component: ButtonComponent( background: ButtonComponent.Background( @@ -192,12 +230,13 @@ private final class SheetContent: CombinedComponent { pressedColor: theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9) ), content: AnyComponentWithIdentity( - id: AnyHashable("transfer"), + id: AnyHashable("import"), component: AnyComponent( - MultilineTextComponent(text: .plain(NSAttributedString(string: "Transfer", font: Font.semibold(17.0), textColor: theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center))) + MultilineTextComponent(text: .plain(NSAttributedString(string: strings.WebApp_ImportData_Import, font: Font.semibold(17.0), textColor: theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center))) ) ), isEnabled: state.selectedUuid != nil, + allowActionWhenDisabled: true, displaysProgress: false, action: { [weak state] in guard let state else { @@ -231,15 +270,18 @@ private final class SheetContainerComponent: CombinedComponent { typealias EnvironmentType = ViewControllerComponentContainer.Environment let context: AccountContext + let peer: EnginePeer let existingKeys: [WebAppSecureStorage.ExistingKey] let completion: (String) -> Void init( context: AccountContext, + peer: EnginePeer, existingKeys: [WebAppSecureStorage.ExistingKey], completion: @escaping (String) -> Void ) { self.context = context + self.peer = peer self.existingKeys = existingKeys self.completion = completion } @@ -248,6 +290,9 @@ private final class SheetContainerComponent: CombinedComponent { if lhs.context !== rhs.context { return false } + if lhs.peer != rhs.peer { + return false + } if lhs.existingKeys != rhs.existingKeys { return false } @@ -270,6 +315,7 @@ private final class SheetContainerComponent: CombinedComponent { component: SheetComponent( content: AnyComponent(SheetContent( context: context.component.context, + peer: context.component.peer, existingKeys: context.component.existingKeys, completion: context.component.completion, dismiss: { @@ -340,6 +386,7 @@ private final class SheetContainerComponent: CombinedComponent { final class WebAppSecureStorageTransferScreen: ViewControllerComponentContainer { init( context: AccountContext, + peer: EnginePeer, existingKeys: [WebAppSecureStorage.ExistingKey], completion: @escaping (String?) -> Void ) { @@ -347,6 +394,7 @@ final class WebAppSecureStorageTransferScreen: ViewControllerComponentContainer context: context, component: SheetContainerComponent( context: context, + peer: peer, existingKeys: existingKeys, completion: completion ), @@ -368,3 +416,79 @@ final class WebAppSecureStorageTransferScreen: ViewControllerComponentContainer } } } + +private final class AvatarComponent: Component { + let context: AccountContext + let peer: EnginePeer + let size: CGSize? + + init(context: AccountContext, peer: EnginePeer, size: CGSize? = nil) { + self.context = context + self.peer = peer + self.size = size + } + + static func ==(lhs: AvatarComponent, rhs: AvatarComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.peer != rhs.peer { + return false + } + if lhs.size != rhs.size { + return false + } + return true + } + + final class View: UIView { + private var avatarNode: AvatarNode? + + private var component: AvatarComponent? + private weak var state: EmptyComponentState? + + override init(frame: CGRect) { + super.init(frame: frame) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: AvatarComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.component = component + self.state = state + + let size = component.size ?? availableSize + + let avatarNode: AvatarNode + if let current = self.avatarNode { + avatarNode = current + } else { + avatarNode = AvatarNode(font: avatarPlaceholderFont(size: floor(size.width * 0.5))) + avatarNode.displaysAsynchronously = false + self.avatarNode = avatarNode + self.addSubview(avatarNode.view) + } + avatarNode.frame = CGRect(origin: CGPoint(), size: size) + avatarNode.setPeer( + context: component.context, + theme: component.context.sharedContext.currentPresentationData.with({ $0 }).theme, + peer: component.peer, + synchronousLoad: true, + displayDimensions: size + ) + avatarNode.updateSize(size: size) + + return size + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +}