Various improvements

This commit is contained in:
Ilya Laktyushin 2025-06-25 12:00:37 +02:00
parent f2e96efdb5
commit cae2388f95
26 changed files with 875 additions and 162 deletions

View File

@ -1213,7 +1213,6 @@ public protocol SharedAccountContext: AnyObject {
func makeStoryStatsController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?, peerId: EnginePeer.Id, storyId: Int32, storyItem: EngineStoryItem, fromStory: Bool) -> ViewController
func makeStarsTransactionsScreen(context: AccountContext, starsContext: StarsContext) -> ViewController
func makeTonTransactionsScreen(context: AccountContext, tonContext: StarsContext) -> ViewController
func makeStarsPurchaseScreen(context: AccountContext, starsContext: StarsContext, options: [Any], purpose: StarsPurchasePurpose, completion: @escaping (Int64) -> Void) -> ViewController
func makeStarsTransferScreen(context: AccountContext, starsContext: StarsContext, invoice: TelegramMediaInvoice, source: BotPaymentInvoiceSource, extendedMedia: [TelegramExtendedMedia], inputData: Signal<(StarsContext.State, BotPaymentForm, EnginePeer?, EnginePeer?)?, NoError>, completion: @escaping (Bool) -> Void) -> ViewController
func makeStarsSubscriptionTransferScreen(context: AccountContext, starsContext: StarsContext, invoice: TelegramMediaInvoice, link: String, inputData: Signal<(StarsContext.State, BotPaymentForm, EnginePeer?, EnginePeer?)?, NoError>, navigateToPeer: @escaping (EnginePeer) -> Void) -> ViewController

View File

