Various improvements

This commit is contained in:
Ilya Laktyushin 2025-03-31 15:24:55 +04:00
parent 7d8920db82
commit 0186f12972
8 changed files with 325 additions and 58 deletions

View File

@ -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";

View File

@ -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
}

View File

@ -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)
}
}

View File

@ -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
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -53,6 +53,7 @@ swift_library(
"//submodules/DeviceLocationManager",
"//submodules/DeviceAccess",
"//submodules/TelegramUI/Components/Utils/GenerateStickerPlaceholderImage",
"//submodules/AvatarNode",
],
visibility = [
"//visibility:public",

View File

@ -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() {

View File

@ -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<EnvironmentType>(
content: AnyComponent<EnvironmentType>(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<Empty>, 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<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}