mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-07-24 12:10:49 +00:00
Various improvements
This commit is contained in:
parent
f2e96efdb5
commit
cae2388f95
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
BIN
submodules/PremiumUI/Resources/back.png
Normal file
BIN
submodules/PremiumUI/Resources/back.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.8 KiB |
BIN
submodules/PremiumUI/Resources/bottom.png
Normal file
BIN
submodules/PremiumUI/Resources/bottom.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.9 KiB |
BIN
submodules/PremiumUI/Resources/business
Normal file
BIN
submodules/PremiumUI/Resources/business
Normal file
Binary file not shown.
Binary file not shown.
BIN
submodules/PremiumUI/Resources/front.png
Normal file
BIN
submodules/PremiumUI/Resources/front.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.9 KiB |
BIN
submodules/PremiumUI/Resources/gift2
Normal file
BIN
submodules/PremiumUI/Resources/gift2
Normal file
Binary file not shown.
Binary file not shown.
BIN
submodules/PremiumUI/Resources/left.png
Normal file
BIN
submodules/PremiumUI/Resources/left.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.3 KiB |
BIN
submodules/PremiumUI/Resources/right.png
Normal file
BIN
submodules/PremiumUI/Resources/right.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.4 KiB |
BIN
submodules/PremiumUI/Resources/top.png
Normal file
BIN
submodules/PremiumUI/Resources/top.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.2 KiB |
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
))))
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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",
|
||||
|
@ -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;
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user