Repository: instructure/canvas-ios
Pull request #3269: [Horizon] Inbox - Conversations details double back button
Author: login: Ahmed-Naguib93
Labels: name: horizon
Created: 2025-03-25T14:54:32Z | Merged: 2025-03-27T15:05:08Z

### What is new ?
1- Fix notebook in page view
2- https://instructure.atlassian.net/browse/CLX-1016
3- https://instructure.atlassian.net/browse/CLX-1065
4- https://instructure.atlassian.net/browse/CLX-1058
5- https://instructure.atlassian.net/browse/CLX-1064


Diff:
diff --git a/Core/Core/Common/Extensions/WebKit/WKWebViewExtensions.swift b/Core/Core/Common/Extensions/WebKit/WKWebViewExtensions.swift
index 07eb27146c..b588074364 100644
--- a/Core/Core/Common/Extensions/WebKit/WKWebViewExtensions.swift
+++ b/Core/Core/Common/Extensions/WebKit/WKWebViewExtensions.swift
@@ -64,15 +64,15 @@ public extension WKWebView {
     }
 }
 
-private class MessagePasser: NSObject, WKScriptMessageHandler {
+public class MessagePasser: NSObject, WKScriptMessageHandler {
     let handler: MessageHandler
 
-    init(handler: @escaping MessageHandler) {
+    public init(handler: @escaping MessageHandler) {
         self.handler = handler
         super.init()
     }
 
-    func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
+    public func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
         handler(message)
     }
 }
diff --git a/Horizon/Horizon/Sources/Common/View/EmbeddedWebPage/EmbeddedWebPageNavigation.swift b/Horizon/Horizon/Sources/Common/View/EmbeddedWebPage/EmbeddedWebPageNavigation.swift
index d842de3759..4f0b6b44e6 100644
--- a/Horizon/Horizon/Sources/Common/View/EmbeddedWebPage/EmbeddedWebPageNavigation.swift
+++ b/Horizon/Horizon/Sources/Common/View/EmbeddedWebPage/EmbeddedWebPageNavigation.swift
@@ -18,6 +18,7 @@
 
 import Core
 import Foundation
+import WebKit
 
 protocol EmbeddedWebPageNavigation: AnyObject {
     func openURL(_ url: URL, viewController: WeakViewController)
diff --git a/Horizon/Horizon/Sources/Common/View/EmbeddedWebPage/HEmbeddedWebPageContainerView.swift b/Horizon/Horizon/Sources/Common/View/EmbeddedWebPage/HEmbeddedWebPageContainerView.swift
index a6f168347c..bf1ea84b8b 100644
--- a/Horizon/Horizon/Sources/Common/View/EmbeddedWebPage/HEmbeddedWebPageContainerView.swift
+++ b/Horizon/Horizon/Sources/Common/View/EmbeddedWebPage/HEmbeddedWebPageContainerView.swift
@@ -24,25 +24,27 @@ struct HEmbeddedWebPageContainerView: View {
     @Environment(\.viewController) private var viewController
     private var features: [CoreWebViewFeature] = [
         .disableZoom,
-        .darkModeForWebDiscussions,
         .forceDisableHorizontalScroll,
-        .hidePeerReviewLinkInWebDiscussions
+        .pullToRefresh(color: UIColor(Color.huiColors.surface.institution))
     ]
 
     // MARK: - Dependencies
 
     private let viewModel: HEmbeddedWebPageContainerViewModel
 
-    init(viewModel: HEmbeddedWebPageContainerViewModel) {
+    init(
+        viewModel: HEmbeddedWebPageContainerViewModel,
+        features: [CoreWebViewFeature] = []
+    ) {
         self.viewModel = viewModel
-        features.append(.pullToRefresh(color: UIColor(Color.huiColors.surface.institution)))
+        self.features.append(contentsOf: features)
     }
 
     var body: some View {
         if let url = viewModel.url {
             contentView(url: url)
                 .navigationBarTitleView(title: viewModel.navTitle, subtitle: nil)
-                .toolbar(.visible)
+                .toolbar(.hidden)
         }
     }
 
@@ -59,7 +61,10 @@ struct HEmbeddedWebPageContainerView: View {
                 return true
             }
             .onProvisionalNavigationStarted { webView, navigation in
-                viewModel.webView(webView, didStartProvisionalNavigation: navigation)
+                viewModel.webView(
+                    webView,
+                    didStartProvisionalNavigation: navigation
+                )
             }
         }
     }
diff --git a/Horizon/Horizon/Sources/Common/View/EmbeddedWebPage/HEmbeddedWebPageContainerViewModel.swift b/Horizon/Horizon/Sources/Common/View/EmbeddedWebPage/HEmbeddedWebPageContainerViewModel.swift
index 8a797ff2b0..5440b4b368 100644
--- a/Horizon/Horizon/Sources/Common/View/EmbeddedWebPage/HEmbeddedWebPageContainerViewModel.swift
+++ b/Horizon/Horizon/Sources/Common/View/EmbeddedWebPage/HEmbeddedWebPageContainerViewModel.swift
@@ -64,8 +64,7 @@ final class HEmbeddedWebPageContainerViewModel {
         navigationDelegate?.openURL(url, viewController: viewController)
     }
 
-    func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
-        webPage.webView(
+    func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {        webPage.webView(
             webView,
             didStartProvisionalNavigation: navigation
         )
diff --git a/Horizon/Horizon/Sources/Features/Inbox/HInboxAssembly.swift b/Horizon/Horizon/Sources/Features/Inbox/HInboxAssembly.swift
index db8cece664..96d2525774 100644
--- a/Horizon/Horizon/Sources/Features/Inbox/HInboxAssembly.swift
+++ b/Horizon/Horizon/Sources/Features/Inbox/HInboxAssembly.swift
@@ -27,12 +27,8 @@ struct HInboxAssembly {
         )
 
         let viewController = CoreHostingController(
-            HEmbeddedWebPageContainerView(
-                viewModel: viewModel
-            )
+            HInboxView(viewModel: viewModel)
         )
-        let nav = CoreNavigationController(rootViewController: viewController)
-        nav.navigationBar.useGlobalNavStyle()
         return viewController
     }
 }
diff --git a/Horizon/Horizon/Sources/Features/Inbox/View/BackButton.swift b/Horizon/Horizon/Sources/Features/Inbox/View/BackButton.swift
new file mode 100644
index 0000000000..e3df19e1e4
--- /dev/null
+++ b/Horizon/Horizon/Sources/Features/Inbox/View/BackButton.swift
@@ -0,0 +1,57 @@
+//
+// This file is part of Canvas.
+// Copyright (C) 2025-present  Instructure, Inc.
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program.  If not, see <https://www.gnu.org/licenses/>.
+//
+
+import Core
+
+private class BackButton: CoreWebViewFeature {
+    private let handlerName = "buttonTapped"
+    private let script: String
+    private let callback: () -> Void
+
+    init(callback: @escaping () -> Void) {
+        self.callback = callback
+
+        self.script =
+            """
+            document.addEventListener('click', function(event) {
+                var element = event.target;
+                while (element) {
+                    if (element.id === 'native-inbox-back-button') {
+                        window.webkit.messageHandlers.buttonTapped.postMessage(element.innerText);
+                        break;
+                    }
+                    element = element.parentElement;
+                }
+            });
+            """
+    }
+
+    override func apply(on webView: CoreWebView) {
+        webView.handle(handlerName) { [callback] _ in
+            callback()
+        }
+
+        webView.addScript(script)
+    }
+}
+
+extension CoreWebViewFeature {
+    static func onTapBackButton(callback: @escaping () -> Void) -> CoreWebViewFeature {
+        BackButton(callback: callback)
+    }
+}
diff --git a/Horizon/Horizon/Sources/Features/Inbox/View/HInboxView.swift b/Horizon/Horizon/Sources/Features/Inbox/View/HInboxView.swift
new file mode 100644
index 0000000000..9d23a0a28b
--- /dev/null
+++ b/Horizon/Horizon/Sources/Features/Inbox/View/HInboxView.swift
@@ -0,0 +1,33 @@
+//
+// This file is part of Canvas.
+// Copyright (C) 2025-present  Instructure, Inc.
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program.  If not, see <https://www.gnu.org/licenses/>.
+//
+
+import SwiftUI
+import Core
+
+struct HInboxView: View {
+    @Environment(\.dismiss) private var dismiss
+    let viewModel: HEmbeddedWebPageContainerViewModel
+
+    var body: some View {
+        HEmbeddedWebPageContainerView(
+            viewModel: viewModel,
+            features: [.onTapBackButton { dismiss() }]
+        )
+        .background(Color.huiColors.surface.pagePrimary)
+    }
+}
diff --git a/Horizon/Horizon/Sources/Features/Inbox/View/HInboxViewModel.swift b/Horizon/Horizon/Sources/Features/Inbox/View/HInboxViewModel.swift
index 49acbf8b1e..fa3b6858d8 100644
--- a/Horizon/Horizon/Sources/Features/Inbox/View/HInboxViewModel.swift
+++ b/Horizon/Horizon/Sources/Features/Inbox/View/HInboxViewModel.swift
@@ -18,6 +18,7 @@
 
 import Core
 import Foundation
+import WebKit
 
 final class HInboxViewModel: EmbeddedWebPageViewModel {
     var urlPathComponent: String
diff --git a/Horizon/Horizon/Sources/Features/LearningObjects/Assignment/MyAssignmentSubmissions/View/MyAssignmentSubmissionsView.swift b/Horizon/Horizon/Sources/Features/LearningObjects/Assignment/MyAssignmentSubmissions/View/MyAssignmentSubmissionsView.swift
index 8dfc7492cc..16f567fae7 100644
--- a/Horizon/Horizon/Sources/Features/LearningObjects/Assignment/MyAssignmentSubmissions/View/MyAssignmentSubmissionsView.swift
+++ b/Horizon/Horizon/Sources/Features/LearningObjects/Assignment/MyAssignmentSubmissions/View/MyAssignmentSubmissionsView.swift
@@ -138,7 +138,8 @@ struct MyAssignmentSubmissionsView: View {
                 isFinishLoading: .constant(false),
                 contentHeight: $uploadedFileHeight,
                 context: .course(courseId),
-                fileID: fileID
+                fileID: fileID,
+                isScrollEnabled: false
             )
             .id(fileID)
             .frame(height: uploadedFileHeight)
diff --git a/Horizon/Horizon/Sources/Features/LearningObjects/FileDetails/Views/FileDetailsViewRepresentable.swift b/Horizon/Horizon/Sources/Features/LearningObjects/FileDetails/Views/FileDetailsViewRepresentable.swift
index a84ade22d0..fafe444468 100644
--- a/Horizon/Horizon/Sources/Features/LearningObjects/FileDetails/Views/FileDetailsViewRepresentable.swift
+++ b/Horizon/Horizon/Sources/Features/LearningObjects/FileDetails/Views/FileDetailsViewRepresentable.swift
@@ -28,28 +28,31 @@ struct FileDetailsViewRepresentable: UIViewControllerRepresentable {
     @Binding private var contentHeight: CGFloat
     private let context: Core.Context?
     private let fileID: String
+    private let isScrollEnabled: Bool
 
     init(isScrollTopReached: Binding<Bool>,
          isFinishLoading: Binding<Bool>,
          contentHeight: Binding<CGFloat> = .constant(0.0),
          context: Core.Context?,
-         fileID: String
+         fileID: String,
+         isScrollEnabled: Bool = true
     ) {
         self._isScrollTopReached = isScrollTopReached
         self._isFinishLoading = isFinishLoading
         self._contentHeight = contentHeight
         self.context = context
         self.fileID = fileID
+        self.isScrollEnabled = isScrollEnabled
     }
 
     func makeUIViewController(context: Self.Context) -> UIViewController {
         let viewController = FileDetailsViewController.create(context: self.context, fileID: fileID)
         viewController.didFinishLoading = {
-                isFinishLoading = true
+            isFinishLoading = true
             if let scrollView = findScrollView(in: viewController.view) {
                 // Set the file pin at the top.
                 scrollView.setContentOffset(CGPoint(x: 0, y: 0), animated: true)
-                scrollView.isScrollEnabled = false
+                scrollView.isScrollEnabled = isScrollEnabled
                 // Wait till render the view
                 DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {
                     contentHeight = max(300, scrollView.contentSize.height)
@@ -94,16 +97,52 @@ struct FileDetailsViewRepresentable: UIViewControllerRepresentable {
     // MARK: - Coordinator
 
     final class Coordinator: NSObject, UIScrollViewDelegate {
-        let didScroll: (Bool) -> Void
         private let threshold: CGFloat = 100
+        private var lastYOffset: CGFloat = 0
+        private var lastZoomScale: CGFloat = 1.0
+        private let minDelta: CGFloat = 0.1
+        private let minScrollThreshold: CGFloat = 5.0
+        private var yOffsets: [CGFloat] = []
+
+        var isZoomingIn = false
+        let didScroll: (Bool) -> Void
 
         init(didScroll: @escaping (Bool) -> Void) {
             self.didScroll = didScroll
         }
 
         func scrollViewDidScroll(_ scrollView: UIScrollView) {
-            let yOffset = scrollView.contentOffset.y
-            didScroll(yOffset > threshold)
+            let yOffset = scrollView.contentOffset.y.rounded()
+            let delta = abs(yOffset - lastYOffset)
+
+            guard delta > minScrollThreshold else { return }
+
+            lastYOffset = yOffset
+            yOffsets.append(yOffset)
+
+            if yOffsets.count == 10 {
+                let averageYOffset = yOffsets.reduce(0, +) / CGFloat(yOffsets.count)
+                didScroll(isZoomingIn || averageYOffset > threshold)
+                yOffsets.removeAll()
+            }
+        }
+
+        func scrollViewDidZoom(_ scrollView: UIScrollView) {
+            let zoomScale = scrollView.zoomScale
+            guard abs(zoomScale - lastZoomScale) > minDelta else { return }
+
+            isZoomingIn = zoomScale > 1
+            lastZoomScale = zoomScale
+        }
+
+        func scrollViewDidEndZooming(
+            _ scrollView: UIScrollView,
+            with view: UIView?,
+            atScale scale: CGFloat
+        ) {
+            DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { [weak self] in
+                self?.isZoomingIn = false
+            }
         }
     }
 }
diff --git a/Horizon/Horizon/Sources/Features/ModuleItemSequence/Sequence/View/PageDetails/View/PageDetailsView.swift b/Horizon/Horizon/Sources/Features/ModuleItemSequence/Sequence/View/PageDetails/View/PageDetailsView.swift
index 0779f7cf99..797ea0914b 100644
--- a/Horizon/Horizon/Sources/Features/ModuleItemSequence/Sequence/View/PageDetails/View/PageDetailsView.swift
+++ b/Horizon/Horizon/Sources/Features/ModuleItemSequence/Sequence/View/PageDetails/View/PageDetailsView.swift
@@ -36,16 +36,13 @@ struct PageDetailsView: View {
     }
 
     var body: some View {
-        ScrollView {
-            VStack(spacing: .zero) {
-                topView
-                WebView(
-                    html: viewModel.content,
-                    baseURL: viewModel.url
-                )
-                .frameToFit()
-            }
-
+        ZStack(alignment: .bottomTrailing) {
+            PageViewRepresentable(
+                isScrollTopReached: $isShowHeader,
+                context: viewModel.context,
+                pageURL: viewModel.pageURL,
+                itemID: viewModel.itemID
+            )
             if viewModel.isMarkedAsDoneButtonVisible {
                 MarkAsDoneButton(
                     isCompleted: viewModel.isCompletedItem,
@@ -53,28 +50,13 @@ struct PageDetailsView: View {
                 ) {
                     viewModel.markAsDone()
                 }
-                .padding(.huiSpaces.space24)
+                .padding(.horizontal, .huiSpaces.space24)
+                .padding(.bottom, .huiSpaces.space16)
             }
         }
-        .overlay { loaderView }
         .preference(key: HeaderVisibilityKey.self, value: isShowHeader)
         .alert(isPresented: $viewModel.isShowErrorAlert) {
             Alert(title: Text(viewModel.errorMessage))
         }
     }
-
-    @ViewBuilder
-    private var loaderView: some View {
-        if viewModel.isLoaderVisible {
-            HorizonUI.Spinner(size: .small, showBackground: true)
-        }
-    }
-
-    private var topView: some View {
-        Color.clear
-            .frame(height: 0)
-            .readingFrame { frame in
-                isShowHeader = frame.minY > -100
-            }
-    }
 }
diff --git a/Horizon/Horizon/Sources/Features/ModuleItemSequence/Sequence/View/PageDetails/View/PageDetailsViewModel.swift b/Horizon/Horizon/Sources/Features/ModuleItemSequence/Sequence/View/PageDetails/View/PageDetailsViewModel.swift
index c491dbb82c..ad62ee728a 100644
--- a/Horizon/Horizon/Sources/Features/ModuleItemSequence/Sequence/View/PageDetails/View/PageDetailsViewModel.swift
+++ b/Horizon/Horizon/Sources/Features/ModuleItemSequence/Sequence/View/PageDetails/View/PageDetailsViewModel.swift
@@ -28,7 +28,6 @@ final class PageDetailsViewModel {
     private(set) var url: URL?
     private(set) var content: String?
     private(set) var isCompletedItem: Bool
-    private(set) var isLoaderVisible = true
     private(set) var isMarkAsDoneLoaderVisible = false
     private(set) var errorMessage = ""
 
@@ -44,7 +43,9 @@ final class PageDetailsViewModel {
 
     private let moduleItemInteractor: ModuleItemSequenceInteractor
     private let moduleID: String
-    private let itemID: String
+    let context: Core.Context
+    let itemID: String
+    let pageURL: String
     let isMarkedAsDoneButtonVisible: Bool
 
     // MARK: - Init
@@ -59,23 +60,12 @@ final class PageDetailsViewModel {
         isMarkedAsDoneButtonVisible: Bool
     ) {
         self.moduleItemInteractor = moduleItemInteractor
+        self.context = context
         self.isCompletedItem = isCompletedItem
+        self.pageURL = pageURL
         self.moduleID = moduleID
         self.itemID = itemID
         self.isMarkedAsDoneButtonVisible = isMarkedAsDoneButtonVisible
-
-        ReactiveStore(
-            useCase: GetPage(context: context, url: pageURL)
-        )
-        .getEntities()
-        .replaceError(with: [])
-        .sink { [weak self] values in
-            let page = values.first
-            self?.content = page?.body
-            self?.url = page?.htmlURL
-            self?.isLoaderVisible = false
-        }
-        .store(in: &subscriptions)
     }
 
     func markAsDone() {
diff --git a/Horizon/Horizon/Sources/Features/ModuleItemSequence/Sequence/View/PageDetails/View/PageViewRepresentable.swift b/Horizon/Horizon/Sources/Features/ModuleItemSequence/Sequence/View/PageDetails/View/PageViewRepresentable.swift
new file mode 100644
index 0000000000..5aceef353d
--- /dev/null
+++ b/Horizon/Horizon/Sources/Features/ModuleItemSequence/Sequence/View/PageDetails/View/PageViewRepresentable.swift
@@ -0,0 +1,104 @@
+//
+// This file is part of Canvas.
+// Copyright (C) 2025-present  Instructure, Inc.
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program.  If not, see <https://www.gnu.org/licenses/>.
+//
+
+import Core
+import SwiftUI
+
+struct PageViewRepresentable: UIViewControllerRepresentable {
+    // MARK: - Dependencies
+
+    @Binding private var isScrollTopReached: Bool
+    let context: Core.Context
+    let pageURL: String
+    let itemID: String
+
+    init(
+        isScrollTopReached: Binding<Bool>,
+        context: Core.Context,
+        pageURL: String,
+        itemID: String
+    ) {
+        self._isScrollTopReached = isScrollTopReached
+        self.context = context
+        self.pageURL = pageURL
+        self.itemID = itemID
+    }
+
+    func makeUIViewController(context: Self.Context) -> UIViewController {
+        let viewController = PageDetailsViewController.create(
+            context: self.context,
+            pageURL: pageURL,
+            app: .student,
+            env: .shared
+        )
+
+        viewController.webView = HighlightWebView(
+            courseId: self.context.id,
+            itemId: itemID,
+            moduleType: .page(pageURL),
+            viewController: WeakViewController(viewController)
+        )
+        if let scrollView = findScrollView(in: viewController.view) {
+            scrollView.delegate = context.coordinator
+        }
+        return viewController
+    }
+
+    func updateUIViewController(
+        _ uiViewController: UIViewController,
+        context: Self.Context
+    ) {
+        if let scrollView = findScrollView(in: uiViewController.view) {
+            scrollView.delegate = context.coordinator
+        }
+    }
+
+    func makeCoordinator() -> Coordinator {
+        Coordinator { value in
+            isScrollTopReached = !value
+        }
+    }
+
+    private func findScrollView(in view: UIView) -> UIScrollView? {
+        if let scrollView = view as? UIScrollView {
+            return scrollView
+        }
+        for subview in view.subviews {
+            if let scrollView = findScrollView(in: subview) {
+                return scrollView
+            }
+        }
+        return nil
+    }
+
+    // MARK: - Coordinator
+
+    final class Coordinator: NSObject, UIScrollViewDelegate {
+        let didScroll: (Bool) -> Void
+        private let threshold: CGFloat = 100
+
+        init(didScroll: @escaping (Bool) -> Void) {
+            self.didScroll = didScroll
+        }
+
+        func scrollViewDidScroll(_ scrollView: UIScrollView) {
+            let yOffset = scrollView.contentOffset.y
+            didScroll(yOffset > threshold)
+        }
+    }
+}


Reviews:
user: login: szabinst
body: Thanks for these fixes, I still found some issues: 

- I think we should add a refresh mechanism to the Inbox
- We should make the status bar the same background color as the inbox. 
- Entering full screen video view within a page will not cover the top part, see the attached video. 
 
https://github.com/user-attachments/assets/c3f66043-95e5-44a0-ab3f-49a150f21dc0


state: CHANGES_REQUESTED
author_association: COLLABORATOR
submitted_at: 2025-03-25T17:49:21Z
user: login: Ahmed-Naguib93
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-27T09:04:06Z
user: login: szabinst
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-03-27T15:02:43Z

Review comments:
diff_hunk: @@ -23,16 +23,32 @@ struct HInboxAssembly {
     static func makeView() -> UIViewController {
         let inboxViewModel = HInboxViewModel()
         let viewModel = HEmbeddedWebPageContainerViewModel(
-            webPage: inboxViewModel
+            webPage: inboxViewModel,
+            navigationDelegate: inboxViewModel,
+            javaScript: buttonTappedJS,
+            observeKey: "buttonTapped"
         )
 
         let viewController = CoreHostingController(
             HEmbeddedWebPageContainerView(
                 viewModel: viewModel
             )
         )
-        let nav = CoreNavigationController(rootViewController: viewController)
-        nav.navigationBar.useGlobalNavStyle()
         return viewController
     }
+
+    private static var buttonTappedJS: String {
+         """
+        document.addEventListener('click', function(event) {
+            var element = event.target;
+            while (element) {
+                if (element.tagName === 'BUTTON') {
+                    window.webkit.messageHandlers.buttonTapped.postMessage(element.innerText);
+                    break;
+                }
+                element = element.parentElement;
+            }
+        });
+        """
+    }
path: Horizon/Horizon/Sources/Features/Inbox/HInboxAssembly.swift
user: login: szabinst
body: This will catch all the button events so there's no way to navigate inside the webview. 

https://github.com/user-attachments/assets/419cab6e-3aa0-495a-8ecd-aaf2c9a6e123


created_at: 2025-03-25T17:44:08Z
author_association: COLLABORATOR
diff_hunk: @@ -27,24 +27,53 @@ final class HEmbeddedWebPageContainerViewModel {
 
     private(set) var url: URL?
     let navTitle: String
-
+    private(set) var configuration: WKWebViewConfiguration = .defaultConfiguration
+    
     // MARK: - Dependencies
 
     private let webPage: EmbeddedWebPageViewModel
     private weak var navigationDelegate: EmbeddedWebPageNavigation?
+    private let javaScript: String?
+    private let observeKey: String?
+    var viewController: WeakViewController = .init()
 
     // MARK: - Init
 
     init(
         webPage: EmbeddedWebPageViewModel,
-        navigationDelegate: EmbeddedWebPageNavigation? = nil
+        navigationDelegate: EmbeddedWebPageNavigation? = nil,
+        javaScript: String? = nil,
+        observeKey: String? = nil
     ) {
         self.webPage = webPage
         self.navigationDelegate = navigationDelegate
         self.navTitle = webPage.navigationBarTitle
+        self.javaScript = javaScript
+        self.observeKey = observeKey
         self.url = constructURL(from: webPage)
+
+        if let javaScript, let observeKey {
+            addObservation(javaScript: javaScript, observeKey: observeKey)
+        }
     }
 
+    private func addObservation(javaScript js: String, observeKey: String) {
+        let script = WKUserScript(source: js, injectionTime: .atDocumentEnd, forMainFrameOnly: true)
+        configuration.userContentController.addUserScript(script)
+        handle(observeKey) { [weak self] message in
+            guard let self else {
+                return
+            }
+            self.navigationDelegate?.observer(scriptMessage: message, viewController: self.viewController)
+        }
+    }
+
+    private func handle(_ name: String, handler: @escaping MessageHandler) {
+         let passer = MessagePasser(handler: handler)
+         configuration.userContentController.removeScriptMessageHandler(forName: name)
+         configuration.userContentController.add(passer, name: name)
+     }
path: Horizon/Horizon/Sources/Common/View/EmbeddedWebPage/HEmbeddedWebPageContainerViewModel.swift
user: login: szabinst
body: Can you make this whole javascript logic a `CoreWebViewFeature`? 
created_at: 2025-03-25T17:44:14Z
author_association: COLLABORATOR
diff_hunk: @@ -59,23 +60,12 @@ final class PageDetailsViewModel {
         isMarkedAsDoneButtonVisible: Bool
     ) {
         self.moduleItemInteractor = moduleItemInteractor
+        self.context = context
         self.isCompletedItem = isCompletedItem
+        self.pageURL = pageURL
         self.moduleID = moduleID
         self.itemID = itemID
         self.isMarkedAsDoneButtonVisible = isMarkedAsDoneButtonVisible
-
-        ReactiveStore(
-            useCase: GetPage(context: context, url: pageURL)
-        )
-        .getEntities()
-        .replaceError(with: [])
-        .sink { [weak self] values in
-            let page = values.first
-            self?.content = page?.body
-            self?.url = page?.htmlURL
-            self?.isLoaderVisible = false
-        }
-        .store(in: &subscriptions)
     }
path: Horizon/Horizon/Sources/Features/ModuleItemSequence/Sequence/View/PageDetails/View/PageDetailsViewModel.swift
user: login: szabinst
body: We might want to keep this because what happens if we open this page from a notification? 
created_at: 2025-03-25T17:45:52Z
author_association: COLLABORATOR
path: Horizon/Horizon/Sources/Features/LearningObjects/FileDetails/Views/FileDetailsViewRepresentable.swift
user: login: szabinst
body: Zooming in a file still jitters.

https://github.com/user-attachments/assets/7b6d8896-6f70-40eb-adc2-05ca30bab861


created_at: 2025-03-25T17:46:46Z
author_association: COLLABORATOR
diff_hunk: @@ -59,23 +60,12 @@ final class PageDetailsViewModel {
         isMarkedAsDoneButtonVisible: Bool
     ) {
         self.moduleItemInteractor = moduleItemInteractor
+        self.context = context
         self.isCompletedItem = isCompletedItem
+        self.pageURL = pageURL
         self.moduleID = moduleID
         self.itemID = itemID
         self.isMarkedAsDoneButtonVisible = isMarkedAsDoneButtonVisible
-
-        ReactiveStore(
-            useCase: GetPage(context: context, url: pageURL)
-        )
-        .getEntities()
-        .replaceError(with: [])
-        .sink { [weak self] values in
-            let page = values.first
-            self?.content = page?.body
-            self?.url = page?.htmlURL
-            self?.isLoaderVisible = false
-        }
-        .store(in: &subscriptions)
     }
path: Horizon/Horizon/Sources/Features/ModuleItemSequence/Sequence/View/PageDetails/View/PageDetailsViewModel.swift
user: login: Ahmed-Naguib93
body: No, now we are using PageDetailsViewController 
created_at: 2025-03-27T09:04:06Z
author_association: CONTRIBUTOR

PR comments:
user: login: inst-danger
created_at: 2025-03-25T15:05:07Z
body: <details><summary>Horizon Build QR Code:</summary>
<img src="https://api.qrserver.com/v1/create-qr-code/?data=https%3A%2F%2Fapp.bitrise.io%2Fapp%2F88a3641e29e4745c%2Finstallable-artifacts%2Fd0abc2eb4fc6a6a3%2Fpublic-install-page%2Fd70f9bf79a46441e954a4752659e7c96&size=150x150" />
</details>


<!-- HorizonBuild -->
author_association: CONTRIBUTOR
user: login: inst-danger
created_at: 2025-03-25T15:09:45Z
body: 
<!--
  1 failure:  Build failed, ski...
  0 warning: 
  
  1 markdown notices
  DangerID: danger-id-Danger;
-->

<table>
  <thead>
    <tr>
      <th width="50"></th>
      <th width="100%" data-danger-table="true">Fails</th>
    </tr>
  </thead>
  <tbody><tr>
      <td>:no_entry_sign:</td>
      <td>Build failed, skipping coverage check</td>
    </tr>
  </tbody>
</table>



<details><summary>:x: XCTest failed: ParentUnitTests/ParentInboxCoursePickerInteractorLiveTests/testPopulatesListItems</summary>


    XCTAssertEqual failed: ("Optional("Course 2")") is not equal to ("Optional("Course 1")")
    XCTAssertEqual failed: ("Student 2") is not equal to ("Student 1")
    XCTAssertEqual failed: ("Optional("Course 1")") is not equal to ("Optional("Course 2")")
    XCTAssertEqual failed: ("Student 1") is not equal to ("Student 2")
</details>

<p align="right">
  Generated by :no_entry_sign: <a href="https://danger.systems/js">dangerJS</a> against fe890967399b316fbeffc3da915dff55a8f04ef4
</p>

author_association: CONTRIBUTOR