Skip to content

Commit cb97543

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 0b9715e commit cb97543

File tree

9 files changed

+83
-63
lines changed

9 files changed

+83
-63
lines changed

inject/android.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@ function init () {
88
if (isGloballyDisabled(processedConfig)) {
99
return
1010
}
11-
// @ts-expect-error https://app.asana.com/0/1201614831475344/1203979574128023/f
11+
1212
contentScopeFeatures.load({
1313
platform: processedConfig.platform
1414
})
15-
// @ts-expect-error https://app.asana.com/0/1201614831475344/1203979574128023/f
15+
1616
contentScopeFeatures.init(processedConfig)
1717

1818
// Not supported:

inject/apple.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@ function init () {
88
if (isGloballyDisabled(processedConfig)) {
99
return
1010
}
11-
// @ts-expect-error https://app.asana.com/0/1201614831475344/1203979574128023/f
11+
1212
contentScopeFeatures.load({
1313
platform: processedConfig.platform
1414
})
15-
// @ts-expect-error https://app.asana.com/0/1201614831475344/1203979574128023/f
15+
1616
contentScopeFeatures.init(processedConfig)
1717

1818
// Not supported:

inject/chrome-mv3.js

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import { isTrackerOrigin } from '../src/trackers'
44

55
const secret = (crypto.getRandomValues(new Uint32Array(1))[0] / 2 ** 32).toString().replace('0.', '')
66

7-
// @ts-expect-error https://app.asana.com/0/1201614831475344/1203979574128023/f
87
contentScopeFeatures.load({
98
platform: {
109
name: 'extension'
@@ -21,13 +20,11 @@ window.addEventListener(secret, ({ detail: message }) => {
2120

2221
switch (message.type) {
2322
case 'update':
24-
// @ts-expect-error https://app.asana.com/0/1201614831475344/1203979574128023/f
2523
contentScopeFeatures.update(message)
2624
break
2725
case 'register':
2826
if (message.argumentsObject) {
2927
message.argumentsObject.messageSecret = secret
30-
// @ts-expect-error https://app.asana.com/0/1201614831475344/1203979574128023/f
3128
contentScopeFeatures.init(message.argumentsObject)
3229
}
3330
break

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/integration.js

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,6 @@ function mergeDeep (target, ...sources) {
7070
async function init () {
7171
const topLevelUrl = getTopLevelURL()
7272
const processedConfig = generateConfig()
73-
// @ts-expect-error https://app.asana.com/0/1201614831475344/1203979574128023/f
7473
await contentScopeFeatures.load({
7574
platform: processedConfig.platform
7675
})
@@ -79,7 +78,6 @@ async function init () {
7978
setStatus('loaded')
8079

8180
if (!topLevelUrl.searchParams.has('wait-for-init-args')) {
82-
// @ts-expect-error https://app.asana.com/0/1201614831475344/1203979574128023/f
8381
await contentScopeFeatures.init(processedConfig)
8482
setStatus('initialized')
8583
return
@@ -90,7 +88,6 @@ async function init () {
9088
// @ts-expect-error https://app.asana.com/0/1201614831475344/1203979574128023/f
9189
const merged = mergeDeep(processedConfig, evt.detail)
9290
// init features
93-
// @ts-expect-error https://app.asana.com/0/1201614831475344/1203979574128023/f
9491
await contentScopeFeatures.init(merged)
9592

9693
// set status to initialized so that tests can resume

inject/mozilla.js

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ function randomString () {
1717
}
1818

1919
function init () {
20-
// @ts-expect-error https://app.asana.com/0/1201614831475344/1203979574128023/f
2120
contentScopeFeatures.load({
2221
platform: {
2322
name: 'extension'
@@ -50,29 +49,36 @@ function init () {
5049
})
5150
}
5251
message.messageSecret = messageSecret
53-
// @ts-expect-error https://app.asana.com/0/1201614831475344/1203979574128023/f
5452
contentScopeFeatures.init(message)
5553
})
5654

5755
chrome.runtime.onMessage.addListener((message) => {
5856
// forward update messages to the embedded script
5957
if (message && message.type === 'update') {
60-
// @ts-expect-error https://app.asana.com/0/1201614831475344/1203979574128023/f
6158
contentScopeFeatures.update(message)
6259
}
6360
})
6461

65-
window.addEventListener('sendMessageProxy' + messageSecret, (m) => {
66-
// @ts-expect-error https://app.asana.com/0/1201614831475344/1203979574128023/f
67-
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
6870
if (!allowedMessages.includes(messageType)) {
6971
return console.warn('Ignoring invalid sendMessage messageType', messageType)
7072
}
71-
// @ts-expect-error https://app.asana.com/0/1201614831475344/1203979574128023/f
72-
chrome.runtime.sendMessage(m && m.detail, response => {
73-
const msg = { func: messageType, response }
74-
// @ts-expect-error https://app.asana.com/0/1201614831475344/1203979574128023/f
75-
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)
7682
})
7783
})
7884
}

inject/windows.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,11 @@ function init () {
88
if (isGloballyDisabled(processedConfig)) {
99
return
1010
}
11-
// @ts-expect-error https://app.asana.com/0/1201614831475344/1203979574128023/f
11+
1212
contentScopeFeatures.load({
1313
platform: processedConfig.platform
1414
})
1515

16-
// @ts-expect-error https://app.asana.com/0/1201614831475344/1203979574128023/f
1716
contentScopeFeatures.init(processedConfig)
1817

1918
// Not supported:

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
}

src/globals.d.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,8 @@ declare function exportFunction(fn: Function, desc: object, out: object): void;
33
declare function exportFunction(fn: Function, desc: object): void;
44
declare function cloneInto(fn: object, desc: object, out: object): void;
55
declare function cloneInto(fn: object, desc: object): void;
6+
declare namespace contentScopeFeatures {
7+
function init(args: Object): void;
8+
function load(args: Object): void;
9+
function update(args: Object): void;
10+
}

0 commit comments

Comments
 (0)