@playpilot/tpi 3.3.3 → 3.4.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/link-injections.js +8 -8
- package/package.json +1 -1
- package/src/lib/api.ts +6 -9
- package/src/lib/hash.ts +4 -0
- package/src/lib/linkInjection.ts +15 -40
- package/src/lib/localization.ts +7 -7
- package/src/lib/scss/_mixins.scss +0 -13
- package/src/lib/scss/global.scss +0 -5
- package/src/lib/scss/variables.scss +1 -1
- package/src/lib/session.ts +78 -0
- package/src/lib/tracking.ts +1 -2
- package/src/lib/types/injection.d.ts +2 -0
- package/src/lib/types/session.d.ts +14 -0
- package/src/routes/+page.svelte +37 -7
- package/src/routes/components/AfterArticlePlaylinks.svelte +6 -5
- package/src/routes/components/Editorial/Editor.svelte +36 -26
- package/src/routes/components/Editorial/Session.svelte +97 -0
- package/src/routes/components/Modal.svelte +1 -3
- package/src/routes/components/Playlinks.svelte +4 -5
- package/src/routes/components/Popover.svelte +1 -3
- package/src/routes/components/Title.svelte +0 -5
- package/src/tests/lib/api.test.js +14 -14
- package/src/tests/lib/linkInjection.test.js +56 -51
- package/src/tests/lib/localization.test.js +0 -7
- package/src/tests/lib/session.test.js +95 -0
- package/src/tests/lib/tracking.test.js +0 -16
- package/src/tests/routes/+page.test.js +14 -4
- package/src/tests/routes/components/Editorial/Editor.test.js +17 -1
- package/src/tests/routes/components/Editorial/EditorItem.test.js +7 -7
- package/src/tests/routes/components/Editorial/Session.test.js +80 -0
- package/src/tests/setup.js +23 -5
- package/src/lib/event.ts +0 -6
- package/src/lib/viewTransition.ts +0 -25
- package/src/tests/lib/event.test.js +0 -22
- package/src/tests/lib/viewTransition.test.js +0 -13
package/package.json
CHANGED
package/src/lib/api.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { authorize, getAuthToken, isEditorialModeEnabled } from './auth'
|
|
2
2
|
import { apiBaseUrl } from './constants'
|
|
3
|
-
import { stringToHash } from './hash'
|
|
3
|
+
import { generateRandomHash, stringToHash } from './hash'
|
|
4
4
|
import { getLanguage } from './localization'
|
|
5
5
|
import { getPageMetaData } from './meta'
|
|
6
6
|
import type { ConfigResponse } from './types/config'
|
|
@@ -11,13 +11,13 @@ let pollTimeout: ReturnType<typeof setTimeout> | null = null
|
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* Fetch link injections for a URL.
|
|
14
|
-
* @param url URL of the given article
|
|
15
14
|
* @param html HTML to be crawled
|
|
16
15
|
* @param options
|
|
16
|
+
* @param [options.url] URL of the given article
|
|
17
17
|
* @param [options.hash] unique key to identify the HTML
|
|
18
18
|
* @param [options.params] Any rest params to include in the request body
|
|
19
19
|
*/
|
|
20
|
-
export async function fetchLinkInjections(
|
|
20
|
+
export async function fetchLinkInjections(html: string, { url = getFullUrlPath(), hash = stringToHash(html), params = {} }: { url?: string, hash?: string; params?: object } = {}): Promise<LinkInjectionResponse> {
|
|
21
21
|
const headers = new Headers({ 'Content-Type': 'application/json' })
|
|
22
22
|
const apiToken = getApiToken()
|
|
23
23
|
const isEditorialMode = isEditorialModeEnabled() ? await authorize() : false
|
|
@@ -48,16 +48,13 @@ export async function fetchLinkInjections(url: string, html: string, { hash = st
|
|
|
48
48
|
/**
|
|
49
49
|
* Link injections take a while to be ready. During this time we poll the endpoint until it returns the result we want.
|
|
50
50
|
* The results return `injections_ready=false` while the injections are not yet ready.
|
|
51
|
-
* @param url URL of the given article
|
|
52
51
|
* @param html HTML to be crawled
|
|
53
52
|
*/
|
|
54
53
|
export async function pollLinkInjections(
|
|
55
|
-
url: string,
|
|
56
54
|
html: string,
|
|
57
55
|
{ requireCompletedResult = false, pollInterval = 3000, maxTries = 600, onpoll = () => null }:
|
|
58
56
|
{ requireCompletedResult?: boolean, pollInterval?: number, maxTries?: number, onpoll?: (_response: LinkInjectionResponse) => void } = {}
|
|
59
57
|
): Promise<LinkInjectionResponse> {
|
|
60
|
-
let hash = stringToHash(html)
|
|
61
58
|
let currentTry = 0
|
|
62
59
|
|
|
63
60
|
// Clear pollTimeout if it is already running to prevent multiple timeouts from running at the same time
|
|
@@ -72,7 +69,7 @@ export async function pollLinkInjections(
|
|
|
72
69
|
const poll = async (resolve: Function, reject: Function): Promise<void> => {
|
|
73
70
|
let response
|
|
74
71
|
try {
|
|
75
|
-
response = await fetchLinkInjections(
|
|
72
|
+
response = await fetchLinkInjections(html)
|
|
76
73
|
|
|
77
74
|
if (requireCompletedResult && (response.automation_enabled && response.ai_running)) throw new Error
|
|
78
75
|
|
|
@@ -117,7 +114,7 @@ export async function saveLinkInjections(linkInjections: LinkInjection[], html:
|
|
|
117
114
|
removed: !!linkInjection.removed
|
|
118
115
|
}))
|
|
119
116
|
|
|
120
|
-
const response = await fetchLinkInjections(
|
|
117
|
+
const response = await fetchLinkInjections(html, {
|
|
121
118
|
params: {
|
|
122
119
|
private_token: getAuthToken(),
|
|
123
120
|
link_injections: newLinkInjections,
|
|
@@ -166,7 +163,7 @@ function insertRandomKeys(linkInjections: LinkInjection[]): LinkInjection[] {
|
|
|
166
163
|
* @returns Random string preprending with title sid.
|
|
167
164
|
*/
|
|
168
165
|
export function generateInjectionKey(sid: string): string {
|
|
169
|
-
return sid + '-' + (
|
|
166
|
+
return sid + '-' + generateRandomHash()
|
|
170
167
|
}
|
|
171
168
|
|
|
172
169
|
export function getApiToken(): string | undefined {
|
package/src/lib/hash.ts
CHANGED
package/src/lib/linkInjection.ts
CHANGED
|
@@ -1,12 +1,9 @@
|
|
|
1
1
|
import { mount, unmount } from 'svelte'
|
|
2
|
-
import TitleModal from '../routes/components/TitleModal.svelte'
|
|
3
2
|
import TitlePopover from '../routes/components/TitlePopover.svelte'
|
|
4
3
|
import AfterArticlePlaylinks from '../routes/components/AfterArticlePlaylinks.svelte'
|
|
5
4
|
import { cleanPhrase, findTextNodeContaining, isNodeInLink, replaceStartingFrom } from './text'
|
|
6
5
|
import { getLargestValueInArray } from './array'
|
|
7
6
|
import type { LinkInjection, LinkInjectionTypes, LinkInjectionRanges } from './types/injection'
|
|
8
|
-
import { isHoldingSpecialKey } from './event'
|
|
9
|
-
import { playFallbackViewTransition } from './viewTransition'
|
|
10
7
|
|
|
11
8
|
const keyDataAttribute = 'data-playpilot-injection-key'
|
|
12
9
|
const keySelector = `[${keyDataAttribute}]`
|
|
@@ -15,7 +12,6 @@ const activePopovers: Record<string, { injection: LinkInjection; component: obje
|
|
|
15
12
|
|
|
16
13
|
let currentlyHoveredInjection: EventTarget | null = null
|
|
17
14
|
let afterArticlePlaylinkInsertedComponent: object | null = null
|
|
18
|
-
let activeModalInsertedComponent: object | null = null
|
|
19
15
|
|
|
20
16
|
/**
|
|
21
17
|
* Return a list of all valid text containing elements that may get injected into.
|
|
@@ -32,7 +28,7 @@ export function getLinkInjectionElements(parentElement: HTMLElement): HTMLElemen
|
|
|
32
28
|
if (validElements.includes(element)) continue
|
|
33
29
|
|
|
34
30
|
// Ignore links, buttons, and headers
|
|
35
|
-
if (/^(A|BUTTON|SCRIPT|NOSCRIPT|STYLE|IFRAME|
|
|
31
|
+
if (/^(A|BUTTON|SCRIPT|NOSCRIPT|STYLE|IFRAME|H[1-6])$/.test(element.tagName)) continue
|
|
36
32
|
|
|
37
33
|
// Check if this element has a direct text node
|
|
38
34
|
const hasTextNode = Array.from(element.childNodes).some(
|
|
@@ -94,10 +90,9 @@ export function getLinkInjectionsParentElement(): HTMLElement {
|
|
|
94
90
|
* Replace all found injections within all given elements on the page
|
|
95
91
|
* @returns Returns an array of injections with injections that failed to be inserted marked as `failed`.
|
|
96
92
|
*/
|
|
97
|
-
export function injectLinksInDocument(elements: HTMLElement[], injections: LinkInjectionTypes = { aiInjections: [], manualInjections: [] }): LinkInjection[] {
|
|
98
|
-
clearLinkInjections()
|
|
99
|
-
|
|
93
|
+
export function injectLinksInDocument(elements: HTMLElement[], onclick: (LinkInjection: LinkInjection) => void, injections: LinkInjectionTypes = { aiInjections: [], manualInjections: [] }): LinkInjection[] {
|
|
100
94
|
const mergedInjections = mergeInjectionTypes(injections)
|
|
95
|
+
|
|
101
96
|
if (!mergedInjections) return []
|
|
102
97
|
|
|
103
98
|
// Find injection in text content of all elements together, ignore potential HTML elements.
|
|
@@ -167,11 +162,11 @@ export function injectLinksInDocument(elements: HTMLElement[], injections: LinkI
|
|
|
167
162
|
}
|
|
168
163
|
}
|
|
169
164
|
|
|
170
|
-
addLinkInjectionEventListeners(validInjections)
|
|
165
|
+
addLinkInjectionEventListeners(validInjections, onclick)
|
|
171
166
|
addCSSVariablesToLinks()
|
|
172
167
|
|
|
173
168
|
const afterArticleInjections = filterInvalidAfterArticleInjections(mergedInjections)
|
|
174
|
-
if (afterArticleInjections.length) insertAfterArticlePlaylinks(elements, afterArticleInjections)
|
|
169
|
+
if (afterArticleInjections.length) insertAfterArticlePlaylinks(elements, afterArticleInjections, onclick)
|
|
175
170
|
|
|
176
171
|
return mergedInjections.filter(i => i.title_details).map((injection, index) => {
|
|
177
172
|
// Favour manual injections over AI injections
|
|
@@ -224,11 +219,9 @@ function addCSSVariablesToLinks(): void {
|
|
|
224
219
|
/**
|
|
225
220
|
* Add event listeners to all injected links. These events are for both the popover and the modal.
|
|
226
221
|
*/
|
|
227
|
-
function addLinkInjectionEventListeners(injections: LinkInjection[]): void {
|
|
222
|
+
function addLinkInjectionEventListeners(injections: LinkInjection[], onclick: (injection: LinkInjection) => void): void {
|
|
228
223
|
// Open modal on click
|
|
229
224
|
window.addEventListener('click', (event) => {
|
|
230
|
-
if (isHoldingSpecialKey(event)) return
|
|
231
|
-
|
|
232
225
|
const target = event.target as HTMLElement | null
|
|
233
226
|
if (!target?.parentElement) return
|
|
234
227
|
|
|
@@ -238,12 +231,7 @@ function addLinkInjectionEventListeners(injections: LinkInjection[]): void {
|
|
|
238
231
|
const injection = injections.find(injection => key === injection.key)
|
|
239
232
|
if (!injection) return
|
|
240
233
|
|
|
241
|
-
event
|
|
242
|
-
|
|
243
|
-
playFallbackViewTransition(() => {
|
|
244
|
-
destroyLinkPopover(injection, false)
|
|
245
|
-
openLinkModal(event, injection)
|
|
246
|
-
}, window.innerWidth >= 600 && !window.matchMedia("(pointer: coarse)").matches)
|
|
234
|
+
openLinkModal(event, injection, onclick)
|
|
247
235
|
})
|
|
248
236
|
|
|
249
237
|
const createdInjectionElements = Array.from(document.querySelectorAll(keySelector)) as HTMLElement[]
|
|
@@ -262,26 +250,16 @@ function addLinkInjectionEventListeners(injections: LinkInjection[]): void {
|
|
|
262
250
|
}
|
|
263
251
|
|
|
264
252
|
/**
|
|
265
|
-
*
|
|
266
|
-
*
|
|
253
|
+
* Prevent default click and run onclick from parent. Ignore clicks that used modifier keys or that were not left click.
|
|
254
|
+
* The event is not fired when the click happens from inside a popover.
|
|
267
255
|
*/
|
|
268
|
-
function openLinkModal(event: MouseEvent, injection: LinkInjection): void {
|
|
269
|
-
if (
|
|
270
|
-
if (activeModalInsertedComponent) return
|
|
256
|
+
function openLinkModal(event: MouseEvent, injection: LinkInjection, onclick: (injection: LinkInjection) => void): void {
|
|
257
|
+
if (event.ctrlKey || event.metaKey || event.button !== 0) return
|
|
271
258
|
|
|
272
259
|
event.preventDefault()
|
|
273
260
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
/**
|
|
278
|
-
* Unmount the modal, removing it from the dom
|
|
279
|
-
*/
|
|
280
|
-
function destroyLinkModal(outro: boolean = true): void {
|
|
281
|
-
if (!activeModalInsertedComponent) return
|
|
282
|
-
|
|
283
|
-
unmount(activeModalInsertedComponent, { outro })
|
|
284
|
-
activeModalInsertedComponent = null
|
|
261
|
+
onclick(injection)
|
|
262
|
+
destroyLinkPopover(injection)
|
|
285
263
|
}
|
|
286
264
|
|
|
287
265
|
/**
|
|
@@ -325,8 +303,7 @@ function destroyLinkPopover(injection: LinkInjection, outro: boolean = true) {
|
|
|
325
303
|
* The config object contains a selector option as well as a position. This way a selector can be given and you can
|
|
326
304
|
* choose to insert the after article before or after the given element.
|
|
327
305
|
*/
|
|
328
|
-
export function insertAfterArticlePlaylinks(elements: HTMLElement[], injections: LinkInjection[]
|
|
329
|
-
if (afterArticlePlaylinkInsertedComponent) return
|
|
306
|
+
export function insertAfterArticlePlaylinks(elements: HTMLElement[], injections: LinkInjection[], onclickmodal: (linkInjection: LinkInjection) => void) {
|
|
330
307
|
if (!injections.length) return
|
|
331
308
|
|
|
332
309
|
const target = document.createElement('div')
|
|
@@ -337,7 +314,7 @@ export function insertAfterArticlePlaylinks(elements: HTMLElement[], injections:
|
|
|
337
314
|
target.dataset.playpilotAfterArticlePlaylinks = 'true'
|
|
338
315
|
insertElement.insertAdjacentElement(insertPosition, target)
|
|
339
316
|
|
|
340
|
-
afterArticlePlaylinkInsertedComponent = mount(AfterArticlePlaylinks, { target, props: { linkInjections: injections, onclickmodal
|
|
317
|
+
afterArticlePlaylinkInsertedComponent = mount(AfterArticlePlaylinks, { target, props: { linkInjections: injections, onclickmodal } })
|
|
341
318
|
}
|
|
342
319
|
|
|
343
320
|
function clearAfterArticlePlaylinks(): void {
|
|
@@ -360,7 +337,6 @@ export function clearLinkInjections(): void {
|
|
|
360
337
|
Object.values(activePopovers).forEach(({ injection }) => destroyLinkPopover(injection, false))
|
|
361
338
|
|
|
362
339
|
clearAfterArticlePlaylinks()
|
|
363
|
-
destroyLinkModal()
|
|
364
340
|
}
|
|
365
341
|
|
|
366
342
|
/**
|
|
@@ -446,4 +422,3 @@ export function isAvailableAsManualInjection(injection: LinkInjection, injection
|
|
|
446
422
|
export function isEquivalentInjection(injection1: LinkInjection, injection2: LinkInjection): boolean {
|
|
447
423
|
return injection1.title === injection2.title && cleanPhrase(injection1.sentence) === cleanPhrase(injection2.sentence)
|
|
448
424
|
}
|
|
449
|
-
|
package/src/lib/localization.ts
CHANGED
|
@@ -10,7 +10,11 @@ export function t(key: string, language: LanguageCode = getLanguage()): string {
|
|
|
10
10
|
return translations[key]?.[language] || key
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
/**
|
|
14
|
+
* @returns {LanguageCode}
|
|
15
|
+
*/
|
|
13
16
|
export function getLanguage(): LanguageCode {
|
|
17
|
+
// @ts-ignore
|
|
14
18
|
const configLanguage = window.PlayPilotLinkInjections?.language as LanguageCode | undefined
|
|
15
19
|
const languageCodes = Object.values(Language) as LanguageCode[]
|
|
16
20
|
|
|
@@ -20,14 +24,10 @@ export function getLanguage(): LanguageCode {
|
|
|
20
24
|
console.warn(`PlayPilot Link Injections: ${configLanguage} is not an accepted language`)
|
|
21
25
|
}
|
|
22
26
|
|
|
23
|
-
const documentLanguage = document.querySelector('html')?.getAttribute('lang')
|
|
24
|
-
|
|
25
|
-
if (documentLanguage) {
|
|
26
|
-
if (languageCodes.includes(documentLanguage as LanguageCode)) return documentLanguage as LanguageCode
|
|
27
|
+
const documentLanguage = document.querySelector('html')?.getAttribute('lang') as LanguageCode | undefined
|
|
27
28
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
if (matched) return matched
|
|
29
|
+
if (documentLanguage && languageCodes.includes(documentLanguage)) {
|
|
30
|
+
return documentLanguage
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
return Language.English
|
package/src/lib/scss/global.scss
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
--playpilot-content: #354367;
|
|
9
9
|
--playpilot-content-light: #4b5b82;
|
|
10
10
|
--playpilot-green: #53bca0;
|
|
11
|
-
--playpilot-font-family: "Poppins",
|
|
11
|
+
--playpilot-font-family: "Poppins", sans-serif;
|
|
12
12
|
--playpilot-text-color: #fff;
|
|
13
13
|
--playpilot-text-color-alt: #c8d4de;
|
|
14
14
|
--playpilot-shadow:
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { fetchLinkInjections } from "./api"
|
|
2
|
+
import { generateRandomHash } from "./hash"
|
|
3
|
+
import type { SessionResponse } from "./types/session"
|
|
4
|
+
|
|
5
|
+
export const sessionKey = 'PlayPilotEditorialSessionId'
|
|
6
|
+
export const sessionPollPeriodMilliseconds = 5000
|
|
7
|
+
export const sessionPeriodMilliseconds = sessionPollPeriodMilliseconds * 2
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Fetch link injections and infer if the current session is allowed to edit. If they are, we make an additional request
|
|
11
|
+
* where we save the session.
|
|
12
|
+
* Along with that, we also return automation_enabled and injections_enabled as they are used to update the state of
|
|
13
|
+
* the current editing session.
|
|
14
|
+
* @param html Despite not being used, the request still requires a valid html string
|
|
15
|
+
*/
|
|
16
|
+
export async function fetchAsSession(html: string): Promise<SessionResponse> {
|
|
17
|
+
const { automation_enabled, injections_enabled, session_id, session_last_ping } = await fetchLinkInjections(html)
|
|
18
|
+
|
|
19
|
+
const isCurrentlyAllowToEdit = isAllowedToEdit(session_id || null, session_last_ping || null)
|
|
20
|
+
|
|
21
|
+
const response = {
|
|
22
|
+
automation_enabled,
|
|
23
|
+
injections_enabled,
|
|
24
|
+
session_id,
|
|
25
|
+
session_last_ping,
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (!isCurrentlyAllowToEdit) return response
|
|
29
|
+
|
|
30
|
+
return await saveCurrentSession(html)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Save the current users session id as the currently active session. This is always completed regardless of if the user
|
|
35
|
+
* is the current owner of the session, so check that first if necessary!
|
|
36
|
+
* @param html Despite not being used, the request still requires a valid html string
|
|
37
|
+
*/
|
|
38
|
+
export async function saveCurrentSession(html: string): Promise<SessionResponse> {
|
|
39
|
+
const sessionId = getSessionId()
|
|
40
|
+
const now = new Date(Date.now()).toISOString()
|
|
41
|
+
|
|
42
|
+
const params = {
|
|
43
|
+
session_id: sessionId,
|
|
44
|
+
session_last_ping: now.toString(),
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const { automation_enabled, injections_enabled } = await fetchLinkInjections(html, { params })
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
automation_enabled,
|
|
51
|
+
injections_enabled,
|
|
52
|
+
...params,
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function isAllowedToEdit(responseSessionId: string | null, responseSessionLastPing: string | null): boolean {
|
|
57
|
+
if (!responseSessionId || !responseSessionLastPing) return true
|
|
58
|
+
|
|
59
|
+
const now = Date.now()
|
|
60
|
+
const responseSessionLastPingDate = new Date(responseSessionLastPing)
|
|
61
|
+
const currentSessionId = getSessionId()
|
|
62
|
+
|
|
63
|
+
// Last session ping was older than given period, so the previous session has likely ended
|
|
64
|
+
if (now - responseSessionLastPingDate.getTime() > sessionPeriodMilliseconds) return true
|
|
65
|
+
|
|
66
|
+
return currentSessionId === responseSessionId
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function getSessionId(): string {
|
|
70
|
+
return sessionStorage.getItem(sessionKey) || setSessionId()
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function setSessionId(): string {
|
|
74
|
+
const id = sessionStorage.getItem(sessionKey) || generateRandomHash()
|
|
75
|
+
sessionStorage.setItem(sessionKey, id)
|
|
76
|
+
|
|
77
|
+
return id
|
|
78
|
+
}
|
package/src/lib/tracking.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { get } from "svelte/store"
|
|
2
2
|
import { currentDomainSid, currentOrganizationSid } from "./stores/organization"
|
|
3
3
|
import type { TitleData } from "./types/title"
|
|
4
|
-
import { getFullUrlPath } from "./url"
|
|
5
4
|
|
|
6
5
|
const baseUrl = 'https://insights.playpilot.net'
|
|
7
6
|
|
|
@@ -23,7 +22,7 @@ export async function track(event: string, title: TitleData | null = null, paylo
|
|
|
23
22
|
}
|
|
24
23
|
}
|
|
25
24
|
|
|
26
|
-
payload.url =
|
|
25
|
+
payload.url = window.location.href
|
|
27
26
|
payload.organization_sid = get(currentOrganizationSid)
|
|
28
27
|
payload.domain_sid = get(currentDomainSid)
|
|
29
28
|
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { LinkInjectionResponse } from "./injection"
|
|
2
|
+
|
|
3
|
+
export type SessionResponse = Omit<
|
|
4
|
+
LinkInjectionResponse,
|
|
5
|
+
'injections_ready' |
|
|
6
|
+
'page_updated' |
|
|
7
|
+
'ai_running' |
|
|
8
|
+
'ai_progress_message' |
|
|
9
|
+
'ai_progress_percentage' |
|
|
10
|
+
'ai_injections' |
|
|
11
|
+
'organization_sid' |
|
|
12
|
+
'domain_sid' |
|
|
13
|
+
'link_injections'
|
|
14
|
+
>
|
package/src/routes/+page.svelte
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import { isCrawler } from '$lib/crawler'
|
|
8
8
|
import { TrackingEvent } from '$lib/enums/TrackingEvent'
|
|
9
9
|
import { authorize, getAuthToken, isEditorialModeEnabled, removeAuthCookie, setEditorialParamInUrl } from '$lib/auth'
|
|
10
|
+
import TitleModal from './components/TitleModal.svelte'
|
|
10
11
|
import Editor from './components/Editorial/Editor.svelte'
|
|
11
12
|
import EditorTrigger from './components/Editorial/EditorTrigger.svelte'
|
|
12
13
|
import type { LinkInjectionResponse, LinkInjection, LinkInjectionTypes } from '$lib/types/injection'
|
|
@@ -16,12 +17,12 @@
|
|
|
16
17
|
const htmlString = elements.map(p => p.outerHTML).join('')
|
|
17
18
|
|
|
18
19
|
let response: LinkInjectionResponse | null = $state(null)
|
|
20
|
+
let activeInjection: LinkInjection | null = $state(null)
|
|
19
21
|
let isEditorialMode = $state(isEditorialModeEnabled())
|
|
20
22
|
let hasAuthToken = $state(!!getAuthToken())
|
|
21
23
|
let authorized = $state(false)
|
|
22
24
|
let loading = $state(true)
|
|
23
25
|
let linkInjections: LinkInjection[] = $state([])
|
|
24
|
-
let editor = $state()
|
|
25
26
|
|
|
26
27
|
// @ts-ignore It's ok if the response is empty
|
|
27
28
|
const { ai_injections: aiInjections = [], link_injections: manualInjections = [] } = $derived(response || {})
|
|
@@ -47,10 +48,9 @@
|
|
|
47
48
|
|
|
48
49
|
if (isEditorialMode) authorized = await authorize()
|
|
49
50
|
|
|
50
|
-
const url = getFullUrlPath()
|
|
51
|
-
|
|
52
51
|
try {
|
|
53
52
|
const config = await fetchConfig()
|
|
53
|
+
const url = getFullUrlPath()
|
|
54
54
|
|
|
55
55
|
// URL was marked as being excluded, we stop injections here unless we're in editorial mode.
|
|
56
56
|
if (!isEditorialMode && config?.exclude_urls_pattern && url.match(config.exclude_urls_pattern)) return
|
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
|
|
65
65
|
// Only trying once when not in editorial mode to prevent late injections (as well as a ton of requests)
|
|
66
66
|
// by users who are not in the editorial view.
|
|
67
|
-
response = await pollLinkInjections(
|
|
67
|
+
response = await pollLinkInjections(htmlString, { maxTries: 1 })
|
|
68
68
|
|
|
69
69
|
loading = false
|
|
70
70
|
|
|
@@ -84,18 +84,28 @@
|
|
|
84
84
|
// so as not to suddenly insert new links while a user is reading the article.
|
|
85
85
|
if (!isEditorialMode) return
|
|
86
86
|
|
|
87
|
-
response = await pollLinkInjections(
|
|
87
|
+
response = await pollLinkInjections(htmlString, { requireCompletedResult: true, onpoll: (update) => response = update })
|
|
88
88
|
inject({ aiInjections, manualInjections })
|
|
89
89
|
}
|
|
90
90
|
|
|
91
91
|
function rerender(): void {
|
|
92
|
+
clearLinkInjections()
|
|
92
93
|
inject(separateLinkInjectionTypes(linkInjections))
|
|
93
94
|
}
|
|
94
95
|
|
|
96
|
+
function reinitializeEditor(): void {
|
|
97
|
+
isEditorialMode = false
|
|
98
|
+
|
|
99
|
+
requestAnimationFrame(() => {
|
|
100
|
+
isEditorialMode = true
|
|
101
|
+
initialize()
|
|
102
|
+
})
|
|
103
|
+
}
|
|
104
|
+
|
|
95
105
|
function inject(injections: LinkInjectionTypes = { aiInjections, manualInjections }): void {
|
|
96
106
|
// Get filtered injections as they are shown on the page.
|
|
97
107
|
// Only update state if it they are different from current injections.
|
|
98
|
-
const filteredInjections = injectLinksInDocument(elements, injections)
|
|
108
|
+
const filteredInjections = injectLinksInDocument(elements, setTarget, injections)
|
|
99
109
|
if (JSON.stringify(filteredInjections) !== JSON.stringify(linkInjections)) linkInjections = filteredInjections
|
|
100
110
|
|
|
101
111
|
const successfulInjections = filteredInjections.filter(i => !i.failed)
|
|
@@ -109,6 +119,10 @@
|
|
|
109
119
|
})
|
|
110
120
|
}
|
|
111
121
|
|
|
122
|
+
function setTarget(injection: LinkInjection): void {
|
|
123
|
+
activeInjection = injection
|
|
124
|
+
}
|
|
125
|
+
|
|
112
126
|
function openEditorialMode() {
|
|
113
127
|
isEditorialMode = true
|
|
114
128
|
setEditorialParamInUrl()
|
|
@@ -127,9 +141,9 @@
|
|
|
127
141
|
{#if isEditorialMode && authorized}
|
|
128
142
|
<Editor
|
|
129
143
|
bind:linkInjections
|
|
130
|
-
bind:this={editor}
|
|
131
144
|
{htmlString}
|
|
132
145
|
{loading}
|
|
146
|
+
onreinitialize={reinitializeEditor}
|
|
133
147
|
injectionsEnabled={response?.injections_enabled}
|
|
134
148
|
aiStatus={{
|
|
135
149
|
automationEnabled: response?.automation_enabled,
|
|
@@ -138,9 +152,14 @@
|
|
|
138
152
|
percentage: response?.ai_progress_percentage,
|
|
139
153
|
}} />
|
|
140
154
|
{/if}
|
|
155
|
+
|
|
156
|
+
{#if activeInjection && activeInjection.title_details}
|
|
157
|
+
<TitleModal title={activeInjection.title_details} onclose={() => activeInjection = null} />
|
|
158
|
+
{/if}
|
|
141
159
|
</div>
|
|
142
160
|
|
|
143
161
|
<style lang="scss">
|
|
162
|
+
@import url('https://fonts.googleapis.com/css?family=Poppins:400,600,700');
|
|
144
163
|
@import url('$lib/scss/variables.scss');
|
|
145
164
|
@import url('$lib/scss/global.scss');
|
|
146
165
|
|
|
@@ -148,5 +167,16 @@
|
|
|
148
167
|
:global(*) {
|
|
149
168
|
box-sizing: border-box;
|
|
150
169
|
}
|
|
170
|
+
|
|
171
|
+
:global(.playpilot-link-injections button),
|
|
172
|
+
:global(.playpilot-link-injections input) {
|
|
173
|
+
transition: outline-offset 100ms;
|
|
174
|
+
|
|
175
|
+
&:focus-visible,
|
|
176
|
+
&:focus-visible {
|
|
177
|
+
outline: 2px solid white;
|
|
178
|
+
outline-offset: 2px;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
151
181
|
}
|
|
152
182
|
</style>
|
|
@@ -8,10 +8,11 @@
|
|
|
8
8
|
interface Props {
|
|
9
9
|
linkInjections: LinkInjection[],
|
|
10
10
|
// eslint-disable-next-line no-unused-vars
|
|
11
|
-
onclickmodal?: (
|
|
11
|
+
onclickmodal?: (linkInjection: LinkInjection) => void
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
// eslint-disable-next-line no-unused-vars
|
|
15
|
+
const { linkInjections, onclickmodal = (linkInjection) => null }: Props = $props()
|
|
15
16
|
|
|
16
17
|
function onclick(title: TitleData, playlink: string): void {
|
|
17
18
|
track(TrackingEvent.AfterArticlePlaylinkClick, title, { playlink })
|
|
@@ -20,9 +21,9 @@
|
|
|
20
21
|
/**
|
|
21
22
|
* Open a modal for the given injection and track the click
|
|
22
23
|
*/
|
|
23
|
-
function openModal(
|
|
24
|
+
function openModal(title: TitleData, linkInjection: LinkInjection): void {
|
|
24
25
|
track(TrackingEvent.AfterArticleModalButtonClick, title)
|
|
25
|
-
onclickmodal(
|
|
26
|
+
onclickmodal(linkInjection)
|
|
26
27
|
}
|
|
27
28
|
</script>
|
|
28
29
|
|
|
@@ -38,7 +39,7 @@
|
|
|
38
39
|
"{title}" {t('Is Available To Stream')}
|
|
39
40
|
|
|
40
41
|
<span>
|
|
41
|
-
<button onclick={(
|
|
42
|
+
<button onclick={() => openModal(title_details as TitleData, linkInjection)}>
|
|
42
43
|
{t('View Streaming Options')}
|
|
43
44
|
</button>
|
|
44
45
|
</span>
|