Skip to content

Commit 46ca757

Browse files
authored
Display Click to Load placeholders on demand (#287)
Historically, the Click to Load placeholders were only drawn once at the point page load completed. That had two problems: 1. Sometimes placeholders would take too long to display, if the page took a long time to load fully. 2. Sometimes placeholders would not be displayed at all, if the blocked content was only created after page load had finished. To fix that, let's display the placeholders when instructed by the platform using a "displayClickToLoadPlaceholders" update message. Let's also restructure the response messages, so that they can be clearly distinguished.
1 parent e27d4e7 commit 46ca757

File tree

3 files changed

+78
-49
lines changed

3 files changed

+78
-49
lines changed

inject/chrome.js

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -126,16 +126,25 @@ function init () {
126126
}
127127
})
128128

129-
window.addEventListener('sendMessageProxy' + messageSecret, (m) => {
130-
// @ts-expect-error https://app.asana.com/0/1201614831475344/1203979574128023/f
131-
const messageType = m.detail.messageType
129+
window.addEventListener('sendMessageProxy' + messageSecret, event => {
130+
event.stopImmediatePropagation()
131+
132+
if (!(event instanceof CustomEvent) || !event?.detail) {
133+
return console.warn('no details in sendMessage proxy', event)
134+
}
135+
136+
const messageType = event.detail?.messageType
132137
if (!allowedMessages.includes(messageType)) {
133138
return console.warn('Ignoring invalid sendMessage messageType', messageType)
134139
}
135-
// @ts-expect-error https://app.asana.com/0/1201614831475344/1203979574128023/f
136-
chrome.runtime.sendMessage(m && m.detail, response => {
137-
const msg = { func: messageType, response }
138-
const stringifiedArgs = JSON.stringify({ detail: msg })
140+
141+
chrome.runtime.sendMessage(event.detail, response => {
142+
const message = {
143+
messageType: 'response',
144+
responseMessageType: messageType,
145+
response
146+
}
147+
const stringifiedArgs = JSON.stringify(message)
139148
const callRandomUpdateFunction = `
140149
window.${reusableMethodName}('${reusableSecret}', ${stringifiedArgs});
141150
`

inject/mozilla.js

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -59,16 +59,26 @@ function init () {
5959
}
6060
})
6161

62-
window.addEventListener('sendMessageProxy' + messageSecret, (m) => {
63-
// @ts-expect-error https://app.asana.com/0/1201614831475344/1203979574128023/f
64-
const messageType = m.detail.messageType
62+
window.addEventListener('sendMessageProxy' + messageSecret, event => {
63+
event.stopImmediatePropagation()
64+
65+
if (!(event instanceof CustomEvent) || !event?.detail) {
66+
return console.warn('no details in sendMessage proxy', event)
67+
}
68+
69+
const messageType = event.detail?.messageType
6570
if (!allowedMessages.includes(messageType)) {
6671
return console.warn('Ignoring invalid sendMessage messageType', messageType)
6772
}
68-
// @ts-expect-error https://app.asana.com/0/1201614831475344/1203979574128023/f
69-
chrome.runtime.sendMessage(m && m.detail, response => {
70-
const msg = { func: messageType, response }
71-
contentScopeFeatures.update({ detail: msg })
73+
74+
chrome.runtime.sendMessage(event.detail, response => {
75+
const message = {
76+
messageType: 'response',
77+
responseMessageType: messageType,
78+
response
79+
}
80+
81+
contentScopeFeatures.update(message)
7282
})
7383
})
7484
}

src/features/click-to-play.js

Lines changed: 45 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ let sharedStrings = null
2020
const entities = []
2121
const entityData = {}
2222

23+
let readyResolver
24+
const ready = new Promise(resolve => { readyResolver = resolve })
25+
2326
/*********************************************************
2427
* Widget Replacement logic
2528
*********************************************************/
@@ -364,22 +367,6 @@ class DuckWidget {
364367
}
365368
}
366369

367-
/**
368-
* Initialise the Click to Load feature, once the necessary details have been
369-
* returned by the platform.
370-
* @returns {Promise}
371-
*/
372-
async function initCTL () {
373-
await replaceClickToLoadElements()
374-
375-
window.addEventListener('ddg-ctp-replace-element', ({ target }) => {
376-
replaceClickToLoadElements(target)
377-
}, { capture: true })
378-
379-
// Inform surrogate scripts that CTP is ready
380-
originalWindowDispatchEvent(createCustomEvent('ddg-ctp-ready'))
381-
}
382-
383370
function replaceTrackingElement (widget, trackingElement, placeholderElement, hideTrackingElement = false, currentPlaceholder = null) {
384371
widget.dispatchEvent(trackingElement, 'ddg-ctp-tracking-element')
385372

@@ -492,11 +479,14 @@ async function replaceYouTubeCTL (trackingElement, widget, togglePlaceholder = f
492479
}
493480

494481
// Show YouTube Preview for embedded video
482+
// TODO: Fix the hideTrackingElement option and reenable, or remove it. It's
483+
// disabled for YouTube videos so far since it caused multiple
484+
// placeholders to be displayed on the page.
495485
if (isYoutubePreviewsEnabled === true) {
496486
const { youTubePreview, shadowRoot } = await createYouTubePreview(trackingElement, widget)
497487
const currentPlaceholder = togglePlaceholder ? document.getElementById(`yt-ctl-dialog-${widget.widgetID}`) : null
498488
replaceTrackingElement(
499-
widget, trackingElement, youTubePreview, /* hideTrackingElement= */ true, currentPlaceholder
489+
widget, trackingElement, youTubePreview, /* hideTrackingElement= */ false, currentPlaceholder
500490
)
501491
showExtraUnblockIfShortPlaceholder(shadowRoot, youTubePreview)
502492

@@ -506,7 +496,7 @@ async function replaceYouTubeCTL (trackingElement, widget, togglePlaceholder = f
506496
const { blockingDialog, shadowRoot } = await createYouTubeBlockingDialog(trackingElement, widget)
507497
const currentPlaceholder = togglePlaceholder ? document.getElementById(`yt-ctl-preview-${widget.widgetID}`) : null
508498
replaceTrackingElement(
509-
widget, trackingElement, blockingDialog, /* hideTrackingElement= */ true, currentPlaceholder
499+
widget, trackingElement, blockingDialog, /* hideTrackingElement= */ false, currentPlaceholder
510500
)
511501
showExtraUnblockIfShortPlaceholder(shadowRoot, blockingDialog)
512502
}
@@ -537,6 +527,8 @@ function showExtraUnblockIfShortPlaceholder (shadowRoot, placeholder) {
537527
* in the document will be replaced instead.
538528
*/
539529
async function replaceClickToLoadElements (targetElement) {
530+
await ready
531+
540532
for (const entity of Object.keys(config)) {
541533
for (const widgetData of Object.values(config[entity].elementData)) {
542534
const selector = widgetData.selectors.join()
@@ -1312,7 +1304,7 @@ async function createYouTubePreview (originalElement, widget) {
13121304
// Convention is that each function should be named the same as the sendMessage
13131305
// method we are calling into eg. calling `sendMessage('getClickToLoadState')`
13141306
// will result in a response routed to `updateHandlers.getClickToLoadState()`.
1315-
const updateHandlers = {
1307+
const messageResponseHandlers = {
13161308
getClickToLoadState (response) {
13171309
devMode = response.devMode
13181310
isYoutubePreviewsEnabled = response.youtubePreviewsEnabled
@@ -1322,14 +1314,15 @@ const updateHandlers = {
13221314
// first.
13231315

13241316
// Start Click to Load
1325-
if (document.readyState === 'complete') {
1326-
initCTL()
1327-
} else {
1328-
// Content script loaded before page content, so wait for load.
1329-
window.addEventListener('load', (event) => {
1330-
initCTL()
1331-
})
1332-
}
1317+
window.addEventListener('ddg-ctp-replace-element', ({ target }) => {
1318+
replaceClickToLoadElements(target)
1319+
}, { capture: true })
1320+
1321+
// Inform surrogate scripts that CTP is ready
1322+
originalWindowDispatchEvent(createCustomEvent('ddg-ctp-ready'))
1323+
1324+
// Mark the feature as ready, to allow placeholder replacements.
1325+
readyResolver()
13331326
},
13341327
setYoutubePreviewsEnabled: function (resp) {
13351328
if (resp?.messageType && typeof resp?.value === 'boolean') {
@@ -1346,6 +1339,8 @@ const updateHandlers = {
13461339
}
13471340
}
13481341

1342+
const knownMessageResponseType = Object.prototype.hasOwnProperty.bind(messageResponseHandlers)
1343+
13491344
export function init (args) {
13501345
const websiteOwner = args?.site?.parentEntity
13511346
const settings = args?.featureSettings?.clickToPlay || {}
@@ -1411,17 +1406,32 @@ export function init (args) {
14111406
})
14121407

14131408
// Request the current state of Click to Load from the platform.
1414-
// Note: When the response is received, initCTL() is then called by the
1415-
// response handler to finish starting up the feature.
1409+
// Note: When the response is received, the response handler finishes
1410+
// starting up the feature.
14161411
sendMessage('getClickToLoadState')
14171412
}
14181413

1419-
export function update (args) {
1420-
const detail = args && args.detail
1421-
if (!(detail && detail.func)) { return }
1414+
export function update (message) {
1415+
// TODO: Once all Click to Load messages include the feature property, drop
1416+
// messages that don't include the feature property too.
1417+
if (message?.feature && message?.feature !== 'clickToLoad') return
14221418

1423-
const fn = updateHandlers[detail.func]
1424-
if (typeof fn !== 'function') { return }
1419+
const messageType = message?.messageType
1420+
if (!messageType) return
14251421

1426-
fn(detail.response)
1422+
// Message responses.
1423+
if (messageType === 'response') {
1424+
const messageResponseType = message?.responseMessageType
1425+
if (messageResponseType && knownMessageResponseType(messageResponseType)) {
1426+
return messageResponseHandlers[messageResponseType](message.response)
1427+
}
1428+
}
1429+
1430+
// Other known update messages.
1431+
if (messageType === 'displayClickToLoadPlaceholders') {
1432+
// TODO: Pass `message.options.ruleAction` through, that way only
1433+
// content corresponding to the entity for that ruleAction need to
1434+
// be replaced with a placeholder.
1435+
return replaceClickToLoadElements()
1436+
}
14271437
}

0 commit comments

Comments
 (0)