@playpilot/tpi 3.3.4 → 3.4.0-beta.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@playpilot/tpi",
3
- "version": "3.3.4",
3
+ "version": "3.4.0-beta.2",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "dev": "vite dev",
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(url: string, html: string, { hash = stringToHash(html), params = {} }: { hash?: string; params?: object } = {}): Promise<LinkInjectionResponse> {
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(url, html, { hash })
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(getFullUrlPath(), html, {
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 + '-' + (Math.random() + 1).toString(36).substring(7)
166
+ return sid + '-' + generateRandomHash()
170
167
  }
171
168
 
172
169
  export function getApiToken(): string | undefined {
package/src/lib/hash.ts CHANGED
@@ -12,3 +12,7 @@ export function stringToHash(string: string): string {
12
12
 
13
13
  return hash.toString(16)
14
14
  }
15
+
16
+ export function generateRandomHash() {
17
+ return (Math.random() + 1).toString(36).substring(7)
18
+ }
@@ -5,8 +5,6 @@ import AfterArticlePlaylinks from '../routes/components/AfterArticlePlaylinks.sv
5
5
  import { cleanPhrase, findTextNodeContaining, isNodeInLink, replaceStartingFrom } from './text'
6
6
  import { getLargestValueInArray } from './array'
7
7
  import type { LinkInjection, LinkInjectionTypes, LinkInjectionRanges } from './types/injection'
8
- import { isHoldingSpecialKey } from './event'
9
- import { playFallbackViewTransition } from './viewTransition'
10
8
 
11
9
  const keyDataAttribute = 'data-playpilot-injection-key'
12
10
  const keySelector = `[${keyDataAttribute}]`
@@ -227,8 +225,6 @@ function addCSSVariablesToLinks(): void {
227
225
  function addLinkInjectionEventListeners(injections: LinkInjection[]): void {
228
226
  // Open modal on click
229
227
  window.addEventListener('click', (event) => {
230
- if (isHoldingSpecialKey(event)) return
231
-
232
228
  const target = event.target as HTMLElement | null
233
229
  if (!target?.parentElement) return
234
230
 
@@ -238,12 +234,7 @@ function addLinkInjectionEventListeners(injections: LinkInjection[]): void {
238
234
  const injection = injections.find(injection => key === injection.key)
239
235
  if (!injection) return
240
236
 
241
- event.preventDefault()
242
-
243
- playFallbackViewTransition(() => {
244
- destroyLinkPopover(injection, false)
245
- openLinkModal(event, injection)
246
- }, window.innerWidth >= 600 && !window.matchMedia("(pointer: coarse)").matches)
237
+ openLinkModal(event, injection)
247
238
  })
248
239
 
249
240
  const createdInjectionElements = Array.from(document.querySelectorAll(keySelector)) as HTMLElement[]
@@ -266,11 +257,12 @@ function addLinkInjectionEventListeners(injections: LinkInjection[]): void {
266
257
  * Ignore clicks that used modifier keys or that were not left click.
267
258
  */
268
259
  function openLinkModal(event: MouseEvent, injection: LinkInjection): void {
269
- if (isHoldingSpecialKey(event)) return
260
+ if (event.ctrlKey || event.metaKey || event.button !== 0) return
270
261
  if (activeModalInsertedComponent) return
271
262
 
272
263
  event.preventDefault()
273
264
 
265
+ destroyLinkPopover(injection)
274
266
  activeModalInsertedComponent = mount(TitleModal, { target: document.body, props: { title: injection.title_details!, onclose: destroyLinkModal } })
275
267
  }
276
268
 
@@ -446,4 +438,3 @@ export function isAvailableAsManualInjection(injection: LinkInjection, injection
446
438
  export function isEquivalentInjection(injection1: LinkInjection, injection2: LinkInjection): boolean {
447
439
  return injection1.title === injection2.title && cleanPhrase(injection1.sentence) === cleanPhrase(injection2.sentence)
448
440
  }
449
-
@@ -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
- // Match against shorthand language codes like da -> da-DK
29
- const matched = languageCodes.find(code => code.toLowerCase().startsWith(documentLanguage + '-'))
30
- if (matched) return matched
29
+ if (documentLanguage && languageCodes.includes(documentLanguage)) {
30
+ return documentLanguage
31
31
  }
32
32
 
33
33
  return Language.English
@@ -73,8 +73,3 @@
73
73
  background-color: var(--playpilot-injection-background-color-hover);
74
74
  }
75
75
  }
76
-
77
- ::view-transition-old(playpilot-title-content),
78
- ::view-transition-new(playpilot-title-content) {
79
- height: 100%;
80
- }
@@ -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", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif;
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
+ }
@@ -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 = getFullUrlPath()
25
+ payload.url = window.location.href
27
26
  payload.organization_sid = get(currentOrganizationSid)
28
27
  payload.domain_sid = get(currentDomainSid)
29
28
 
@@ -32,6 +32,8 @@ export type LinkInjectionResponse = {
32
32
  link_injections: LinkInjection[] | null
33
33
  organization_sid?: string
34
34
  domain_sid?: string
35
+ session_id?: string | null
36
+ session_last_ping?: string | null
35
37
  }
36
38
 
37
39
  export type LinkInjectionTypes = {
@@ -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
+ >
@@ -21,7 +21,6 @@
21
21
  let authorized = $state(false)
22
22
  let loading = $state(true)
23
23
  let linkInjections: LinkInjection[] = $state([])
24
- let editor = $state()
25
24
 
26
25
  // @ts-ignore It's ok if the response is empty
27
26
  const { ai_injections: aiInjections = [], link_injections: manualInjections = [] } = $derived(response || {})
@@ -47,10 +46,9 @@
47
46
 
48
47
  if (isEditorialMode) authorized = await authorize()
49
48
 
50
- const url = getFullUrlPath()
51
-
52
49
  try {
53
50
  const config = await fetchConfig()
51
+ const url = getFullUrlPath()
54
52
 
55
53
  // URL was marked as being excluded, we stop injections here unless we're in editorial mode.
56
54
  if (!isEditorialMode && config?.exclude_urls_pattern && url.match(config.exclude_urls_pattern)) return
@@ -64,7 +62,7 @@
64
62
 
65
63
  // Only trying once when not in editorial mode to prevent late injections (as well as a ton of requests)
66
64
  // by users who are not in the editorial view.
67
- response = await pollLinkInjections(url, htmlString, { maxTries: 1 })
65
+ response = await pollLinkInjections(htmlString, { maxTries: 1 })
68
66
 
69
67
  loading = false
70
68
 
@@ -84,7 +82,7 @@
84
82
  // so as not to suddenly insert new links while a user is reading the article.
85
83
  if (!isEditorialMode) return
86
84
 
87
- response = await pollLinkInjections(url, htmlString, { requireCompletedResult: true, onpoll: (update) => response = update })
85
+ response = await pollLinkInjections(htmlString, { requireCompletedResult: true, onpoll: (update) => response = update })
88
86
  inject({ aiInjections, manualInjections })
89
87
  }
90
88
 
@@ -92,6 +90,15 @@
92
90
  inject(separateLinkInjectionTypes(linkInjections))
93
91
  }
94
92
 
93
+ function reinitializeEditor(): void {
94
+ isEditorialMode = false
95
+
96
+ requestAnimationFrame(() => {
97
+ isEditorialMode = true
98
+ initialize()
99
+ })
100
+ }
101
+
95
102
  function inject(injections: LinkInjectionTypes = { aiInjections, manualInjections }): void {
96
103
  // Get filtered injections as they are shown on the page.
97
104
  // Only update state if it they are different from current injections.
@@ -127,9 +134,9 @@
127
134
  {#if isEditorialMode && authorized}
128
135
  <Editor
129
136
  bind:linkInjections
130
- bind:this={editor}
131
137
  {htmlString}
132
138
  {loading}
139
+ onreinitialize={reinitializeEditor}
133
140
  injectionsEnabled={response?.injections_enabled}
134
141
  aiStatus={{
135
142
  automationEnabled: response?.automation_enabled,
@@ -141,6 +148,7 @@
141
148
  </div>
142
149
 
143
150
  <style lang="scss">
151
+ @import url('https://fonts.googleapis.com/css?family=Poppins:400,600,700');
144
152
  @import url('$lib/scss/variables.scss');
145
153
  @import url('$lib/scss/global.scss');
146
154
 
@@ -6,6 +6,7 @@
6
6
  import Alert from './Alert.svelte'
7
7
  import ManualInjection from './ManualInjection.svelte'
8
8
  import RoundButton from '../RoundButton.svelte'
9
+ import Session from './Session.svelte'
9
10
  import { saveLinkInjections } from '$lib/api'
10
11
  import { untrack } from 'svelte'
11
12
  import AIIndicator from './AIIndicator.svelte'
@@ -21,12 +22,13 @@
21
22
  htmlString?: string,
22
23
  loading?: boolean,
23
24
  injectionsEnabled?: boolean,
24
- aiStatus: {
25
+ aiStatus?: {
25
26
  automationEnabled?: boolean,
26
27
  aiRunning?: boolean,
27
28
  message?: string,
28
29
  percentage?: number
29
30
  }
31
+ onreinitialize?: () => void,
30
32
  }
31
33
 
32
34
  let {
@@ -35,6 +37,7 @@
35
37
  loading = false,
36
38
  injectionsEnabled = false,
37
39
  aiStatus = {},
40
+ onreinitialize = () => null,
38
41
  }: Props = $props()
39
42
 
40
43
  const editorPositionKey = 'editor-position'
@@ -46,15 +49,17 @@
46
49
  let manualInjectionActive = $state(false)
47
50
  let saving = $state(false)
48
51
  let hasError = $state(false)
52
+ let allowEditing = $state(true)
49
53
  let scrollDistance = $state(0)
50
54
  let initialStateString = $state('')
51
55
 
56
+ const { automationEnabled = false, aiRunning = false } = $derived(aiStatus)
52
57
  const linkInjectionsString = $derived(JSON.stringify(linkInjections))
58
+ const showControls = $derived(!aiRunning && allowEditing)
53
59
  const hasChanged = $derived(initialStateString && initialStateString !== linkInjectionsString)
54
60
  // Filter out injections without title_details, injections that are removed, duplicate, or are AI injections that failed to inject
55
61
  const filteredInjections = $derived(linkInjections.filter((i) => i.title_details && !i.removed && !i.duplicate && (i.manual || (!i.manual && !i.failed))))
56
62
  const sortedInjections = $derived(sortInjections(filteredInjections))
57
- const { automationEnabled = false, aiRunning = false } = $derived(aiStatus)
58
63
  const initialAiRunning = $derived(!loading && untrack(() => aiStatus.aiRunning))
59
64
 
60
65
  $effect(() => {
@@ -136,10 +141,6 @@
136
141
  }
137
142
  </script>
138
143
 
139
- <svelte:head>
140
- <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Poppins:400,600,700">
141
- </svelte:head>
142
-
143
144
  <section class="editor playpilot-styled-scrollbar" class:panel-open={manualInjectionActive} class:loading bind:this={editorElement} {onscroll}>
144
145
  {#if editorElement && !loading}
145
146
  <div class="handles">
@@ -158,7 +159,7 @@
158
159
 
159
160
  {#if loading}
160
161
  <div class="loading">Loading...</div>
161
- {:else if !aiRunning}
162
+ {:else if showControls}
162
163
  <div class="bubble" aria-label="{filteredInjections.length} found playlinks">
163
164
  {filteredInjections.length}
164
165
  </div>
@@ -170,26 +171,36 @@
170
171
  </header>
171
172
 
172
173
  {#if !loading}
173
- {#if !injectionsEnabled}
174
- <div class="alert">
175
- <Alert type="warning">
176
- <strong>Playlinks are currently not published.</strong> Visitors to this page will not see any of the injected links.
177
- Publish playlinks from the <a href="https://partner.playpilot.net">Partner Portal</a>
178
- </Alert>
179
- </div>
180
- {/if}
174
+ <Session
175
+ {htmlString}
176
+ onallow={() => allowEditing = true}
177
+ ondisallow={() => allowEditing = false}
178
+ ontakeover={onreinitialize}
179
+ onpoll={(updated) => {
180
+ injectionsEnabled = updated.injectionsEnabled
181
+ aiStatus.automationEnabled = updated.automationEnabled
182
+ }} />
183
+
184
+ {#if showControls}
185
+ {#if !injectionsEnabled}
186
+ <div class="alert">
187
+ <Alert type="warning">
188
+ <strong>Playlinks are currently not published.</strong> Visitors to this page will not see any of the injected links.
189
+ Publish playlinks from the <a href="https://partner.playpilot.net">Partner Portal.</a>
190
+ </Alert>
191
+ </div>
192
+ {/if}
181
193
 
182
- {#if initialAiRunning || !automationEnabled}
183
- <AIIndicator {...aiStatus} aiInjectionsCount={separateLinkInjectionTypes(linkInjections).aiInjections.length} />
184
- {/if}
194
+ {#if initialAiRunning || !automationEnabled}
195
+ <AIIndicator {...aiStatus} aiInjectionsCount={separateLinkInjectionTypes(linkInjections).aiInjections.length} />
196
+ {/if}
185
197
 
186
- {#if hasError}
187
- <div class="error" transition:slide|global={{ duration: 150 }}>
188
- <Alert>Something went wrong, check your links below.</Alert>
189
- </div>
190
- {/if}
198
+ {#if hasError}
199
+ <div class="error" transition:slide|global={{ duration: 150 }}>
200
+ <Alert>Something went wrong, check your links below.</Alert>
201
+ </div>
202
+ {/if}
191
203
 
192
- {#if !aiRunning}
193
204
  <div class="items">
194
205
  {#each sortedInjections as linkInjection (linkInjection.key)}
195
206
  <!-- We want to bind to the original object, not the derived object, so we get the index of the injection in the original object by it's key -->
@@ -201,7 +212,7 @@
201
212
  {/each}
202
213
  </div>
203
214
 
204
- {#if hasChanged && linkInjections.length}
215
+ {#if hasChanged && linkInjections.length && allowEditing}
205
216
  <button class="save" disabled={saving} onclick={save} in:fade={{ duration: 100 }}>
206
217
  {saving ? 'Saving...' : 'Save links'}
207
218
  </button>
@@ -0,0 +1,97 @@
1
+ <script lang="ts">
2
+ import { fetchAsSession, isAllowedToEdit, saveCurrentSession, sessionPollPeriodMilliseconds } from '$lib/session'
3
+ import { onMount } from 'svelte'
4
+ import Alert from './Alert.svelte'
5
+
6
+ interface Props {
7
+ htmlString?: string,
8
+ // eslint-disable-next-line no-unused-vars
9
+ onpoll?: ({ injectionsEnabled, automationEnabled }: { injectionsEnabled: boolean, automationEnabled: boolean }) => void,
10
+ onallow?: () => void,
11
+ ondisallow?: () => void,
12
+ ontakeover?: () => void,
13
+ }
14
+
15
+ const {
16
+ htmlString = '',
17
+ onpoll = () => null,
18
+ onallow = () => null,
19
+ ondisallow = () => null,
20
+ ontakeover = () => null,
21
+ }: Props = $props()
22
+
23
+ let pollInterval: ReturnType<typeof setInterval> | null = null
24
+ let isEditingAllowed = $state(true)
25
+
26
+ onMount(() => {
27
+ startPolling()
28
+
29
+ return () => {
30
+ if (pollInterval) clearInterval(pollInterval)
31
+ }
32
+ })
33
+
34
+ function startPolling(): void {
35
+ setSession()
36
+ pollInterval = setInterval(setSession, sessionPollPeriodMilliseconds)
37
+ }
38
+
39
+ async function setSession(): Promise<void> {
40
+ const result = await fetchAsSession(htmlString)
41
+
42
+ if (!result) return
43
+
44
+ isEditingAllowed = isAllowedToEdit(result.session_id || null, result.session_last_ping || null)
45
+
46
+ if (isEditingAllowed) onallow()
47
+ else ondisallow()
48
+
49
+ onpoll({ injectionsEnabled: !!result.injections_enabled, automationEnabled: !!result.automation_enabled })
50
+ }
51
+
52
+ function takeOverEditing(): void {
53
+ saveCurrentSession(htmlString)
54
+ ontakeover()
55
+ }
56
+ </script>
57
+
58
+ {#if !isEditingAllowed}
59
+ <div class="alert">
60
+ <Alert type="warning">
61
+ <p>
62
+ <strong>Someone else is currently editing this document.</strong>
63
+ Wait for them to exit the page or take over edit permissions, preventing them from making more changes.
64
+ </p>
65
+
66
+ <button class="button" onclick={takeOverEditing}>Take over editing</button>
67
+ </Alert>
68
+ </div>
69
+ {/if}
70
+
71
+ <style lang="scss">
72
+ p {
73
+ margin: 0 0 margin(0.5);
74
+ }
75
+
76
+ .alert {
77
+ margin: 0 margin(0.5);
78
+ }
79
+
80
+ .button {
81
+ appearance: none;
82
+ width: 100%;
83
+ padding: margin(0.25);
84
+ border: 0;
85
+ border-radius: margin(0.25);
86
+ background: var(--playpilot-warning);
87
+ transition: opacity 100ms;
88
+ font-family: inherit;
89
+ color: var(--playpilot-dark);
90
+ font-size: 0.85rem;
91
+ cursor: pointer;
92
+
93
+ &:hover {
94
+ background: white;
95
+ }
96
+ }
97
+ </style>
@@ -32,7 +32,7 @@
32
32
  <svelte:window on:keydown={({ key }) => { if (key === 'Escape') onclose() }} />
33
33
 
34
34
  <div class="modal" transition:fade={{ duration: 150 }}>
35
- <div class="dialog" {onscroll} role="dialog" transition:scaleOrFly|global data-view-transition-new>
35
+ <div class="dialog" {onscroll} role="dialog" transition:scaleOrFly>
36
36
  <div class="close">
37
37
  <RoundButton onclick={() => onclose()}>
38
38
  <IconClose />
@@ -50,7 +50,6 @@
50
50
  <style lang="scss">
51
51
  .modal {
52
52
  z-index: 2147483647; // As high as she goes
53
- box-sizing: border-box;
54
53
  position: fixed;
55
54
  display: flex;
56
55
  justify-content: center;
@@ -71,7 +70,6 @@
71
70
 
72
71
 
73
72
  .dialog {
74
- z-index: 1;
75
73
  position: relative;
76
74
  width: 100%;
77
75
  max-width: 600px;
@@ -90,7 +88,6 @@
90
88
 
91
89
 
92
90
  .backdrop {
93
- z-index: 0;
94
91
  position: absolute;
95
92
  top: 0;
96
93
  right: 0;
@@ -65,14 +65,12 @@
65
65
 
66
66
  <style lang="scss">
67
67
  img {
68
- --size: #{margin(2)};
69
- height: var(--size);
70
- width: var(--size);
71
68
  border-radius: margin(0.5);
72
69
  background: rgba(0, 0, 0, 0.25);
73
70
 
74
71
  @media (min-width: 640px) {
75
- --size: #{margin(2.5)};
72
+ height: margin(2.5);
73
+ width: margin(2.5);
76
74
  }
77
75
  }
78
76
 
@@ -106,7 +104,8 @@
106
104
 
107
105
  img {
108
106
  @media (min-width: 640px) {
109
- --size: #{margin(2)};
107
+ height: margin(2);
108
+ width: margin(2);
110
109
  }
111
110
  }
112
111
  }