diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index 619c1b6b99..639cbb16d3 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -1213,7 +1213,6 @@ public protocol SharedAccountContext: AnyObject { func makeStoryStatsController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)?, 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 diff --git a/submodules/AccountContext/Sources/ChatController.swift b/submodules/AccountContext/Sources/ChatController.swift index 80789c546d..f130be618c 100644 --- a/submodules/AccountContext/Sources/ChatController.swift +++ b/submodules/AccountContext/Sources/ChatController.swift @@ -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 = 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 } } diff --git a/submodules/PremiumUI/Resources/back.png b/submodules/PremiumUI/Resources/back.png new file mode 100644 index 0000000000..b097d52b67 Binary files /dev/null and b/submodules/PremiumUI/Resources/back.png differ diff --git a/submodules/PremiumUI/Resources/bottom.png b/submodules/PremiumUI/Resources/bottom.png new file mode 100644 index 0000000000..7c971bb503 Binary files /dev/null and b/submodules/PremiumUI/Resources/bottom.png differ diff --git a/submodules/PremiumUI/Resources/business b/submodules/PremiumUI/Resources/business new file mode 100644 index 0000000000..cb937febef Binary files /dev/null and b/submodules/PremiumUI/Resources/business differ diff --git a/submodules/PremiumUI/Resources/business.scn b/submodules/PremiumUI/Resources/business.scn deleted file mode 100644 index a88303fa79..0000000000 Binary files a/submodules/PremiumUI/Resources/business.scn and /dev/null differ diff --git a/submodules/PremiumUI/Resources/front.png b/submodules/PremiumUI/Resources/front.png new file mode 100644 index 0000000000..d262c7b948 Binary files /dev/null and b/submodules/PremiumUI/Resources/front.png differ diff --git a/submodules/PremiumUI/Resources/gift2 b/submodules/PremiumUI/Resources/gift2 new file mode 100644 index 0000000000..041b93ff30 Binary files /dev/null and b/submodules/PremiumUI/Resources/gift2 differ diff --git a/submodules/PremiumUI/Resources/gift2.scn b/submodules/PremiumUI/Resources/gift2.scn deleted file mode 100644 index ffbaec38e3..0000000000 Binary files a/submodules/PremiumUI/Resources/gift2.scn and /dev/null differ diff --git a/submodules/PremiumUI/Resources/left.png b/submodules/PremiumUI/Resources/left.png new file mode 100644 index 0000000000..0ef61740c8 Binary files /dev/null and b/submodules/PremiumUI/Resources/left.png differ diff --git a/submodules/PremiumUI/Resources/right.png b/submodules/PremiumUI/Resources/right.png new file mode 100644 index 0000000000..54b4f70b1a Binary files /dev/null and b/submodules/PremiumUI/Resources/right.png differ diff --git a/submodules/PremiumUI/Resources/top.png b/submodules/PremiumUI/Resources/top.png new file mode 100644 index 0000000000..d4681c1fb0 Binary files /dev/null and b/submodules/PremiumUI/Resources/top.png differ diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift index c80f535da0..7b947a4e7d 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift @@ -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 { diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift index 52e44d335a..8fe7861387 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift @@ -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 diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift index 8c606a3afa..c7c84b197b 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift @@ -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) { diff --git a/submodules/TelegramUI/Components/ComposeTodoScreen/Sources/ComposeTodoScreen.swift b/submodules/TelegramUI/Components/ComposeTodoScreen/Sources/ComposeTodoScreen.swift index 0a6cc6b3bc..9f467571e0 100644 --- a/submodules/TelegramUI/Components/ComposeTodoScreen/Sources/ComposeTodoScreen.swift +++ b/submodules/TelegramUI/Components/ComposeTodoScreen/Sources/ComposeTodoScreen.swift @@ -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 )))) diff --git a/submodules/TelegramUI/Components/ListComposePollOptionComponent/Sources/ListComposePollOptionComponent.swift b/submodules/TelegramUI/Components/ListComposePollOptionComponent/Sources/ListComposePollOptionComponent.swift index cda17c5ea1..2828c2def0 100644 --- a/submodules/TelegramUI/Components/ListComposePollOptionComponent/Sources/ListComposePollOptionComponent.swift +++ b/submodules/TelegramUI/Components/ListComposePollOptionComponent/Sources/ListComposePollOptionComponent.swift @@ -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 { diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift index 0f88b26a3e..cce46fcecd 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift @@ -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)) } } } diff --git a/submodules/TelegramUI/Components/Premium/PremiumDiamondComponent/BUILD b/submodules/TelegramUI/Components/Premium/PremiumDiamondComponent/BUILD index bb9178dd5f..567ee0e5c9 100644 --- a/submodules/TelegramUI/Components/Premium/PremiumDiamondComponent/BUILD +++ b/submodules/TelegramUI/Components/Premium/PremiumDiamondComponent/BUILD @@ -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 = + """ + CFBundleIdentifier + org.telegram.PremiumDiamondComponent + CFBundleDevelopmentRegion + en + CFBundleName + StoryPeerList + """ +) + +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", diff --git a/submodules/TelegramUI/Components/Premium/PremiumDiamondComponent/MetalResources/diamond.metal b/submodules/TelegramUI/Components/Premium/PremiumDiamondComponent/MetalResources/diamond.metal new file mode 100644 index 0000000000..47e8547dae --- /dev/null +++ b/submodules/TelegramUI/Components/Premium/PremiumDiamondComponent/MetalResources/diamond.metal @@ -0,0 +1,271 @@ +#include +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 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 outputTexture [[texture(0)]], + texturecube 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 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; +} diff --git a/submodules/TelegramUI/Components/Premium/PremiumDiamondComponent/Sources/DiamondLayer.swift b/submodules/TelegramUI/Components/Premium/PremiumDiamondComponent/Sources/DiamondLayer.swift new file mode 100644 index 0000000000..d2f9ef255b --- /dev/null +++ b/submodules/TelegramUI/Components/Premium/PremiumDiamondComponent/Sources/DiamondLayer.swift @@ -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(value: -15.0 * .pi / 180.0) + private var rotationY = AnimatableProperty(value: 0.0) + private var rotationZ = AnimatableProperty(value: 0.0 * .pi / 180.0) + private var time = AnimatableProperty(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(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.size, index: 0) + computeEncoder.setBytes(&iResolution, length: MemoryLayout.size, index: 1) + computeEncoder.setBytes(&cameraRotation, length: MemoryLayout.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(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.size, index: 0) + encoder.setFragmentBytes(&iResolution, length: MemoryLayout.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 +} diff --git a/submodules/TelegramUI/Components/Premium/PremiumDiamondComponent/Sources/PremiumDiamondComponent.swift b/submodules/TelegramUI/Components/Premium/PremiumDiamondComponent/Sources/PremiumDiamondComponent.swift index 0aeb9e849f..3a62fc5987 100644 --- a/submodules/TelegramUI/Components/Premium/PremiumDiamondComponent/Sources/PremiumDiamondComponent.swift +++ b/submodules/TelegramUI/Components/Premium/PremiumDiamondComponent/Sources/PremiumDiamondComponent.swift @@ -45,12 +45,11 @@ public final class PremiumDiamondComponent: Component { public var ready: Signal { 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 } diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift index a693c1cc02..6f2f6d2faf 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift @@ -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 + 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() } } diff --git a/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift b/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift index 8fa75ffef1..b07f313b0c 100644 --- a/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift +++ b/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift @@ -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 } diff --git a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift index ddd6aaddea..e9316c1180 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift @@ -367,7 +367,8 @@ private func extractAssociatedData( chatThemes: [TelegramTheme], deviceContactsNumbers: Set, 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 { diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index ee7e2a1b93..c39b3ac5d2 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -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) }