From 4c626fdd13e7e4238cfd0da8a49df87de5a2bdb7 Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Mon, 17 Mar 2025 21:56:57 +0100 Subject: [PATCH] [Temp] --- .../AvatarNode/Sources/AvatarNode.swift | 1 + .../Sources/DebugController.swift | 22 +- .../SyncCore/SyncCore_CachedUserData.swift | 6 +- .../SyncCore_CloudFileMediaResource.swift | 16 +- .../SyncCore/SyncCore_InstantPage.swift | 60 +- .../SyncCore_MediaReferenceFlatBuffers.swift | 8 +- .../SyncCore/SyncCore_PeerAccessHash.swift | 6 +- .../SyncCore/SyncCore_PeerReference.swift | 8 +- .../Sources/SyncCore/SyncCore_RichText.swift | 30 +- .../SyncCore/SyncCore_TelegramChannel.swift | 8 +- .../SyncCore/SyncCore_TelegramGroup.swift | 8 +- .../SyncCore/SyncCore_TelegramMediaFile.swift | 22 +- .../SyncCore_TelegramMediaImage.swift | 6 +- .../Sources/Utils/FlatBuffersUtils.swift | 30 +- .../Utils/TelegramMedia_FlatBuffers.swift | 6 +- .../Components/JoinSubjectScreen/BUILD | 22 + .../Sources/JoinSubjectScreen.swift | 2179 +++++++++++++++++ .../Sources/SharedAccountContext.swift | 2 + .../Sources/ExperimentalUISettings.swift | 10 +- 19 files changed, 2343 insertions(+), 107 deletions(-) create mode 100644 submodules/TelegramUI/Components/JoinSubjectScreen/BUILD create mode 100644 submodules/TelegramUI/Components/JoinSubjectScreen/Sources/JoinSubjectScreen.swift diff --git a/submodules/AvatarNode/Sources/AvatarNode.swift b/submodules/AvatarNode/Sources/AvatarNode.swift index a82f3f78f9..43c13079ce 100644 --- a/submodules/AvatarNode/Sources/AvatarNode.swift +++ b/submodules/AvatarNode/Sources/AvatarNode.swift @@ -686,6 +686,7 @@ public final class AvatarNode: ASDisplayNode { self.imageNode.contents = image.cgImage } if let loadSignal = result.loadSignal { + self.loadDisposable?.dispose() self.loadDisposable = (loadSignal |> deliverOnMainQueue).start(next: { [weak self] image in guard let self else { return diff --git a/submodules/DebugSettingsUI/Sources/DebugController.swift b/submodules/DebugSettingsUI/Sources/DebugController.swift index 56398e6f7c..aab0a2f6ef 100644 --- a/submodules/DebugSettingsUI/Sources/DebugController.swift +++ b/submodules/DebugSettingsUI/Sources/DebugController.swift @@ -101,6 +101,7 @@ private enum DebugControllerEntry: ItemListNodeEntry { case storiesExperiment(Bool) case storiesJpegExperiment(Bool) case conferenceDebug(Bool) + case checkSerializedData(Bool) case enableQuickReactionSwitch(Bool) case disableReloginTokens(Bool) case liveStreamV2(Bool) @@ -133,7 +134,7 @@ private enum DebugControllerEntry: ItemListNodeEntry { return DebugControllerSection.web.rawValue case .keepChatNavigationStack, .skipReadHistory, .dustEffect, .crashOnSlowQueries, .crashOnMemoryPressure: return DebugControllerSection.experiments.rawValue - case .clearTips, .resetNotifications, .crash, .fillLocalSavedMessageCache, .resetDatabase, .resetDatabaseAndCache, .resetHoles, .resetTagHoles, .reindexUnread, .resetCacheIndex, .reindexCache, .resetBiometricsData, .optimizeDatabase, .photoPreview, .knockoutWallpaper, .storiesExperiment, .storiesJpegExperiment, .conferenceDebug, .enableQuickReactionSwitch, .experimentalCompatibility, .enableDebugDataDisplay, .rippleEffect, .browserExperiment, .localTranscription, .enableReactionOverrides, .restorePurchases, .disableReloginTokens, .liveStreamV2, .experimentalCallMute, .playerV2, .devRequests, .fakeAds, .enableLocalTranslation: + case .clearTips, .resetNotifications, .crash, .fillLocalSavedMessageCache, .resetDatabase, .resetDatabaseAndCache, .resetHoles, .resetTagHoles, .reindexUnread, .resetCacheIndex, .reindexCache, .resetBiometricsData, .optimizeDatabase, .photoPreview, .knockoutWallpaper, .storiesExperiment, .storiesJpegExperiment, .conferenceDebug, .checkSerializedData, .enableQuickReactionSwitch, .experimentalCompatibility, .enableDebugDataDisplay, .rippleEffect, .browserExperiment, .localTranscription, .enableReactionOverrides, .restorePurchases, .disableReloginTokens, .liveStreamV2, .experimentalCallMute, .playerV2, .devRequests, .fakeAds, .enableLocalTranslation: return DebugControllerSection.experiments.rawValue case .logTranslationRecognition, .resetTranslationStates: return DebugControllerSection.translation.rawValue @@ -244,12 +245,14 @@ private enum DebugControllerEntry: ItemListNodeEntry { return 48 case .conferenceDebug: return 49 - case .enableQuickReactionSwitch: + case .checkSerializedData: return 50 - case .liveStreamV2: + case .enableQuickReactionSwitch: return 51 - case .experimentalCallMute: + case .liveStreamV2: return 52 + case .experimentalCallMute: + return 53 case .playerV2: return 54 case .devRequests: @@ -1318,6 +1321,16 @@ private enum DebugControllerEntry: ItemListNodeEntry { }) }).start() }) + case let .checkSerializedData(value): + return ItemListSwitchItem(presentationData: presentationData, title: "Check Serialized Data", value: value, sectionId: self.section, style: .blocks, updated: { value in + let _ = arguments.sharedContext.accountManager.transaction ({ transaction in + transaction.updateSharedData(ApplicationSpecificSharedDataKeys.experimentalUISettings, { settings in + var settings = settings?.get(ExperimentalUISettings.self) ?? ExperimentalUISettings.defaultSettings + settings.checkSerializedData = value + return PreferencesEntry(settings) + }) + }).start() + }) case let .enableQuickReactionSwitch(value): return ItemListSwitchItem(presentationData: presentationData, title: "Enable Quick Reaction", value: value, sectionId: self.section, style: .blocks, updated: { value in let _ = arguments.sharedContext.accountManager.transaction ({ transaction in @@ -1541,6 +1554,7 @@ private func debugControllerEntries(sharedContext: SharedAccountContext, present entries.append(.disableReloginTokens(experimentalSettings.disableReloginTokens)) } entries.append(.conferenceDebug(experimentalSettings.conferenceDebug)) + entries.append(.checkSerializedData(experimentalSettings.checkSerializedData)) entries.append(.enableQuickReactionSwitch(!experimentalSettings.disableQuickReaction)) entries.append(.liveStreamV2(experimentalSettings.liveStreamV2)) entries.append(.experimentalCallMute(experimentalSettings.experimentalCallMute)) diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedUserData.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedUserData.swift index 669dabef07..7a7c1218ea 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedUserData.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_CachedUserData.swift @@ -337,13 +337,13 @@ public struct PeerEmojiStatus: Equatable, Codable { switch flatBuffersObject.valueType { case .peeremojistatuscontentemoji: guard let emoji = flatBuffersObject.value(type: TelegramCore_PeerEmojiStatusContentEmoji.self) else { - throw FlatBuffersError.missingRequiredField(file: #file, line: #line) + throw FlatBuffersError.missingRequiredField() } self = .emoji(fileId: emoji.fileId) case .peeremojistatuscontentstargift: guard let starGift = flatBuffersObject.value(type: TelegramCore_PeerEmojiStatusContentStarGift.self) else { - throw FlatBuffersError.missingRequiredField(file: #file, line: #line) + throw FlatBuffersError.missingRequiredField() } self = .starGift( id: starGift.id, @@ -358,7 +358,7 @@ public struct PeerEmojiStatus: Equatable, Codable { ) case .none_: - throw FlatBuffersError.missingRequiredField(file: #file, line: #line) + throw FlatBuffersError.missingRequiredField() } } diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_CloudFileMediaResource.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_CloudFileMediaResource.swift index 8119febf8f..b3568d693e 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_CloudFileMediaResource.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_CloudFileMediaResource.swift @@ -970,7 +970,7 @@ public func TelegramMediaResource_parse(flatBuffersObject: TelegramCore_Telegram switch flatBuffersObject.valueType { case .telegrammediaresourceCloudfilemediaresource: guard let value = flatBuffersObject.value(type: TelegramCore_TelegramMediaResource_CloudFileMediaResource.self) else { - throw FlatBuffersError.missingRequiredField(file: #file, line: #line) + throw FlatBuffersError.missingRequiredField() } return CloudFileMediaResource( datacenterId: Int(value.datacenterId), @@ -982,7 +982,7 @@ public func TelegramMediaResource_parse(flatBuffersObject: TelegramCore_Telegram ) case .telegrammediaresourceClouddocumentsizemediaresource: guard let value = flatBuffersObject.value(type: TelegramCore_TelegramMediaResource_CloudDocumentSizeMediaResource.self) else { - throw FlatBuffersError.missingRequiredField(file: #file, line: #line) + throw FlatBuffersError.missingRequiredField() } return CloudDocumentSizeMediaResource( datacenterId: value.datacenterId, @@ -993,7 +993,7 @@ public func TelegramMediaResource_parse(flatBuffersObject: TelegramCore_Telegram ) case .telegrammediaresourceCloudphotosizemediaresource: guard let value = flatBuffersObject.value(type: TelegramCore_TelegramMediaResource_CloudPhotoSizeMediaResource.self) else { - throw FlatBuffersError.missingRequiredField(file: #file, line: #line) + throw FlatBuffersError.missingRequiredField() } return CloudPhotoSizeMediaResource( datacenterId: value.datacenterId, @@ -1005,7 +1005,7 @@ public func TelegramMediaResource_parse(flatBuffersObject: TelegramCore_Telegram ) case .telegrammediaresourceCloudpeerphotosizemediaresource: guard let value = flatBuffersObject.value(type: TelegramCore_TelegramMediaResource_CloudPeerPhotoSizeMediaResource.self) else { - throw FlatBuffersError.missingRequiredField(file: #file, line: #line) + throw FlatBuffersError.missingRequiredField() } let sizeSpec: CloudPeerPhotoSizeSpec switch value.sizeSpec { @@ -1023,7 +1023,7 @@ public func TelegramMediaResource_parse(flatBuffersObject: TelegramCore_Telegram ) case .telegrammediaresourceCloudstickerpackthumbnailmediaresource: guard let value = flatBuffersObject.value(type: TelegramCore_TelegramMediaResource_CloudStickerPackThumbnailMediaResource.self) else { - throw FlatBuffersError.missingRequiredField(file: #file, line: #line) + throw FlatBuffersError.missingRequiredField() } return CloudStickerPackThumbnailMediaResource( datacenterId: value.datacenterId, @@ -1033,7 +1033,7 @@ public func TelegramMediaResource_parse(flatBuffersObject: TelegramCore_Telegram ) case .telegrammediaresourceClouddocumentmediaresource: guard let value = flatBuffersObject.value(type: TelegramCore_TelegramMediaResource_CloudDocumentMediaResource.self) else { - throw FlatBuffersError.missingRequiredField(file: #file, line: #line) + throw FlatBuffersError.missingRequiredField() } return CloudDocumentMediaResource( datacenterId: Int(value.datacenterId), @@ -1045,7 +1045,7 @@ public func TelegramMediaResource_parse(flatBuffersObject: TelegramCore_Telegram ) case .telegrammediaresourceLocalfilemediaresource: guard let value = flatBuffersObject.value(type: TelegramCore_TelegramMediaResource_LocalFileMediaResource.self) else { - throw FlatBuffersError.missingRequiredField(file: #file, line: #line) + throw FlatBuffersError.missingRequiredField() } return LocalFileMediaResource( fileId: value.fileId, @@ -1053,7 +1053,7 @@ public func TelegramMediaResource_parse(flatBuffersObject: TelegramCore_Telegram isSecretRelated: value.isSecretRelated ) case .none_: - throw FlatBuffersError.missingRequiredField(file: #file, line: #line) + throw FlatBuffersError.missingRequiredField() } } diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_InstantPage.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_InstantPage.swift index 24288a45ba..e79671bbda 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_InstantPage.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_InstantPage.swift @@ -522,139 +522,139 @@ public indirect enum InstantPageBlock: PostboxCoding, Equatable { self = .unsupported case .instantpageblockTitle: guard let value = flatBuffersObject.value(type: TelegramCore_InstantPageBlock_Title.self) else { - throw FlatBuffersError.missingRequiredField(file: #file, line: #line) + throw FlatBuffersError.missingRequiredField() } self = .title(try RichText(flatBuffersObject: value.text)) case .instantpageblockSubtitle: guard let value = flatBuffersObject.value(type: TelegramCore_InstantPageBlock_Subtitle.self) else { - throw FlatBuffersError.missingRequiredField(file: #file, line: #line) + throw FlatBuffersError.missingRequiredField() } self = .subtitle(try RichText(flatBuffersObject: value.text)) case .instantpageblockAuthordate: guard let value = flatBuffersObject.value(type: TelegramCore_InstantPageBlock_AuthorDate.self) else { - throw FlatBuffersError.missingRequiredField(file: #file, line: #line) + throw FlatBuffersError.missingRequiredField() } self = .authorDate(author: try RichText(flatBuffersObject: value.author), date: value.date) case .instantpageblockHeader: guard let value = flatBuffersObject.value(type: TelegramCore_InstantPageBlock_Header.self) else { - throw FlatBuffersError.missingRequiredField(file: #file, line: #line) + throw FlatBuffersError.missingRequiredField() } self = .header(try RichText(flatBuffersObject: value.text)) case .instantpageblockSubheader: guard let value = flatBuffersObject.value(type: TelegramCore_InstantPageBlock_Subheader.self) else { - throw FlatBuffersError.missingRequiredField(file: #file, line: #line) + throw FlatBuffersError.missingRequiredField() } self = .subheader(try RichText(flatBuffersObject: value.text)) case .instantpageblockParagraph: guard let value = flatBuffersObject.value(type: TelegramCore_InstantPageBlock_Paragraph.self) else { - throw FlatBuffersError.missingRequiredField(file: #file, line: #line) + throw FlatBuffersError.missingRequiredField() } self = .paragraph(try RichText(flatBuffersObject: value.text)) case .instantpageblockPreformatted: guard let value = flatBuffersObject.value(type: TelegramCore_InstantPageBlock_Preformatted.self) else { - throw FlatBuffersError.missingRequiredField(file: #file, line: #line) + throw FlatBuffersError.missingRequiredField() } self = .preformatted(try RichText(flatBuffersObject: value.text)) case .instantpageblockFooter: guard let value = flatBuffersObject.value(type: TelegramCore_InstantPageBlock_Footer.self) else { - throw FlatBuffersError.missingRequiredField(file: #file, line: #line) + throw FlatBuffersError.missingRequiredField() } self = .footer(try RichText(flatBuffersObject: value.text)) case .instantpageblockDivider: self = .divider case .instantpageblockAnchor: guard let value = flatBuffersObject.value(type: TelegramCore_InstantPageBlock_Anchor.self) else { - throw FlatBuffersError.missingRequiredField(file: #file, line: #line) + throw FlatBuffersError.missingRequiredField() } self = .anchor(value.name) case .instantpageblockList: guard let value = flatBuffersObject.value(type: TelegramCore_InstantPageBlock_List.self) else { - throw FlatBuffersError.missingRequiredField(file: #file, line: #line) + throw FlatBuffersError.missingRequiredField() } self = .list(items: try (0 ..< value.itemsCount).map { try InstantPageListItem(flatBuffersObject: value.items(at: $0)!) }, ordered: value.ordered) case .instantpageblockBlockquote: guard let value = flatBuffersObject.value(type: TelegramCore_InstantPageBlock_BlockQuote.self) else { - throw FlatBuffersError.missingRequiredField(file: #file, line: #line) + throw FlatBuffersError.missingRequiredField() } self = .blockQuote(text: try RichText(flatBuffersObject: value.text), caption: try RichText(flatBuffersObject: value.caption)) case .instantpageblockPullquote: guard let value = flatBuffersObject.value(type: TelegramCore_InstantPageBlock_PullQuote.self) else { - throw FlatBuffersError.missingRequiredField(file: #file, line: #line) + throw FlatBuffersError.missingRequiredField() } self = .pullQuote(text: try RichText(flatBuffersObject: value.text), caption: try RichText(flatBuffersObject: value.caption)) case .instantpageblockImage: guard let value = flatBuffersObject.value(type: TelegramCore_InstantPageBlock_Image.self) else { - throw FlatBuffersError.missingRequiredField(file: #file, line: #line) + throw FlatBuffersError.missingRequiredField() } self = .image(id: MediaId(value.id), caption: try InstantPageCaption(flatBuffersObject: value.caption), url: value.url, webpageId: value.webpageId.flatMap(MediaId.init)) case .instantpageblockVideo: guard let value = flatBuffersObject.value(type: TelegramCore_InstantPageBlock_Video.self) else { - throw FlatBuffersError.missingRequiredField(file: #file, line: #line) + throw FlatBuffersError.missingRequiredField() } self = .video(id: MediaId(value.id), caption: try InstantPageCaption(flatBuffersObject: value.caption), autoplay: value.autoplay, loop: value.loop) case .instantpageblockAudio: guard let value = flatBuffersObject.value(type: TelegramCore_InstantPageBlock_Audio.self) else { - throw FlatBuffersError.missingRequiredField(file: #file, line: #line) + throw FlatBuffersError.missingRequiredField() } self = .audio(id: MediaId(value.id), caption: try InstantPageCaption(flatBuffersObject: value.caption)) case .instantpageblockCover: guard let value = flatBuffersObject.value(type: TelegramCore_InstantPageBlock_Cover.self) else { - throw FlatBuffersError.missingRequiredField(file: #file, line: #line) + throw FlatBuffersError.missingRequiredField() } self = .cover(try InstantPageBlock(flatBuffersObject: value.block)) case .instantpageblockWebembed: guard let value = flatBuffersObject.value(type: TelegramCore_InstantPageBlock_WebEmbed.self) else { - throw FlatBuffersError.missingRequiredField(file: #file, line: #line) + throw FlatBuffersError.missingRequiredField() } self = .webEmbed(url: value.url, html: value.html, dimensions: value.dimensions.flatMap(PixelDimensions.init), caption: try InstantPageCaption(flatBuffersObject: value.caption), stretchToWidth: value.stretchToWidth, allowScrolling: value.allowScrolling, coverId: value.coverId.flatMap(MediaId.init)) case .instantpageblockPostembed: guard let value = flatBuffersObject.value(type: TelegramCore_InstantPageBlock_PostEmbed.self) else { - throw FlatBuffersError.missingRequiredField(file: #file, line: #line) + throw FlatBuffersError.missingRequiredField() } self = .postEmbed(url: value.url, webpageId: value.webpageId.flatMap(MediaId.init), avatarId: value.avatarId.flatMap(MediaId.init), author: value.author, date: value.date, blocks: try (0 ..< value.blocksCount).map { try InstantPageBlock(flatBuffersObject: value.blocks(at: $0)!) }, caption: try InstantPageCaption(flatBuffersObject: value.caption)) case .instantpageblockCollage: guard let value = flatBuffersObject.value(type: TelegramCore_InstantPageBlock_Collage.self) else { - throw FlatBuffersError.missingRequiredField(file: #file, line: #line) + throw FlatBuffersError.missingRequiredField() } self = .collage(items: try (0 ..< value.itemsCount).map { try InstantPageBlock(flatBuffersObject: value.items(at: $0)!) }, caption: try InstantPageCaption(flatBuffersObject: value.caption)) case .instantpageblockSlideshow: guard let value = flatBuffersObject.value(type: TelegramCore_InstantPageBlock_Slideshow.self) else { - throw FlatBuffersError.missingRequiredField(file: #file, line: #line) + throw FlatBuffersError.missingRequiredField() } self = .slideshow(items: try (0 ..< value.itemsCount).map { try InstantPageBlock(flatBuffersObject: value.items(at: $0)!) }, caption: try InstantPageCaption(flatBuffersObject: value.caption)) case .instantpageblockChannelbanner: guard let value = flatBuffersObject.value(type: TelegramCore_InstantPageBlock_ChannelBanner.self) else { - throw FlatBuffersError.missingRequiredField(file: #file, line: #line) + throw FlatBuffersError.missingRequiredField() } let channel = try value.channel.flatMap { try TelegramChannel(flatBuffersObject: $0) } self = .channelBanner(channel) case .instantpageblockKicker: guard let value = flatBuffersObject.value(type: TelegramCore_InstantPageBlock_Kicker.self) else { - throw FlatBuffersError.missingRequiredField(file: #file, line: #line) + throw FlatBuffersError.missingRequiredField() } self = .kicker(try RichText(flatBuffersObject: value.text)) case .instantpageblockTable: guard let value = flatBuffersObject.value(type: TelegramCore_InstantPageBlock_Table.self) else { - throw FlatBuffersError.missingRequiredField(file: #file, line: #line) + throw FlatBuffersError.missingRequiredField() } self = .table(title: try RichText(flatBuffersObject: value.title), rows: try (0 ..< value.rowsCount).map { try InstantPageTableRow(flatBuffersObject: value.rows(at: $0)!) }, bordered: value.bordered, striped: value.striped) case .instantpageblockDetails: guard let value = flatBuffersObject.value(type: TelegramCore_InstantPageBlock_Details.self) else { - throw FlatBuffersError.missingRequiredField(file: #file, line: #line) + throw FlatBuffersError.missingRequiredField() } self = .details(title: try RichText(flatBuffersObject: value.title), blocks: try (0 ..< value.blocksCount).map { try InstantPageBlock(flatBuffersObject: value.blocks(at: $0)!) }, expanded: value.expanded) case .instantpageblockRelatedarticles: guard let value = flatBuffersObject.value(type: TelegramCore_InstantPageBlock_RelatedArticles.self) else { - throw FlatBuffersError.missingRequiredField(file: #file, line: #line) + throw FlatBuffersError.missingRequiredField() } self = .relatedArticles(title: try RichText(flatBuffersObject: value.title), articles: try (0 ..< value.articlesCount).map { try InstantPageRelatedArticle(flatBuffersObject: value.articles(at: $0)!) }) case .instantpageblockMap: guard let value = flatBuffersObject.value(type: TelegramCore_InstantPageBlock_Map.self) else { - throw FlatBuffersError.missingRequiredField(file: #file, line: #line) + throw FlatBuffersError.missingRequiredField() } self = .map(latitude: value.latitude, longitude: value.longitude, zoom: value.zoom, dimensions: PixelDimensions(value.dimensions), caption: try InstantPageCaption(flatBuffersObject: value.caption)) case .none_: - throw FlatBuffersError.missingRequiredField(file: #file, line: #line) + throw FlatBuffersError.missingRequiredField() } } @@ -1023,13 +1023,13 @@ public indirect enum InstantPageListItem: PostboxCoding, Equatable { switch flatBuffersObject.valueType { case .instantpagelistitemText: guard let textValue = flatBuffersObject.value(type: TelegramCore_InstantPageListItem_Text.self) else { - throw FlatBuffersError.missingRequiredField(file: #file, line: #line) + throw FlatBuffersError.missingRequiredField() } self = .text(try RichText(flatBuffersObject: textValue.text), textValue.number) case .instantpagelistitemBlocks: guard let blocksValue = flatBuffersObject.value(type: TelegramCore_InstantPageListItem_Blocks.self) else { - throw FlatBuffersError.missingRequiredField(file: #file, line: #line) + throw FlatBuffersError.missingRequiredField() } let blocks = try (0 ..< blocksValue.blocksCount).map { i in return try InstantPageBlock(flatBuffersObject: blocksValue.blocks(at: i)!) @@ -1038,7 +1038,7 @@ public indirect enum InstantPageListItem: PostboxCoding, Equatable { case .instantpagelistitemUnknown: self = .unknown case .none_: - throw FlatBuffersError.missingRequiredField(file: #file, line: #line) + throw FlatBuffersError.missingRequiredField() } } diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_MediaReferenceFlatBuffers.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_MediaReferenceFlatBuffers.swift index 776cfd00f0..1fd6e987de 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_MediaReferenceFlatBuffers.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_MediaReferenceFlatBuffers.swift @@ -79,7 +79,7 @@ public extension PartialMediaReference { switch flatBuffersObject.valueType { case .partialmediareferenceMessage: guard let value = flatBuffersObject.value(type: TelegramCore_PartialMediaReference_Message.self) else { - throw FlatBuffersError.missingRequiredField(file: #file, line: #line) + throw FlatBuffersError.missingRequiredField() } if let message = value.message { self = .message(message: try MessageReference(flatBuffersObject: message)) @@ -88,7 +88,7 @@ public extension PartialMediaReference { } case .partialmediareferenceWebpage: guard let value = flatBuffersObject.value(type: TelegramCore_PartialMediaReference_WebPage.self) else { - throw FlatBuffersError.missingRequiredField(file: #file, line: #line) + throw FlatBuffersError.missingRequiredField() } if let webPage = value.webPage { self = .webPage(webPage: try WebpageReference(flatBuffersObject: webPage)) @@ -97,7 +97,7 @@ public extension PartialMediaReference { } case .partialmediareferenceStickerpack: guard let value = flatBuffersObject.value(type: TelegramCore_PartialMediaReference_StickerPack.self) else { - throw FlatBuffersError.missingRequiredField(file: #file, line: #line) + throw FlatBuffersError.missingRequiredField() } self = .stickerPack(stickerPack: try StickerPackReference(flatBuffersObject: value.stickerPack)) case .partialmediareferenceSavedgif: @@ -107,7 +107,7 @@ public extension PartialMediaReference { case .partialmediareferenceRecentsticker: self = .recentSticker case .none_: - throw FlatBuffersError.missingRequiredField(file: #file, line: #line) + throw FlatBuffersError.missingRequiredField() } } diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_PeerAccessHash.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_PeerAccessHash.swift index b7e3d540ea..d0554d4198 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_PeerAccessHash.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_PeerAccessHash.swift @@ -19,16 +19,16 @@ public enum TelegramPeerAccessHash: Hashable { switch flatBuffersObject.valueType { case .telegrampeeraccesshashPersonal: guard let personal = flatBuffersObject.value(type: TelegramCore_TelegramPeerAccessHash_Personal.self) else { - throw FlatBuffersError.missingRequiredField(file: #file, line: #line) + throw FlatBuffersError.missingRequiredField() } self = .personal(personal.accessHash) case .telegrampeeraccesshashGenericpublic: guard let genericPublic = flatBuffersObject.value(type: TelegramCore_TelegramPeerAccessHash_GenericPublic.self) else { - throw FlatBuffersError.missingRequiredField(file: #file, line: #line) + throw FlatBuffersError.missingRequiredField() } self = .genericPublic(genericPublic.accessHash) case .none_: - throw FlatBuffersError.missingRequiredField(file: #file, line: #line) + throw FlatBuffersError.missingRequiredField() } } diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_PeerReference.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_PeerReference.swift index a012c16601..d2b949878f 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_PeerReference.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_PeerReference.swift @@ -80,21 +80,21 @@ public enum PeerReference: PostboxCoding, Hashable, Equatable { switch flatBuffersObject.valueType { case .peerreferenceUser: guard let value = flatBuffersObject.value(type: TelegramCore_PeerReference_User.self) else { - throw FlatBuffersError.missingRequiredField(file: #file, line: #line) + throw FlatBuffersError.missingRequiredField() } self = .user(id: value.id, accessHash: value.accessHash) case .peerreferenceGroup: guard let value = flatBuffersObject.value(type: TelegramCore_PeerReference_Group.self) else { - throw FlatBuffersError.missingRequiredField(file: #file, line: #line) + throw FlatBuffersError.missingRequiredField() } self = .group(id: value.id) case .peerreferenceChannel: guard let value = flatBuffersObject.value(type: TelegramCore_PeerReference_Channel.self) else { - throw FlatBuffersError.missingRequiredField(file: #file, line: #line) + throw FlatBuffersError.missingRequiredField() } self = .channel(id: value.id, accessHash: value.accessHash) case .none_: - throw FlatBuffersError.missingRequiredField(file: #file, line: #line) + throw FlatBuffersError.missingRequiredField() } } diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_RichText.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_RichText.swift index 406e91a74a..69311fda00 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_RichText.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_RichText.swift @@ -302,79 +302,79 @@ extension RichText { self = .empty case .richtextPlain: guard let value = flatBuffersObject.value(type: TelegramCore_RichText_Plain.self) else { - throw FlatBuffersError.missingRequiredField(file: #file, line: #line) + throw FlatBuffersError.missingRequiredField() } self = .plain(value.text) case .richtextBold: guard let value = flatBuffersObject.value(type: TelegramCore_RichText_Bold.self) else { - throw FlatBuffersError.missingRequiredField(file: #file, line: #line) + throw FlatBuffersError.missingRequiredField() } self = .bold(try RichText(flatBuffersObject: value.text)) case .richtextItalic: guard let value = flatBuffersObject.value(type: TelegramCore_RichText_Italic.self) else { - throw FlatBuffersError.missingRequiredField(file: #file, line: #line) + throw FlatBuffersError.missingRequiredField() } self = .italic(try RichText(flatBuffersObject: value.text)) case .richtextUnderline: guard let value = flatBuffersObject.value(type: TelegramCore_RichText_Underline.self) else { - throw FlatBuffersError.missingRequiredField(file: #file, line: #line) + throw FlatBuffersError.missingRequiredField() } self = .underline(try RichText(flatBuffersObject: value.text)) case .richtextStrikethrough: guard let value = flatBuffersObject.value(type: TelegramCore_RichText_Strikethrough.self) else { - throw FlatBuffersError.missingRequiredField(file: #file, line: #line) + throw FlatBuffersError.missingRequiredField() } self = .strikethrough(try RichText(flatBuffersObject: value.text)) case .richtextFixed: guard let value = flatBuffersObject.value(type: TelegramCore_RichText_Fixed.self) else { - throw FlatBuffersError.missingRequiredField(file: #file, line: #line) + throw FlatBuffersError.missingRequiredField() } self = .fixed(try RichText(flatBuffersObject: value.text)) case .richtextUrl: guard let value = flatBuffersObject.value(type: TelegramCore_RichText_Url.self) else { - throw FlatBuffersError.missingRequiredField(file: #file, line: #line) + throw FlatBuffersError.missingRequiredField() } self = .url(text: try RichText(flatBuffersObject: value.text), url: value.url, webpageId: value.webpageId.flatMap { MediaId($0) }) case .richtextEmail: guard let value = flatBuffersObject.value(type: TelegramCore_RichText_Email.self) else { - throw FlatBuffersError.missingRequiredField(file: #file, line: #line) + throw FlatBuffersError.missingRequiredField() } self = .email(text: try RichText(flatBuffersObject: value.text), email: value.email) case .richtextConcat: guard let value = flatBuffersObject.value(type: TelegramCore_RichText_Concat.self) else { - throw FlatBuffersError.missingRequiredField(file: #file, line: #line) + throw FlatBuffersError.missingRequiredField() } self = .concat(try (0..( byteBuffer: inout ByteBuffer, fileId: String? = nil, options: VerifierOptions = .init() ) -> T { - return try! getCheckedRoot(byteBuffer: &byteBuffer, fileId: fileId, options: options) + if flatBuffers_checkedGet { + return getRoot(byteBuffer: &byteBuffer) + } else { + return try! getCheckedRoot(byteBuffer: &byteBuffer, fileId: fileId, options: options) + } } -#else -@inline(__always) -public func FlatBuffers_getRoot(byteBuffer: inout ByteBuffer) -> T { - return getRoot(byteBuffer: &byteBuffer) -} -#endif public enum FlatBuffersError: Error { - case missingRequiredField(file: String, line: Int) + case value_missingRequiredField(file: String, line: Int) case invalidUnionType + + public static func missingRequiredField(file: String = #file, line: Int = #line) -> FlatBuffersError { + if flatBuffers_checkedGet { + preconditionFailure() + } + + return .value_missingRequiredField(file: file, line: line) + } } public extension PeerId { diff --git a/submodules/TelegramCore/Sources/Utils/TelegramMedia_FlatBuffers.swift b/submodules/TelegramCore/Sources/Utils/TelegramMedia_FlatBuffers.swift index f87d067282..e33ec795c9 100644 --- a/submodules/TelegramCore/Sources/Utils/TelegramMedia_FlatBuffers.swift +++ b/submodules/TelegramCore/Sources/Utils/TelegramMedia_FlatBuffers.swift @@ -8,16 +8,16 @@ public func TelegramMedia_parse(flatBuffersObject: TelegramCore_Media) throws -> switch flatBuffersObject.valueType { case .mediaTelegrammediafile: guard let value = flatBuffersObject.value(type: TelegramCore_Media_TelegramMediaFile.self) else { - throw FlatBuffersError.missingRequiredField(file: #file, line: #line) + throw FlatBuffersError.missingRequiredField() } return try TelegramMediaFile(flatBuffersObject: value.file) case .mediaTelegrammediaimage: guard let value = flatBuffersObject.value(type: TelegramCore_Media_TelegramMediaImage.self) else { - throw FlatBuffersError.missingRequiredField(file: #file, line: #line) + throw FlatBuffersError.missingRequiredField() } return try TelegramMediaImage(flatBuffersObject: value.image) case .none_: - throw FlatBuffersError.missingRequiredField(file: #file, line: #line) + throw FlatBuffersError.missingRequiredField() } } diff --git a/submodules/TelegramUI/Components/JoinSubjectScreen/BUILD b/submodules/TelegramUI/Components/JoinSubjectScreen/BUILD new file mode 100644 index 0000000000..188bc8ff19 --- /dev/null +++ b/submodules/TelegramUI/Components/JoinSubjectScreen/BUILD @@ -0,0 +1,22 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "JoinSubjectScreen", + module_name = "JoinSubjectScreen", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/Display", + "//submodules/TelegramPresentationData", + "//submodules/ComponentFlow", + "//submodules/Components/ComponentDisplayAdapters", + "//submodules/AppBundle", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/JoinSubjectScreen/Sources/JoinSubjectScreen.swift b/submodules/TelegramUI/Components/JoinSubjectScreen/Sources/JoinSubjectScreen.swift new file mode 100644 index 0000000000..e2cc357808 --- /dev/null +++ b/submodules/TelegramUI/Components/JoinSubjectScreen/Sources/JoinSubjectScreen.swift @@ -0,0 +1,2179 @@ +import Foundation +import UIKit +import Display +import TelegramPresentationData +import ComponentFlow +import ComponentDisplayAdapters + +private final class JoinSubjectScreenComponent: Component { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let context: AccountContext + let sourcePeer: EnginePeer + let commissionPermille: Int32 + let programDuration: Int32? + let revenuePerUser: Double + let mode: JoinSubjectScreen.Mode + + init( + context: AccountContext, + sourcePeer: EnginePeer, + commissionPermille: Int32, + programDuration: Int32?, + revenuePerUser: Double, + mode: JoinSubjectScreen.Mode + ) { + self.context = context + self.sourcePeer = sourcePeer + self.commissionPermille = commissionPermille + self.programDuration = programDuration + self.revenuePerUser = revenuePerUser + self.mode = mode + } + + static func ==(lhs: JoinSubjectScreenComponent, rhs: JoinSubjectScreenComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + return true + } + + private final class ScrollView: UIScrollView { + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + return super.hitTest(point, with: event) + } + } + + private struct ItemLayout: Equatable { + var containerSize: CGSize + var containerInset: CGFloat + var bottomInset: CGFloat + var topInset: CGFloat + + init(containerSize: CGSize, containerInset: CGFloat, bottomInset: CGFloat, topInset: CGFloat) { + self.containerSize = containerSize + self.containerInset = containerInset + self.bottomInset = bottomInset + self.topInset = topInset + } + } + + final class View: UIView, UIScrollViewDelegate { + private let dimView: UIView + private let backgroundLayer: SimpleLayer + private let navigationBarContainer: SparseContainerView + private let navigationBackgroundView: BlurredBackgroundView + private let navigationBarSeparator: SimpleLayer + private let scrollView: ScrollView + private let scrollContentClippingView: SparseContainerView + private let scrollContentView: UIView + + private let closeButton = ComponentView() + + private var toast: ComponentView? + + private let sourceAvatar = ComponentView() + private let sourceAvatarBadge = ComponentView() + private let targetAvatar = ComponentView() + private let targetAvatarBadge = ComponentView() + private let sourceTargetArrow = UIImageView() + + private let linkIconBackground = ComponentView() + private let linkIcon = ComponentView() + private var linkIconBadge: ComponentView? + + private let title = ComponentView() + private let subtitle = ComponentView() + private let openBotButton = ComponentView() + private var dailyRevenueText: ComponentView? + private let titleTransformContainer: UIView + private let bottomPanelContainer: UIView + private let actionButton = ComponentView() + private let bottomText = ComponentView() + private let linkText = ComponentView() + + private let targetText = ComponentView() + private let targetPeer = ComponentView() + + private let bottomOverscrollLimit: CGFloat + + private var isFirstTimeApplyingModalFactor: Bool = true + private var ignoreScrolling: Bool = false + + private var component: JoinSubjectScreenComponent? + private weak var state: EmptyComponentState? + private var environment: ViewControllerComponentContainer.Environment? + private var isUpdating: Bool = false + + private var itemLayout: ItemLayout? + private var topOffsetDistance: CGFloat? + + private var currentTargetPeer: EnginePeer? + private var currentMode: JoinSubjectScreen.Mode? + + private var possibleTargetPeers: [EnginePeer] = [] + private var possibleTargetPeersDisposable: Disposable? + + private var changeTargetPeerDisposable: Disposable? + private var isChangingTargetPeer: Bool = false + + private var cachedCloseImage: UIImage? + private var inlineTextStarImage: UIImage? + + override init(frame: CGRect) { + self.bottomOverscrollLimit = 200.0 + + self.dimView = UIView() + + self.backgroundLayer = SimpleLayer() + self.backgroundLayer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] + self.backgroundLayer.cornerRadius = 10.0 + + self.navigationBarContainer = SparseContainerView() + + self.navigationBackgroundView = BlurredBackgroundView(color: .clear, enableBlur: true) + self.navigationBarSeparator = SimpleLayer() + + self.scrollView = ScrollView() + + self.scrollContentClippingView = SparseContainerView() + self.scrollContentClippingView.clipsToBounds = true + + self.scrollContentView = UIView() + + self.titleTransformContainer = UIView() + self.titleTransformContainer.isUserInteractionEnabled = false + + self.bottomPanelContainer = UIView() + + super.init(frame: frame) + + self.addSubview(self.dimView) + self.layer.addSublayer(self.backgroundLayer) + + self.scrollView.delaysContentTouches = true + self.scrollView.canCancelContentTouches = true + self.scrollView.clipsToBounds = false + if #available(iOSApplicationExtension 11.0, iOS 11.0, *) { + self.scrollView.contentInsetAdjustmentBehavior = .never + } + if #available(iOS 13.0, *) { + self.scrollView.automaticallyAdjustsScrollIndicatorInsets = false + } + self.scrollView.showsVerticalScrollIndicator = false + self.scrollView.showsHorizontalScrollIndicator = false + self.scrollView.alwaysBounceHorizontal = false + self.scrollView.alwaysBounceVertical = true + self.scrollView.scrollsToTop = false + self.scrollView.delegate = self + self.scrollView.clipsToBounds = true + + self.addSubview(self.scrollContentClippingView) + self.scrollContentClippingView.addSubview(self.scrollView) + + self.scrollView.addSubview(self.scrollContentView) + + self.addSubview(self.navigationBarContainer) + self.addSubview(self.titleTransformContainer) + self.addSubview(self.bottomPanelContainer) + + self.navigationBarContainer.addSubview(self.navigationBackgroundView) + self.navigationBarContainer.layer.addSublayer(self.navigationBarSeparator) + + self.dimView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:)))) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.possibleTargetPeersDisposable?.dispose() + self.changeTargetPeerDisposable?.dispose() + } + + func scrollViewDidScroll(_ scrollView: UIScrollView) { + if !self.ignoreScrolling { + self.updateScrolling(transition: .immediate) + } + } + + func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer) { + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if !self.bounds.contains(point) { + return nil + } + if !self.backgroundLayer.frame.contains(point) { + return self.dimView + } + + if let result = self.navigationBarContainer.hitTest(self.convert(point, to: self.navigationBarContainer), with: event) { + return result + } + + let result = super.hitTest(point, with: event) + return result + } + + @objc private func dimTapGesture(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state { + guard let environment = self.environment, let controller = environment.controller() else { + return + } + controller.dismiss() + } + } + + private func updateScrolling(transition: ComponentTransition) { + guard let environment = self.environment, let controller = environment.controller(), let itemLayout = self.itemLayout else { + return + } + var topOffset = -self.scrollView.bounds.minY + itemLayout.topInset + + let titleCenterY: CGFloat = -itemLayout.topInset + itemLayout.containerInset + 54.0 * 0.5 + + let titleTransformDistance: CGFloat = 20.0 + let titleY: CGFloat = max(titleCenterY, self.titleTransformContainer.center.y + topOffset + itemLayout.containerInset) + + transition.setSublayerTransform(view: self.titleTransformContainer, transform: CATransform3DMakeTranslation(0.0, titleY - self.titleTransformContainer.center.y, 0.0)) + + let titleYDistance: CGFloat = titleY - titleCenterY + let titleTransformFraction: CGFloat = 1.0 - max(0.0, min(1.0, titleYDistance / titleTransformDistance)) + let titleMinScale: CGFloat = 17.0 / 24.0 + let titleScale: CGFloat = 1.0 * (1.0 - titleTransformFraction) + titleMinScale * titleTransformFraction + if let titleView = self.title.view { + transition.setScale(view: titleView, scale: titleScale) + } + + let navigationAlpha: CGFloat = titleTransformFraction + transition.setAlpha(view: self.navigationBackgroundView, alpha: navigationAlpha) + transition.setAlpha(layer: self.navigationBarSeparator, alpha: navigationAlpha) + + topOffset = max(0.0, topOffset) + transition.setTransform(layer: self.backgroundLayer, transform: CATransform3DMakeTranslation(0.0, topOffset + itemLayout.containerInset, 0.0)) + + if let toastView = self.toast?.view { + let toastY = topOffset + itemLayout.containerInset - toastView.bounds.height - 16.0 + transition.setTransform(layer: toastView.layer, transform: CATransform3DMakeTranslation(0.0, toastY, 0.0)) + + let toastAlpha: CGFloat + if toastY < itemLayout.containerInset { + toastAlpha = 0.0 + } else { + toastAlpha = 1.0 + } + if toastAlpha != toastView.alpha { + ComponentTransition.easeInOut(duration: 0.2).setAlpha(view: toastView, alpha: toastAlpha) + } + } + + transition.setPosition(view: self.navigationBarContainer, position: CGPoint(x: 0.0, y: topOffset + itemLayout.containerInset)) + + let topOffsetDistance: CGFloat = min(200.0, floor(itemLayout.containerSize.height * 0.25)) + self.topOffsetDistance = topOffsetDistance + var topOffsetFraction = topOffset / topOffsetDistance + topOffsetFraction = max(0.0, min(1.0, topOffsetFraction)) + + let transitionFactor: CGFloat = 1.0 - topOffsetFraction + var modalOverlayTransition = transition + if self.isFirstTimeApplyingModalFactor { + self.isFirstTimeApplyingModalFactor = false + modalOverlayTransition = .spring(duration: 0.5) + } + if self.isUpdating { + DispatchQueue.main.async { [weak controller] in + guard let controller else { + return + } + controller.updateModalStyleOverlayTransitionFactor(transitionFactor, transition: modalOverlayTransition.containedViewLayoutTransition) + } + } else { + controller.updateModalStyleOverlayTransitionFactor(transitionFactor, transition: modalOverlayTransition.containedViewLayoutTransition) + } + } + + func animateIn() { + self.dimView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + let animateOffset: CGFloat = self.bounds.height - self.backgroundLayer.frame.minY + self.scrollContentClippingView.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + self.backgroundLayer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + self.navigationBarContainer.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + self.titleTransformContainer.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + self.bottomPanelContainer.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + if let toastView = self.toast?.view { + toastView.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + toastView.layer.animateAlpha(from: 0.0, to: toastView.alpha, duration: 0.15) + } + } + + func animateOut(completion: @escaping () -> Void) { + let animateOffset: CGFloat = self.bounds.height - self.backgroundLayer.frame.minY + + self.dimView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false) + self.scrollContentClippingView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true, completion: { _ in + completion() + }) + self.backgroundLayer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true) + self.navigationBarContainer.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true) + self.titleTransformContainer.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true) + self.bottomPanelContainer.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true) + if let toastView = self.toast?.view { + toastView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true) + toastView.layer.animateAlpha(from: toastView.alpha, to: 0.0, duration: 0.15, removeOnCompletion: false) + } + + if let environment = self.environment, let controller = environment.controller() { + controller.updateModalStyleOverlayTransitionFactor(0.0, transition: .animated(duration: 0.3, curve: .easeInOut)) + } + } + + private func displayTargetSelectionMenu(sourceView: UIView) { + guard let component = self.component, let environment = self.environment, let controller = environment.controller() else { + return + } + guard let currentTargetPeer = self.currentTargetPeer else { + return + } + + var items: [ContextMenuItem] = [] + + let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }) + + let peers: [EnginePeer] = self.possibleTargetPeers.isEmpty ? [ + currentTargetPeer + ] : self.possibleTargetPeers + + let avatarSize = CGSize(width: 30.0, height: 30.0) + + for peer in peers { + let peerLabel: String + if peer.id == component.context.account.peerId { + peerLabel = environment.strings.AffiliateProgram_PeerTypeSelf + } else if case .channel = peer { + peerLabel = environment.strings.Channel_Status + } else { + peerLabel = environment.strings.Bot_GenericBotStatus + } + let isSelected = peer.id == self.currentTargetPeer?.id + let accentColor = environment.theme.list.itemAccentColor + let avatarSignal = peerAvatarCompleteImage(account: component.context.account, peer: peer, size: avatarSize) + |> map { image in + let context = DrawingContext(size: avatarSize, scale: 0.0, clear: true) + context?.withContext { c in + UIGraphicsPushContext(c) + defer { + UIGraphicsPopContext() + } + if isSelected { + + } + c.saveGState() + let scaleFactor = (avatarSize.width - 3.0 * 2.0) / avatarSize.width + if isSelected { + c.translateBy(x: avatarSize.width * 0.5, y: avatarSize.height * 0.5) + c.scaleBy(x: scaleFactor, y: scaleFactor) + c.translateBy(x: -avatarSize.width * 0.5, y: -avatarSize.height * 0.5) + } + if let image { + image.draw(in: CGRect(origin: CGPoint(), size: avatarSize)) + } + c.restoreGState() + + if isSelected { + c.setStrokeColor(accentColor.cgColor) + let lineWidth: CGFloat = 1.0 + UIScreenPixel + c.setLineWidth(lineWidth) + c.strokeEllipse(in: CGRect(origin: CGPoint(), size: avatarSize).insetBy(dx: lineWidth * 0.5, dy: lineWidth * 0.5)) + } + } + return context?.generateImage() + } + items.append(.action(ContextMenuActionItem(text: peer.displayTitle(strings: environment.strings, displayOrder: presentationData.nameDisplayOrder), textLayout: .secondLineWithValue(peerLabel), icon: { _ in nil }, iconSource: ContextMenuActionItemIconSource(size: avatarSize, signal: avatarSignal), action: { [weak self] c, _ in + c?.dismiss(completion: {}) + + guard let self, let currentMode = self.currentMode, let component = self.component else { + return + } + if self.currentTargetPeer?.id == peer.id { + return + } + + self.currentTargetPeer = peer + + switch currentMode { + case .join: + self.currentTargetPeer = peer + case let .active(active): + self.isChangingTargetPeer = true + self.changeTargetPeerDisposable?.dispose() + self.changeTargetPeerDisposable = (component.context.engine.peers.connectStarRefBot(id: peer.id, botId: component.sourcePeer.id) + |> deliverOnMainQueue).startStrict(next: { [weak self] result in + guard let self else { + return + } + self.isChangingTargetPeer = false + + self.currentMode = .active(JoinSubjectScreen.Mode.Active( + targetPeer: peer, + bot: result, + copyLink: active.copyLink + )) + self.state?.updated(transition: .immediate) + }, error: { [weak self] _ in + guard let self else { + return + } + self.isChangingTargetPeer = false + self.state?.updated(transition: .immediate) + }) + } + + self.state?.updated(transition: .immediate) + }))) + } + + let contextController = ContextController(presentationData: presentationData, source: .reference(HeaderContextReferenceContentSource(controller: controller, sourceView: sourceView, actionsOnTop: true)), items: .single(ContextController.Items(id: AnyHashable(0), content: .list(items))), gesture: nil) + controller.presentInGlobalOverlay(contextController) + } + + func update(component: JoinSubjectScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.isUpdating = true + defer { + self.isUpdating = false + } + + let environment = environment[ViewControllerComponentContainer.Environment.self].value + let themeUpdated = self.environment?.theme !== environment.theme + + let resetScrolling = self.scrollView.bounds.width != availableSize.width + + let sideInset: CGFloat = 16.0 + environment.safeInsets.left + + let currentMode = self.currentMode ?? component.mode + + if self.component == nil { + self.currentMode = component.mode + + var loadPossibleTargetPeers = false + switch component.mode { + case let .join(join): + self.currentTargetPeer = join.initialTargetPeer + loadPossibleTargetPeers = join.canSelectTargetPeer + case let .active(active): + self.currentTargetPeer = active.targetPeer + loadPossibleTargetPeers = true + } + + if loadPossibleTargetPeers { + self.possibleTargetPeersDisposable = (component.context.engine.peers.getPossibleStarRefBotTargets() + |> deliverOnMainQueue).startStrict(next: { [weak self] result in + guard let self else { + return + } + self.possibleTargetPeers = result + }) + } + } + + self.component = component + self.state = state + self.environment = environment + + if themeUpdated { + self.dimView.backgroundColor = UIColor(white: 0.0, alpha: 0.5) + self.backgroundLayer.backgroundColor = environment.theme.list.plainBackgroundColor.cgColor + + self.navigationBackgroundView.updateColor(color: environment.theme.rootController.navigationBar.blurredBackgroundColor, transition: .immediate) + self.navigationBarSeparator.backgroundColor = environment.theme.rootController.navigationBar.separatorColor.cgColor + } + + transition.setFrame(view: self.dimView, frame: CGRect(origin: CGPoint(), size: availableSize)) + + var contentHeight: CGFloat = 0.0 + + let closeImage: UIImage + if let image = self.cachedCloseImage, !themeUpdated { + closeImage = image + } else { + closeImage = generateCloseButtonImage(backgroundColor: environment.theme.list.itemPrimaryTextColor.withMultipliedAlpha(0.05), foregroundColor: environment.theme.list.itemPrimaryTextColor.withMultipliedAlpha(0.4))! + self.cachedCloseImage = closeImage + } + + let closeButtonSize = self.closeButton.update( + transition: transition, + component: AnyComponent(Button( + content: AnyComponent(Image(image: closeImage, size: closeImage.size)), + action: { [weak self] in + guard let self, let controller = self.environment?.controller() else { + return + } + controller.dismiss() + } + ).minSize(CGSize(width: 62.0, height: 56.0))), + environment: {}, + containerSize: CGSize(width: 100.0, height: 100.0) + ) + let closeButtonFrame = CGRect(origin: CGPoint(x: availableSize.width - environment.safeInsets.right - closeButtonSize.width, y: 0.0), size: closeButtonSize) + if let closeButtonView = self.closeButton.view { + if closeButtonView.superview == nil { + self.navigationBarContainer.addSubview(closeButtonView) + } + transition.setFrame(view: closeButtonView, frame: closeButtonFrame) + } + + let containerInset: CGFloat = environment.statusBarHeight + 10.0 + + let clippingY: CGFloat + + if let currentTargetPeer = self.currentTargetPeer, case .join = currentMode { + contentHeight += 34.0 + + let sourceAvatarSize = self.sourceAvatar.update( + transition: transition, + component: AnyComponent(AvatarComponent( + context: component.context, + peer: component.sourcePeer + )), + environment: {}, + containerSize: CGSize(width: 78.0, height: 78.0) + ) + let targetAvatarSize = self.targetAvatar.update( + transition: transition, + component: AnyComponent(AvatarComponent( + context: component.context, + peer: currentTargetPeer + )), + environment: {}, + containerSize: CGSize(width: 78.0, height: 78.0) + ) + + let avatarSpacing: CGFloat = 41.0 + + let sourceAvatarFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - sourceAvatarSize.width - avatarSpacing - targetAvatarSize.width) * 0.5), y: contentHeight), size: sourceAvatarSize) + let targetAvatarFrame = CGRect(origin: CGPoint(x: sourceAvatarFrame.maxX + avatarSpacing, y: contentHeight), size: targetAvatarSize) + + if let sourceAvatarView = self.sourceAvatar.view { + if sourceAvatarView.superview == nil { + self.scrollContentView.addSubview(sourceAvatarView) + } + transition.setFrame(view: sourceAvatarView, frame: sourceAvatarFrame) + } + if let targetAvatarView = self.targetAvatar.view { + if targetAvatarView.superview == nil { + self.scrollContentView.addSubview(targetAvatarView) + } + transition.setFrame(view: targetAvatarView, frame: targetAvatarFrame) + } + + if component.revenuePerUser != 0.0 { + var revenueString = String(format: "%.1f", component.revenuePerUser) + if revenueString.hasSuffix(".0") { + revenueString = String(revenueString[revenueString.startIndex ..< revenueString.index(revenueString.endIndex, offsetBy: -2)]) + } + let sourceAvatarBadgeSize = self.sourceAvatarBadge.update( + transition: transition, + component: AnyComponent(BorderedBadgeComponent( + backgroundColor: environment.theme.list.itemDisclosureActions.constructive.fillColor, + cutoutColor: environment.theme.list.plainBackgroundColor, + content: AnyComponent(HStack([ + AnyComponentWithIdentity(id: 0, component: AnyComponent(TransformContents( + content: AnyComponent(BundleIconComponent( + name: "Premium/PremiumStar", + tintColor: environment.theme.list.itemDisclosureActions.constructive.foregroundColor, + scaleFactor: 0.58 + )), + fixedSize: CGSize(width: 13.0, height: 10.0), + translation: CGPoint(x: 0.0, y: 1.0 + UIScreenPixel) + ))), + AnyComponentWithIdentity(id: 1, component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: revenueString, font: Font.regular(13.0), textColor: environment.theme.list.itemDisclosureActions.constructive.foregroundColor)) + ))) + ], spacing: 2.0)), + insets: UIEdgeInsets(top: 3.0, left: 6.0, bottom: 3.0, right: 6.0), + cutoutWidth: 1.0 + UIScreenPixel) + ), + environment: {}, + containerSize: CGSize(width: 100.0, height: 100.0) + ) + let sourceAvatarBadgeFrame = CGRect(origin: CGPoint(x: sourceAvatarFrame.minX + floor((sourceAvatarFrame.width - sourceAvatarBadgeSize.width) * 0.5), y: sourceAvatarFrame.maxY - 7.0 - floor(sourceAvatarBadgeSize.height * 0.5)), size: sourceAvatarBadgeSize) + if let sourceAvatarBadgeView = self.sourceAvatarBadge.view { + if sourceAvatarBadgeView.superview == nil { + self.scrollContentView.addSubview(sourceAvatarBadgeView) + } + transition.setFrame(view: sourceAvatarBadgeView, frame: sourceAvatarBadgeFrame) + } + } + + let targetAvatarBadgeSize = self.targetAvatarBadge.update( + transition: transition, + component: AnyComponent(BorderedBadgeComponent( + backgroundColor: environment.theme.list.itemCheckColors.fillColor, + cutoutColor: environment.theme.list.plainBackgroundColor, + content: AnyComponent(HStack([ + AnyComponentWithIdentity(id: 0, component: AnyComponent(TransformContents( + content: AnyComponent(BundleIconComponent( + name: "Media Editor/Link", + tintColor: environment.theme.list.itemCheckColors.foregroundColor, + scaleFactor: 0.75 + )), + translation: CGPoint(x: 0.0, y: 0.0) + )) + ), + AnyComponentWithIdentity(id: 1, component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: "\(formatPermille(component.commissionPermille))%", font: Font.regular(13.0), textColor: environment.theme.list.itemCheckColors.foregroundColor)) + ))) + ], spacing: 2.0)), + insets: UIEdgeInsets(top: 3.0, left: 6.0, bottom: 3.0, right: 6.0), + cutoutWidth: 1.0 + UIScreenPixel) + ), + environment: {}, + containerSize: CGSize(width: 100.0, height: 100.0) + ) + let targetAvatarBadgeFrame = CGRect(origin: CGPoint(x: targetAvatarFrame.minX + floor((targetAvatarFrame.width - targetAvatarBadgeSize.width) * 0.5), y: targetAvatarFrame.maxY - 7.0 - floor(targetAvatarBadgeSize.height * 0.5)), size: targetAvatarBadgeSize) + if let targetAvatarBadgeView = self.targetAvatarBadge.view { + if targetAvatarBadgeView.superview == nil { + self.scrollContentView.addSubview(targetAvatarBadgeView) + } + transition.setFrame(view: targetAvatarBadgeView, frame: targetAvatarBadgeFrame) + } + + contentHeight += sourceAvatarSize.height + 16.0 + + if self.sourceTargetArrow.image == nil || themeUpdated { + self.sourceTargetArrow.image = generateImage(CGSize(width: 12.0, height: 22.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setStrokeColor(environment.theme.list.itemPrimaryTextColor.withMultipliedAlpha(0.2).cgColor) + + let lineWidth: CGFloat = 3.0 + context.setLineWidth(lineWidth) + context.setLineJoin(.round) + context.setLineCap(.round) + + context.move(to: CGPoint(x: lineWidth * 0.5, y: lineWidth * 0.5)) + context.addLine(to: CGPoint(x: size.width - lineWidth * 0.5, y: size.height * 0.5)) + context.addLine(to: CGPoint(x: lineWidth * 0.5, y: size.height - lineWidth * 0.5)) + context.strokePath() + }) + } + if let sourceTargetArrowSize = self.sourceTargetArrow.image?.size { + let sourceTargetArrowFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - sourceTargetArrowSize.width) * 0.5), y: sourceAvatarFrame.minY + floor((sourceAvatarFrame.height - sourceTargetArrowSize.height) * 0.5)), size: sourceTargetArrowSize) + + if self.sourceTargetArrow.superview == nil { + self.scrollContentView.addSubview(self.sourceTargetArrow) + } + transition.setFrame(view: self.sourceTargetArrow, frame: sourceTargetArrowFrame) + } + } else if case let .active(active) = currentMode { + contentHeight += 31.0 + + let linkIconBackgroundSize = self.linkIconBackground.update( + transition: transition, + component: AnyComponent(FilledRoundedRectangleComponent( + color: environment.theme.list.itemCheckColors.fillColor, + cornerRadius: .minEdge, + smoothCorners: false + )), + environment: {}, + containerSize: CGSize(width: 90.0, height: 90.0) + ) + let linkIconBackgroundFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - linkIconBackgroundSize.width) * 0.5), y: contentHeight), size: linkIconBackgroundSize) + if let linkIconBackgroundView = self.linkIconBackground.view { + if linkIconBackgroundView.superview == nil { + self.scrollContentView.addSubview(linkIconBackgroundView) + } + transition.setFrame(view: linkIconBackgroundView, frame: linkIconBackgroundFrame) + } + + let linkIconSize = self.linkIcon.update( + transition: transition, + component: AnyComponent(BundleIconComponent( + name: "Chat/Links/LargeLink", + tintColor: environment.theme.list.itemCheckColors.foregroundColor, + scaleFactor: 0.88 + )), + environment: {}, + containerSize: linkIconBackgroundSize + ) + let linkIconFrame = CGRect(origin: CGPoint(x: linkIconBackgroundFrame.minX + floor((linkIconBackgroundFrame.width - linkIconSize.width) * 0.5), y: linkIconBackgroundFrame.minY + floor((linkIconBackgroundFrame.height - linkIconSize.height) * 0.5)), size: linkIconSize) + if let linkIconView = self.linkIcon.view { + if linkIconView.superview == nil { + self.scrollContentView.addSubview(linkIconView) + } + transition.setFrame(view: linkIconView, frame: linkIconFrame) + } + + if active.bot.participants != 0 { + let linkIconBadge: ComponentView + var linkIconBadgeTransition = transition + if let current = self.linkIconBadge { + linkIconBadge = current + } else { + linkIconBadgeTransition = linkIconBadgeTransition.withAnimation(.none) + linkIconBadge = ComponentView() + self.linkIconBadge = linkIconBadge + } + + let linkIconBadgeSize = linkIconBadge.update( + transition: .immediate, + component: AnyComponent(BorderedBadgeComponent( + backgroundColor: UIColor(rgb: 0x34C759), + cutoutColor: environment.theme.list.plainBackgroundColor, + content: AnyComponent(HStack([ + AnyComponentWithIdentity(id: 0, component: AnyComponent(BundleIconComponent( + name: "Stories/RepostUser", + tintColor: environment.theme.list.itemCheckColors.foregroundColor, + scaleFactor: 1.0 + ))), + AnyComponentWithIdentity(id: 1, component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: "\(active.bot.participants)", font: Font.bold(14.0), textColor: .white)) + ))) + ], spacing: 4.0)), + insets: UIEdgeInsets(top: 4.0, left: 9.0, bottom: 4.0, right: 8.0), + cutoutWidth: 1.0 + UIScreenPixel) + ), + environment: {}, + containerSize: CGSize(width: 100.0, height: 100.0) + ) + let linkIconBadgeFrame = CGRect(origin: CGPoint(x: linkIconBackgroundFrame.minX + floor((linkIconBackgroundFrame.width - linkIconBadgeSize.width) * 0.5), y: linkIconBackgroundFrame.maxY - floor(linkIconBadgeSize.height * 0.5)), size: linkIconBadgeSize) + if let linkIconBadgeView = linkIconBadge.view { + if linkIconBadgeView.superview == nil { + self.scrollContentView.addSubview(linkIconBadgeView) + } + linkIconBadgeTransition.setFrame(view: linkIconBadgeView, frame: linkIconBadgeFrame) + } + } else if let linkIconBadge = self.linkIconBadge { + self.linkIconBadge = nil + linkIconBadge.view?.removeFromSuperview() + } + + contentHeight += linkIconBackgroundSize.height + 21.0 + } + + let commissionTitle = "\(formatPermille(component.commissionPermille))%" + + let titleString: String + var subtitleString: String + var dailyRevenueString: String? + let termsString: String + switch currentMode { + case .join: + titleString = environment.strings.AffiliateProgram_JoinTitle + + if let programDuration = component.programDuration { + if programDuration < 12 { + subtitleString = environment.strings.AffiliateProgram_JoinSubtitleMonths(Int32(programDuration)).replacingOccurrences(of: "{bot}", with: component.sourcePeer.compactDisplayTitle).replacingOccurrences(of: "{commission}", with: commissionTitle) + } else { + subtitleString = environment.strings.AffiliateProgram_JoinSubtitleYears(Int32(programDuration / 12)).replacingOccurrences(of: "{bot}", with: component.sourcePeer.compactDisplayTitle).replacingOccurrences(of: "{commission}", with: commissionTitle) + } + } else { + subtitleString = environment.strings.AffiliateProgram_JoinSubtitleLifetime.replacingOccurrences(of: "{bot}", with: component.sourcePeer.compactDisplayTitle).replacingOccurrences(of: "{commission}", with: commissionTitle) + } + + if component.revenuePerUser != 0.0 { + var revenueString = String(format: "%.1f", component.revenuePerUser) + if revenueString.hasSuffix(".0") { + revenueString = String(revenueString[revenueString.startIndex ..< revenueString.index(revenueString.endIndex, offsetBy: -2)]) + } + dailyRevenueString = environment.strings.AffiliateProgram_DailyRevenueText(revenueString).string + } + + termsString = environment.strings.AffiliateProgram_JoinTerms + case let .active(active): + titleString = environment.strings.AffiliateProgram_LinkTitle + if let programDuration = component.programDuration { + if programDuration < 12 { + subtitleString = environment.strings.AffiliateProgram_LinkSubtitleMonths(Int32(programDuration)).replacingOccurrences(of: "{bot}", with: component.sourcePeer.compactDisplayTitle).replacingOccurrences(of: "{commission}", with: commissionTitle) + } else { + subtitleString = environment.strings.AffiliateProgram_LinkSubtitleYears(Int32(programDuration / 12)).replacingOccurrences(of: "{bot}", with: component.sourcePeer.compactDisplayTitle).replacingOccurrences(of: "{commission}", with: commissionTitle) + } + } else { + subtitleString = environment.strings.AffiliateProgram_LinkSubtitleLifetime.replacingOccurrences(of: "{bot}", with: component.sourcePeer.compactDisplayTitle).replacingOccurrences(of: "{commission}", with: commissionTitle) + } + + termsString = environment.strings.AffiliateProgram_UserCountFooter(Int32(active.bot.participants)).replacingOccurrences(of: "{bot}", with: component.sourcePeer.compactDisplayTitle) + } + let titleSize = self.title.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: titleString, font: Font.bold(24.0), textColor: environment.theme.list.itemPrimaryTextColor)) + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100.0) + ) + let titleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) * 0.5), y: contentHeight), size: titleSize) + if let titleView = title.view { + if titleView.superview == nil { + self.titleTransformContainer.addSubview(titleView) + } + titleView.bounds = CGRect(origin: CGPoint(), size: titleFrame.size) + transition.setPosition(view: self.titleTransformContainer, position: titleFrame.center) + } + contentHeight += titleSize.height + 14.0 + + let navigationBackgroundFrame = CGRect(origin: CGPoint(), size: CGSize(width: availableSize.width, height: 54.0)) + transition.setFrame(view: self.navigationBackgroundView, frame: navigationBackgroundFrame) + self.navigationBackgroundView.update(size: navigationBackgroundFrame.size, cornerRadius: 10.0, maskedCorners: [.layerMinXMinYCorner, .layerMaxXMinYCorner], transition: transition.containedViewLayoutTransition) + transition.setFrame(layer: self.navigationBarSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: 54.0), size: CGSize(width: availableSize.width, height: UIScreenPixel))) + + var openBotComponents: [AnyComponentWithIdentity] = [] + var openBotLeftInset: CGFloat = 12.0 + if case .active = component.mode { + openBotLeftInset = 1.0 + openBotComponents.append(AnyComponentWithIdentity(id: 0, component: AnyComponent(TransformContents( + content: AnyComponent(AvatarComponent( + context: component.context, + peer: component.sourcePeer, + size: CGSize(width: 30.0, height: 30.0) + )), fixedSize: CGSize(width: 30.0, height: 2.0), + translation: CGPoint(x: 0.0, y: 1.0))))) + } + openBotComponents.append(AnyComponentWithIdentity(id: 1, component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: environment.strings.AffiliateProgram_OpenBot(component.sourcePeer.compactDisplayTitle).string, font: Font.medium(15.0), textColor: environment.theme.list.itemInputField.primaryColor)) + )))) + openBotComponents.append(AnyComponentWithIdentity(id: 2, component: AnyComponent(TransformContents( + content: AnyComponent(BundleIconComponent( + name: "Item List/DisclosureArrow", + tintColor: environment.theme.list.itemInputField.primaryColor.withMultipliedAlpha(0.5), + scaleFactor: 0.8 + )), + fixedSize: CGSize(width: 8.0, height: 2.0), + translation: CGPoint(x: 0.0, y: 2.0) + )))) + let openBotButtonSize = self.openBotButton.update( + transition: .immediate, + component: AnyComponent(PlainButtonComponent( + content: AnyComponent(HStack(openBotComponents, spacing: 2.0)), + background: AnyComponent(FilledRoundedRectangleComponent(color: environment.theme.list.itemInputField.backgroundColor, cornerRadius: .minEdge, smoothCorners: false)), + effectAlignment: .center, + minSize: CGSize(width: 1.0, height: 30.0 + 2.0), + contentInsets: UIEdgeInsets(top: 0.0, left: openBotLeftInset, bottom: 0.0, right: 12.0), + action: { [weak self] in + guard let self, let component = self.component, let environment = self.environment else { + return + } + guard let controller = environment.controller(), let navigationController = controller.navigationController as? NavigationController else { + return + } + guard let infoController = component.context.sharedContext.makePeerInfoController( + context: component.context, + updatedPresentationData: nil, + peer: component.sourcePeer._asPeer(), + mode: .generic, + avatarInitiallyExpanded: false, + fromChat: false, + requestsContext: nil + ) else { + return + } + controller.dismiss(completion: { [weak navigationController] in + DispatchQueue.main.async { + guard let navigationController else { + return + } + navigationController.pushViewController(infoController) + } + }) + }, + animateAlpha: true, + animateScale: true, + animateContents: false + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0) + ) + let openBotButtonFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - openBotButtonSize.width) * 0.5), y: contentHeight), size: openBotButtonSize) + if let openBotButtonView = self.openBotButton.view { + if openBotButtonView.superview == nil { + self.scrollContentView.addSubview(openBotButtonView) + } + transition.setPosition(view: openBotButtonView, position: openBotButtonFrame.center) + openBotButtonView.bounds = CGRect(origin: CGPoint(), size: openBotButtonFrame.size) + } + contentHeight += openBotButtonSize.height + contentHeight += 20.0 + + let subtitleSize = self.subtitle.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .markdown( + text: subtitleString, + attributes: MarkdownAttributes( + body: MarkdownAttributeSet(font: Font.regular(15.0), textColor: environment.theme.list.itemPrimaryTextColor), + bold: MarkdownAttributeSet(font: Font.semibold(15.0), textColor: environment.theme.list.itemPrimaryTextColor), + link: MarkdownAttributeSet(font: Font.regular(15.0), textColor: environment.theme.list.itemAccentColor), + linkAttribute: { url in + return ("URL", url) + } + ) + ), + horizontalAlignment: .center, + maximumNumberOfLines: 0, + lineSpacing: 0.2 + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0) + ) + let subtitleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - subtitleSize.width) * 0.5), y: contentHeight), size: subtitleSize) + if let subtitleView = self.subtitle.view { + if subtitleView.superview == nil { + self.scrollContentView.addSubview(subtitleView) + } + transition.setPosition(view: subtitleView, position: subtitleFrame.center) + subtitleView.bounds = CGRect(origin: CGPoint(), size: subtitleFrame.size) + } + contentHeight += subtitleSize.height + + if let dailyRevenueString { + let dailyRevenueText: ComponentView + if let current = self.dailyRevenueText { + dailyRevenueText = current + } else { + dailyRevenueText = ComponentView() + self.dailyRevenueText = dailyRevenueText + } + + var inlineTextStarImage: UIImage? + if let current = self.inlineTextStarImage { + inlineTextStarImage = current + } else { + if let image = UIImage(bundleImageName: "Premium/Stars/StarSmall") { + let starInsets = UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0) + inlineTextStarImage = generateImage(CGSize(width: starInsets.left + image.size.width + starInsets.right, height: image.size.height), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + UIGraphicsPushContext(context) + defer { + UIGraphicsPopContext() + } + + image.draw(at: CGPoint(x: starInsets.left, y: starInsets.top)) + })?.withRenderingMode(.alwaysOriginal) + self.inlineTextStarImage = inlineTextStarImage + } + } + + let attributedDailyRevenueString = NSMutableAttributedString(attributedString: parseMarkdownIntoAttributedString(dailyRevenueString, attributes: MarkdownAttributes( + body: MarkdownAttributeSet(font: Font.regular(15.0), textColor: environment.theme.list.itemPrimaryTextColor), + bold: MarkdownAttributeSet(font: Font.semibold(15.0), textColor: environment.theme.list.itemPrimaryTextColor), + link: MarkdownAttributeSet(font: Font.regular(15.0), textColor: environment.theme.list.itemAccentColor), + linkAttribute: { url in + return ("URL", url) + } + ), textAlignment: .center)) + if let range = attributedDailyRevenueString.string.range(of: "#"), let starImage = inlineTextStarImage { + final class RunDelegateData { + let ascent: CGFloat + let descent: CGFloat + let width: CGFloat + + init(ascent: CGFloat, descent: CGFloat, width: CGFloat) { + self.ascent = ascent + self.descent = descent + self.width = width + } + } + + let runDelegateData = RunDelegateData( + ascent: Font.regular(15.0).ascender, + descent: Font.regular(15.0).descender, + width: starImage.size.width + 2.0 + ) + var callbacks = CTRunDelegateCallbacks( + version: kCTRunDelegateCurrentVersion, + dealloc: { dataRef in + Unmanaged.fromOpaque(dataRef).release() + }, + getAscent: { dataRef in + let data = Unmanaged.fromOpaque(dataRef) + return data.takeUnretainedValue().ascent + }, + getDescent: { dataRef in + let data = Unmanaged.fromOpaque(dataRef) + return data.takeUnretainedValue().descent + }, + getWidth: { dataRef in + let data = Unmanaged.fromOpaque(dataRef) + return data.takeUnretainedValue().width + } + ) + if let runDelegate = CTRunDelegateCreate(&callbacks, Unmanaged.passRetained(runDelegateData).toOpaque()) { + attributedDailyRevenueString.addAttribute(NSAttributedString.Key(kCTRunDelegateAttributeName as String), value: runDelegate, range: NSRange(range, in: attributedDailyRevenueString.string)) + } + attributedDailyRevenueString.addAttribute(.attachment, value: starImage, range: NSRange(range, in: attributedDailyRevenueString.string)) + attributedDailyRevenueString.addAttribute(.foregroundColor, value: UIColor(rgb: 0xffffff), range: NSRange(range, in: attributedDailyRevenueString.string)) + attributedDailyRevenueString.addAttribute(.baselineOffset, value: 1.0, range: NSRange(range, in: attributedDailyRevenueString.string)) + } + + let dailyRevenueTextSize = dailyRevenueText.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(attributedDailyRevenueString), + horizontalAlignment: .center, + maximumNumberOfLines: 0, + lineSpacing: 0.2 + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0) + ) + contentHeight += 16.0 + let dailyRevenueTextFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - dailyRevenueTextSize.width) * 0.5), y: contentHeight), size: dailyRevenueTextSize) + if let dailyRevenueTextView = dailyRevenueText.view { + if dailyRevenueTextView.superview == nil { + self.scrollContentView.addSubview(dailyRevenueTextView) + } + transition.setPosition(view: dailyRevenueTextView, position: dailyRevenueTextFrame.center) + dailyRevenueTextView.bounds = CGRect(origin: CGPoint(), size: dailyRevenueTextFrame.size) + } + contentHeight += dailyRevenueTextSize.height + } else if let dailyRevenueText = self.dailyRevenueText { + self.dailyRevenueText = nil + dailyRevenueText.view?.removeFromSuperview() + } + + contentHeight += 23.0 + + var displayTargetPeer = false + var isTargetPeerSelectable = false + switch currentMode { + case let .join(join): + displayTargetPeer = join.canSelectTargetPeer + isTargetPeerSelectable = join.canSelectTargetPeer + case .active: + displayTargetPeer = true + isTargetPeerSelectable = true + } + + if displayTargetPeer { + let targetTextSize = self.targetText.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: environment.strings.AffiliateProgram_CommistionDestinationText, font: Font.regular(15.0), textColor: environment.theme.list.itemPrimaryTextColor)), + horizontalAlignment: .center, + maximumNumberOfLines: 0, + lineSpacing: 0.2 + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0) + ) + let targetTextFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - targetTextSize.width) * 0.5), y: contentHeight), size: targetTextSize) + if let targetTextView = self.targetText.view { + if targetTextView.superview == nil { + self.scrollContentView.addSubview(targetTextView) + } + transition.setPosition(view: targetTextView, position: targetTextFrame.center) + targetTextView.bounds = CGRect(origin: CGPoint(), size: targetTextFrame.size) + } + contentHeight += targetTextSize.height + 12.0 + + if let currentTargetPeer = self.currentTargetPeer { + let targetPeerSize = self.targetPeer.update( + transition: transition, + component: AnyComponent(PeerBadgeComponent( + context: component.context, + theme: environment.theme, + strings: environment.strings, + peer: currentTargetPeer, + action: isTargetPeerSelectable ? { [weak self] sourceView in + guard let self else { + return + } + self.displayTargetSelectionMenu(sourceView: sourceView) + } : nil + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100.0) + ) + let targetPeerFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - targetPeerSize.width) * 0.5), y: contentHeight), size: targetPeerSize) + if let targetPeerView = self.targetPeer.view { + if targetPeerView.superview == nil { + self.scrollContentView.addSubview(targetPeerView) + } + transition.setFrame(view: targetPeerView, frame: targetPeerFrame) + } + contentHeight += targetPeerSize.height + contentHeight += 20.0 + } + } + contentHeight += 12.0 + + if case let .active(active) = currentMode { + var cleanLink = active.bot.url + let removePrefixes: [String] = ["http://", "https://"] + for prefix in removePrefixes { + if cleanLink.hasPrefix(prefix) { + cleanLink = String(cleanLink[cleanLink.index(cleanLink.startIndex, offsetBy: prefix.count)...]) + } + } + let linkTextSize = self.linkText.update( + transition: transition, + component: AnyComponent(PlainButtonComponent( + content: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: cleanLink, font: Font.regular(17.0), textColor: environment.theme.list.itemInputField.primaryColor)), + truncationType: .middle + )), + background: AnyComponent(FilledRoundedRectangleComponent( + color: environment.theme.list.itemInputField.backgroundColor, + cornerRadius: .value(8.0), + smoothCorners: true + )), + effectAlignment: .center, + minSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 50.0), + contentInsets: UIEdgeInsets(top: 0.0, left: 10.0, bottom: 0.0, right: 10.0), + action: { [weak self] in + guard let self, case let .active(active) = self.currentMode else { + return + } + self.environment?.controller()?.dismiss() + active.copyLink(active.bot) + }, + animateAlpha: true, + animateScale: false, + animateContents: false + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 50.0) + ) + let linkTextFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - linkTextSize.width) * 0.5), y: contentHeight), size: linkTextSize) + if let linkTextView = self.linkText.view { + if linkTextView.superview == nil { + self.scrollContentView.addSubview(linkTextView) + } + transition.setFrame(view: linkTextView, frame: linkTextFrame) + transition.setAlpha(view: linkTextView, alpha: self.isChangingTargetPeer ? 0.6 : 1.0) + } + contentHeight += linkTextSize.height + contentHeight += 24.0 + } + + let actionButtonTitle: String + switch currentMode { + case .join: + actionButtonTitle = environment.strings.AffiliateProgram_ActionJoin + case .active: + actionButtonTitle = environment.strings.AffiliateProgram_ActionCopyLink + } + let actionButtonSize = self.actionButton.update( + transition: transition, + component: AnyComponent(ButtonComponent( + background: ButtonComponent.Background( + color: environment.theme.list.itemCheckColors.fillColor, + foreground: environment.theme.list.itemCheckColors.foregroundColor, + pressedColor: environment.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9) + ), + content: AnyComponentWithIdentity( + id: AnyHashable(0), + component: AnyComponent(ButtonTextContentComponent( + text: actionButtonTitle, + badge: 0, + textColor: environment.theme.list.itemCheckColors.foregroundColor, + badgeBackground: environment.theme.list.itemCheckColors.foregroundColor, + badgeForeground: environment.theme.list.itemCheckColors.fillColor + )) + ), + isEnabled: true, + displaysProgress: false, + action: { [weak self] in + guard let self, let currentMode = self.currentMode else { + return + } + self.environment?.controller()?.dismiss() + + switch currentMode { + case let .join(join): + if let currentTargetPeer = self.currentTargetPeer { + join.completion(currentTargetPeer) + } + case let .active(active): + active.copyLink(active.bot) + } + } + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 50.0) + ) + + let bottomTextSize = self.bottomText.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .markdown( + text: termsString, + attributes: MarkdownAttributes( + body: MarkdownAttributeSet(font: Font.regular(13.0), textColor: environment.theme.list.itemSecondaryTextColor), + bold: MarkdownAttributeSet(font: Font.semibold(13.0), textColor: environment.theme.list.itemSecondaryTextColor), + link: MarkdownAttributeSet(font: Font.regular(13.0), textColor: environment.theme.list.itemAccentColor), + linkAttribute: { url in + return ("URL", url) + } + ) + ), + horizontalAlignment: .center, + maximumNumberOfLines: 0 + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 1000.0) + ) + + let bottomTextSpacing: CGFloat = 10.0 + let bottomPanelHeight = 10.0 + environment.safeInsets.bottom + actionButtonSize.height + bottomTextSpacing + bottomTextSize.height + + let bottomPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - bottomPanelHeight), size: CGSize(width: availableSize.width, height: bottomPanelHeight)) + transition.setFrame(view: self.bottomPanelContainer, frame: bottomPanelFrame) + + let actionButtonFrame = CGRect(origin: CGPoint(x: sideInset, y: 0.0), size: actionButtonSize) + if let actionButtonView = self.actionButton.view { + if actionButtonView.superview == nil { + self.bottomPanelContainer.addSubview(actionButtonView) + } + transition.setFrame(view: actionButtonView, frame: actionButtonFrame) + } + + let bottomTextFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - bottomTextSize.width) * 0.5), y: actionButtonFrame.maxY + bottomTextSpacing), size: bottomTextSize) + if let bottomTextView = self.bottomText.view { + if bottomTextView.superview == nil { + self.bottomPanelContainer.addSubview(bottomTextView) + } + transition.setPosition(view: bottomTextView, position: bottomTextFrame.center) + bottomTextView.bounds = CGRect(origin: CGPoint(), size: bottomTextFrame.size) + } + + contentHeight += bottomPanelHeight + + clippingY = bottomPanelFrame.minY - 8.0 + + let topInset: CGFloat = max(0.0, availableSize.height - containerInset - contentHeight) + + let scrollContentHeight = max(topInset + contentHeight + containerInset, availableSize.height - containerInset) + + //self.scrollContentClippingView.layer.cornerRadius = 10.0 + + self.itemLayout = ItemLayout(containerSize: availableSize, containerInset: containerInset, bottomInset: environment.safeInsets.bottom, topInset: topInset) + + if case .active = currentMode { + let toast: ComponentView + if let current = self.toast { + toast = current + } else { + toast = ComponentView() + self.toast = toast + } + let body = MarkdownAttributeSet(font: Font.regular(14.0), textColor: .white) + let bold = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: .white) + let toastSize = toast.update( + transition: transition, + component: AnyComponent(ToastContentComponent( + icon: AnyComponent(AvatarComponent( + context: component.context, + peer: component.sourcePeer, + size: CGSize(width: 30.0, height: 30.0) + )), + content: AnyComponent(VStack([ + AnyComponentWithIdentity(id: 0, component: AnyComponent(MultilineTextComponent( + text: .markdown(text: environment.strings.AffiliateProgram_ToastJoined_Title, attributes: MarkdownAttributes(body: bold, bold: bold, link: body, linkAttribute: { _ in nil })), + maximumNumberOfLines: 0 + ))), + AnyComponentWithIdentity(id: 1, component: AnyComponent(MultilineTextComponent( + text: .markdown(text: environment.strings.AffiliateProgram_ToastJoined_Text, attributes: MarkdownAttributes(body: body, bold: bold, link: body, linkAttribute: { _ in nil })), + maximumNumberOfLines: 0 + ))) + ], alignment: .left, spacing: 6.0)), + insets: UIEdgeInsets(top: 10.0, left: 12.0, bottom: 10.0, right: 10.0), + iconSpacing: 12.0 + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - environment.safeInsets.left - environment.safeInsets.right - 12.0 * 2.0, height: 1000.0) + ) + let toastFrame = CGRect(origin: CGPoint(x: environment.safeInsets.left + 12.0, y: 0.0), size: toastSize) + if let toastView = toast.view { + if toastView.superview == nil { + self.addSubview(toastView) + } + transition.setPosition(view: toastView, position: toastFrame.center) + transition.setBounds(view: toastView, bounds: CGRect(origin: CGPoint(), size: toastFrame.size)) + } + } + + transition.setFrame(view: self.scrollContentView, frame: CGRect(origin: CGPoint(x: 0.0, y: topInset + containerInset), size: CGSize(width: availableSize.width, height: contentHeight))) + + transition.setPosition(layer: self.backgroundLayer, position: CGPoint(x: availableSize.width / 2.0, y: availableSize.height / 2.0)) + transition.setBounds(layer: self.backgroundLayer, bounds: CGRect(origin: CGPoint(), size: availableSize)) + + let scrollClippingFrame = CGRect(origin: CGPoint(x: sideInset, y: containerInset), size: CGSize(width: availableSize.width - sideInset * 2.0, height: clippingY - containerInset)) + transition.setPosition(view: self.scrollContentClippingView, position: scrollClippingFrame.center) + transition.setBounds(view: self.scrollContentClippingView, bounds: CGRect(origin: CGPoint(x: scrollClippingFrame.minX, y: scrollClippingFrame.minY), size: scrollClippingFrame.size)) + + self.ignoreScrolling = true + let previousBounds = self.scrollView.bounds + transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: availableSize.width, height: availableSize.height))) + let contentSize = CGSize(width: availableSize.width, height: scrollContentHeight) + if contentSize != self.scrollView.contentSize { + self.scrollView.contentSize = contentSize + } + if resetScrolling { + self.scrollView.bounds = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: availableSize) + } else { + if !previousBounds.isEmpty, !transition.animation.isImmediate { + let bounds = self.scrollView.bounds + if bounds.maxY != previousBounds.maxY { + let offsetY = previousBounds.maxY - bounds.maxY + transition.animateBoundsOrigin(view: self.scrollView, from: CGPoint(x: 0.0, y: offsetY), to: CGPoint(), additive: true) + } + } + } + self.ignoreScrolling = false + self.updateScrolling(transition: transition) + + return availableSize + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +public class JoinSubjectScreen: ViewControllerComponentContainer { + public typealias Mode = JoinSubjectScreenMode + + private let context: AccountContext + + private var isDismissed: Bool = false + + public init( + context: AccountContext, + sourcePeer: EnginePeer, + commissionPermille: Int32, + programDuration: Int32?, + revenuePerUser: Double, + mode: Mode + ) { + self.context = context + + super.init(context: context, component: JoinSubjectScreenComponent( + context: context, + sourcePeer: sourcePeer, + commissionPermille: commissionPermille, + programDuration: programDuration, + revenuePerUser: revenuePerUser, + mode: mode + ), navigationBarAppearance: .none) + + self.statusBar.statusBarStyle = .Ignore + self.navigationPresentation = .flatModal + self.blocksBackgroundWhenInOverlay = true + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + } + + override public func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + self.view.disablesInteractiveModalDismiss = true + + if let componentView = self.node.hostView.componentView as? JoinSubjectScreenComponent.View { + componentView.animateIn() + } + } + + override public func dismiss(completion: (() -> Void)? = nil) { + if !self.isDismissed { + self.isDismissed = true + + if let componentView = self.node.hostView.componentView as? JoinSubjectScreenComponent.View { + componentView.animateOut(completion: { [weak self] in + completion?() + self?.dismiss(animated: false) + }) + } else { + self.dismiss(animated: false) + } + } + } +} + +private final class PeerBadgeComponent: Component { + let context: AccountContext + let theme: PresentationTheme + let strings: PresentationStrings + let peer: EnginePeer + let action: ((UIView) -> Void)? + + init( + context: AccountContext, + theme: PresentationTheme, + strings: PresentationStrings, + peer: EnginePeer, + action: ((UIView) -> Void)? + ) { + self.context = context + self.theme = theme + self.strings = strings + self.peer = peer + self.action = action + } + + static func ==(lhs: PeerBadgeComponent, rhs: PeerBadgeComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.theme !== rhs.theme { + return false + } + if lhs.strings !== rhs.strings { + return false + } + if lhs.peer != rhs.peer { + return false + } + if (lhs.action == nil) != (rhs.action == nil) { + return false + } + return true + } + + final class View: HighlightableButton { + private let background = ComponentView() + private let title = ComponentView() + private var avatarNode: AvatarNode? + private var selectorIcon: ComponentView? + + private var component: PeerBadgeComponent? + + override init(frame: CGRect) { + super.init(frame: frame) + + self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc private func pressed() { + guard let component = self.component else { + return + } + component.action?(self) + } + + func update(component: PeerBadgeComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.component = component + + self.isEnabled = component.action != nil + + let height: CGFloat = 32.0 + let avatarPadding: CGFloat = 1.0 + + let avatarDiameter = height - avatarPadding * 2.0 + let avatarTextSpacing: CGFloat = 4.0 + let rightTextInset: CGFloat = component.action != nil ? 26.0 : 12.0 + + let titleSize = self.title.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: component.peer.displayTitle(strings: component.strings, displayOrder: .firstLast), font: Font.medium(15.0), textColor: component.action != nil ? component.theme.list.itemInputField.primaryColor : component.theme.list.itemInputField.primaryColor)) + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - avatarPadding - avatarDiameter - avatarTextSpacing - rightTextInset, height: height) + ) + let titleFrame = CGRect(origin: CGPoint(x: avatarPadding + avatarDiameter + avatarTextSpacing, y: floorToScreenPixels((height - titleSize.height) * 0.5)), size: titleSize) + if let titleView = self.title.view { + if titleView.superview == nil { + titleView.isUserInteractionEnabled = false + self.addSubview(titleView) + } + titleView.frame = titleFrame + } + + let avatarNode: AvatarNode + if let current = self.avatarNode { + avatarNode = current + } else { + avatarNode = AvatarNode(font: avatarPlaceholderFont(size: floor(avatarDiameter * 0.5))) + avatarNode.isUserInteractionEnabled = false + avatarNode.displaysAsynchronously = false + self.avatarNode = avatarNode + self.addSubview(avatarNode.view) + } + + let avatarFrame = CGRect(origin: CGPoint(x: avatarPadding, y: avatarPadding), size: CGSize(width: avatarDiameter, height: avatarDiameter)) + avatarNode.frame = avatarFrame + avatarNode.updateSize(size: avatarFrame.size) + avatarNode.setPeer(context: component.context, theme: component.theme, peer: component.peer) + + let size = CGSize(width: avatarPadding + avatarDiameter + avatarTextSpacing + titleSize.width + rightTextInset, height: height) + + if component.action != nil { + let selectorIcon: ComponentView + if let current = self.selectorIcon { + selectorIcon = current + } else { + selectorIcon = ComponentView() + self.selectorIcon = selectorIcon + } + let selectorIconSize = selectorIcon.update( + transition: transition, + component: AnyComponent(BundleIconComponent( + name: "Item List/ContextDisclosureArrow", tintColor: component.theme.list.itemAccentColor)), + environment: {}, + containerSize: CGSize(width: 10.0, height: 10.0) + ) + let selectorIconFrame = CGRect(origin: CGPoint(x: size.width - 8.0 - selectorIconSize.width, y: floorToScreenPixels((size.height - selectorIconSize.height) * 0.5)), size: selectorIconSize) + if let selectorIconView = selectorIcon.view { + if selectorIconView.superview == nil { + selectorIconView.isUserInteractionEnabled = false + self.addSubview(selectorIconView) + } + transition.setFrame(view: selectorIconView, frame: selectorIconFrame) + } + } else if let selectorIcon = self.selectorIcon { + self.selectorIcon = nil + selectorIcon.view?.removeFromSuperview() + } + + let _ = self.background.update( + transition: transition, + component: AnyComponent(FilledRoundedRectangleComponent( + color: component.action != nil ? component.theme.list.itemAccentColor.withMultipliedAlpha(0.1) : component.theme.list.itemInputField.backgroundColor, + cornerRadius: .minEdge, + smoothCorners: false + )), + environment: {}, + containerSize: size + ) + if let backgroundView = self.background.view { + if backgroundView.superview == nil { + backgroundView.isUserInteractionEnabled = false + self.insertSubview(backgroundView, at: 0) + } + transition.setFrame(view: backgroundView, frame: CGRect(origin: CGPoint(), size: size)) + } + + return size + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +private final class AvatarComponent: Component { + let context: AccountContext + let peer: EnginePeer + let size: CGSize? + + init(context: AccountContext, peer: EnginePeer, size: CGSize? = nil) { + self.context = context + self.peer = peer + self.size = size + } + + static func ==(lhs: AvatarComponent, rhs: AvatarComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.peer != rhs.peer { + return false + } + if lhs.size != rhs.size { + return false + } + return true + } + + final class View: UIView { + private var avatarNode: AvatarNode? + + private var component: AvatarComponent? + private weak var state: EmptyComponentState? + + override init(frame: CGRect) { + super.init(frame: frame) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: AvatarComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.component = component + self.state = state + + let size = component.size ?? availableSize + + let avatarNode: AvatarNode + if let current = self.avatarNode { + avatarNode = current + } else { + avatarNode = AvatarNode(font: avatarPlaceholderFont(size: floor(size.width * 0.5))) + avatarNode.displaysAsynchronously = false + self.avatarNode = avatarNode + self.addSubview(avatarNode.view) + } + avatarNode.frame = CGRect(origin: CGPoint(), size: size) + avatarNode.setPeer( + context: component.context, + theme: component.context.sharedContext.currentPresentationData.with({ $0 }).theme, + peer: component.peer, + synchronousLoad: true, + displayDimensions: size + ) + avatarNode.updateSize(size: size) + + return size + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +private func generateCloseButtonImage(backgroundColor: UIColor, foregroundColor: UIColor) -> UIImage? { + return generateImage(CGSize(width: 30.0, height: 30.0), contextGenerator: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + + context.setFillColor(backgroundColor.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(), size: size)) + + context.setLineWidth(2.0) + context.setLineCap(.round) + context.setStrokeColor(foregroundColor.cgColor) + + context.move(to: CGPoint(x: 10.0, y: 10.0)) + context.addLine(to: CGPoint(x: 20.0, y: 20.0)) + context.strokePath() + + context.move(to: CGPoint(x: 20.0, y: 10.0)) + context.addLine(to: CGPoint(x: 10.0, y: 20.0)) + context.strokePath() + }) +} + +private final class BorderedBadgeComponent: Component { + let backgroundColor: UIColor + let cutoutColor: UIColor + let content: AnyComponent + let insets: UIEdgeInsets + let aspect: CGFloat? + let cutoutWidth: CGFloat + + init( + backgroundColor: UIColor, + cutoutColor: UIColor, + content: AnyComponent, + insets: UIEdgeInsets, + aspect: CGFloat? = nil, + cutoutWidth: CGFloat + ) { + self.backgroundColor = backgroundColor + self.cutoutColor = cutoutColor + self.content = content + self.insets = insets + self.aspect = aspect + self.cutoutWidth = cutoutWidth + } + + static func ==(lhs: BorderedBadgeComponent, rhs: BorderedBadgeComponent) -> Bool { + if lhs.backgroundColor !== rhs.backgroundColor { + return false + } + if lhs.cutoutColor != rhs.cutoutColor { + return false + } + if lhs.content != rhs.content { + return false + } + if lhs.insets != rhs.insets { + return false + } + if lhs.aspect != rhs.aspect { + return false + } + if lhs.cutoutWidth != rhs.cutoutWidth { + return false + } + return true + } + + final class View: UIView { + private let cutoutBackground = ComponentView() + private let background = ComponentView() + private let content = ComponentView() + + private var component: BorderedBadgeComponent? + private weak var state: EmptyComponentState? + + override init(frame: CGRect) { + super.init(frame: frame) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: BorderedBadgeComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.component = component + self.state = state + + let contentSize = self.content.update( + transition: transition, + component: component.content, + environment: {}, + containerSize: CGSize(width: availableSize.width - component.insets.left - component.insets.right, height: availableSize.height - component.insets.top - component.insets.bottom) + ) + var size = CGSize(width: contentSize.width + component.insets.left + component.insets.right, height: contentSize.height + component.insets.top + component.insets.bottom) + if let aspect = component.aspect { + size.width = size.height * aspect + } + + let backgroundFrame = CGRect(origin: CGPoint(), size: size) + let cutoutBackgroundFrame = backgroundFrame.insetBy(dx: -component.cutoutWidth, dy: -component.cutoutWidth) + + let _ = self.cutoutBackground.update( + transition: transition, + component: AnyComponent(FilledRoundedRectangleComponent( + color: component.cutoutColor, + cornerRadius: .minEdge, + smoothCorners: false + )), + environment: {}, containerSize: cutoutBackgroundFrame.size + ) + let _ = self.background.update( + transition: transition, + component: AnyComponent(FilledRoundedRectangleComponent( + color: component.backgroundColor, + cornerRadius: .minEdge, + smoothCorners: false + )), + environment: {}, containerSize: backgroundFrame.size + ) + + if let cutoutBackgroundView = self.cutoutBackground.view { + if cutoutBackgroundView.superview == nil { + self.addSubview(cutoutBackgroundView) + } + transition.setFrame(view: cutoutBackgroundView, frame: cutoutBackgroundFrame) + } + if let backgroundView = self.background.view { + if backgroundView.superview == nil { + self.addSubview(backgroundView) + } + transition.setFrame(view: backgroundView, frame: backgroundFrame) + } + + let contentFrame = CGRect(origin: CGPoint(x: backgroundFrame.minX + component.insets.left, y: backgroundFrame.minY + component.insets.top), size: contentSize) + if let contentView = self.content.view { + if contentView.superview == nil { + self.addSubview(contentView) + } + transition.setFrame(view: contentView, frame: contentFrame) + } + + return size + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +final class AffiliatePeerSubtitleComponent: Component { + let theme: PresentationTheme + let percentText: String + let text: String + + init( + theme: PresentationTheme, + percentText: String, + text: String + ) { + self.theme = theme + self.percentText = percentText + self.text = text + } + + static func ==(lhs: AffiliatePeerSubtitleComponent, rhs: AffiliatePeerSubtitleComponent) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + if lhs.percentText != rhs.percentText { + return false + } + if lhs.text != rhs.text { + return false + } + return true + } + + final class View: UIView { + private let badgeBackground = ComponentView() + private let badgeText = ComponentView() + private let text = ComponentView() + + private var component: AffiliatePeerSubtitleComponent? + private weak var state: EmptyComponentState? + + override init(frame: CGRect) { + super.init(frame: frame) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: AffiliatePeerSubtitleComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.component = component + self.state = state + + let badgeSpacing: CGFloat = 5.0 + let badgeInsets = UIEdgeInsets(top: 2.0, left: 4.0, bottom: 2.0, right: 4.0) + + let badgeTextSize = self.badgeText.update( + transition: transition, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: component.percentText, font: Font.regular(13.0), textColor: component.theme.list.itemCheckColors.foregroundColor)) + )), + environment: {}, + containerSize: CGSize(width: availableSize.width, height: 100.0) + ) + + let badgeSize = CGSize(width: badgeTextSize.width + badgeInsets.left + badgeInsets.right, height: badgeTextSize.height + badgeInsets.top + badgeInsets.bottom) + + let textSize = self.text.update( + transition: transition, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: component.text, font: Font.regular(15.0), textColor: component.theme.list.itemSecondaryTextColor)) + )), + environment: {}, + containerSize: CGSize(width: availableSize.width, height: 100.0) + ) + + let size = CGSize(width: badgeSize.width + badgeSpacing + textSize.width, height: textSize.height) + + let badgeFrame = CGRect(origin: CGPoint(x: 0.0, y: floorToScreenPixels((size.height - badgeSize.height) * 0.5)), size: badgeSize) + let _ = self.badgeBackground.update( + transition: transition, + component: AnyComponent(FilledRoundedRectangleComponent( + color: component.theme.list.itemCheckColors.fillColor, + cornerRadius: .value(5.0), + smoothCorners: true + )), + environment: {}, + containerSize: badgeFrame.size + ) + if let badgeBackgroundView = self.badgeBackground.view { + if badgeBackgroundView.superview == nil { + self.addSubview(badgeBackgroundView) + } + transition.setFrame(view: badgeBackgroundView, frame: badgeFrame) + } + + let badgeTextFrame = CGRect(origin: CGPoint(x: badgeFrame.minX + badgeInsets.left, y: badgeFrame.minY + badgeInsets.top), size: badgeTextSize) + if let badgeTextView = self.badgeText.view { + if badgeTextView.superview == nil { + self.addSubview(badgeTextView) + } + transition.setPosition(view: badgeTextView, position: badgeTextFrame.center) + badgeTextView.bounds = CGRect(origin: CGPoint(), size: badgeTextFrame.size) + } + + let textFrame = CGRect(origin: CGPoint(x: badgeSize.width + badgeSpacing, y: 0.0), size: textSize) + if let textView = self.text.view { + if textView.superview == nil { + self.addSubview(textView) + } + transition.setFrame(view: textView, frame: textFrame) + } + + return size + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +final class BotSectionSortButtonComponent: Component { + let theme: PresentationTheme + let strings: PresentationStrings + let sortMode: EngineSuggestedStarRefBotsContext.SortMode + let action: (UIView) -> Void + + init( + theme: PresentationTheme, + strings: PresentationStrings, + sortMode: EngineSuggestedStarRefBotsContext.SortMode, + action: @escaping (UIView) -> Void + ) { + self.theme = theme + self.strings = strings + self.sortMode = sortMode + self.action = action + } + + static func ==(lhs: BotSectionSortButtonComponent, rhs: BotSectionSortButtonComponent) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + if lhs.strings !== rhs.strings { + return false + } + if lhs.sortMode != rhs.sortMode { + return false + } + return true + } + + final class View: HighlightableButton { + private let text = ComponentView() + private let icon = ComponentView() + + private var component: BotSectionSortButtonComponent? + + override init(frame: CGRect) { + super.init(frame: frame) + + self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc private func pressed() { + guard let component = self.component else { + return + } + component.action(self) + } + + func update(component: BotSectionSortButtonComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.component = component + + let sortByString: String + switch component.sortMode { + case .date: + sortByString = component.strings.AffiliateSetup_SortSectionHeader_Date + case .profitability: + sortByString = component.strings.AffiliateSetup_SortSectionHeader_Profitability + case .revenue: + sortByString = component.strings.AffiliateSetup_SortSectionHeader_Revenue + } + let textSize = self.text.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .markdown(text: sortByString, attributes: MarkdownAttributes( + body: MarkdownAttributeSet(font: Font.regular(13.0), textColor: component.theme.list.freeTextColor), + bold: MarkdownAttributeSet(font: Font.semibold(13.0), textColor: component.theme.list.freeTextColor), + link: MarkdownAttributeSet(font: Font.regular(13.0), textColor: component.theme.list.itemAccentColor), + linkAttribute: { url in + return ("URL", url) + } + )) + )), + environment: {}, + containerSize: availableSize + ) + + let iconSize = self.icon.update( + transition: .immediate, + component: AnyComponent(BundleIconComponent( + name: "Item List/ContextDisclosureArrow", + tintColor: component.theme.list.itemAccentColor, + scaleFactor: 0.7 + )), + environment: {}, + containerSize: CGSize(width: 100.0, height: 100.0) + ) + + let spacing: CGFloat = 2.0 + + let size = CGSize(width: textSize.width + spacing + iconSize.width, height: textSize.height) + + let textFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: textSize) + if let textView = self.text.view { + if textView.superview == nil { + textView.isUserInteractionEnabled = false + self.addSubview(textView) + } + transition.setPosition(view: textView, position: textFrame.center) + textView.bounds = CGRect(origin: CGPoint(), size: textFrame.size) + } + + let iconFrame = CGRect(origin: CGPoint(x: textFrame.maxX + spacing, y: floorToScreenPixels((size.height - iconSize.height) * 0.5)), size: iconSize) + if let iconView = self.icon.view { + if iconView.superview == nil { + iconView.isUserInteractionEnabled = false + self.addSubview(iconView) + } + transition.setFrame(view: iconView, frame: iconFrame) + } + + return size + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +final class PeerBadgeAvatarComponent: Component { + final class SynchronousLoadHint { + } + + let context: AccountContext + let peer: EnginePeer + let theme: PresentationTheme + let hasBadge: Bool + + init(context: AccountContext, peer: EnginePeer, theme: PresentationTheme, hasBadge: Bool) { + self.context = context + self.peer = peer + self.theme = theme + self.hasBadge = hasBadge + } + + static func ==(lhs: PeerBadgeAvatarComponent, rhs: PeerBadgeAvatarComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.peer != rhs.peer { + return false + } + if lhs.theme !== rhs.theme { + return false + } + if lhs.hasBadge != rhs.hasBadge { + return false + } + return true + } + + final class View: UIView { + private var avatarNode: AvatarNode? + + private let badgeBackground = UIImageView() + private let badgeIcon = UIImageView() + + private var component: PeerBadgeAvatarComponent? + private weak var state: EmptyComponentState? + + private static let badgeBackgroundImage = generateFilledCircleImage(diameter: 18.0, color: .white)?.withRenderingMode(.alwaysTemplate) + private static let badgeIconImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Links/Link"), color: .white)?.withRenderingMode(.alwaysTemplate) + + override init(frame: CGRect) { + super.init(frame: frame) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: PeerBadgeAvatarComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.component = component + self.state = state + + var synchronousLoad = false + if transition.userData(SynchronousLoadHint.self) != nil { + synchronousLoad = true + } + + let size = CGSize(width: 40.0, height: 40.0) + let badgeSize: CGFloat = 18.0 + + let badgeFrame = CGRect(origin: CGPoint(x: size.width - badgeSize, y: size.height - badgeSize), size: CGSize(width: badgeSize, height: badgeSize)) + + let avatarNode: AvatarNode + if let current = self.avatarNode { + avatarNode = current + } else { + avatarNode = AvatarNode(font: avatarPlaceholderFont(size: floor(size.width * 0.5))) + avatarNode.displaysAsynchronously = false + self.avatarNode = avatarNode + self.addSubview(avatarNode.view) + } + + avatarNode.frame = CGRect(origin: CGPoint(), size: size) + avatarNode.setPeer( + context: component.context, + theme: component.context.sharedContext.currentPresentationData.with({ $0 }).theme, + peer: component.peer, + synchronousLoad: synchronousLoad, + displayDimensions: size, + cutoutRect: component.hasBadge ? badgeFrame.insetBy(dx: -(1.0 + UIScreenPixel), dy: -(1.0 + UIScreenPixel)) : nil + ) + + if self.badgeBackground.image == nil { + self.badgeBackground.image = View.badgeBackgroundImage + } + if self.badgeBackground.superview == nil { + self.addSubview(self.badgeBackground) + } + if self.badgeIcon.image == nil { + self.badgeIcon.image = View.badgeIconImage + } + if self.badgeIcon.superview == nil { + self.addSubview(self.badgeIcon) + } + + self.badgeBackground.tintColor = component.theme.list.itemCheckColors.fillColor + self.badgeIcon.tintColor = component.theme.list.itemCheckColors.foregroundColor + + transition.setFrame(view: self.badgeBackground, frame: badgeFrame) + + if let badgeIconSize = self.badgeIcon.image?.size { + let badgeIconFactor: CGFloat = 0.45 + let badgeIconSize = CGSize(width: badgeIconSize.width * badgeIconFactor, height: badgeIconSize.height * badgeIconFactor) + let badgeIconFrame = CGRect(origin: CGPoint(x: badgeFrame.minX + floorToScreenPixels((badgeSize - badgeIconSize.width) * 0.5), y: badgeFrame.minY + floorToScreenPixels((badgeSize - badgeIconSize.height) * 0.5)), size: badgeIconSize) + transition.setFrame(view: self.badgeIcon, frame: badgeIconFrame) + } + + self.badgeBackground.isHidden = !component.hasBadge + self.badgeIcon.isHidden = !component.hasBadge + + return size + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index 16fdd3eaf9..0c5d06d2d4 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -472,6 +472,8 @@ public final class SharedAccountContextImpl: SharedAccountContext { |> deliverOnMainQueue).start(next: { sharedData in if let settings = sharedData.entries[ApplicationSpecificSharedDataKeys.experimentalUISettings]?.get(ExperimentalUISettings.self) { let _ = immediateExperimentalUISettingsValue.swap(settings) + + flatBuffers_checkedGet = immediateExperimentalUISettingsValue.checkSerializedData } }) diff --git a/submodules/TelegramUIPreferences/Sources/ExperimentalUISettings.swift b/submodules/TelegramUIPreferences/Sources/ExperimentalUISettings.swift index 3a2400cd6d..c9ac4a7da1 100644 --- a/submodules/TelegramUIPreferences/Sources/ExperimentalUISettings.swift +++ b/submodules/TelegramUIPreferences/Sources/ExperimentalUISettings.swift @@ -65,6 +65,7 @@ public struct ExperimentalUISettings: Codable, Equatable { public var devRequests: Bool public var fakeAds: Bool public var conferenceDebug: Bool + public var checkSerializedData: Bool public static var defaultSettings: ExperimentalUISettings { return ExperimentalUISettings( @@ -107,7 +108,8 @@ public struct ExperimentalUISettings: Codable, Equatable { playerV2: false, devRequests: false, fakeAds: false, - conferenceDebug: false + conferenceDebug: false, + checkSerializedData: false ) } @@ -151,7 +153,8 @@ public struct ExperimentalUISettings: Codable, Equatable { playerV2: Bool, devRequests: Bool, fakeAds: Bool, - conferenceDebug: Bool + conferenceDebug: Bool, + checkSerializedData: Bool ) { self.keepChatNavigationStack = keepChatNavigationStack self.skipReadHistory = skipReadHistory @@ -193,6 +196,7 @@ public struct ExperimentalUISettings: Codable, Equatable { self.devRequests = devRequests self.fakeAds = fakeAds self.conferenceDebug = conferenceDebug + self.checkSerializedData = checkSerializedData } public init(from decoder: Decoder) throws { @@ -238,6 +242,7 @@ public struct ExperimentalUISettings: Codable, Equatable { self.devRequests = try container.decodeIfPresent(Bool.self, forKey: "devRequests") ?? false self.fakeAds = try container.decodeIfPresent(Bool.self, forKey: "fakeAds") ?? false self.conferenceDebug = try container.decodeIfPresent(Bool.self, forKey: "conferenceDebug") ?? false + self.checkSerializedData = try container.decodeIfPresent(Bool.self, forKey: "checkSerializedData") ?? false } public func encode(to encoder: Encoder) throws { @@ -283,6 +288,7 @@ public struct ExperimentalUISettings: Codable, Equatable { try container.encodeIfPresent(self.devRequests, forKey: "devRequests") try container.encodeIfPresent(self.fakeAds, forKey: "fakeAds") try container.encodeIfPresent(self.conferenceDebug, forKey: "conferenceDebug") + try container.encodeIfPresent(self.checkSerializedData, forKey: "checkSerializedData") } }