templates/js/initialJS.html.twig line 1

Open in your IDE?
  1. <script type="text/javascript">
  2.     const ROOT_PATH = window.location.origin; // always match current host to avoid CORS
  3.     const CADDY_PATH = `${ROOT_PATH}/hub`;
  4.     window.Application = {};
  5.     const APP_CONTEXT = window.Application;
  6.     window.APP_CONTEXT = APP_CONTEXT; // keep a single shared reference for controllers
  7.     APP_CONTEXT.toast = {}; // Toast dans ToasPageLoading
  8.     try {
  9.         const storedThemeVersion = window.localStorage?.getItem('themeVersion');
  10.         APP_CONTEXT.themeVersion = storedThemeVersion || document.documentElement.getAttribute('data-theme') || 'v2';
  11.         document.documentElement.setAttribute('data-theme', APP_CONTEXT.themeVersion);
  12.     } catch (e) {
  13.         APP_CONTEXT.themeVersion = document.documentElement.getAttribute('data-theme') || 'v2';
  14.     }
  15. </script>
  16. {% if app.user %}
  17. <script type="text/javascript">
  18.     APP_CONTEXT.APP_USER = {
  19.         'id': '{{app.user.id}}',
  20.         'email': '{{app.user.email}}',
  21.         'username': '{{app.user.username}}',
  22.         'image': '{{app.user.image}}',
  23.         'isPremium': {{ app.user.isPremium ? 'true' : 'false' }},
  24.         'locationOptIn': {{ app.user.locationOptIn ? 'true' : 'false' }},
  25.         'roles': {{ app.user.roles|json_encode|raw }},
  26.         'var' : {},
  27.         'function' : {},
  28.         'history' : [],
  29.     }
  30.     APP_CONTEXT.pendingMessages = [];
  31.     APP_CONTEXT.function = {};
  32.     APP_CONTEXT.var = {};
  33.     APP_CONTEXT.toast = {}; // Toast dans ToasPageLoading
  34.     /**
  35.     * JSON command utils
  36.     */
  37.     APP_CONTEXT.scrollToBottom = (element = document.querySelector('.messageContainer')) => {
  38.         if (!element) {
  39.             return;
  40.         }
  41.         /** Temps de traitement du .dispatch(), setState(), etc... */
  42.         setTimeout(() => {
  43.             element.scrollTop = element.scrollHeight ?? 99999;
  44.         }, 10);
  45.     }
  46.     APP_CONTEXT.executeCommands = function(commands){
  47.         const list = Array.isArray(commands) ? commands : (commands && typeof commands === 'object' ? [commands] : []);
  48.         for (const command of list) {
  49.             if (!command) {
  50.                 continue;
  51.             }
  52.             if (!command.command) {
  53.                 if (command.notification || command.id) {
  54.                     if (APP_CONTEXT.Notification?.Global?.toast) {
  55.                         APP_CONTEXT.Notification.Global.toast(command);
  56.                     }
  57.                 }
  58.                 continue;
  59.             }
  60.             const context = {...APP_CONTEXT};
  61.             APP_CONTEXT.executeFunctionByName(command.command, context, command); // Passer toute la commande envoyer par le serveur
  62.         }
  63.     }
  64.     APP_CONTEXT.executeFunctionByName = function(functionName, context, args) {
  65.         // console.log('executeFunctionByName', functionName, context, args);
  66.         var namespaces = functionName.split(".");
  67.         var func = namespaces.pop();
  68.         for(var i = 0; i < namespaces.length; i++) {
  69.             if (!context || typeof context !== 'object') {
  70.                 break;
  71.             }
  72.             context = context[namespaces[i]]; // context est remplacer par le premier context trouvé, puis cherche dans celui-ci pour tout les namespace (Post.article.page)
  73.         }
  74.         if (!context || typeof context[func] !== 'function') {
  75.             if (functionName.startsWith('Notification.') && APP_CONTEXT.Notification?.Global?.toast) {
  76.                 return APP_CONTEXT.Notification.Global.toast(args);
  77.             }
  78.             return APP_CONTEXT.deferCommand(functionName, args);
  79.         }
  80.         return context[func].apply(context, [args]); // /!\ [args] != args : apply() attent un tableau à décomposer
  81.     }
  82.     APP_CONTEXT.deferCommand = function(functionName, args) {
  83.         // Queue messages until the SSE controller is ready
  84.         if (functionName === 'Message.add') {
  85.             APP_CONTEXT.pendingMessages = APP_CONTEXT.pendingMessages || [];
  86.             APP_CONTEXT.pendingMessages.push(args);
  87.         } else {
  88.             APP_CONTEXT.pendingCommands = APP_CONTEXT.pendingCommands || [];
  89.             APP_CONTEXT.pendingCommands.push({ functionName, args });
  90.         }
  91.         console.warn('[SSE] Command deferred', functionName, args);
  92.     }
  93.     /**
  94.     * SSE Subscribe to the topic and set commands listener.
  95.     * Only started once the Stimulus SSE controller marks itself ready to avoid early command defers.
  96.     */
  97.     APP_CONTEXT.SSE = APP_CONTEXT.SSE || {};
  98.     APP_CONTEXT.SSE.start = function setApplicationSSELitener() {
  99.         if (APP_CONTEXT.SSE.eventSource) {
  100.             return; // already started
  101.         }
  102.         const uri = ROOT_PATH + '/hub'
  103.         const url = new URL(uri); // generate URL
  104.         // url.searchParams.append('topic', 'GLOBALTOPIC') // add topic params
  105.         url.searchParams.append('topic', '{{ app.user.flux.topicLabel }}') // add topic params
  106.         const eventSource = new EventSource(url, { withCredentials: true }) // set event source
  107.         APP_CONTEXT.SSE.eventSource = eventSource;
  108.         eventSource.addEventListener('message', event => { // set callback
  109.             console.log('SSE Event: ', JSON.parse(event.data));
  110.             APP_CONTEXT.executeCommands(JSON.parse(event.data));
  111.         })
  112.     }
  113.     /**
  114.     * Récupère les notifications en attente (file backend).
  115.     */
  116.     APP_CONTEXT.fetchPendingNotifications = async function(attempt = 0) {
  117.         const toastReady = APP_CONTEXT.toast && typeof APP_CONTEXT.toast.setShow === 'function';
  118.         if (!toastReady && attempt < 10) {
  119.             // Attendre que le composant ToastPageLoading enregistre ses setters
  120.             setTimeout(() => APP_CONTEXT.fetchPendingNotifications(attempt + 1), 300);
  121.             return;
  122.         }
  123.         try {
  124.             const resp = await fetch('/api/notifications/pending', { credentials: 'include' });
  125.             if (!resp.ok) return;
  126.             const data = await resp.json();
  127.             let toastScheduled = false;
  128.             if (data.total && data.total > 0) {
  129.                 const latest = data.items?.[data.items.length - 1];
  130.                 const unreadByConversation = {};
  131.                 (data.items || []).forEach((item) => {
  132.                     if (!item?.conversationId) return;
  133.                     const convKey = String(item.conversationId).match(/(\d+)(?!.*\d)/);
  134.                     const key = convKey ? convKey[1] : String(item.conversationId);
  135.                     unreadByConversation[key] = (unreadByConversation[key] || 0) + 1;
  136.                 });
  137.                 const preview = latest?.content || '';
  138.                 const sender = latest?.sender || 'Nouveau message';
  139.                 const msg = preview
  140.                     ? `${preview}`.substring(0, 140)
  141.                     : (data.total === 1
  142.                         ? '1 nouveau message reçu pendant votre absence.'
  143.                         : `${data.total} nouveaux messages reçus pendant votre absence.`);
  144.                 if (APP_CONTEXT.toast?.setContent) {
  145.                     APP_CONTEXT.toast.setTitle(sender);
  146.                     APP_CONTEXT.toast.setContent(msg);
  147.                     APP_CONTEXT.toast.setRedirectUrl('/conversation');
  148.                     APP_CONTEXT.toast.setShow(true);
  149.                     toastScheduled = true;
  150.                 }
  151.                 APP_CONTEXT.APP_USER = APP_CONTEXT.APP_USER || {};
  152.                 APP_CONTEXT.APP_USER.pendingNotifications = data.total;
  153.                 const unreadList = data.items?.map(i => ({
  154.                     conversationId: i.conversationId,
  155.                     messageId: i.messageId,
  156.                     content: i.content,
  157.                     sender: i.sender,
  158.                     date: i.createdAt * 1000
  159.                 })) || [];
  160.                 const messageNotifications = (data.items || []).map((item) => {
  161.                     const sender = item.sender || 'Quelqu’un';
  162.                     const content = item.content || '';
  163.                     const createdAt = item.createdAt ? new Date(item.createdAt * 1000).toISOString() : new Date().toISOString();
  164.                     return {
  165.                         id: item.messageId ? `msg-${item.messageId}` : `msg-${item.conversationId || 'unknown'}-${item.createdAt || Date.now()}`,
  166.                         type: 'Message.offline',
  167.                         message: content ? `${sender}: ${content}` : `${sender} vous a envoyé un message`,
  168.                         redirectUrl: item.conversationId ? `/conversation/${item.conversationId}` : '/conversation',
  169.                         dateCreation: createdAt,
  170.                         sourceUser: item.sender ? { username: item.sender } : null,
  171.                         conversationId: item.conversationId,
  172.                         messageId: item.messageId,
  173.                         isPendingMessage: true,
  174.                     };
  175.                 });
  176.                 if (window.NotificationBus?.setUnreadMap) {
  177.                     window.NotificationBus.setUnreadMap(unreadByConversation);
  178.                     window.NotificationBus.setUnreadTotal(data.total);
  179.                     window.NotificationBus.emitNewMessages({
  180.                         unreadCount: data.total,
  181.                         unread: unreadList,
  182.                         unreadByConversation,
  183.                     });
  184.                     if (messageNotifications.length > 0 && window.NotificationBus.appendNotifications) {
  185.                         window.NotificationBus.appendNotifications(messageNotifications);
  186.                         window.NotificationBus.emitNotifications(messageNotifications);
  187.                     }
  188.                 } else {
  189.                     try {
  190.                         window.localStorage.setItem('app_unread_conversations', data.total);
  191.                         window.localStorage.setItem('app_unread_by_conv', JSON.stringify(unreadByConversation));
  192.                         window.localStorage.setItem('app_unread_conversations_map', JSON.stringify(unreadByConversation)); // legacy compat
  193.                     } catch (e) {}
  194.                     window.dispatchEvent(new CustomEvent('app:newMessages', {
  195.                         detail: {
  196.                             unreadCount: data.total,
  197.                             unread: unreadList,
  198.                             unreadByConversation,
  199.                         }
  200.                     }));
  201.                     if (messageNotifications.length > 0) {
  202.                         APP_CONTEXT.notificationsList = [
  203.                             ...messageNotifications,
  204.                             ...(APP_CONTEXT.notificationsList || []),
  205.                         ];
  206.                         window.dispatchEvent(new CustomEvent('app:notifications', {
  207.                             detail: { notifications: messageNotifications },
  208.                         }));
  209.                     }
  210.                 }
  211.             }
  212.             const notifTotal = Number(data.notifications_total || 0);
  213.             if (window.NotificationBus?.setPendingCount) {
  214.                 window.NotificationBus.setPendingCount(notifTotal);
  215.             } else {
  216.                 try {
  217.                     window.localStorage.setItem('app_pending_notifications', String(notifTotal));
  218.                 } catch (e) {}
  219.             }
  220.             if (window.NotificationBus?.emitPendingCount) {
  221.                 window.NotificationBus.emitPendingCount(notifTotal);
  222.             } else {
  223.                 window.dispatchEvent(new CustomEvent('app:pendingNotifications', {
  224.                     detail: { count: notifTotal },
  225.                 }));
  226.             }
  227.             if (Array.isArray(data.notifications) && data.notifications.length > 0) {
  228.                 if (window.NotificationBus?.setPendingList) {
  229.                     window.NotificationBus.setPendingList(data.notifications);
  230.                 } else {
  231.                     APP_CONTEXT.pendingNotificationsList = data.notifications;
  232.                 }
  233.                 if (window.NotificationBus?.emitPendingList) {
  234.                     window.NotificationBus.emitPendingList(data.notifications);
  235.                 } else {
  236.                     window.dispatchEvent(new CustomEvent('app:pendingNotificationsList', {
  237.                         detail: { notifications: data.notifications },
  238.                     }));
  239.                 }
  240.             }
  241.             if (notifTotal > 0) {
  242.                 const lastNotif = data.notifications?.[data.notifications.length - 1];
  243.                 const notifFallback = "{{ 'header.notifications.fallback'|trans }}";
  244.                 const title = lastNotif?.sourceUser?.username || notifFallback;
  245.                 const content = lastNotif?.message || notifFallback;
  246.                 const redirectUrl = lastNotif?.redirectUrl || '/conversation';
  247.                 const showToast = () => {
  248.                     if (APP_CONTEXT.toast?.setContent) {
  249.                         APP_CONTEXT.toast.setTitle(title);
  250.                         APP_CONTEXT.toast.setContent(content);
  251.                         APP_CONTEXT.toast.setRedirectUrl(redirectUrl);
  252.                         APP_CONTEXT.toast.setShow(true);
  253.                     }
  254.                 };
  255.                 if (toastScheduled) {
  256.                     setTimeout(showToast, 1200);
  257.                 } else {
  258.                     showToast();
  259.                 }
  260.             }
  261.         } catch (e) {
  262.             console.warn('Unable to fetch pending notifications', e);
  263.         }
  264.     }
  265.     /**
  266.     * Presence heartbeat (online/offline) via SSE.
  267.     */
  268.     APP_CONTEXT.presence = APP_CONTEXT.presence || {};
  269.     APP_CONTEXT.presence.intervalMs = 30000;
  270.     APP_CONTEXT.location = APP_CONTEXT.location || {};
  271.     APP_CONTEXT.location.optIn = !!APP_CONTEXT.APP_USER?.locationOptIn;
  272.     APP_CONTEXT.location.getCurrent = function() {
  273.         return new Promise((resolve) => {
  274.             if (!APP_CONTEXT.location.optIn || !navigator.geolocation) {
  275.                 return resolve(null);
  276.             }
  277.             navigator.geolocation.getCurrentPosition(
  278.                 (pos) => resolve({
  279.                     latitude: pos.coords.latitude,
  280.                     longitude: pos.coords.longitude,
  281.                     accuracy: pos.coords.accuracy,
  282.                 }),
  283.                 () => resolve(null),
  284.                 {
  285.                     enableHighAccuracy: false,
  286.                     timeout: 2000,
  287.                     maximumAge: 60000,
  288.                 }
  289.             );
  290.         });
  291.     };
  292.     APP_CONTEXT.presence.send = async function(isOnline) {
  293.         const url = isOnline ? '/user/heart-beat/actif' : '/user/heart-beat/inactif';
  294.         if (!isOnline) {
  295.             fetch(url, { credentials: 'include', keepalive: true, priority: 'low' }).catch(() => {});
  296.             return;
  297.         }
  298.         const coords = await APP_CONTEXT.location.getCurrent();
  299.         const payload = coords ? JSON.stringify(coords) : '';
  300.         fetch(url, {
  301.             method: 'POST',
  302.             credentials: 'include',
  303.             keepalive: true,
  304.             priority: 'low',
  305.             headers: payload ? { 'Content-Type': 'application/json' } : undefined,
  306.             body: payload || undefined,
  307.         }).catch(() => {});
  308.     };
  309.     APP_CONTEXT.presence.ping = function() {
  310.         APP_CONTEXT.presence.send(true);
  311.     };
  312.     APP_CONTEXT.presence.start = function() {
  313.         const boot = () => {
  314.             APP_CONTEXT.presence.send(true);
  315.             if (!APP_CONTEXT.presence._interval) {
  316.                 APP_CONTEXT.presence._interval = setInterval(APP_CONTEXT.presence.ping, APP_CONTEXT.presence.intervalMs);
  317.             }
  318.             document.addEventListener('visibilitychange', () => {
  319.                 if (document.visibilityState === 'visible') {
  320.                     APP_CONTEXT.presence.send(true);
  321.                 }
  322.             });
  323.             window.addEventListener('pagehide', () => {
  324.                 APP_CONTEXT.presence.send(false, true);
  325.             });
  326.         };
  327.         if (window.requestIdleCallback) {
  328.             window.requestIdleCallback(boot, { timeout: 1500 });
  329.         } else {
  330.             setTimeout(boot, 300);
  331.         }
  332.     };
  333.     APP_CONTEXT.presence.start();
  334.     // déclenchement immédiat
  335.     APP_CONTEXT.fetchPendingNotifications();
  336. </script>
  337. {% endif %}