Skip to content

Commit f41a73c

Browse files
franfaccinshakyShaneShane Osbourne
authored
Add new Messaging format to Click to Load (#546)
* Remove sendMessage calls from click-to-load.js and replace it with new messaging layer wrapper * Add custom messaging transport just for Click to Load using TestTransportConfig. Move messagingContext creation to ContentFeature. Rename lazy.messaging to ctl.messaging. Clean up request() code to handle getYouTubeVideoDetails requests better. Clean up the code. * Fix request payloads to use key/value pair params but unwrap it on ClickToLoadMessagingTransport to match sendMessage() messages format * Add await to unblockClickToLoadContent call in `runLogin`. Fix return type. Move `displayClickToLoadPlaceholders` subscribe up to avoid race conditions. * Fix TS errors * Fix ClickToLoadMessagingTransport constructor and fix special message handlers to notify * Increase artifact bundle size allowed to account for increase by messaging size * fixed formatting --------- Co-authored-by: Shane Osbourne <[email protected]> Co-authored-by: Shane Osbourne <[email protected]>
1 parent 86351d2 commit f41a73c

File tree

4 files changed

+282
-90
lines changed

4 files changed

+282
-90
lines changed

src/content-feature.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { camelcase, matchHostname, processAttr, computeEnabledFeatures, parseFeatureSettings } from './utils.js'
22
import { immutableJSONPatch } from 'immutable-json-patch'
33
import { PerformanceMonitor } from './performance.js'
4+
import { MessagingContext } from '@duckduckgo/messaging'
45

56
/**
67
* @typedef {object} AssetConfig
@@ -25,6 +26,8 @@ export default class ContentFeature {
2526
#documentOriginIsTracker
2627
/** @type {Record<string, unknown> | undefined} */
2728
#bundledfeatureSettings
29+
/** @type {MessagingContext} */
30+
#messagingContext
2831

2932
/** @type {{ debug?: boolean, featureSettings?: Record<string, unknown>, assets?: AssetConfig | undefined, site: Site } | null} */
3033
#args
@@ -79,6 +82,19 @@ export default class ContentFeature {
7982
return this.#bundledConfig
8083
}
8184

85+
/**
86+
* @returns {MessagingContext}
87+
*/
88+
get messagingContext () {
89+
if (this.#messagingContext) return this.#messagingContext
90+
this.#messagingContext = new MessagingContext({
91+
context: 'contentScopeScripts',
92+
featureName: this.name,
93+
env: this.isDebug ? 'development' : 'production'
94+
})
95+
return this.#messagingContext
96+
}
97+
8298
/**
8399
* Get the value of a config setting.
84100
* If the value is not set, return the default value.

src/features/click-to-load.js

Lines changed: 114 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1-
import { createCustomEvent, sendMessage, originalWindowDispatchEvent } from '../utils.js'
1+
import { Messaging, TestTransportConfig, WebkitMessagingConfig } from '@duckduckgo/messaging'
2+
import { createCustomEvent, originalWindowDispatchEvent } from '../utils.js'
23
import { logoImg, loadingImages, closeIcon } from './click-to-load/ctl-assets.js'
34
import { getStyles, getConfig } from './click-to-load/ctl-config.js'
5+
import { ClickToLoadMessagingTransport } from './click-to-load/ctl-messaging-transport.js'
46
import ContentFeature from '../content-feature.js'
57
import { DDGCtlPlaceholderBlockedElement } from './click-to-load/components/ctl-placeholder-blocked.js'
68
import { registerCustomElements } from './click-to-load/components'
@@ -25,6 +27,9 @@ const titleID = 'DuckDuckGoPrivacyEssentialsCTLElementTitle'
2527
let config = null
2628
let sharedStrings = null
2729
let styles = null
30+
// Used to choose between extension/desktop flow or mobile apps flow.
31+
// Updated on ClickToLoad.init()
32+
let isMobileApp
2833

2934
// TODO: Remove these redundant data structures and refactor the related code.
3035
// There should be no need to have the entity configuration stored in two
@@ -49,9 +54,20 @@ const readyToDisplayPlaceholders = new Promise(resolve => {
4954
let afterPageLoadResolver
5055
const afterPageLoad = new Promise(resolve => { afterPageLoadResolver = resolve })
5156

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+
}
5571

5672
/*********************************************************
5773
* Widget Replacement logic
@@ -377,7 +393,9 @@ class DuckWidget {
377393
if (this.replaceSettings.type === 'loginButton') {
378394
isLogin = true
379395
}
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(() => {
381399
const parent = replacementElement.parentNode
382400

383401
// The placeholder was removed from the DOM while we loaded
@@ -455,9 +473,7 @@ class DuckWidget {
455473
if (onError) {
456474
fbElement.addEventListener('error', onError, { once: true })
457475
}
458-
}, { once: true })
459-
const action = this.entity === 'Youtube' ? 'block-ctl-yt' : 'block-ctl-fb'
460-
unblockClickToLoadContent({ entity: this.entity, action, isLogin })
476+
})
461477
}
462478
}
463479
// If this is a login button, show modal if needed
@@ -617,14 +633,14 @@ function createPlaceholderElementAndReplace (widget, trackingElement) {
617633

618634
// YouTube
619635
if (widget.replaceSettings.type === 'youtube-video') {
620-
sendMessage('updateYouTubeCTLAddedFlag', true)
636+
ctl.messaging.notify('updateYouTubeCTLAddedFlag', { youTubeCTLAddedFlag: true })
621637
replaceYouTubeCTL(trackingElement, widget)
622638

623639
// Subscribe to changes to youtubePreviewsEnabled setting
624640
// 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 }) => {
628644
isYoutubePreviewsEnabled = value
629645
replaceYouTubeCTL(trackingElement, widget)
630646
}
@@ -678,7 +694,7 @@ function replaceYouTubeCTL (trackingElement, widget) {
678694
dataKey: 'yt-preview-toggle', // data-key attribute for button
679695
label: widget.replaceSettings.previewToggleText, // Text to be presented with toggle
680696
size: isMobileApp ? 'lg' : 'md',
681-
onClick: () => sendMessage('setYoutubePreviewsEnabled', true) // Toggle click callback
697+
onClick: () => ctl.messaging.notify('setYoutubePreviewsEnabled', { youtubePreviewsEnabled: true }) // Toggle click callback
682698
},
683699
withFeedback: {
684700
label: sharedStrings.shareFeedback,
@@ -844,9 +860,10 @@ async function replaceClickToLoadElements (targetElement) {
844860
* the page.
845861
* @param {unblockClickToLoadContentRequest} message
846862
* @see {@link ddg-ctp-unblockClickToLoadContent-complete} for the response handler.
863+
* @returns {Promise<void>}
847864
*/
848865
function unblockClickToLoadContent (message) {
849-
sendMessage('unblockClickToLoadContent', message)
866+
return ctl.messaging.request('unblockClickToLoadContent', message)
850867
}
851868