@ -63,6 +63,7 @@ public final class ChatMessageItemAssociatedData: Equatable {
public let isStandalone: Bool
public let isInline: Bool
public let showSensitiveContent: Bool
public let isSuspiciousPeer: Bool
public init(
automaticDownloadPeerType: MediaAutoDownloadPeerType,
@ -96,7 +97,8 @@ public final class ChatMessageItemAssociatedData: Equatable {
deviceContactsNumbers: Set<String> = Set(),
isStandalone: Bool = false,
isInline: Bool = false,
showSensitiveContent: Bool = false
showSensitiveContent: Bool = false,
isSuspiciousPeer: Bool = false
) {
self.automaticDownloadPeerType = automaticDownloadPeerType
self.automaticDownloadPeerId = automaticDownloadPeerId
@ -130,6 +132,7 @@ public final class ChatMessageItemAssociatedData: Equatable {
self.isStandalone = isStandalone
self.isInline = isInline
self.showSensitiveContent = showSensitiveContent
self.isSuspiciousPeer = isSuspiciousPeer
}
public static func == (lhs: ChatMessageItemAssociatedData, rhs: ChatMessageItemAssociatedData) -> Bool {
@ -223,6 +226,9 @@ public final class ChatMessageItemAssociatedData: Equatable {
if lhs.showSensitiveContent != rhs.showSensitiveContent {
return false
}
if lhs.isSuspiciousPeer != rhs.isSuspiciousPeer {
return false
}
return true
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@ -442,10 +442,10 @@ private func _internal_requestStarsState(account: Account, peerId: EnginePeer.Id
break
}
if let _ = subscriptionId {
flags = 1 << 3
flags |= 1 << 3
}
if ton {
flags = 1 << 4
flags |= 1 << 4
}
signal = account.network.request(Api.functions.payments.getStarsTransactions(flags: flags, subscriptionId: subscriptionId, peer: inputPeer, offset: offset, limit: limit))
} else {

View File

@ -430,7 +430,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
let cryptoAmount = cryptoAmount ?? 0
title = "$ \(formatTonAmountText(cryptoAmount, dateTimeFormat: item.presentationData.dateTimeFormat))"
text = incoming ? "Use TON to unlock content and services on Telegram." : "With TON, \(peerName) will be able to unlock content and services on Telegram."
text = incoming ? "Use TON to submit post suggestions to channels." : "With TON, \(peerName) will be able to submit post suggestions to channels."
case let .prizeStars(count, _, channelId, _, _):
if count <= 1000 {
months = 3

View File

@ -412,6 +412,18 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
}
}
if item.associatedData.isSuspiciousPeer, let entities = messageEntities {
messageEntities = entities.filter { entity in
switch entity.type {
case .Url, .TextUrl, .Mention, .TextMention, .Hashtag, .Email, .BankCard:
return false
default:
return true
}
}
}
var entities: [MessageTextEntity]?
var updatedCachedChatMessageText: CachedChatMessageText?
if let cached = currentCachedChatMessageText, cached.matches(text: rawText, inputEntities: messageEntities) {

View File

@ -921,6 +921,31 @@ final class ComposeTodoScreenComponent: Component {
self.todoItems.removeAll(where: { $0.id == optionId })
self.state?.updated(transition: .spring(duration: 0.4))
} : nil,
paste: { [weak self] data in
guard let self else {
return
}
if case let .text(text) = data {
let lines = text.string.components(separatedBy: "\n")
if !lines.isEmpty {
var i = 0
for line in lines {
if i < self.todoItems.count {
self.todoItems[i].resetText = line
} else {
let todoItem = ComposeTodoScreenComponent.TodoItem(
id: self.nextTodoItemId
)
todoItem.resetText = line
self.todoItems.append(todoItem)
self.nextTodoItemId += 1
}
i += 1
}
self.state?.updated()
}
}
},
tag: todoItem.textFieldTag
))))

View File

@ -87,6 +87,7 @@ public final class ListComposePollOptionComponent: Component {
public let alwaysDisplayInputModeSelector: Bool
public let toggleInputMode: (() -> Void)?
public let deleteAction: (() -> Void)?
public let paste: ((TextFieldComponent.PasteData) -> Void)?
public let tag: AnyObject?
public init(
@ -109,6 +110,7 @@ public final class ListComposePollOptionComponent: Component {
alwaysDisplayInputModeSelector: Bool = false,
toggleInputMode: (() -> Void)?,
deleteAction: (() -> Void)? = nil,
paste: ((TextFieldComponent.PasteData) -> Void)? = nil,
tag: AnyObject? = nil
) {
self.externalState = externalState
@ -130,6 +132,7 @@ public final class ListComposePollOptionComponent: Component {
self.alwaysDisplayInputModeSelector = alwaysDisplayInputModeSelector
self.toggleInputMode = toggleInputMode
self.deleteAction = deleteAction
self.paste = paste
self.tag = tag
}
@ -589,13 +592,20 @@ public final class ListComposePollOptionComponent: Component {
characterLimit: component.characterLimit,
enableInlineAnimations: component.enableInlineAnimations,
emptyLineHandling: component.emptyLineHandling,
externalHandlingForMultilinePaste: true,
formatMenuAvailability: .none,
returnKeyType: .next,
lockedFormatAction: {
},
present: { _ in
},
paste: { _ in
paste: { [weak self] data in
guard let self, let component = self.component else {
return
}
if let paste = component.paste, case .text = data {
paste(data)
}
},
returnKeyAction: { [weak self] in
guard let self, let component = self.component else {

View File

@ -10670,7 +10670,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
}
case .ton:
if let tonContext = self.controller?.tonContext {
push(self.context.sharedContext.makeTonTransactionsScreen(context: self.context, tonContext: tonContext))
push(self.context.sharedContext.makeStarsTransactionsScreen(context: self.context, starsContext: tonContext))
}
}
}

View File

@ -1,4 +1,44 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
load(
"@build_bazel_rules_apple//apple:resources.bzl",
"apple_resource_bundle",
"apple_resource_group",
)
load("//build-system/bazel-utils:plist_fragment.bzl",
"plist_fragment",
)
filegroup(
name = "PremiumDiamondComponentMetalResources",
srcs = glob([
"MetalResources/**/*.*",
]),
visibility = ["//visibility:public"],
)
plist_fragment(
name = "PremiumDiamondComponentBundleInfoPlist",
extension = "plist",
template =
"""
<key>CFBundleIdentifier</key>
<string>org.telegram.PremiumDiamondComponent</string>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleName</key>
<string>StoryPeerList</string>
"""
)
apple_resource_bundle(
name = "PremiumDiamondComponentBundle",
infoplists = [
":PremiumDiamondComponentBundleInfoPlist",
],
resources = [
":PremiumDiamondComponentMetalResources",
],
)
swift_library(
name = "PremiumDiamondComponent",
@ -9,10 +49,14 @@ swift_library(
copts = [
"-warnings-as-errors",
],
data = [
":PremiumDiamondComponentBundle",
],
deps = [
"//submodules/AsyncDisplayKit",
"//submodules/Display",
"//submodules/SSignalKit/SwiftSignalKit",
"//submodules/MetalEngine",
"//submodules/ComponentFlow",
"//submodules/AccountContext",
"//submodules/AppBundle",
@ -20,6 +64,7 @@ swift_library(
"//submodules/LegacyComponents",
"//submodules/Components/MultilineTextComponent:MultilineTextComponent",
"//submodules/TelegramUI/Components/Premium/PremiumStarComponent",
"//submodules/TelegramUI/Components/Utils/AnimatableProperty",
],
visibility = [
"//visibility:public",

View File

@ -0,0 +1,271 @@
#include <metal_stdlib>
using namespace metal;
#define EPS 1e-4
#define EPS2 1e-4
#define NEAR 1.0
#define FAR 10.0
#define NEAR2 0.02
#define ITER 96
#define ITER2 48
#define RI1 2.40
#define RI2 2.44
#define PI 3.14159265359
float3 hsv(float h, float s, float v) {
float3 k = float3(1.0, 2.0 / 3.0, 1.0 / 3.0);
float3 p = abs(fract(h + k.xyz) * 6.0 - 3.0);
return v * mix(float3(1.0), clamp(p - 1.0, 0.0, 1.0), s);
}
float2x2 rot(float a) {
float s = sin(a), c = cos(a);
return float2x2(c, s, -s, c);
}
float sdTable(float3 p) {
float2 d = abs(float2(length(p.xz), (p.y + 0.159) * 1.650)) - float2(1.0);
return min(max(d.x, d.y), 0.0) + length(max(d, 0.0));
}
float sdCut(float3 p, float a, float h) {
p.y *= a;
p.y -= (abs(p.x) + abs(p.z)) * h;
p = abs(p);
return (p.x + p.y + p.z - 1.0) * 0.5;
}
constant float2x2 ROT4 = float2x2(0.70710678, 0.70710678, -0.70710678, 0.70710678);
constant float2x2 ROT3 = float2x2(0.92387953, 0.38268343, -0.38268343, 0.92387953);
constant float2x2 ROT2 = float2x2(0.38268343, 0.92387953, -0.92387953, 0.38268343);
constant float2x2 ROT1 = float2x2(0.19509032, 0.98078528, -0.98078528, 0.19509032);
float map(float3 p, float time, float3 cameraRotation) {
p.y *= 0.72;
p.yz = p.yz;
p.xz = rot(time * 0.45) * p.xz;
float d = sdTable(p);
float3 q = p * 0.3000;
q.y += 0.0808;
q.xz = ROT2 * q.xz;
q.xz = abs(q.xz);
q.xz = ROT4 * q.xz;
q.xz = abs(q.xz);
q.xz = ROT2 * q.xz;
d = max(d, sdCut(q, 3.700, 0.0000));
q = p * 0.691;
q.xz = abs(q.xz);
q.xz = ROT4 * q.xz;
q.xz = abs(q.xz);
q.xz = ROT2 * q.xz;
d = max(d, sdCut(q, 1.868, 0.1744));
q *= 1.022;
q.y -= 0.034;
q.xz = ROT1 * q.xz;
d = max(d, sdCut(q, 1.650, 0.1000));
q.xz = ROT3 * q.xz;
d = max(d, sdCut(q, 1.650, 0.1000));
return d;
}
float3 normal(float3 p, float time, float3 cameraRotation) {
float2 e = float2(EPS, 0);
return normalize(float3(
map(p + e.xyy, time, cameraRotation) - map(p - e.xyy, time, cameraRotation),
map(p + e.yxy, time, cameraRotation) - map(p - e.yxy, time, cameraRotation),
map(p + e.yyx, time, cameraRotation) - map(p - e.yyx, time, cameraRotation)
));
}
float trace(float3 ro, float3 rd, thread float3 &p, thread float3 &n, float time, float3 cameraRotation) {
float t = NEAR, d;
for (int i = 0; i < ITER; i++) {
p = ro + rd * t;
d = map(p, time, cameraRotation);
if (abs(d) < EPS || t > FAR) break;
t += step(d, 1.0) * d * 0.5 + d * 0.5;
}
n = normal(p, time, cameraRotation);
return min(t, FAR);
}
float trace2(float3 ro, float3 rd, thread float3 &p, thread float3 &n, float time, float3 cameraRotation) {
float t = NEAR2, d;
for (int i = 0; i < ITER2; i++) {
p = ro + rd * t;
d = -map(p, time, cameraRotation);
if (abs(d) < EPS2 || d < EPS2) break;
t += d;
}
n = -normal(p, time, cameraRotation);
return t;
}
float schlickFresnel(float ri, float co) {
float r = (1.0 - ri) / (1.0 + ri);
r = r * r;
return r + (1.0 - r) * pow(1.0 - co, 5.0);
}
float3 lightPath(float3 p, float3 rd, float ri, float time, float3 cameraRotation) {
float3 n;
float3 r0 = -rd;
trace2(p, rd, p, n, time, cameraRotation);
rd = reflect(rd, n);
float3 r1 = refract(rd, n, ri);
r1 = length(r1) < EPS ? r0 : r1;
trace2(p, rd, p, n, time, cameraRotation);
rd = reflect(rd, n);
float3 r2 = refract(rd, n, ri);
r2 = length(r2) < EPS ? r1 : r2;
trace2(p, rd, p, n, time, cameraRotation);
float3 r3 = refract(rd, n, ri);
return length(r3) < EPS ? r2 : r3;
}
float3 material(float3 p, float3 rd, float3 n, texturecube<float> cubemap, float time, float3 cameraRotation) {
float3 l0 = reflect(rd, n);
float co = max(0.0, dot(-rd, n));
float f1 = schlickFresnel(RI1, co);
float3 l1 = lightPath(p, refract(rd, n, 1.0 / RI1), RI1, time, cameraRotation);
float f2 = schlickFresnel(RI2, co);
float3 l2 = lightPath(p, refract(rd, n, 1.0 / RI2), RI2, time, cameraRotation);
float a = 0.0;
float3 dc = float3(0.0);
float3 r = cubemap.sample(sampler(mag_filter::linear, min_filter::linear), l0).rgb;
for (int i = 0; i < 10; i++) {
float3 l = normalize(mix(l1, l2, a));
float f = mix(f1, f2, a);
dc += cubemap.sample(sampler(mag_filter::linear, min_filter::linear), l).rgb * hsv(a + 0.9, 1.0, 1.0) * (1.0 - f) + r * f;
a += 0.1;
}
dc *= 0.19;
return dc;
}
kernel void compute_main(texture2d<float, access::write> outputTexture [[texture(0)]],
texturecube<float> cubemap [[texture(1)]],
constant float &iTime [[buffer(0)]],
constant float2 &iResolution [[buffer(1)]],
constant float3 &cameraRotation [[buffer(2)]],
uint2 gid [[thread_position_in_grid]]) {
if (gid.x >= uint(iResolution.x) || gid.y >= uint(iResolution.y)) {
return;
}
float2 fragCoord = float2(gid.x, gid.y);
float2 uv = (fragCoord - 0.5 * iResolution) / iResolution.y;
float3 ro = float3(0.0, 0.0, -4.0);
float3 rd = normalize(float3(uv, 1.1));
float2x2 ry = rot(cameraRotation.y); // Yaw
ro.yz = ry * ro.yz;
rd.yz = ry * rd.yz;
float2x2 rx = rot(cameraRotation.x); // Pitch
ro.xz = rx * ro.xz;
rd.xz = rx * rd.xz;
float2x2 rz = rot(0.0); // cameraRotation.z); // Roll
ro.xy = rz * ro.xy;
rd.xy = rz * rd.xy;
float3 p, n;
float t = trace(ro, rd, p, n, iTime, cameraRotation);
float3 c = float3(0.0);
float w = 0.0;
if (t > 9.0) {
c = float3(1.0, 0.0, 0.0);
//c = cubemap.sample(sampler(mag_filter::linear, min_filter::linear), rd).rgb;
} else {
c = material(p, rd, n, cubemap, iTime, cameraRotation);
w = smoothstep(1.60, 1.61, length(c));
}
outputTexture.write(float4(c, w), gid);
}
#define POST_ITER 36.0
#define RADIUS 0.05
struct QuadVertexOut {
float4 position [[position]];
float2 uv;
};
constant static float2 quadVertices[6] = {
float2(0.0, 0.0),
float2(1.0, 0.0),
float2(0.0, 1.0),
float2(1.0, 0.0),
float2(0.0, 1.0),
float2(1.0, 1.0)
};
vertex QuadVertexOut post_vertex_main(
constant float4 &rect [[ buffer(0) ]],
uint vid [[ vertex_id ]]
) {
float2 quadVertex = quadVertices[vid];
QuadVertexOut out;
out.position = float4(rect.x + quadVertex.x * rect.z, rect.y + quadVertex.y * rect.w, 0.0, 1.0);
out.position.x = -1.0 + out.position.x * 2.0;
out.position.y = -1.0 + out.position.y * 2.0;
out.uv = quadVertex;
return out;
}
fragment float4 post_fragment_main(QuadVertexOut in [[stage_in]],
constant float &iTime [[buffer(0)]],
constant float2 &iResolution [[buffer(1)]],
texture2d<float> inputTexture [[texture(0)]]) {
constexpr sampler textureSampler(mag_filter::linear, min_filter::linear, address::clamp_to_edge);
float2 uv = in.uv;
float2 m = float2(1.0, iResolution.x / iResolution.y);
float4 co = inputTexture.sample(textureSampler, uv);
float4 c = co;
float a = sin(iTime * 0.1) * 6.283;
float v = 0.0;
float b = 1.0 / POST_ITER;
for (int j = 0; j < 6; j++) {
float r = RADIUS / POST_ITER;
float2 d = float2(cos(a), sin(a)) * m;
for (int i = 0; i < int(POST_ITER); i++) {
float4 sample = inputTexture.sample(textureSampler, uv + d * r * RADIUS);
v += sample.w * (1.0 - r);
r += b;
}
a += 1.047;
}
v *= 0.01;
c += float4(v, v, v, 0.0);
c.w = 1.0;
if (co.r == 1.0 && co.g == 0.0 && co.b == 0.0) {
c.w = 0.0;
} else {
c.w = 1.0;
}
return c;
}

View File

@ -0,0 +1,393 @@
import Foundation
import Display
import Metal
import MetalKit
import MetalEngine
import ComponentFlow
import TelegramPresentationData
import AnimatableProperty
import SwiftSignalKit
private var metalLibraryValue: MTLLibrary?
func metalLibrary(device: MTLDevice) -> MTLLibrary? {
if let metalLibraryValue {
return metalLibraryValue
}
let mainBundle = Bundle(for: DiamondLayer.self)
guard let path = mainBundle.path(forResource: "PremiumDiamondComponentBundle", ofType: "bundle") else {
return nil
}
guard let bundle = Bundle(path: path) else {
return nil
}
guard let library = try? device.makeDefaultLibrary(bundle: bundle) else {
return nil
}
metalLibraryValue = library
return library
}
final class DiamondLayer: MetalEngineSubjectLayer, MetalEngineSubject {
var internalData: MetalEngineSubjectInternalData?
private final class RenderState: RenderToLayerState {
let pipelineState: MTLRenderPipelineState
required init?(device: MTLDevice) {
guard let library = metalLibrary(device: device) else {
return nil
}
guard let vertexFunction = library.makeFunction(name: "post_vertex_main"),
let fragmentFunction = library.makeFunction(name: "post_fragment_main") else {
return nil
}
let pipelineDescriptor = MTLRenderPipelineDescriptor()
pipelineDescriptor.vertexFunction = vertexFunction
pipelineDescriptor.fragmentFunction = fragmentFunction
pipelineDescriptor.colorAttachments[0].pixelFormat = .bgra8Unorm
pipelineDescriptor.colorAttachments[0].isBlendingEnabled = true
pipelineDescriptor.colorAttachments[0].rgbBlendOperation = .add
pipelineDescriptor.colorAttachments[0].alphaBlendOperation = .add
pipelineDescriptor.colorAttachments[0].sourceRGBBlendFactor = .sourceAlpha
pipelineDescriptor.colorAttachments[0].sourceAlphaBlendFactor = .sourceAlpha
pipelineDescriptor.colorAttachments[0].destinationRGBBlendFactor = .oneMinusSourceAlpha
pipelineDescriptor.colorAttachments[0].destinationAlphaBlendFactor = .oneMinusSourceAlpha
guard let pipelineState = try? device.makeRenderPipelineState(descriptor: pipelineDescriptor) else {
return nil
}
self.pipelineState = pipelineState
}
}
final class DiamondState: ComputeState {
let computePipelineState: MTLComputePipelineState
let cubemapTexture: MTLTexture?
required init?(device: MTLDevice) {
guard let library = metalLibrary(device: device) else {
return nil
}
guard let functionComputeMain = library.makeFunction(name: "compute_main") else {
return nil
}
guard let computePipelineState = try? device.makeComputePipelineState(function: functionComputeMain) else {
return nil
}
self.computePipelineState = computePipelineState
self.cubemapTexture = loadCubemap(device: device)
}
}
private var offscreenTexture: PooledTexture?
private var rotationX = AnimatableProperty<CGFloat>(value: -15.0 * .pi / 180.0)
private var rotationY = AnimatableProperty<CGFloat>(value: 0.0)
private var rotationZ = AnimatableProperty<CGFloat>(value: 0.0 * .pi / 180.0)
private var time = AnimatableProperty<CGFloat>(value: 0.0)
private var startTime = CFAbsoluteTimeGetCurrent()
private var interactionStartTme: Double?
private var displayLinkSubscription: SharedDisplayLinkDriver.Link?
private var hasActiveAnimations: Bool = false
private var isExploding = false
private var currentRenderSize: CGSize = .zero
override init() {
super.init()
self.isOpaque = false
self.didEnterHierarchy = { [weak self] in
guard let self else {
return
}
self.displayLinkSubscription = SharedDisplayLinkDriver.shared.add { [weak self] _ in
guard let self else {
return
}
self.updateAnimations()
self.setNeedsUpdate()
}
}
self.didExitHierarchy = { [weak self] in
guard let self else {
return
}
self.displayLinkSubscription = nil
}
}
override init(layer: Any) {
super.init(layer: layer)
if let layer = layer as? DiamondLayer {
self.rotationX = layer.rotationX
self.rotationY = layer.rotationY
self.rotationZ = layer.rotationZ
self.time = layer.time
self.startTime = layer.startTime
self.currentRenderSize = layer.currentRenderSize
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc func handlePan(_ gesture: UIPanGestureRecognizer) {
switch gesture.state {
case .began:
self.interactionStartTme = CFAbsoluteTimeGetCurrent()
case .changed:
let translation = gesture.translation(in: gesture.view)
let yawPan = -Float(translation.x) * Float.pi / 180.0
func rubberBandingOffset(offset: CGFloat, bandingStart: CGFloat) -> CGFloat {
let bandedOffset = offset - bandingStart
let range: CGFloat = 75.0
let coefficient: CGFloat = 0.4
return bandingStart + (1.0 - (1.0 / ((bandedOffset * coefficient / range) + 1.0))) * range
}
var pitchTranslation = rubberBandingOffset(offset: abs(translation.y), bandingStart: 0.0)
if translation.y < 0.0 {
pitchTranslation *= -1.0
}
let pitchPan = Float(pitchTranslation) * Float.pi / 180.0
self.rotationX.update(value: CGFloat(yawPan), transition: .immediate)
self.rotationY.update(value: CGFloat(pitchPan), transition: .immediate)
case .ended:
let velocity = gesture.velocity(in: gesture.view)
if let interactionStartTme = self.interactionStartTme {
let delta = CFAbsoluteTimeGetCurrent() - interactionStartTme
self.startTime += delta
self.interactionStartTme = nil
}
//
// var smallAngle = false
// let previousYaw = Float(self.rotationX.presentationValue)
// if (previousYaw < .pi / 2 && previousYaw > -.pi / 2) && abs(velocity.x) < 200 {
// smallAngle = true
// }
playAppearanceAnimation(velocity: velocity.x, smallAngle: true, explode: false) //, smallAngle: smallAngle, explode: !smallAngle && abs(velocity.x) > 600)
default:
break
}
self.setNeedsUpdate()
}
func playAppearanceAnimation(velocity: CGFloat?, smallAngle: Bool, explode: Bool) {
if explode {
self.isExploding = true
self.time.update(value: 8.0, transition: .spring(duration: 2.0))
Queue.mainQueue().after(1.2) {
if self.isExploding {
self.isExploding = false
self.startTime = CFAbsoluteTimeGetCurrent() - 8.0
}
}
} else if smallAngle {
let transition = ComponentTransition.easeInOut(duration: 0.3)
self.rotationX.update(value: 0.0, transition: transition)
self.rotationY.update(value: 0.0, transition: transition)
}
}
private func updateAnimations() {
let properties = [
self.rotationX,
self.rotationY,
self.rotationZ
]
let timestamp = CACurrentMediaTime()
var hasAnimations = false
for property in properties {
if property.tick(timestamp: timestamp) {
hasAnimations = true
}
}
let currentTime = CFAbsoluteTimeGetCurrent()
if self.time.tick(timestamp: timestamp) {
hasAnimations = true
}
self.hasActiveAnimations = hasAnimations
if !self.isExploding && self.interactionStartTme == nil {
let elapsedTime = currentTime - self.startTime
self.time.update(value: CGFloat(elapsedTime), transition: .immediate)
}
}
func update(context: MetalEngineSubjectContext) {
if self.bounds.isEmpty {
return
}
let drawableSize = CGSize(width: self.bounds.width * UIScreen.main.scale, height: self.bounds.height * UIScreen.main.scale)
let offscreenTextureSpec = TextureSpec(width: Int(drawableSize.width), height: Int(drawableSize.height), pixelFormat: .rgba8UnsignedNormalized)
if self.offscreenTexture == nil || self.offscreenTexture?.spec != offscreenTextureSpec {
self.offscreenTexture = MetalEngine.shared.pooledTexture(spec: offscreenTextureSpec)
}
guard let offscreenTexture = self.offscreenTexture?.get(context: context) else {
return
}
let diamondTexture = context.compute(state: DiamondState.self, inputs: offscreenTexture.placeholer, commands: { commandBuffer, computeState, offscreenTexture -> MTLTexture? in
guard let offscreenTexture, let cubemapTexture = computeState.cubemapTexture else {
return nil
}
do {
guard let computeEncoder = commandBuffer.makeComputeCommandEncoder() else {
return nil
}
let threadgroupSize = MTLSize(width: 16, height: 16, depth: 1)
let threadgroupCount = MTLSize(width: (offscreenTextureSpec.width + threadgroupSize.width - 1) / threadgroupSize.width, height: (offscreenTextureSpec.height + threadgroupSize.height - 1) / threadgroupSize.height, depth: 1)
var iTime = Float(self.time.presentationValue)
var iResolution = simd_float2(
Float(drawableSize.width),
Float(drawableSize.height)
)
var cameraRotation = SIMD3<Float>(
Float(180.0 * .pi / 180.0 + self.rotationX.presentationValue),
Float(18.0 * .pi / 180.0 + self.rotationY.presentationValue),
Float(0.0)
)
computeEncoder.setComputePipelineState(computeState.computePipelineState)
computeEncoder.setBytes(&iTime, length: MemoryLayout<Float>.size, index: 0)
computeEncoder.setBytes(&iResolution, length: MemoryLayout<simd_float2>.size, index: 1)
computeEncoder.setBytes(&cameraRotation, length: MemoryLayout<simd_float3>.size, index: 2)
computeEncoder.setTexture(offscreenTexture, index: 0)
computeEncoder.setTexture(cubemapTexture, index: 1)
computeEncoder.dispatchThreadgroups(threadgroupCount, threadsPerThreadgroup: threadgroupSize)
computeEncoder.endEncoding()
}
return offscreenTexture
})
context.renderToLayer(spec: RenderLayerSpec(size: RenderSize(width: Int(drawableSize.width), height: Int(drawableSize.height))), state: RenderState.self, layer: self, inputs: diamondTexture, commands: { encoder, placement, diamondTexture in
guard let diamondTexture else {
return
}
let effectiveRect = placement.effectiveRect
var iTime = Float(self.time.presentationValue)
var rect = SIMD4<Float>(Float(effectiveRect.minX), Float(effectiveRect.minY), Float(effectiveRect.width), Float(effectiveRect.height))
encoder.setVertexBytes(&rect, length: 4 * 4, index: 0)
var iResolution = simd_float2(
Float(drawableSize.width),
Float(drawableSize.height)
)
encoder.setFragmentBytes(&iTime, length: MemoryLayout<Float>.size, index: 0)
encoder.setFragmentBytes(&iResolution, length: MemoryLayout<simd_float2>.size, index: 1)
encoder.setFragmentTexture(diamondTexture, index: 0)
encoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 6)
})
}
}
private func loadCubemap(device: MTLDevice) -> MTLTexture? {
let faceNames = ["right", "left", "top", "bottom", "front", "back"].map { "\($0).png" }
guard let firstImage = UIImage(named: faceNames[0]) else {
return nil
}
let width = Int(firstImage.size.width)
let height = Int(firstImage.size.height)
let textureDescriptor = MTLTextureDescriptor.textureCubeDescriptor(
pixelFormat: .rgba8Unorm,
size: width,
mipmapped: true
)
textureDescriptor.usage = [.shaderRead]
guard let cubemapTexture = device.makeTexture(descriptor: textureDescriptor) else {
return nil
}
for (index, faceName) in faceNames.enumerated() {
guard let image = UIImage(named: faceName),
let cgImage = image.cgImage else {
return nil
}
let colorSpace = CGColorSpaceCreateDeviceRGB()
let bytesPerPixel = 4
let bytesPerRow = width * bytesPerPixel
let bitsPerComponent = 8
var pixelData = [UInt8](repeating: 0, count: width * height * bytesPerPixel)
guard let context = CGContext(
data: &pixelData,
width: width,
height: height,
bitsPerComponent: bitsPerComponent,
bytesPerRow: bytesPerRow,
space: colorSpace,
bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue
) else {
return nil
}
context.draw(cgImage, in: CGRect(x: 0, y: 0, width: width, height: height))
let region = MTLRegion(origin: MTLOrigin(x: 0, y: 0, z: 0), size: MTLSize(width: width, height: height, depth: 1))
cubemapTexture.replace(
region: region,
mipmapLevel: 0,
slice: index,
withBytes: pixelData,
bytesPerRow: bytesPerRow,
bytesPerImage: 0
)
}
if textureDescriptor.mipmapLevelCount > 1 {
let commandQueue = device.makeCommandQueue()
let commandBuffer = commandQueue?.makeCommandBuffer()
let blitEncoder = commandBuffer?.makeBlitCommandEncoder()
blitEncoder?.generateMipmaps(for: cubemapTexture)
blitEncoder?.endEncoding()
commandBuffer?.commit()
commandBuffer?.waitUntilCompleted()
}
return cubemapTexture
}

View File

@ -45,12 +45,11 @@ public final class PremiumDiamondComponent: Component {
public var ready: Signal<Bool, NoError> {
return self._ready.get()
}
weak var animateFrom: UIView?
weak var containerView: UIView?
private let sceneView: SCNView
private let sceneView: SCNView
private let diamondLayer: DiamondLayer
private var timer: SwiftSignalKit.Timer?
private var component: PremiumDiamondComponent?
@ -63,13 +62,17 @@ public final class PremiumDiamondComponent: Component {
self.sceneView.preferredFramesPerSecond = 60
self.sceneView.isJitteringEnabled = true
self.diamondLayer = DiamondLayer()
super.init(frame: frame)
self.addSubview(self.sceneView)
self.layer.addSublayer(self.diamondLayer)
self.setup()
let panGestureRecoginzer = UIPanGestureRecognizer(target: self, action: #selector(self.handlePan(_:)))
let panGestureRecoginzer = UIPanGestureRecognizer(target: self.diamondLayer, action: #selector(self.diamondLayer.handlePan(_:)))
self.addGestureRecognizer(panGestureRecoginzer)
self.disablesInteractiveModalDismiss = true
@ -84,61 +87,6 @@ public final class PremiumDiamondComponent: Component {
self.timer?.invalidate()
}
private var previousYaw: Float = 0.0
@objc private func handlePan(_ gesture: UIPanGestureRecognizer) {
guard let scene = self.sceneView.scene, let node = scene.rootNode.childNode(withName: "star", recursively: false) else {
return
}
let keys = [
"rotate",
"tapRotate",
"continuousRotation"
]
for key in keys {
node.removeAnimation(forKey: key)
}
switch gesture.state {
case .began:
self.previousYaw = 0.0
case .changed:
let translation = gesture.translation(in: gesture.view)
let yawPan = deg2rad(Float(translation.x))
func rubberBandingOffset(offset: CGFloat, bandingStart: CGFloat) -> CGFloat {
let bandedOffset = offset - bandingStart
let range: CGFloat = 60.0
let coefficient: CGFloat = 0.4
return bandingStart + (1.0 - (1.0 / ((bandedOffset * coefficient / range) + 1.0))) * range
}
var pitchTranslation = rubberBandingOffset(offset: abs(translation.y), bandingStart: 0.0)
if translation.y < 0.0 {
pitchTranslation *= -1.0
}
let pitchPan = deg2rad(Float(pitchTranslation))
self.previousYaw = yawPan
// Maintain the initial tilt while adding pan gestures
let initialTiltX: Float = deg2rad(-15.0)
let initialTiltZ: Float = deg2rad(5.0)
node.eulerAngles = SCNVector3(initialTiltX + pitchPan, yawPan, initialTiltZ)
case .ended:
let velocity = gesture.velocity(in: gesture.view)
var smallAngle = false
if (self.previousYaw < .pi / 2 && self.previousYaw > -.pi / 2) && abs(velocity.x) < 200 {
smallAngle = true
}
self.playAppearanceAnimation(velocity: velocity.x, smallAngle: smallAngle, explode: !smallAngle && abs(velocity.x) > 600)
default:
break
}
}
private func setup() {
guard let scene = loadCompressedScene(name: "diamond", version: sceneVersion) else {
return
@ -163,9 +111,23 @@ public final class PremiumDiamondComponent: Component {
}
private func onReady() {
self.setupScaleAnimation()
self.playAppearanceAnimation(mirror: true, explode: true)
}
private func setupScaleAnimation() {
let animation = CABasicAnimation(keyPath: "transform.scale")
animation.duration = 2.0
animation.fromValue = 0.9
animation.toValue = 1.0
animation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
animation.autoreverses = true
animation.repeatCount = .infinity
self.diamondLayer.add(animation, forKey: "scale")
}
private func playAppearanceAnimation(velocity: CGFloat? = nil, smallAngle: Bool = false, mirror: Bool = false, explode: Bool = false) {
guard let scene = self.sceneView.scene else {
return
@ -244,56 +206,19 @@ public final class PremiumDiamondComponent: Component {
rightParticleSystem.pop_add(rightAnimation, forKey: "speedFactor")
}
}
self.diamondLayer.playAppearanceAnimation(velocity:nil, smallAngle: false, explode: true)
}
// var from = node.presentation.eulerAngles
// if abs(from.y - .pi * 2.0) < 0.001 {
// from.y = 0.0
// }
// node.removeAnimation(forKey: "tapRotate")
//
// var toValue: Float = smallAngle ? 0.0 : .pi * 2.0
// if let velocity = velocity, !smallAngle && abs(velocity) > 200 && velocity < 0.0 {
// toValue *= -1
// }
// if mirror {
// toValue *= -1
// }
//
//
// let to = SCNVector3(x: from.x, y: toValue, z: from.z)
// let distance = rad2deg(to.y - from.y)
//
// guard !distance.isZero else {
// Queue.mainQueue().after(0.1) { [weak self] in
// self?.setupContinuousRotation()
// }
// return
// }
//
// let springAnimation = CASpringAnimation(keyPath: "eulerAngles")
// springAnimation.fromValue = NSValue(scnVector3: from)
// springAnimation.toValue = NSValue(scnVector3: to)
// springAnimation.mass = 1.0
// springAnimation.stiffness = 21.0
// springAnimation.damping = 5.8
// springAnimation.duration = springAnimation.settlingDuration * 0.75
// springAnimation.initialVelocity = velocity.flatMap { abs($0 / CGFloat(distance)) } ?? 1.7
// springAnimation.completion = { [weak self] finished in
// if finished {
// self?.setupContinuousRotation()
// }
// }
// node.addAnimation(springAnimation, forKey: "rotate")
}
func update(component: PremiumDiamondComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize {
self.component = component
self.sceneView.bounds = CGRect(origin: .zero, size: CGSize(width: availableSize.width * 2.0, height: availableSize.height * 2.0))
if self.sceneView.superview == self {
self.sceneView.center = CGPoint(x: availableSize.width / 2.0, y: availableSize.height / 2.0)
}
self.sceneView.center = CGPoint(x: availableSize.width / 2.0, y: availableSize.height / 2.0)
self.diamondLayer.bounds = CGRect(origin: .zero, size: CGSize(width: availableSize.height, height: availableSize.height))
self.diamondLayer.position = CGPoint(x: availableSize.width / 2.0, y: availableSize.height / 2.0 - 8.0)
return availableSize
}

View File

@ -24,6 +24,7 @@ import TelegramStringFormatting
import ListItemComponentAdaptor
import ItemListUI
import StarsWithdrawalScreen
import PremiumDiamondComponent
private let initialSubscriptionsDisplayedLimit: Int32 = 3
@ -33,7 +34,7 @@ final class StarsTransactionsScreenComponent: Component {
let context: AccountContext
let starsContext: StarsContext
let starsRevenueStatsContext: StarsRevenueStatsContext
let subscriptionsContext: StarsSubscriptionsContext
let subscriptionsContext: StarsSubscriptionsContext?
let openTransaction: (StarsContext.State.Transaction) -> Void
let openSubscription: (StarsContext.State.Subscription) -> Void
let buy: () -> Void
@ -45,7 +46,7 @@ final class StarsTransactionsScreenComponent: Component {
context: AccountContext,
starsContext: StarsContext,
starsRevenueStatsContext: StarsRevenueStatsContext,
subscriptionsContext: StarsSubscriptionsContext,
subscriptionsContext: StarsSubscriptionsContext?,
openTransaction: @escaping (StarsContext.State.Transaction) -> Void,
openSubscription: @escaping (StarsContext.State.Subscription) -> Void,
buy: @escaping () -> Void,
@ -399,22 +400,24 @@ final class StarsTransactionsScreenComponent: Component {
}
})
self.subscriptionsStateDisposable = (component.subscriptionsContext.state
|> deliverOnMainQueue).start(next: { [weak self] state in
guard let self else {
return
}
let isFirstTime = self.subscriptionsState == nil
if !state.subscriptions.isEmpty {
self.subscriptionsState = state
} else {
self.subscriptionsState = nil
}
if !self.isUpdating {
self.state?.updated(transition: isFirstTime ? .immediate : .spring(duration: 0.4))
}
})
if let subscriptionsContext = component.subscriptionsContext {
self.subscriptionsStateDisposable = (subscriptionsContext.state
|> deliverOnMainQueue).start(next: { [weak self] state in
guard let self else {
return
}
let isFirstTime = self.subscriptionsState == nil
if !state.subscriptions.isEmpty {
self.subscriptionsState = state
} else {
self.subscriptionsState = nil
}
if !self.isUpdating {
self.state?.updated(transition: isFirstTime ? .immediate : .spring(duration: 0.4))
}
})
}
}
var wasLockedAtPanels = false
@ -495,10 +498,12 @@ final class StarsTransactionsScreenComponent: Component {
}
starTransition.setFrame(view: fadeView, frame: fadeFrame)
}
let starSize = self.starView.update(
transition: .immediate,
component: AnyComponent(PremiumStarComponent(
let headerComponent: AnyComponent<Empty>
if component.starsContext.ton {
headerComponent = AnyComponent(PremiumDiamondComponent())
} else {
headerComponent = AnyComponent(PremiumStarComponent(
theme: environment.theme,
isIntro: true,
isVisible: true,
@ -511,7 +516,12 @@ final class StarsTransactionsScreenComponent: Component {
],
particleColor: UIColor(rgb: 0xf9b004),
backgroundColor: environment.theme.list.blocksBackgroundColor
)),
))
}
let starSize = self.starView.update(
transition: .immediate,
component: headerComponent,
environment: {},
containerSize: CGSize(width: min(414.0, availableSize.width), height: 220.0)
)
@ -528,7 +538,7 @@ final class StarsTransactionsScreenComponent: Component {
if component.starsContext.ton {
//TODO:localize
titleString = "TON"
descriptionString = "Use TON to unlock content and services on Telegram"
descriptionString = "Use TON to submit post suggestions to channels on Telegram."
} else {
titleString = environment.strings.Stars_Intro_Title
descriptionString = environment.strings.Stars_Intro_Description
@ -657,8 +667,9 @@ final class StarsTransactionsScreenComponent: Component {
contentHeight += descriptionSize.height
contentHeight += 29.0
let withdrawAvailable = (self.revenueState?.balances.overallRevenue.value ?? 0) > 0
let withdrawAvailable = component.starsContext.ton ? (self.starsState?.balance.value ?? 0) > 0 : (self.revenueState?.balances.overallRevenue.value ?? 0) > 0
//TODO:localize
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: component.context.currentAppConfiguration.with { $0 })
let balanceSize = self.balanceView.update(
transition: .immediate,
@ -674,25 +685,29 @@ final class StarsTransactionsScreenComponent: Component {
count: self.starsState?.balance ?? StarsAmount.zero,
currency: component.starsContext.ton ? .ton : .stars,
rate: nil,
actionTitle: withdrawAvailable ? environment.strings.Stars_Intro_BuyShort : environment.strings.Stars_Intro_Buy,
actionAvailable: !premiumConfiguration.areStarsDisabled && !premiumConfiguration.isPremiumDisabled,
actionTitle: component.starsContext.ton ? "Withdraw via Fragment" : (withdrawAvailable ? environment.strings.Stars_Intro_BuyShort : environment.strings.Stars_Intro_Buy),
actionAvailable: (component.starsContext.ton && withdrawAvailable) || (!premiumConfiguration.areStarsDisabled && !premiumConfiguration.isPremiumDisabled),
actionIsEnabled: true,
actionIcon: PresentationResourcesItemList.itemListRoundTopupIcon(environment.theme),
actionIcon: component.starsContext.ton ? nil : PresentationResourcesItemList.itemListRoundTopupIcon(environment.theme),
action: { [weak self] in
guard let self, let component = self.component else {
return
}
component.buy()
if component.starsContext.ton {
component.withdraw()
} else {
component.buy()
}
},
secondaryActionTitle: withdrawAvailable ? environment.strings.Stars_Intro_Stats : nil,
secondaryActionIcon: withdrawAvailable ? PresentationResourcesItemList.itemListStatsIcon(environment.theme) : nil,
secondaryAction: withdrawAvailable ? { [weak self] in
secondaryActionTitle: withdrawAvailable && !component.starsContext.ton ? environment.strings.Stars_Intro_Stats : nil,
secondaryActionIcon: withdrawAvailable && !component.starsContext.ton ? PresentationResourcesItemList.itemListStatsIcon(environment.theme) : nil,
secondaryAction: withdrawAvailable && !component.starsContext.ton ? { [weak self] in
guard let self, let component = self.component else {
return
}
component.withdraw()
} : nil,
additionalAction: (premiumConfiguration.starsGiftsPurchaseAvailable && !premiumConfiguration.isPremiumDisabled) ? AnyComponent(
additionalAction: (premiumConfiguration.starsGiftsPurchaseAvailable && !premiumConfiguration.isPremiumDisabled && !component.starsContext.ton) ? AnyComponent(
Button(
content: AnyComponent(
HStack([
@ -918,7 +933,7 @@ final class StarsTransactionsScreenComponent: Component {
self.subscriptionsExpanded = true
}
self.state?.updated(transition: .spring(duration: 0.4))
component.subscriptionsContext.loadMore()
component.subscriptionsContext?.loadMore()
},
highlighting: .default,
updateIsHighlighted: { view, _ in
@ -1111,7 +1126,7 @@ public final class StarsTransactionsScreen: ViewControllerComponentContainer {
private let context: AccountContext
private let starsContext: StarsContext
private let starsRevenueStatsContext: StarsRevenueStatsContext
private let subscriptionsContext: StarsSubscriptionsContext
private let subscriptionsContext: StarsSubscriptionsContext?
private let options = Promise<[StarsTopUpOption]>()
@ -1125,7 +1140,11 @@ public final class StarsTransactionsScreen: ViewControllerComponentContainer {
self.starsContext = starsContext
self.starsRevenueStatsContext = context.engine.payments.peerStarsRevenueContext(peerId: context.account.peerId)
self.subscriptionsContext = context.engine.payments.peerStarsSubscriptionsContext(starsContext: starsContext)
if !starsContext.ton {
self.subscriptionsContext = context.engine.payments.peerStarsSubscriptionsContext(starsContext: starsContext)
} else {
self.subscriptionsContext = nil
}
var buyImpl: (() -> Void)?
var withdrawImpl: (() -> Void)?
@ -1196,9 +1215,9 @@ public final class StarsTransactionsScreen: ViewControllerComponentContainer {
}
if !updated {
if subscription.flags.contains(.isCancelled) {
self.subscriptionsContext.updateSubscription(id: subscription.id, cancel: false)
self.subscriptionsContext?.updateSubscription(id: subscription.id, cancel: false)
} else {
self.subscriptionsContext.updateSubscription(id: subscription.id, cancel: true)
self.subscriptionsContext?.updateSubscription(id: subscription.id, cancel: true)
}
}
} else {
@ -1423,7 +1442,7 @@ public final class StarsTransactionsScreen: ViewControllerComponentContainer {
}
self.starsContext.load(force: false)
self.subscriptionsContext.loadMore()
self.subscriptionsContext?.loadMore()
self.scrollToTop = { [weak self] in
guard let self else {
@ -1444,6 +1463,6 @@ public final class StarsTransactionsScreen: ViewControllerComponentContainer {
}
public func update() {
self.subscriptionsContext.loadMore()
self.subscriptionsContext?.loadMore()
}
}

View File

@ -87,7 +87,7 @@ public final class TextFieldComponent: Component {
case images([UIImage])
case video(Data)
case gif(Data)
case text
case text(NSAttributedString)
}
@ -158,6 +158,7 @@ public final class TextFieldComponent: Component {
public let characterLimit: Int?
public let enableInlineAnimations: Bool
public let emptyLineHandling: EmptyLineHandling
public let externalHandlingForMultilinePaste: Bool
public let formatMenuAvailability: FormatMenuAvailability
public let returnKeyType: UIReturnKeyType
public let lockedFormatAction: () -> Void
@ -184,6 +185,7 @@ public final class TextFieldComponent: Component {
characterLimit: Int? = nil,
enableInlineAnimations: Bool = true,
emptyLineHandling: EmptyLineHandling = .allowed,
externalHandlingForMultilinePaste: Bool = false,
formatMenuAvailability: FormatMenuAvailability,
returnKeyType: UIReturnKeyType = .default,
lockedFormatAction: @escaping () -> Void,
@ -471,6 +473,10 @@ public final class TextFieldComponent: Component {
if let attributedString = attributedString {
let current = self.inputState
let range = NSMakeRange(current.selectionRange.lowerBound, current.selectionRange.count)
if component.externalHandlingForMultilinePaste, component.emptyLineHandling == .notAllowed, attributedString.string.contains("\n") {
component.paste(.text(attributedString))
return false
}
if !self.chatInputTextNode(shouldChangeTextIn: range, replacementText: attributedString.string) {
return false
}
@ -487,7 +493,7 @@ public final class TextFieldComponent: Component {
if !self.isUpdating {
self.state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.4, curve: .spring)).withUserData(AnimationHint(view: self, kind: .textChanged)))
}
component.paste(.text)
component.paste(.text(attributedString))
return false
}
@ -537,7 +543,7 @@ public final class TextFieldComponent: Component {
}
}
component.paste(.text)
component.paste(.text(NSAttributedString()))
return true
}

View File

@ -367,7 +367,8 @@ private func extractAssociatedData(
chatThemes: [TelegramTheme],
deviceContactsNumbers: Set<String>,
isInline: Bool,
showSensitiveContent: Bool
showSensitiveContent: Bool,
isSuspiciousPeer: Bool
) -> ChatMessageItemAssociatedData {
var automaticDownloadPeerId: EnginePeer.Id?
var automaticMediaDownloadPeerType: MediaAutoDownloadPeerType = .channel
@ -422,7 +423,7 @@ private func extractAssociatedData(
automaticDownloadPeerId = message.peerId
}
return ChatMessageItemAssociatedData(automaticDownloadPeerType: automaticMediaDownloadPeerType, automaticDownloadPeerId: automaticDownloadPeerId, automaticDownloadNetworkType: automaticDownloadNetworkType, preferredStoryHighQuality: preferredStoryHighQuality, isRecentActions: false, subject: subject, contactsPeerIds: contactsPeerIds, channelDiscussionGroup: channelDiscussionGroup, animatedEmojiStickers: animatedEmojiStickers, additionalAnimatedEmojiStickers: additionalAnimatedEmojiStickers, currentlyPlayingMessageId: currentlyPlayingMessageId, isCopyProtectionEnabled: isCopyProtectionEnabled, availableReactions: availableReactions, availableMessageEffects: availableMessageEffects, savedMessageTags: savedMessageTags, defaultReaction: defaultReaction, isPremium: isPremium, accountPeer: accountPeer, alwaysDisplayTranscribeButton: alwaysDisplayTranscribeButton, topicAuthorId: topicAuthorId, hasBots: hasBots, translateToLanguage: translateToLanguage, maxReadStoryId: maxReadStoryId, recommendedChannels: recommendedChannels, audioTranscriptionTrial: audioTranscriptionTrial, chatThemes: chatThemes, deviceContactsNumbers: deviceContactsNumbers, isInline: isInline, showSensitiveContent: showSensitiveContent)
return ChatMessageItemAssociatedData(automaticDownloadPeerType: automaticMediaDownloadPeerType, automaticDownloadPeerId: automaticDownloadPeerId, automaticDownloadNetworkType: automaticDownloadNetworkType, preferredStoryHighQuality: preferredStoryHighQuality, isRecentActions: false, subject: subject, contactsPeerIds: contactsPeerIds, channelDiscussionGroup: channelDiscussionGroup, animatedEmojiStickers: animatedEmojiStickers, additionalAnimatedEmojiStickers: additionalAnimatedEmojiStickers, currentlyPlayingMessageId: currentlyPlayingMessageId, isCopyProtectionEnabled: isCopyProtectionEnabled, availableReactions: availableReactions, availableMessageEffects: availableMessageEffects, savedMessageTags: savedMessageTags, defaultReaction: defaultReaction, isPremium: isPremium, accountPeer: accountPeer, alwaysDisplayTranscribeButton: alwaysDisplayTranscribeButton, topicAuthorId: topicAuthorId, hasBots: hasBots, translateToLanguage: translateToLanguage, maxReadStoryId: maxReadStoryId, recommendedChannels: recommendedChannels, audioTranscriptionTrial: audioTranscriptionTrial, chatThemes: chatThemes, deviceContactsNumbers: deviceContactsNumbers, isInline: isInline, showSensitiveContent: showSensitiveContent, isSuspiciousPeer: isSuspiciousPeer)
}
private extension ChatHistoryLocationInput {
@ -1986,7 +1987,12 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto
translateToLanguage = (normalizeTranslationLanguage(translationState.fromLang), normalizeTranslationLanguage(languageCode))
}
let associatedData = extractAssociatedData(chatLocation: chatLocation, view: view, automaticDownloadNetworkType: networkType, preferredStoryHighQuality: preferredStoryHighQuality, animatedEmojiStickers: animatedEmojiStickers, additionalAnimatedEmojiStickers: additionalAnimatedEmojiStickers, subject: subject, currentlyPlayingMessageId: currentlyPlayingMessageIdAndType?.0, isCopyProtectionEnabled: isCopyProtectionEnabled, availableReactions: availableReactions, availableMessageEffects: availableMessageEffects, savedMessageTags: savedMessageTags, defaultReaction: defaultReaction, isPremium: isPremium, alwaysDisplayTranscribeButton: alwaysDisplayTranscribeButton, accountPeer: accountPeer, topicAuthorId: topicAuthorId, hasBots: chatHasBots, translateToLanguage: translateToLanguage?.toLang, maxReadStoryId: maxReadStoryId, recommendedChannels: recommendedChannels, audioTranscriptionTrial: audioTranscriptionTrial, chatThemes: chatThemes, deviceContactsNumbers: deviceContactsNumbers, isInline: !rotated, showSensitiveContent: contentSettings.ignoreContentRestrictionReasons.contains("sensitive"))
var isSuspiciousPeer = false
if let cachedUserData = data.cachedData as? CachedUserData, let peerStatusSettings = cachedUserData.peerStatusSettings, peerStatusSettings.flags.contains(.canBlock) {
isSuspiciousPeer = true
}
let associatedData = extractAssociatedData(chatLocation: chatLocation, view: view, automaticDownloadNetworkType: networkType, preferredStoryHighQuality: preferredStoryHighQuality, animatedEmojiStickers: animatedEmojiStickers, additionalAnimatedEmojiStickers: additionalAnimatedEmojiStickers, subject: subject, currentlyPlayingMessageId: currentlyPlayingMessageIdAndType?.0, isCopyProtectionEnabled: isCopyProtectionEnabled, availableReactions: availableReactions, availableMessageEffects: availableMessageEffects, savedMessageTags: savedMessageTags, defaultReaction: defaultReaction, isPremium: isPremium, alwaysDisplayTranscribeButton: alwaysDisplayTranscribeButton, accountPeer: accountPeer, topicAuthorId: topicAuthorId, hasBots: chatHasBots, translateToLanguage: translateToLanguage?.toLang, maxReadStoryId: maxReadStoryId, recommendedChannels: recommendedChannels, audioTranscriptionTrial: audioTranscriptionTrial, chatThemes: chatThemes, deviceContactsNumbers: deviceContactsNumbers, isInline: !rotated, showSensitiveContent: contentSettings.ignoreContentRestrictionReasons.contains("sensitive"), isSuspiciousPeer: isSuspiciousPeer)
var includeEmbeddedSavedChatInfo = false
if case let .replyThread(message) = chatLocation, message.peerId == context.account.peerId, !rotated {

View File

@ -3686,11 +3686,7 @@ public final class SharedAccountContextImpl: SharedAccountContext {
public func makeStarsTransactionsScreen(context: AccountContext, starsContext: StarsContext) -> ViewController {
return StarsTransactionsScreen(context: context, starsContext: starsContext)
}
public func makeTonTransactionsScreen(context: AccountContext, tonContext: StarsContext) -> ViewController {
return TonTransactionsScreen(context: context, tonContext: tonContext)
}
public func makeStarsPurchaseScreen(context: AccountContext, starsContext: StarsContext, options: [Any], purpose: StarsPurchasePurpose, completion: @escaping (Int64) -> Void) -> ViewController {
return StarsPurchaseScreen(context: context, starsContext: starsContext, options: options, purpose: purpose, completion: completion)
}