diff --git a/src/Umbraco.Web.UI.Client/src/apps/preview/preview.context.ts b/src/Umbraco.Web.UI.Client/src/apps/preview/preview.context.ts index 75de96a38c..2acbd754de 100644 --- a/src/Umbraco.Web.UI.Client/src/apps/preview/preview.context.ts +++ b/src/Umbraco.Web.UI.Client/src/apps/preview/preview.context.ts @@ -7,6 +7,9 @@ import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; import { UmbDocumentPreviewRepository } from '@umbraco-cms/backoffice/document'; import { UMB_SERVER_CONTEXT } from '@umbraco-cms/backoffice/server'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import { HubConnectionBuilder, type HubConnection } from '@umbraco-cms/backoffice/external/signalr'; +import { UMB_NOTIFICATION_CONTEXT } from '@umbraco-cms/backoffice/notification'; +import { UmbLocalizationController } from '@umbraco-cms/backoffice/localization-api'; const UMB_LOCALSTORAGE_SESSION_KEY = 'umb:previewSessions'; @@ -31,7 +34,7 @@ export class UmbPreviewContext extends UmbContextBase { #culture?: string | null; #segment?: string | null; #serverUrl: string = ''; - #webSocket?: WebSocket; + #connection?: HubConnection; #iframeReady = new UmbBooleanState(false); public readonly iframeReady = this.#iframeReady.asObservable(); @@ -41,12 +44,13 @@ export class UmbPreviewContext extends UmbContextBase { #documentPreviewRepository = new UmbDocumentPreviewRepository(this); + #notificationContext?: typeof UMB_NOTIFICATION_CONTEXT.TYPE; + #localize = new UmbLocalizationController(this); + constructor(host: UmbControllerHost) { super(host, UMB_PREVIEW_CONTEXT); this.consumeContext(UMB_SERVER_CONTEXT, (instance) => { - this.#serverUrl = instance?.getServerUrl() ?? ''; - const params = new URLSearchParams(window.location.search); this.#unique = params.get('id'); @@ -58,37 +62,62 @@ export class UmbPreviewContext extends UmbContextBase { return; } - this.#setPreviewUrl(); + const serverUrl = instance?.getServerUrl(); + + if (!serverUrl) { + console.error('No server URL found in context'); + return; + } + + this.#serverUrl = serverUrl; + + this.#setPreviewUrl({ serverUrl }); + + this.#initHubConnection(serverUrl); + }); + + this.consumeContext(UMB_NOTIFICATION_CONTEXT, (instance) => { + this.#notificationContext = instance; }); } - #configureWebSocket() { - if (this.#webSocket && this.#webSocket.readyState < 2) return; + async #initHubConnection(serverUrl: string) { + const previewHubUrl = `${serverUrl}/umbraco/PreviewHub`; - const url = `${this.#serverUrl.replace('https://', 'wss://')}/umbraco/PreviewHub`; + // Make sure that no previous connection exists. + if (this.#connection) { + await this.#connection.stop(); + this.#connection = undefined; + } - this.#webSocket = new WebSocket(url); + this.#connection = new HubConnectionBuilder().withUrl(previewHubUrl).build(); - this.#webSocket.addEventListener('open', () => { - // NOTE: SignalR protocol handshake; it requires a terminating control character. - const endChar = String.fromCharCode(30); - this.#webSocket?.send(`{"protocol":"json","version":1}${endChar}`); - }); - - this.#webSocket.addEventListener('message', (event: MessageEvent) => { - if (!event?.data) return; - - // NOTE: Strip the terminating control character, (from SignalR). - const data = event.data.substring(0, event.data.length - 1); - const json = JSON.parse(data) as { type: number; target: string; arguments: Array }; - - if (json.type === 1 && json.target === 'refreshed') { - const pageId = json.arguments?.[0]; - if (pageId === this.#unique) { - this.#setPreviewUrl({ rnd: Math.random() }); - } + this.#connection.on('refreshed', (payload) => { + if (payload === this.#unique) { + this.#setPreviewUrl({ rnd: Math.random() }); } }); + + this.#connection.onclose(() => { + this.#notificationContext?.peek('warning', { + data: { + headline: this.#localize.term('general_preview'), + message: this.#localize.term('preview_connectionLost'), + }, + }); + }); + + try { + await this.#connection.start(); + } catch (error) { + console.error('The SignalR connection could not be established', error); + this.#notificationContext?.peek('warning', { + data: { + headline: this.#localize.term('general_preview'), + message: this.#localize.term('preview_connectionFailed'), + }, + }); + } } async #getPublishedUrl(): Promise { @@ -144,7 +173,7 @@ export class UmbPreviewContext extends UmbContextBase { params.delete(segmentParam); } - const previewUrl = new URL(url.pathname + '?' + params.toString(), host); + const previewUrl = new URL(`${url.pathname}?${params.toString()}`, host); const previewUrlString = previewUrl.toString(); this.#previewUrl.setValue(previewUrlString); @@ -180,9 +209,9 @@ export class UmbPreviewContext extends UmbContextBase { await this.#documentPreviewRepository.exit(); } - if (this.#webSocket) { - this.#webSocket.close(); - this.#webSocket = undefined; + if (this.#connection) { + await this.#connection.stop(); + this.#connection = undefined; } let url = await this.#getPublishedUrl(); @@ -202,7 +231,6 @@ export class UmbPreviewContext extends UmbContextBase { iframeLoaded(iframe: HTMLIFrameElement) { if (!iframe) return; - this.#configureWebSocket(); this.#iframeReady.setValue(true); } diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/da.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/da.ts index 2b08aaf2f5..02b918c910 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/da.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/da.ts @@ -2603,13 +2603,16 @@ export default { returnToPreviewHeadline: 'Forhåndsvisning af indholdet?', returnToPreviewDescription: 'Du har afslutet forhåndsvisning, vil du starte forhåndsvisning igen for at\n se seneste gemte version af indholdet?\n ', + returnToPreviewAcceptButton: 'Start forhåndsvisning igen', returnToPreviewDeclineButton: 'Se udgivet indhold', viewPublishedContentHeadline: 'Se udgivet indhold?', viewPublishedContentDescription: 'Du er i forhåndsvisning, vil du afslutte for at se den udgivet\n version?\n ', viewPublishedContentAcceptButton: 'Se udgivet version', viewPublishedContentDeclineButton: 'Forbliv i forhåndsvisning', - returnToPreviewAcceptButton: 'Preview latest version', + connectionFailed: + 'Kunne ikke etablere forbindelse til serveren, forhåndsvisning af liveopdateringer vil ikke fungere.', + connectionLost: 'Forbindelse til serveren mistet, forhåndsvisning af liveopdateringer vil ikke fungere.', }, permissions: { FolderCreation: 'Mappeoprettelse', diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts index d91f0234bb..68370c54c1 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts @@ -2749,6 +2749,8 @@ export default { 'You are in Preview Mode, do you want exit in order to view the published version of your website?', viewPublishedContentAcceptButton: 'View published version', viewPublishedContentDeclineButton: 'Stay in preview mode', + connectionFailed: 'Could not establish a connection to the server, preview live updates will not work.', + connectionLost: 'Connection to the server lost, preview live updates will not work.', }, permissions: { FolderCreation: 'Folder creation',