Skip to content

Commit

Permalink
[DuckPlayer] - 3. URL management & FE comms (#3007)
Browse files Browse the repository at this point in the history
Task/Issue URL: https://app.asana.com/0/1204099484721401/1207658406526197/f

Description:

Introduces a custom OmniBar icon to be used on specific cases (Youtube videos in this case)
Updates the URL to properly display duck:// addresses when DP is enabled
Listen to AppSettings changes for DuckPlayer via NotificationCenter and pass them down to DuckPlayer and the UserScripts > FrontEnd via Combine
Handle reloads when visiting a DuckPlayer page.
Renames methods in DuckNavigationHandling for clarity
@MainActorize all WebView navigation events
Updated tests
  • Loading branch information
afterxleep authored Jul 1, 2024
1 parent b815f41 commit fd7f201
Show file tree
Hide file tree
Showing 17 changed files with 282 additions and 120 deletions.
25 changes: 23 additions & 2 deletions DuckDuckGo/AddressDisplayHelper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,18 +24,24 @@ extension OmniBar {
struct AddressDisplayHelper {

static func addressForDisplay(url: URL, showsFullURL: Bool) -> NSAttributedString {


if url.isDuckPlayer,
let playerURL = getDuckPlayerURL(url: url, showsFullURL: showsFullURL) {
return playerURL
}

if !showsFullURL, let shortAddress = shortURLString(url) {
return NSAttributedString(
string: shortAddress,
attributes: [.foregroundColor: ThemeManager.shared.currentTheme.searchBarTextColor])

} else {
return deemphasisePath(forUrl: url)
}
}

static func deemphasisePath(forUrl url: URL) -> NSAttributedString {

let s = url.absoluteString
let attributedString = NSMutableAttributedString(string: s)
guard let c = URLComponents(url: url, resolvingAgainstBaseURL: true) else {
Expand Down Expand Up @@ -72,5 +78,20 @@ extension OmniBar {

return url.host?.droppingWwwPrefix()
}

private static func getDuckPlayerURL(url: URL, showsFullURL: Bool) -> NSAttributedString? {
if !showsFullURL {
return NSAttributedString(
string: UserText.duckPlayerFeatureName,
attributes: [.foregroundColor: ThemeManager.shared.currentTheme.searchBarTextColor])
} else {
if let (videoID, _) = url.youtubeVideoParams {
return NSAttributedString(
string: URL.duckPlayer(videoID).absoluteString,
attributes: [.foregroundColor: ThemeManager.shared.currentTheme.searchBarTextColor])
}
}
return nil
}
}
}
5 changes: 4 additions & 1 deletion DuckDuckGo/AppUserDefaults.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ public class AppUserDefaults: AppSettings {
public static let addressBarPositionChanged = Notification.Name("com.duckduckgo.app.AddressBarPositionChanged")
public static let showsFullURLAddressSettingChanged = Notification.Name("com.duckduckgo.app.ShowsFullURLAddressSettingChanged")
public static let autofillDebugScriptToggled = Notification.Name("com.duckduckgo.app.DidToggleAutofillDebugScript")
public static let duckPlayerModeChanged = Notification.Name("com.duckduckgo.app.DuckPlayerModeChanged")
}

private let groupName: String
Expand Down Expand Up @@ -389,9 +390,11 @@ public class AppUserDefaults: AppSettings {
return .alwaysAsk
}
set {
// Here we set both the DuckPlayer mode and the overlayInteracte
// Here we set both the DuckPlayer mode and the duckPlayerAskModeOverlayHidden
userDefaults?.set(newValue.stringValue, forKey: Keys.duckPlayerMode)
userDefaults?.set(false, forKey: Keys.duckPlayerAskModeOverlayHidden)
NotificationCenter.default.post(name: AppUserDefaults.Notifications.duckPlayerModeChanged,
object: duckPlayerMode)
}
}
}
Expand Down
11 changes: 6 additions & 5 deletions DuckDuckGo/DuckPlayer/DuckNavigationHandling.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,12 @@ protocol DuckNavigationHandling {
func handleNavigation(_ navigationAction: WKNavigationAction,
webView: WKWebView,
completion: @escaping (WKNavigationActionPolicy) -> Void)
func handleRedirect(url: URL?, webView: WKWebView)
func handleRedirect(_ navigationAction: WKNavigationAction,
completion: @escaping (WKNavigationActionPolicy) -> Void,
webView: WKWebView)
func goBack(webView: WKWebView)
func handleURLChange(url: URL?, webView: WKWebView)
func handleDecidePolicyFor(_ navigationAction: WKNavigationAction,
completion: @escaping (WKNavigationActionPolicy) -> Void,
webView: WKWebView)
func handleGoBack(webView: WKWebView)
func handleReload(webView: WKWebView)
}

extension WKWebView {
Expand Down
29 changes: 25 additions & 4 deletions DuckDuckGo/DuckPlayer/DuckPlayer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ enum DuckPlayerMode: Equatable, Codable, CustomStringConvertible, CaseIterable {

private static let enabledString = "enabled"
private static let alwaysAskString = "alwaysAsk"
private static let neverString = "alwaysAsk"
private static let neverString = "disabled"

var description: String {
switch self {
Expand Down Expand Up @@ -99,7 +99,7 @@ public struct UserValues: Codable {

final class DuckPlayerSettings {

private var appSettings: AppSettings
var appSettings: AppSettings

init(appSettings: AppSettings = AppDependencyProvider.shared.appSettings) {
self.appSettings = appSettings
Expand Down Expand Up @@ -132,9 +132,12 @@ final class DuckPlayer {

private var settings: DuckPlayerSettings


init(settings: DuckPlayerSettings = DuckPlayerSettings()) {
@Published var userValues: UserValues

init(settings: DuckPlayerSettings = DuckPlayerSettings(), userValues: UserValues? = nil) {
self.settings = settings
self.userValues = userValues ?? UserValues(duckPlayerMode: settings.mode, askModeOverlayHidden: settings.askModeOverlayHidden)
registerForNotificationChanges()
}

// MARK: - Common Message Handlers
Expand All @@ -144,8 +147,10 @@ final class DuckPlayer {
assertionFailure("DuckPlayer: expected JSON representation of UserValues")
return nil
}

settings.mode = userValues.duckPlayerMode
settings.askModeOverlayHidden = userValues.askModeOverlayHidden

return userValues
}

Expand Down Expand Up @@ -181,5 +186,21 @@ final class DuckPlayer {

return InitialSetupSettings(userValues: userValues, settings: playerSettings)
}

private func registerForNotificationChanges() {
NotificationCenter.default.addObserver(self,
selector: #selector(updatePlayerMode),
name: AppUserDefaults.Notifications.duckPlayerModeChanged,
object: nil)
}


@objc private func updatePlayerMode(_ notification: Notification) {
userValues = encodeUserValues()
}

deinit {
NotificationCenter.default.removeObserver(self)
}

}
80 changes: 71 additions & 9 deletions DuckDuckGo/DuckPlayer/YouTubePlayerNavigationHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,14 @@ import Foundation
import ContentScopeScripts
import WebKit

struct YoutubePlayerNavigationHandler {
final class YoutubePlayerNavigationHandler {

var duckPlayerMode: DuckPlayerMode

init(duckPlayerMode: DuckPlayerMode = AppDependencyProvider.shared.appSettings.duckPlayerMode) {
self.duckPlayerMode = duckPlayerMode
registerForNotificationChanges()
}

private static let templateDirectory = "pages/duckplayer"
private static let templateName = "index"
Expand Down Expand Up @@ -69,14 +76,40 @@ struct YoutubePlayerNavigationHandler {
let duckPlayerRequest = Self.makeDuckPlayerRequest(from: request)
performNavigation(duckPlayerRequest, responseHTML: html, webView: webView)
}

private func registerForNotificationChanges() {
NotificationCenter.default.addObserver(self,
selector: #selector(updatePlayerMode),
name: AppUserDefaults.Notifications.duckPlayerModeChanged,
object: nil)
}


@objc private func updatePlayerMode(_ notification: Notification) {
if let mode = notification.object as? DuckPlayerMode {
self.duckPlayerMode = mode
}
}

deinit {
NotificationCenter.default.removeObserver(self)
}

}

extension YoutubePlayerNavigationHandler: DuckNavigationHandling {

// Handle rendering the simulated request if the URL is duck://
// and DuckPlayer is either enabled or alwaysAsk
@MainActor
func handleNavigation(_ navigationAction: WKNavigationAction,
webView: WKWebView,
completion: @escaping (WKNavigationActionPolicy) -> Void) {
if let url = navigationAction.request.url, url.isDuckPlayer {

// If DuckPlayer is Enabled or in ask mode, render the video
if let url = navigationAction.request.url,
url.isDuckURLScheme,
duckPlayerMode == .enabled || duckPlayerMode == .alwaysAsk {
let html = Self.makeHTMLFromTemplate()
let newRequest = Self.makeDuckPlayerRequest(from: URLRequest(url: url))
if #available(iOS 15.0, *) {
Expand All @@ -85,20 +118,35 @@ extension YoutubePlayerNavigationHandler: DuckNavigationHandling {
return
}
}
completion(.cancel)

// DuckPlayer is disabled, so we redirect to the video in YouTube
if let url = navigationAction.request.url, let (videoID, timestamp) = url.youtubeVideoParams, duckPlayerMode == .disabled {
webView.load(URLRequest(url: URL.youtube(videoID, timestamp: timestamp)))
completion(.allow)
return
}

completion(.allow)

}

func handleRedirect(url: URL?, webView: WKWebView) {
// Handle URL changes not triggered via Omnibar
// such as changes triggered via JS
@MainActor
func handleURLChange(url: URL?, webView: WKWebView) {
if let url = url, url.isYoutubeVideo, !url.isDuckPlayer, let (videoID, timestamp) = url.youtubeVideoParams {
webView.stopLoading()
let newURL = URL.duckPlayer(videoID, timestamp: timestamp)
webView.load(URLRequest(url: newURL))
}
}

func handleRedirect(_ navigationAction: WKNavigationAction,
completion: @escaping (WKNavigationActionPolicy) -> Void,
webView: WKWebView) {
// DecidePolicyFor handler to redirect relevant requests
// to duck://player
@MainActor
func handleDecidePolicyFor(_ navigationAction: WKNavigationAction,
completion: @escaping (WKNavigationActionPolicy) -> Void,
webView: WKWebView) {
if let url = navigationAction.request.url, url.isYoutubeVideo, !url.isDuckPlayer, let (videoID, timestamp) = url.youtubeVideoParams {
webView.load(URLRequest(url: .duckPlayer(videoID, timestamp: timestamp)))
completion(.allow)
Expand All @@ -107,13 +155,27 @@ extension YoutubePlayerNavigationHandler: DuckNavigationHandling {
completion(.cancel)
}

func goBack(webView: WKWebView) {
// Handle Webview BackButton on DuckPlayer videos
@MainActor
func handleGoBack(webView: WKWebView) {
guard let backURL = webView.backForwardList.backItem?.url,
backURL.isYoutubeVideo,
backURL.youtubeVideoParams?.videoID == webView.url?.youtubeVideoParams?.videoID else {
backURL.youtubeVideoParams?.videoID == webView.url?.youtubeVideoParams?.videoID,
duckPlayerMode == .enabled else {
webView.goBack()
return
}
webView.goBack(skippingHistoryItems: 2)
}


// Handle Reload for DuckPlayer Videos
@MainActor
func handleReload(webView: WKWebView) {
if let url = webView.url, url.isDuckPlayer, !url.isDuckURLScheme, let (videoID, timestamp) = url.youtubeVideoParams {
webView.load(URLRequest(url: .duckPlayer(videoID, timestamp: timestamp)))
} else {
webView.reload()
}
}
}
27 changes: 22 additions & 5 deletions DuckDuckGo/DuckPlayer/YoutubeOverlayUserScript.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,31 @@ import Foundation
import WebKit
import Common
import UserScript
import Combine

final class YoutubeOverlayUserScript: NSObject, Subfeature {

private var duckPlayer: DuckPlayer
private var userValuesCancellable = Set<AnyCancellable>()

struct Constants {
static let featureName = "duckPlayer"
}

init(duckPlayer: DuckPlayer) {
self.duckPlayer = duckPlayer
super.init()
subscribeToDuckPlayerMode()
}

// Listen to DuckPlayer Settings changed
private func subscribeToDuckPlayerMode() {
duckPlayer.$userValues
.dropFirst()
.sink { [weak self] updatedValues in
self?.userValuesUpdated(userValues: updatedValues)
}
.store(in: &userValuesCancellable)
}

enum MessageOrigin {
Expand Down Expand Up @@ -96,10 +110,9 @@ final class YoutubeOverlayUserScript: NSObject, Subfeature {
}

public func userValuesUpdated(userValues: UserValues) {
guard let webView = webView else {
return assertionFailure("Could not access webView")
if let webView {
broker?.push(method: "onUserValuesChanged", params: userValues, for: self, into: webView)
}
broker?.push(method: "onUserValuesChanged", params: userValues, for: self, into: webView)
}

// MARK: - Private Methods
Expand All @@ -123,6 +136,10 @@ final class YoutubeOverlayUserScript: NSObject, Subfeature {
struct UserValuesNotification: Encodable {
let userValuesNotification: UserValues
}

deinit {
userValuesCancellable.removeAll()
}
}

extension YoutubeOverlayUserScript {
Expand All @@ -131,8 +148,8 @@ extension YoutubeOverlayUserScript {
guard let body = message.messageBody as? [String: Any], let parameters = body["params"] as? [String: Any] else {
return nil
}
let pixelName = parameters["pixelName"] as? String

// let pixelName = parameters["pixelName"] as? String
// To be implemented at a later point

return nil
}
Expand Down
23 changes: 21 additions & 2 deletions DuckDuckGo/DuckPlayer/YoutubePlayerUserScript.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import WebKit
import Common
import UserScript
import Combine

final class YoutubePlayerUserScript: NSObject, Subfeature {

Expand All @@ -35,8 +36,22 @@ final class YoutubePlayerUserScript: NSObject, Subfeature {
static let initialSetup = "initialSetup"
}

private var userValuesCancellable = Set<AnyCancellable>()

init(duckPlayer: DuckPlayer) {
self.duckPlayer = duckPlayer
super.init()
subscribeToDuckPlayerMode()
}

// Listen to DuckPlayer Settings changed
private func subscribeToDuckPlayerMode() {
duckPlayer.$userValues
.dropFirst()
.sink { [weak self] updatedValues in
self?.userValuesUpdated(userValues: updatedValues)
}
.store(in: &userValuesCancellable)
}

weak var broker: UserScriptMessageBroker?
Expand Down Expand Up @@ -66,9 +81,13 @@ final class YoutubePlayerUserScript: NSObject, Subfeature {
}
}

func userValuesUpdated(userValues: UserValues) {
if let webView = webView {
public func userValuesUpdated(userValues: UserValues) {
if let webView {
broker?.push(method: "onUserValuesChanged", params: userValues, for: self, into: webView)
}
}

deinit {
userValuesCancellable.removeAll()
}
}
Loading

0 comments on commit fd7f201

Please sign in to comment.