diff --git a/submodules/BrowserUI/Sources/BrowserScreen.swift b/submodules/BrowserUI/Sources/BrowserScreen.swift index 1bfe441e3b..5108eaa2e4 100644 --- a/submodules/BrowserUI/Sources/BrowserScreen.swift +++ b/submodules/BrowserUI/Sources/BrowserScreen.swift @@ -282,7 +282,7 @@ public class BrowserScreen: ViewController { let content: BrowserContent switch controller.subject { case let .webPage(url): - content = BrowserWebContent(url: url) + content = BrowserWebContent(context: controller.context, url: url) } self.content = content diff --git a/submodules/BrowserUI/Sources/BrowserWebContent.swift b/submodules/BrowserUI/Sources/BrowserWebContent.swift index bd4f801433..06b57d7a71 100644 --- a/submodules/BrowserUI/Sources/BrowserWebContent.swift +++ b/submodules/BrowserUI/Sources/BrowserWebContent.swift @@ -10,6 +10,76 @@ import AccountContext import WebKit import AppBundle +private final class IpfsSchemeHandler: NSObject, WKURLSchemeHandler { + private final class PendingTask { + let sourceTask: any WKURLSchemeTask + var urlSessionTask: URLSessionTask? + let isCompleted = Atomic(value: false) + + init(sourceTask: any WKURLSchemeTask) { + self.sourceTask = sourceTask + + var cleanUrl = sourceTask.request.url!.absoluteString + if let range = cleanUrl.range(of: "/ipfs/") { + cleanUrl = "ipfs://" + String(cleanUrl[range.upperBound...]) + } else if let range = cleanUrl.range(of: "/ipns/") { + cleanUrl = "ipns://" + String(cleanUrl[range.upperBound...]) + } + print("Load: \(cleanUrl)") + cleanUrl = cleanUrl.replacingOccurrences(of: "ipns://", with: "ipns/") + cleanUrl = cleanUrl.replacingOccurrences(of: "ipfs://", with: "ipfs/") + let mappedUrl = "https://cloudflare-ipfs.com/\(cleanUrl)" + let isCompleted = self.isCompleted + self.urlSessionTask = URLSession.shared.dataTask(with: URLRequest(url: URL(string: mappedUrl)!), completionHandler: { data, response, error in + if isCompleted.swap(true) { + return + } + + if let error { + sourceTask.didFailWithError(error) + } else { + if let response { + sourceTask.didReceive(response) + } + if let data { + sourceTask.didReceive(data) + } + sourceTask.didFinish() + } + }) + self.urlSessionTask?.resume() + } + + func cancel() { + if let urlSessionTask = self.urlSessionTask { + self.urlSessionTask = nil + if !self.isCompleted.swap(true) { + switch urlSessionTask.state { + case .running, .suspended: + urlSessionTask.cancel() + default: + break + } + } + } + } + } + + private var pendingTasks: [PendingTask] = [] + + func webView(_ webView: WKWebView, start urlSchemeTask: any WKURLSchemeTask) { + self.pendingTasks.append(PendingTask(sourceTask: urlSchemeTask)) + } + + func webView(_ webView: WKWebView, stop urlSchemeTask: any WKURLSchemeTask) { + if let index = self.pendingTasks.firstIndex(where: { $0.sourceTask === urlSchemeTask }) { + let task = self.pendingTasks[index] + self.pendingTasks.remove(at: index) + task.cancel() + } + } +} + final class BrowserWebContent: UIView, BrowserContent, UIScrollViewDelegate { private let webView: WKWebView @@ -22,9 +92,14 @@ final class BrowserWebContent: UIView, BrowserContent, UIScrollViewDelegate { var onScrollingUpdate: (ContentScrollingUpdate) -> Void = { _ in } - init(url: String) { + init(context: AccountContext, url: String) { let configuration = WKWebViewConfiguration() + if context.sharedContext.immediateExperimentalUISettings.browserExperiment { + configuration.setURLSchemeHandler(IpfsSchemeHandler(), forURLScheme: "ipns") + configuration.setURLSchemeHandler(IpfsSchemeHandler(), forURLScheme: "ipfs") + } + self.webView = WKWebView(frame: CGRect(), configuration: configuration) self.webView.allowsLinkPreview = false diff --git a/submodules/DebugSettingsUI/Sources/DebugController.swift b/submodules/DebugSettingsUI/Sources/DebugController.swift index d547cd7b68..569096a4fd 100644 --- a/submodules/DebugSettingsUI/Sources/DebugController.swift +++ b/submodules/DebugSettingsUI/Sources/DebugController.swift @@ -94,7 +94,7 @@ private enum DebugControllerEntry: ItemListNodeEntry { case experimentalCompatibility(Bool) case enableDebugDataDisplay(Bool) case acceleratedStickers(Bool) - case inlineForums(Bool) + case browserExperiment(Bool) case localTranscription(Bool) case enableReactionOverrides(Bool) case storiesExperiment(Bool) @@ -126,7 +126,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, .playlistPlayback, .enableQuickReactionSwitch, .voiceConference, .experimentalCompatibility, .enableDebugDataDisplay, .acceleratedStickers, .inlineForums, .localTranscription, .enableReactionOverrides, .restorePurchases: + case .clearTips, .resetNotifications, .crash, .fillLocalSavedMessageCache, .resetDatabase, .resetDatabaseAndCache, .resetHoles, .resetTagHoles, .reindexUnread, .resetCacheIndex, .reindexCache, .resetBiometricsData, .optimizeDatabase, .photoPreview, .knockoutWallpaper, .storiesExperiment, .storiesJpegExperiment, .playlistPlayback, .enableQuickReactionSwitch, .voiceConference, .experimentalCompatibility, .enableDebugDataDisplay, .acceleratedStickers, .browserExperiment, .localTranscription, .enableReactionOverrides, .restorePurchases: return DebugControllerSection.experiments.rawValue case .logTranslationRecognition, .resetTranslationStates: return DebugControllerSection.translation.rawValue @@ -217,7 +217,7 @@ private enum DebugControllerEntry: ItemListNodeEntry { return 38 case .acceleratedStickers: return 39 - case .inlineForums: + case .browserExperiment: return 40 case .localTranscription: return 41 @@ -1235,12 +1235,12 @@ private enum DebugControllerEntry: ItemListNodeEntry { }) }).start() }) - case let .inlineForums(value): - return ItemListSwitchItem(presentationData: presentationData, title: "Inline Forums", value: value, sectionId: self.section, style: .blocks, updated: { value in + case let .browserExperiment(value): + return ItemListSwitchItem(presentationData: presentationData, title: "Inline UI", 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.inlineForums = value + settings.browserExperiment = value return PreferencesEntry(settings) }) }).start() @@ -1438,7 +1438,7 @@ private func debugControllerEntries(sharedContext: SharedAccountContext, present entries.append(.experimentalCompatibility(experimentalSettings.experimentalCompatibility)) entries.append(.enableDebugDataDisplay(experimentalSettings.enableDebugDataDisplay)) entries.append(.acceleratedStickers(experimentalSettings.acceleratedStickers)) - entries.append(.inlineForums(experimentalSettings.inlineForums)) + entries.append(.browserExperiment(experimentalSettings.browserExperiment)) entries.append(.localTranscription(experimentalSettings.localTranscription)) if case .internal = sharedContext.applicationBindings.appBuildType { entries.append(.enableReactionOverrides(experimentalSettings.enableReactionOverrides)) diff --git a/submodules/TelegramUI/Sources/OpenUrl.swift b/submodules/TelegramUI/Sources/OpenUrl.swift index a79a11cb62..f6b415e8c1 100644 --- a/submodules/TelegramUI/Sources/OpenUrl.swift +++ b/submodules/TelegramUI/Sources/OpenUrl.swift @@ -988,7 +988,17 @@ func openExternalUrlImpl(context: AccountContext, urlContext: OpenURLContext, ur return } + var isInternetUrl = false if parsedUrl.scheme == "http" || parsedUrl.scheme == "https" { + isInternetUrl = true + } + if context.sharedContext.immediateExperimentalUISettings.browserExperiment { + if parsedUrl.scheme == "ipfs" || parsedUrl.scheme == "ipns" { + isInternetUrl = true + } + } + + if isInternetUrl { if parsedUrl.host == "t.me" || parsedUrl.host == "telegram.me" { handleInternalUrl(parsedUrl.absoluteString) } else { @@ -1019,7 +1029,7 @@ func openExternalUrlImpl(context: AccountContext, urlContext: OpenURLContext, ur let _ = (settings |> deliverOnMainQueue).startStandalone(next: { settings in if settings.defaultWebBrowser == nil { - if !"".isEmpty && isCompact { + if isCompact && context.sharedContext.immediateExperimentalUISettings.browserExperiment { let controller = BrowserScreen(context: context, subject: .webPage(url: parsedUrl.absoluteString)) navigationController?.pushViewController(controller) } else { diff --git a/submodules/TelegramUI/Sources/TextLinkHandling.swift b/submodules/TelegramUI/Sources/TextLinkHandling.swift index 4d862fbebd..6218df5b10 100644 --- a/submodules/TelegramUI/Sources/TextLinkHandling.swift +++ b/submodules/TelegramUI/Sources/TextLinkHandling.swift @@ -62,7 +62,8 @@ func handleTextLinkActionImpl(context: AccountContext, peerId: EnginePeer.Id?, n if let controller = controller { switch result { case let .externalUrl(url): - context.sharedContext.applicationBindings.openUrl(url) + context.sharedContext.openExternalUrl(context: context, urlContext: .generic, url: url, forceExternal: false, presentationData: context.sharedContext.currentPresentationData.with({ $0 }), navigationController: controller.navigationController as? NavigationController, dismissInput: { + }) case let .peer(peer, navigation): openResolvedPeerImpl(peer.flatMap(EnginePeer.init), navigation) case let .botStart(peer, payload): diff --git a/submodules/TelegramUIPreferences/Sources/ExperimentalUISettings.swift b/submodules/TelegramUIPreferences/Sources/ExperimentalUISettings.swift index fd9a87700e..feeb42b98f 100644 --- a/submodules/TelegramUIPreferences/Sources/ExperimentalUISettings.swift +++ b/submodules/TelegramUIPreferences/Sources/ExperimentalUISettings.swift @@ -42,7 +42,7 @@ public struct ExperimentalUISettings: Codable, Equatable { public var inlineStickers: Bool public var localTranscription: Bool public var enableReactionOverrides: Bool - public var inlineForums: Bool + public var browserExperiment: Bool public var accountReactionEffectOverrides: [AccountReactionOverrides] public var accountStickerEffectOverrides: [AccountReactionOverrides] public var disableQuickReaction: Bool @@ -76,7 +76,7 @@ public struct ExperimentalUISettings: Codable, Equatable { inlineStickers: false, localTranscription: false, enableReactionOverrides: false, - inlineForums: false, + browserExperiment: false, accountReactionEffectOverrides: [], accountStickerEffectOverrides: [], disableQuickReaction: false, @@ -111,7 +111,7 @@ public struct ExperimentalUISettings: Codable, Equatable { inlineStickers: Bool, localTranscription: Bool, enableReactionOverrides: Bool, - inlineForums: Bool, + browserExperiment: Bool, accountReactionEffectOverrides: [AccountReactionOverrides], accountStickerEffectOverrides: [AccountReactionOverrides], disableQuickReaction: Bool, @@ -143,7 +143,7 @@ public struct ExperimentalUISettings: Codable, Equatable { self.inlineStickers = inlineStickers self.localTranscription = localTranscription self.enableReactionOverrides = enableReactionOverrides - self.inlineForums = inlineForums + self.browserExperiment = browserExperiment self.accountReactionEffectOverrides = accountReactionEffectOverrides self.accountStickerEffectOverrides = accountStickerEffectOverrides self.disableQuickReaction = disableQuickReaction @@ -179,7 +179,7 @@ public struct ExperimentalUISettings: Codable, Equatable { self.inlineStickers = (try container.decodeIfPresent(Int32.self, forKey: "inlineStickers") ?? 0) != 0 self.localTranscription = (try container.decodeIfPresent(Int32.self, forKey: "localTranscription") ?? 0) != 0 self.enableReactionOverrides = try container.decodeIfPresent(Bool.self, forKey: "enableReactionOverrides") ?? false - self.inlineForums = try container.decodeIfPresent(Bool.self, forKey: "inlineForums") ?? false + self.browserExperiment = try container.decodeIfPresent(Bool.self, forKey: "browserExperiment") ?? false self.accountReactionEffectOverrides = (try? container.decodeIfPresent([AccountReactionOverrides].self, forKey: "accountReactionEffectOverrides")) ?? [] self.accountStickerEffectOverrides = (try? container.decodeIfPresent([AccountReactionOverrides].self, forKey: "accountStickerEffectOverrides")) ?? [] self.disableQuickReaction = try container.decodeIfPresent(Bool.self, forKey: "disableQuickReaction") ?? false @@ -215,7 +215,7 @@ public struct ExperimentalUISettings: Codable, Equatable { try container.encode((self.inlineStickers ? 1 : 0) as Int32, forKey: "inlineStickers") try container.encode((self.localTranscription ? 1 : 0) as Int32, forKey: "localTranscription") try container.encode(self.enableReactionOverrides, forKey: "enableReactionOverrides") - try container.encode(self.inlineForums, forKey: "inlineForums") + try container.encode(self.browserExperiment, forKey: "browserExperiment") try container.encode(self.accountReactionEffectOverrides, forKey: "accountReactionEffectOverrides") try container.encode(self.accountStickerEffectOverrides, forKey: "accountStickerEffectOverrides") try container.encode(self.disableQuickReaction, forKey: "disableQuickReaction")