852869
/**
@@ -855,9 +872,10 @@ function unblockClickToLoadContent (message) {
855872
* shown.
856873
* @param {string} entity
857874
*/
858-
function runLogin (entity) {
875+
async function runLogin (entity) {
859876
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
861879
originalWindowDispatchEvent(
862880
createCustomEvent('ddg-ctp-run-login', {
863881
detail: {
@@ -868,8 +886,8 @@ function runLogin (entity) {
868886
}
869887

870888
/**
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.
873891
* @param {string} entity
874892
*/
875893
function cancelModal (entity) {
@@ -883,11 +901,7 @@ function cancelModal (entity) {
883901
}
884902

885903
function openShareFeedbackPage () {
886-
sendMessage('openShareFeedbackPage', '')
887-
}
888-
889-
function getYouTubeVideoDetails (videoURL) {
890-
sendMessage('getYouTubeVideoDetails', videoURL)
904+
ctl.messaging.notify('openShareFeedbackPage')
891905
}
892906

893907
/*********************************************************
@@ -1528,7 +1542,7 @@ function createYouTubeBlockingDialog (trackingElement, widget) {
15281542
)
15291543
previewToggle.addEventListener(
15301544
'click',
1531-
() => makeModal(widget.entity, () => sendMessage('setYoutubePreviewsEnabled', true), widget.entity)
1545+
() => makeModal(widget.entity, () => ctl.messaging.notify('setYoutubePreviewsEnabled', { youtubePreviewsEnabled: true }), widget.entity)
15321546
)
15331547
bottomRow.appendChild(previewToggle)
15341548

@@ -1645,7 +1659,7 @@ function createYouTubePreview (originalElement, widget) {
16451659
)
16461660
previewToggle.addEventListener(
16471661
'click',
1648-
() => sendMessage('setYoutubePreviewsEnabled', false)
1662+
() => ctl.messaging.notify('setYoutubePreviewsEnabled', { youtubePreviewsEnabled: false })
16491663
)
16501664

16511665
/** Preview Info Text */
@@ -1677,12 +1691,10 @@ function createYouTubePreview (originalElement, widget) {
16771691
// We use .then() instead of await here to show the placeholder right away
16781692
// while the YouTube endpoint takes it time to respond.
16791693
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 }
16861698
if (status === 'success') {
16871699
titleElement.innerText = title
16881700
titleElement.title = title
@@ -1691,8 +1703,7 @@ function createYouTubePreview (originalElement, widget) {
16911703
}
16921704
widget.autoplay = true
16931705
}
1694-
}
1695-
)
1706+
})
16961707

16971708
/** Share Feedback Link */
16981709
const feedbackRow = makeShareFeedbackRow()
@@ -1701,48 +1712,17 @@ function createYouTubePreview (originalElement, widget) {
17011712
return { youTubePreview, shadowRoot }
17021713
}
17031714

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-
17441715
export default class ClickToLoad extends ContentFeature {
17451716
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+
17461726
const websiteOwner = args?.site?.parentEntity
17471727
const settings = args?.featureSettings?.clickToLoad || {}
17481728
const locale = args?.locale || 'en'
@@ -1790,8 +1770,8 @@ export default class ClickToLoad extends ContentFeature {
17901770
entityData[entity] = currentEntityData
17911771
}
17921772

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) => {
17951775
if (!('detail' in event)) return
17961776

17971777
const entity = event.detail?.entity
@@ -1811,12 +1791,22 @@ export default class ClickToLoad extends ContentFeature {
18111791
}
18121792
}
18131793
})
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+
)
18141804

18151805
// Request the current state of Click to Load from the platform.
18161806
// Note: When the response is received, the response handler resolves
18171807
// the readyToDisplayPlaceholders Promise.
1818-
sendMessage('getClickToLoadState')
1819-
await readyToDisplayPlaceholders
1808+
const clickToLoadState = await this.messaging.request('getClickToLoadState')
1809+
this.onClickToLoadState(clickToLoadState)
18201810

18211811
// Then wait for the page to finish loading, and resolve the
18221812
// afterPageLoad Promise.
@@ -1844,6 +1834,12 @@ export default class ClickToLoad extends ContentFeature {
18441834
}, 0)
18451835
}
18461836

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+
*/
18471843
update (message) {
18481844
// TODO: Once all Click to Load messages include the feature property, drop
18491845
// messages that don't include the feature property too.
@@ -1852,20 +1848,49 @@ export default class ClickToLoad extends ContentFeature {
18521848
const messageType = message?.messageType
18531849
if (!messageType) return
18541850

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')
18611853
}
18621854

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)
18691894
}
18701895
}
18711896
}

0 commit comments

Comments
 (0)