1
- import { createCustomEvent , sendMessage , originalWindowDispatchEvent } from '../utils.js'
1
+ import { Messaging , TestTransportConfig , WebkitMessagingConfig } from '@duckduckgo/messaging'
2
+ import { createCustomEvent , originalWindowDispatchEvent } from '../utils.js'
2
3
import { logoImg , loadingImages , closeIcon } from './click-to-load/ctl-assets.js'
3
4
import { getStyles , getConfig } from './click-to-load/ctl-config.js'
5
+ import { ClickToLoadMessagingTransport } from './click-to-load/ctl-messaging-transport.js'
4
6
import ContentFeature from '../content-feature.js'
5
7
import { DDGCtlPlaceholderBlockedElement } from './click-to-load/components/ctl-placeholder-blocked.js'
6
8
import { registerCustomElements } from './click-to-load/components'
@@ -25,6 +27,9 @@ const titleID = 'DuckDuckGoPrivacyEssentialsCTLElementTitle'
25
27
let config = null
26
28
let sharedStrings = null
27
29
let styles = null
30
+ // Used to choose between extension/desktop flow or mobile apps flow.
31
+ // Updated on ClickToLoad.init()
32
+ let isMobileApp
28
33
29
34
// TODO: Remove these redundant data structures and refactor the related code.
30
35
// There should be no need to have the entity configuration stored in two
@@ -49,9 +54,20 @@ const readyToDisplayPlaceholders = new Promise(resolve => {
49
54
let afterPageLoadResolver
50
55
const afterPageLoad = new Promise ( resolve => { afterPageLoadResolver = resolve } )
51
56
52
- // Used to choose between extension/desktop flow or mobile apps flow.
53
- // Updated on ClickToLoad.init()
54
- let isMobileApp
57
+ // Messaging layer for Click to Load. The messaging instance is initialized in
58
+ // ClickToLoad.init() and updated here to be used outside ClickToLoad class
59
+ // we need a module scoped reference.
60
+ /** @type {import("@duckduckgo/messaging").Messaging } */
61
+ let _messagingModuleScope
62
+ const ctl = {
63
+ /**
64
+ * @return {import("@duckduckgo/messaging").Messaging }
65
+ */
66
+ get messaging ( ) {
67
+ if ( ! _messagingModuleScope ) throw new Error ( 'Messaging not initialized' )
68
+ return _messagingModuleScope
69
+ }
70
+ }
55
71
56
72
/*********************************************************
57
73
* Widget Replacement logic
@@ -377,7 +393,9 @@ class DuckWidget {
377
393
if ( this . replaceSettings . type === 'loginButton' ) {
378
394
isLogin = true
379
395
}
380
- window . addEventListener ( 'ddg-ctp-unblockClickToLoadContent-complete' , ( ) => {
396
+ const action = this . entity === 'Youtube' ? 'block-ctl-yt' : 'block-ctl-fb'
397
+ // eslint-disable-next-line promise/prefer-await-to-then
398
+ unblockClickToLoadContent ( { entity : this . entity , action, isLogin } ) . then ( ( ) => {
381
399
const parent = replacementElement . parentNode
382
400
383
401
// The placeholder was removed from the DOM while we loaded
@@ -455,9 +473,7 @@ class DuckWidget {
455
473
if ( onError ) {
456
474
fbElement . addEventListener ( 'error' , onError , { once : true } )
457
475
}
458
- } , { once : true } )
459
- const action = this . entity === 'Youtube' ? 'block-ctl-yt' : 'block-ctl-fb'
460
- unblockClickToLoadContent ( { entity : this . entity , action, isLogin } )
476
+ } )
461
477
}
462
478
}
463
479
// If this is a login button, show modal if needed
@@ -617,14 +633,14 @@ function createPlaceholderElementAndReplace (widget, trackingElement) {
617
633
618
634
// YouTube
619
635
if ( widget . replaceSettings . type === 'youtube-video' ) {
620
- sendMessage ( 'updateYouTubeCTLAddedFlag' , true )
636
+ ctl . messaging . notify ( 'updateYouTubeCTLAddedFlag' , { youTubeCTLAddedFlag : true } )
621
637
replaceYouTubeCTL ( trackingElement , widget )
622
638
623
639
// Subscribe to changes to youtubePreviewsEnabled setting
624
640
// and update the CTL state
625
- window . addEventListener (
626
- 'ddg-settings-youtubePreviewsEnabled ' ,
627
- ( /** @type CustomEvent */ { detail : value } ) => {
641
+ ctl . messaging . subscribe (
642
+ 'setYoutubePreviewsEnabled ' ,
643
+ ( { value } ) => {
628
644
isYoutubePreviewsEnabled = value
629
645
replaceYouTubeCTL ( trackingElement , widget )
630
646
}
@@ -678,7 +694,7 @@ function replaceYouTubeCTL (trackingElement, widget) {
678
694
dataKey : 'yt-preview-toggle' , // data-key attribute for button
679
695
label : widget . replaceSettings . previewToggleText , // Text to be presented with toggle
680
696
size : isMobileApp ? 'lg' : 'md' ,
681
- onClick : ( ) => sendMessage ( 'setYoutubePreviewsEnabled' , true ) // Toggle click callback
697
+ onClick : ( ) => ctl . messaging . notify ( 'setYoutubePreviewsEnabled' , { youtubePreviewsEnabled : true } ) // Toggle click callback
682
698
} ,
683
699
withFeedback : {
684
700
label : sharedStrings . shareFeedback ,
@@ -844,9 +860,10 @@ async function replaceClickToLoadElements (targetElement) {
844
860
* the page.
845
861
* @param {unblockClickToLoadContentRequest } message
846
862
* @see {@link ddg-ctp-unblockClickToLoadContent-complete } for the response handler.
863
+ * @returns {Promise<void> }
847
864
*/
848
865
function unblockClickToLoadContent ( message ) {
849
- sendMessage ( 'unblockClickToLoadContent' , message )
866
+ return ctl . messaging . request ( 'unblockClickToLoadContent' , message )
850
867
}
851
868
852
869
/**
@@ -855,9 +872,10 @@ function unblockClickToLoadContent (message) {
855
872
* shown.
856
873
* @param {string } entity
857
874
*/
858
- function runLogin ( entity ) {
875
+ async function runLogin ( entity ) {
859
876
const action = entity === 'Youtube' ? 'block-ctl-yt' : 'block-ctl-fb'
860
- unblockClickToLoadContent ( { entity, action, isLogin : true } )
877
+ await unblockClickToLoadContent ( { entity, action, isLogin : true } )
878
+ // Communicate with surrogate to run login
861
879
originalWindowDispatchEvent (
862
880
createCustomEvent ( 'ddg-ctp-run-login' , {
863
881
detail : {
@@ -868,8 +886,8 @@ function runLogin (entity) {
868
886
}
869
887
870
888
/**
871
- * Close the login dialog and abort. Called after the user clicks to cancel
872
- * after the warning dialog is shown.
889
+ * Close the login dialog and communicate with the surrogate to abort.
890
+ * Called after the user clicks to cancel after the warning dialog is shown.
873
891
* @param {string } entity
874
892
*/
875
893
function cancelModal ( entity ) {
@@ -883,11 +901,7 @@ function cancelModal (entity) {
883
901
}
884
902
885
903
function openShareFeedbackPage ( ) {
886
- sendMessage ( 'openShareFeedbackPage' , '' )
887
- }
888
-
889
- function getYouTubeVideoDetails ( videoURL ) {
890
- sendMessage ( 'getYouTubeVideoDetails' , videoURL )
904
+ ctl . messaging . notify ( 'openShareFeedbackPage' )
891
905
}
892
906
893
907
/*********************************************************
@@ -1528,7 +1542,7 @@ function createYouTubeBlockingDialog (trackingElement, widget) {
1528
1542
)
1529
1543
previewToggle . addEventListener (
1530
1544
'click' ,
1531
- ( ) => makeModal ( widget . entity , ( ) => sendMessage ( 'setYoutubePreviewsEnabled' , true ) , widget . entity )
1545
+ ( ) => makeModal ( widget . entity , ( ) => ctl . messaging . notify ( 'setYoutubePreviewsEnabled' , { youtubePreviewsEnabled : true } ) , widget . entity )
1532
1546
)
1533
1547
bottomRow . appendChild ( previewToggle )
1534
1548
@@ -1645,7 +1659,7 @@ function createYouTubePreview (originalElement, widget) {
1645
1659
)
1646
1660
previewToggle . addEventListener (
1647
1661
'click' ,
1648
- ( ) => sendMessage ( 'setYoutubePreviewsEnabled' , false )
1662
+ ( ) => ctl . messaging . notify ( 'setYoutubePreviewsEnabled' , { youtubePreviewsEnabled : false } )
1649
1663
)
1650
1664
1651
1665
/** Preview Info Text */
@@ -1677,12 +1691,10 @@ function createYouTubePreview (originalElement, widget) {
1677
1691
// We use .then() instead of await here to show the placeholder right away
1678
1692
// while the YouTube endpoint takes it time to respond.
1679
1693
const videoURL = originalElement . src || originalElement . getAttribute ( 'data-src' )
1680
- getYouTubeVideoDetails ( videoURL )
1681
- window . addEventListener ( 'ddg-ctp-youTubeVideoDetails' ,
1682
- ( /** @type {CustomEvent } */ {
1683
- detail : { videoURL : videoURLResp , status, title, previewImage }
1684
- } ) => {
1685
- if ( videoURLResp !== videoURL ) { return }
1694
+ ctl . messaging . request ( 'getYouTubeVideoDetails' , { videoURL } )
1695
+ // eslint-disable-next-line promise/prefer-await-to-then
1696
+ . then ( ( { videoURL : videoURLResp , status, title, previewImage } ) => {
1697
+ if ( ! status || videoURLResp !== videoURL ) { return }
1686
1698
if ( status === 'success' ) {
1687
1699
titleElement . innerText = title
1688
1700
titleElement . title = title
@@ -1691,8 +1703,7 @@ function createYouTubePreview (originalElement, widget) {
1691
1703
}
1692
1704
widget . autoplay = true
1693
1705
}
1694
- }
1695
- )
1706
+ } )
1696
1707
1697
1708
/** Share Feedback Link */
1698
1709
const feedbackRow = makeShareFeedbackRow ( )
@@ -1701,48 +1712,17 @@ function createYouTubePreview (originalElement, widget) {
1701
1712
return { youTubePreview, shadowRoot }
1702
1713
}
1703
1714
1704
- // Convention is that each function should be named the same as the sendMessage
1705
- // method we are calling into eg. calling `sendMessage('getClickToLoadState')`
1706
- // will result in a response routed to `updateHandlers.getClickToLoadState()`.
1707
- const messageResponseHandlers = {
1708
- getClickToLoadState ( response ) {
1709
- devMode = response . devMode
1710
- isYoutubePreviewsEnabled = response . youtubePreviewsEnabled
1711
-
1712
- // Mark the feature as ready, to allow placeholder replacements to
1713
- // start.
1714
- readyToDisplayPlaceholdersResolver ( )
1715
- } ,
1716
- setYoutubePreviewsEnabled ( response ) {
1717
- if ( response ?. messageType && typeof response ?. value === 'boolean' ) {
1718
- originalWindowDispatchEvent (
1719
- createCustomEvent (
1720
- response . messageType , { detail : response . value }
1721
- )
1722
- )
1723
- }
1724
- } ,
1725
- getYouTubeVideoDetails ( response ) {
1726
- if ( response ?. status && typeof response . videoURL === 'string' ) {
1727
- originalWindowDispatchEvent (
1728
- createCustomEvent (
1729
- 'ddg-ctp-youTubeVideoDetails' ,
1730
- { detail : response }
1731
- )
1732
- )
1733
- }
1734
- } ,
1735
- unblockClickToLoadContent ( ) {
1736
- originalWindowDispatchEvent (
1737
- createCustomEvent ( 'ddg-ctp-unblockClickToLoadContent-complete' )
1738
- )
1739
- }
1740
- }
1741
-
1742
- const knownMessageResponseType = Object . prototype . hasOwnProperty . bind ( messageResponseHandlers )
1743
-
1744
1715
export default class ClickToLoad extends ContentFeature {
1745
1716
async init ( args ) {
1717
+ /**
1718
+ * Bail if no messaging backend - this is a debugging feature to ensure we don't
1719
+ * accidentally enabled this
1720
+ */
1721
+ if ( ! this . messaging ) {
1722
+ throw new Error ( 'Cannot operate click to load without a messaging backend' )
1723
+ }
1724
+ _messagingModuleScope = this . messaging
1725
+
1746
1726
const websiteOwner = args ?. site ?. parentEntity
1747
1727
const settings = args ?. featureSettings ?. clickToLoad || { }
1748
1728
const locale = args ?. locale || 'en'
@@ -1790,8 +1770,8 @@ export default class ClickToLoad extends ContentFeature {
1790
1770
entityData [ entity ] = currentEntityData
1791
1771
}
1792
1772
1793
- // Listen for events from "surrogate" scripts.
1794
- addEventListener ( 'ddg-ctp' , ( /** @type {CustomEvent } */ event ) => {
1773
+ // Listen for window events from "surrogate" scripts.
1774
+ window . addEventListener ( 'ddg-ctp' , ( /** @type {CustomEvent } */ event ) => {
1795
1775
if ( ! ( 'detail' in event ) ) return
1796
1776
1797
1777
const entity = event . detail ?. entity
@@ -1811,12 +1791,22 @@ export default class ClickToLoad extends ContentFeature {
1811
1791
}
1812
1792
}
1813
1793
} )
1794
+ // Listen to message from Platform letting CTL know that we're ready to
1795
+ // replace elements in the page
1796
+ // eslint-disable-next-line promise/prefer-await-to-then
1797
+ this . messaging . subscribe (
1798
+ 'displayClickToLoadPlaceholders' ,
1799
+ // TODO: Pass `message.options.ruleAction` through, that way only
1800
+ // content corresponding to the entity for that ruleAction need to
1801
+ // be replaced with a placeholder.
1802
+ ( ) => replaceClickToLoadElements ( )
1803
+ )
1814
1804
1815
1805
// Request the current state of Click to Load from the platform.
1816
1806
// Note: When the response is received, the response handler resolves
1817
1807
// the readyToDisplayPlaceholders Promise.
1818
- sendMessage ( 'getClickToLoadState' )
1819
- await readyToDisplayPlaceholders
1808
+ const clickToLoadState = await this . messaging . request ( 'getClickToLoadState' )
1809
+ this . onClickToLoadState ( clickToLoadState )
1820
1810
1821
1811
// Then wait for the page to finish loading, and resolve the
1822
1812
// afterPageLoad Promise.
@@ -1844,6 +1834,12 @@ export default class ClickToLoad extends ContentFeature {
1844
1834
} , 0 )
1845
1835
}
1846
1836
1837
+ /**
1838
+ * This is only called by the current integration between Android and Extension and is now
1839
+ * used to connect only these Platforms responses with the temporary implementation of
1840
+ * ClickToLoadMessagingTransport that wraps this communication.
1841
+ * This can be removed once they have their own Messaging integration.
1842
+ */
1847
1843
update ( message ) {
1848
1844
// TODO: Once all Click to Load messages include the feature property, drop
1849
1845
// messages that don't include the feature property too.
@@ -1852,20 +1848,49 @@ export default class ClickToLoad extends ContentFeature {
1852
1848
const messageType = message ?. messageType
1853
1849
if ( ! messageType ) return
1854
1850
1855
- // Message responses.
1856
- if ( messageType === 'response' ) {
1857
- const messageResponseType = message ?. responseMessageType
1858
- if ( messageResponseType && knownMessageResponseType ( messageResponseType ) ) {
1859
- return messageResponseHandlers [ messageResponseType ] ( message . response )
1860
- }
1851
+ if ( ! this . _clickToLoadMessagingTransport ) {
1852
+ throw new Error ( '_clickToLoadMessagingTransport not ready. Cannot operate click to load without a messaging backend' )
1861
1853
}
1862
1854
1863
- // Other known update messages.
1864
- if ( messageType === 'displayClickToLoadPlaceholders' ) {
1865
- // TODO: Pass `message.options.ruleAction` through, that way only
1866
- // content corresponding to the entity for that ruleAction need to
1867
- // be replaced with a placeholder.
1868
- return replaceClickToLoadElements ( )
1855
+ // Send to Messaging layer the response or subscription message received
1856
+ // from the Platform.
1857
+ return this . _clickToLoadMessagingTransport . onResponse ( message )
1858
+ }
1859
+
1860
+ /**
1861
+ * Update Click to Load internal state
1862
+ * @param {Object } state Click to Load state response from the Platform
1863
+ * @param {boolean } state.devMode Developer or Production environment
1864
+ * @param {boolean } state.youtubePreviewsEnabled YouTube Click to Load - YT Previews enabled flag
1865
+ */
1866
+ onClickToLoadState ( state ) {
1867
+ devMode = state . devMode
1868
+ isYoutubePreviewsEnabled = state . youtubePreviewsEnabled
1869
+
1870
+ // Mark the feature as ready, to allow placeholder
1871
+ // replacements to start.
1872
+ readyToDisplayPlaceholdersResolver ( )
1873
+ }
1874
+
1875
+ // Messaging layer between Click to Load and the Platform
1876
+ get messaging ( ) {
1877
+ if ( this . _messaging ) return this . _messaging
1878
+
1879
+ if ( this . platform . name === 'android' || this . platform . name === 'extension' || this . platform . name === 'macos' ) {
1880
+ this . _clickToLoadMessagingTransport = new ClickToLoadMessagingTransport ( )
1881
+ const config = new TestTransportConfig ( this . _clickToLoadMessagingTransport )
1882
+ this . _messaging = new Messaging ( this . messagingContext , config )
1883
+ return this . _messaging
1884
+ } else if ( this . platform . name === 'ios' ) {
1885
+ const config = new WebkitMessagingConfig ( {
1886
+ secret : '' ,
1887
+ hasModernWebkitAPI : true ,
1888
+ webkitMessageHandlerNames : [ 'contentScopeScripts' ]
1889
+ } )
1890
+ this . _messaging = new Messaging ( this . messagingContext , config )
1891
+ return this . _messaging
1892
+ } else {
1893
+ throw new Error ( 'Messaging not supported yet on platform: ' + this . name )
1869
1894
}
1870
1895
}
1871
1896
}
0 commit comments