Skip to content

Commit 18796b1

Browse files
committed
Display Click to Load placeholders on demand
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 3b2db7d commit 18796b1

File tree

3 files changed

+73
-47
lines changed

3 files changed

+73
-47
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: 40 additions & 33 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

@@ -537,6 +524,8 @@ function showExtraUnblockIfShortPlaceholder (shadowRoot, placeholder) {
537524
* in the document will be replaced instead.
538525
*/
539526
async function replaceClickToLoadElements (targetElement) {
527+
await ready
528+
540529
for (const entity of Object.keys(config)) {
541530
for (const widgetData of Object.values(config[entity].elementData)) {
542531
const selector = widgetData.selectors.join()
@@ -1312,7 +1301,7 @@ async function createYouTubePreview (originalElement, widget) {
13121301
// Convention is that each function should be named the same as the sendMessage
13131302
// method we are calling into eg. calling `sendMessage('getClickToLoadState')`
13141303
// will result in a response routed to `updateHandlers.getClickToLoadState()`.
1315-
const updateHandlers = {
1304+
const messageResponseHandlers = {
13161305
getClickToLoadState (response) {
13171306
devMode = response.devMode
13181307
isYoutubePreviewsEnabled = response.youtubePreviewsEnabled
@@ -1322,14 +1311,15 @@ const updateHandlers = {
13221311
// first.
13231312

13241313
// 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-
}
1314+
window.addEventListener('ddg-ctp-replace-element', ({ target }) => {
1315+
replaceClickToLoadElements(target)
1316+
}, { capture: true })
1317+
1318+
// Inform surrogate scripts that CTP is ready
1319+
originalWindowDispatchEvent(createCustomEvent('ddg-ctp-ready'))
1320+
1321+
// Mark the feature as ready, to allow placeholder replacements.
1322+
readyResolver()
13331323
},
13341324
setYoutubePreviewsEnabled: function (resp) {
13351325
if (resp?.messageType && typeof resp?.value === 'boolean') {
@@ -1346,6 +1336,8 @@ const updateHandlers = {
13461336
}
13471337
}
13481338

1339+
const knownMessageResponseType = Object.prototype.hasOwnProperty.bind(messageResponseHandlers)
1340+
13491341
export function init (args) {
13501342
const websiteOwner = args?.site?.parentEntity
13511343
const settings = args?.featureSettings?.clickToPlay || {}
@@ -1411,17 +1403,32 @@ export function init (args) {
14111403
})
14121404

14131405
// 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.
1406+
// Note: When the response is received, the response handler finishes
1407+
// starting up the feature.
14161408
sendMessage('getClickToLoadState')
14171409
}
14181410

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

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

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

0 commit comments

Comments
 (0)