diff --git a/worklenz-frontend/public/locales/alb/common.json b/worklenz-frontend/public/locales/alb/common.json index 8b005314..66eacc6f 100644 --- a/worklenz-frontend/public/locales/alb/common.json +++ b/worklenz-frontend/public/locales/alb/common.json @@ -6,5 +6,12 @@ "reconnecting": "Jeni shkëputur nga serveri.", "connection-lost": "Lidhja me serverin dështoi. Ju lutemi kontrolloni lidhjen tuaj me internet.", "connection-restored": "U lidhët me serverin me sukses", - "cancel": "Anulo" + "cancel": "Anulo", + "update-available": "Worklenz u përditesua!", + "update-description": "Një version i ri i Worklenz është i disponueshëm me karakteristikat dhe përmirësimet më të fundit.", + "update-instruction": "Për eksperiencën më të mirë, ju lutemi rifreskoni faqen për të aplikuar ndryshimet e reja.", + "update-whats-new": "💡 <1>Çfarë ka të re: Përmirësim i performancës, rregullime të gabimeve dhe eksperiencön e përmirësuar e përdoruesit", + "update-now": "Përditeso tani", + "update-later": "Më vonë", + "updating": "Duke u përditesuar..." } diff --git a/worklenz-frontend/public/locales/de/common.json b/worklenz-frontend/public/locales/de/common.json index 10ad2bc1..8e880202 100644 --- a/worklenz-frontend/public/locales/de/common.json +++ b/worklenz-frontend/public/locales/de/common.json @@ -6,5 +6,12 @@ "reconnecting": "Vom Server getrennt.", "connection-lost": "Verbindung zum Server fehlgeschlagen. Bitte überprüfen Sie Ihre Internetverbindung.", "connection-restored": "Erfolgreich mit dem Server verbunden", - "cancel": "Abbrechen" + "cancel": "Abbrechen", + "update-available": "Worklenz aktualisiert!", + "update-description": "Eine neue Version von Worklenz ist verfügbar mit den neuesten Funktionen und Verbesserungen.", + "update-instruction": "Für die beste Erfahrung laden Sie bitte die Seite neu, um die neuen Änderungen zu übernehmen.", + "update-whats-new": "💡 <1>Was ist neu: Verbesserte Leistung, Fehlerbehebungen und verbesserte Benutzererfahrung", + "update-now": "Jetzt aktualisieren", + "update-later": "Später", + "updating": "Wird aktualisiert..." } diff --git a/worklenz-frontend/public/locales/en/common.json b/worklenz-frontend/public/locales/en/common.json index 641bd765..192b47b1 100644 --- a/worklenz-frontend/public/locales/en/common.json +++ b/worklenz-frontend/public/locales/en/common.json @@ -6,5 +6,12 @@ "reconnecting": "Disconnected from server.", "connection-lost": "Failed to connect to server. Please check your internet connection.", "connection-restored": "Connected to server successfully", - "cancel": "Cancel" + "cancel": "Cancel", + "update-available": "Worklenz Updated!", + "update-description": "A new version of Worklenz is available with the latest features and improvements.", + "update-instruction": "To get the best experience, please reload the page to apply the new changes.", + "update-whats-new": "💡 <1>What's new: Enhanced performance, bug fixes, and improved user experience", + "update-now": "Update Now", + "update-later": "Later", + "updating": "Updating..." } diff --git a/worklenz-frontend/public/locales/es/common.json b/worklenz-frontend/public/locales/es/common.json index 2f04e775..46753312 100644 --- a/worklenz-frontend/public/locales/es/common.json +++ b/worklenz-frontend/public/locales/es/common.json @@ -6,5 +6,12 @@ "reconnecting": "Reconectando al servidor...", "connection-lost": "Conexión perdida. Intentando reconectarse...", "connection-restored": "Conexión restaurada. Reconectando al servidor...", - "cancel": "Cancelar" + "cancel": "Cancelar", + "update-available": "¡Worklenz actualizado!", + "update-description": "Una nueva versión de Worklenz está disponible con las últimas funciones y mejoras.", + "update-instruction": "Para obtener la mejor experiencia, por favor recarga la página para aplicar los nuevos cambios.", + "update-whats-new": "💡 <1>Qué hay de nuevo: Rendimiento mejorado, correcciones de errores y experiencia de usuario mejorada", + "update-now": "Actualizar ahora", + "update-later": "Más tarde", + "updating": "Actualizando..." } diff --git a/worklenz-frontend/public/locales/pt/common.json b/worklenz-frontend/public/locales/pt/common.json index 0e192458..9f452de7 100644 --- a/worklenz-frontend/public/locales/pt/common.json +++ b/worklenz-frontend/public/locales/pt/common.json @@ -6,5 +6,12 @@ "reconnecting": "Reconectando ao servidor...", "connection-lost": "Conexão perdida. Tentando reconectar...", "connection-restored": "Conexão restaurada. Reconectando ao servidor...", - "cancel": "Cancelar" + "cancel": "Cancelar", + "update-available": "Worklenz atualizado!", + "update-description": "Uma nova versão do Worklenz está disponível com os recursos e melhorias mais recentes.", + "update-instruction": "Para obter a melhor experiência, por favor recarregue a página para aplicar as novas mudanças.", + "update-whats-new": "💡 <1>O que há de novo: Performance aprimorada, correções de bugs e experiência do usuário melhorada", + "update-now": "Atualizar agora", + "update-later": "Mais tarde", + "updating": "Atualizando..." } diff --git a/worklenz-frontend/public/locales/zh/common.json b/worklenz-frontend/public/locales/zh/common.json index 44821492..0bef1fbf 100644 --- a/worklenz-frontend/public/locales/zh/common.json +++ b/worklenz-frontend/public/locales/zh/common.json @@ -6,5 +6,12 @@ "reconnecting": "与服务器断开连接。", "connection-lost": "无法连接到服务器。请检查您的互联网连接。", "connection-restored": "成功连接到服务器", - "cancel": "取消" + "cancel": "取消", + "update-available": "Worklenz 已更新!", + "update-description": "Worklenz 的新版本已可用,具有最新的功能和改进。", + "update-instruction": "为了获得最佳体验,请刷新页面以应用新更改。", + "update-whats-new": "💡 <1>新增内容:性能增强、错误修复和用户体验改善", + "update-now": "立即更新", + "update-later": "稍后", + "updating": "正在更新..." } \ No newline at end of file diff --git a/worklenz-frontend/public/sw.js b/worklenz-frontend/public/sw.js index 15dbef76..2e299274 100644 --- a/worklenz-frontend/public/sw.js +++ b/worklenz-frontend/public/sw.js @@ -325,6 +325,12 @@ self.addEventListener('message', event => { event.ports[0].postMessage({ version: CACHE_VERSION }); break; + case 'CHECK_FOR_UPDATES': + checkForUpdates().then((hasUpdates) => { + event.ports[0].postMessage({ hasUpdates }); + }); + break; + case 'CLEAR_CACHE': clearAllCaches().then(() => { event.ports[0].postMessage({ success: true }); @@ -349,6 +355,44 @@ async function clearAllCaches() { console.log('Service Worker: All caches cleared'); } +async function checkForUpdates() { + try { + // Check if there's a new service worker available + const registration = await self.registration.update(); + const hasNewWorker = registration.installing || registration.waiting; + + if (hasNewWorker) { + console.log('Service Worker: New version detected'); + return true; + } + + // Also check if the main app files have been updated by trying to fetch index.html + // and comparing it with the cached version + try { + const cache = await caches.open(CACHE_NAMES.STATIC); + const cachedResponse = await cache.match('/'); + const networkResponse = await fetch('/', { cache: 'no-cache' }); + + if (cachedResponse && networkResponse.ok) { + const cachedContent = await cachedResponse.text(); + const networkContent = await networkResponse.text(); + + if (cachedContent !== networkContent) { + console.log('Service Worker: App content has changed'); + return true; + } + } + } catch (error) { + console.log('Service Worker: Could not check for content updates', error); + } + + return false; + } catch (error) { + console.error('Service Worker: Error checking for updates', error); + return false; + } +} + async function handleLogout() { try { // Clear all caches diff --git a/worklenz-frontend/src/App.tsx b/worklenz-frontend/src/App.tsx index 0f29cdcd..92beecd6 100644 --- a/worklenz-frontend/src/App.tsx +++ b/worklenz-frontend/src/App.tsx @@ -6,6 +6,7 @@ import i18next from 'i18next'; // Components import ThemeWrapper from './features/theme/ThemeWrapper'; import ModuleErrorBoundary from './components/ModuleErrorBoundary'; +import { UpdateNotificationProvider } from './components/update-notification'; // Routes import router from './app/routes'; @@ -202,14 +203,16 @@ const App: React.FC = memo(() => { return ( }> - - - + + + + + ); diff --git a/worklenz-frontend/src/components/index.ts b/worklenz-frontend/src/components/index.ts index 57cd8bcd..65c4efa9 100644 --- a/worklenz-frontend/src/components/index.ts +++ b/worklenz-frontend/src/components/index.ts @@ -10,3 +10,6 @@ export { default as LabelsSelector } from './LabelsSelector'; export { default as Progress } from './Progress'; export { default as Tag } from './Tag'; export { default as Tooltip } from './Tooltip'; + +// Update Notification Components +export * from './update-notification'; diff --git a/worklenz-frontend/src/components/update-notification/UpdateNotification.tsx b/worklenz-frontend/src/components/update-notification/UpdateNotification.tsx new file mode 100644 index 00000000..8c377223 --- /dev/null +++ b/worklenz-frontend/src/components/update-notification/UpdateNotification.tsx @@ -0,0 +1,121 @@ +// Update Notification Component +// Shows a notification when new build is available and provides update options + +import React from 'react'; +import { Modal, Button, Space, Typography } from '@/shared/antd-imports'; +import { ReloadOutlined, CloseOutlined, DownloadOutlined } from '@/shared/antd-imports'; +import { useTranslation } from 'react-i18next'; +import { useServiceWorker } from '../../utils/serviceWorkerRegistration'; + +const { Text, Title } = Typography; + +interface UpdateNotificationProps { + visible: boolean; + onClose: () => void; + onUpdate: () => void; +} + +const UpdateNotification: React.FC = ({ + visible, + onClose, + onUpdate +}) => { + const { t } = useTranslation('common'); + const [isUpdating, setIsUpdating] = React.useState(false); + const { hardReload } = useServiceWorker(); + + const handleUpdate = async () => { + setIsUpdating(true); + try { + if (hardReload) { + await hardReload(); + } else { + // Fallback to regular reload + window.location.reload(); + } + onUpdate(); + } catch (error) { + console.error('Error during update:', error); + // Fallback to regular reload + window.location.reload(); + } + }; + + const handleLater = () => { + onClose(); + }; + + return ( + + + + {t('update-available')} + + + } + open={visible} + onCancel={handleLater} + footer={null} + centered + closable={false} + maskClosable={false} + width={460} + styles={{ + body: { padding: '20px 24px' } + }} + > +
+ + {t('update-description')} + +
+
+ + {t('update-instruction')} + +
+ +
+ + {t('update-whats-new', { + interpolation: { escapeValue: false } + })} + +
+ + + + + +
+ ); +}; + +export default UpdateNotification; \ No newline at end of file diff --git a/worklenz-frontend/src/components/update-notification/UpdateNotificationProvider.tsx b/worklenz-frontend/src/components/update-notification/UpdateNotificationProvider.tsx new file mode 100644 index 00000000..180286c9 --- /dev/null +++ b/worklenz-frontend/src/components/update-notification/UpdateNotificationProvider.tsx @@ -0,0 +1,50 @@ +// Update Notification Provider +// Provides global update notification management + +import React from 'react'; +import { useUpdateChecker } from '../../hooks/useUpdateChecker'; +import UpdateNotification from './UpdateNotification'; + +interface UpdateNotificationProviderProps { + children: React.ReactNode; + checkInterval?: number; + enableAutoCheck?: boolean; +} + +const UpdateNotificationProvider: React.FC = ({ + children, + checkInterval = 5 * 60 * 1000, // 5 minutes + enableAutoCheck = true +}) => { + const { + showUpdateNotification, + setShowUpdateNotification, + dismissUpdate + } = useUpdateChecker({ + checkInterval, + enableAutoCheck, + showNotificationOnUpdate: true + }); + + const handleClose = () => { + dismissUpdate(); + }; + + const handleUpdate = () => { + // The hardReload function in UpdateNotification will handle the actual update + setShowUpdateNotification(false); + }; + + return ( + <> + {children} + + + ); +}; + +export default UpdateNotificationProvider; \ No newline at end of file diff --git a/worklenz-frontend/src/components/update-notification/index.ts b/worklenz-frontend/src/components/update-notification/index.ts new file mode 100644 index 00000000..47ec8d9b --- /dev/null +++ b/worklenz-frontend/src/components/update-notification/index.ts @@ -0,0 +1,2 @@ +export { default as UpdateNotification } from './UpdateNotification'; +export { default as UpdateNotificationProvider } from './UpdateNotificationProvider'; \ No newline at end of file diff --git a/worklenz-frontend/src/hooks/useUpdateChecker.ts b/worklenz-frontend/src/hooks/useUpdateChecker.ts new file mode 100644 index 00000000..2ecaf1ce --- /dev/null +++ b/worklenz-frontend/src/hooks/useUpdateChecker.ts @@ -0,0 +1,141 @@ +// Update Checker Hook +// Periodically checks for app updates and manages update notifications + +import React from 'react'; +import { useServiceWorker } from '../utils/serviceWorkerRegistration'; + +interface UseUpdateCheckerOptions { + checkInterval?: number; // Check interval in milliseconds (default: 5 minutes) + enableAutoCheck?: boolean; // Enable automatic checking (default: true) + showNotificationOnUpdate?: boolean; // Show notification when update is found (default: true) +} + +interface UseUpdateCheckerReturn { + hasUpdate: boolean; + isChecking: boolean; + lastChecked: Date | null; + checkForUpdates: () => Promise; + dismissUpdate: () => void; + showUpdateNotification: boolean; + setShowUpdateNotification: (show: boolean) => void; +} + +export function useUpdateChecker(options: UseUpdateCheckerOptions = {}): UseUpdateCheckerReturn { + const { + checkInterval = 5 * 60 * 1000, // 5 minutes + enableAutoCheck = true, + showNotificationOnUpdate = true + } = options; + + const { checkForUpdates: serviceWorkerCheckUpdates, swManager } = useServiceWorker(); + + const [hasUpdate, setHasUpdate] = React.useState(false); + const [isChecking, setIsChecking] = React.useState(false); + const [lastChecked, setLastChecked] = React.useState(null); + const [showUpdateNotification, setShowUpdateNotification] = React.useState(false); + const [updateDismissed, setUpdateDismissed] = React.useState(false); + + // Check for updates function + const checkForUpdates = React.useCallback(async () => { + if (!serviceWorkerCheckUpdates || isChecking) return; + + setIsChecking(true); + try { + const hasUpdates = await serviceWorkerCheckUpdates(); + setHasUpdate(hasUpdates); + setLastChecked(new Date()); + + // Show notification if update found and user hasn't dismissed it + if (hasUpdates && showNotificationOnUpdate && !updateDismissed) { + setShowUpdateNotification(true); + } + + console.log('Update check completed:', { hasUpdates }); + } catch (error) { + console.error('Error checking for updates:', error); + } finally { + setIsChecking(false); + } + }, [serviceWorkerCheckUpdates, isChecking, showNotificationOnUpdate, updateDismissed]); + + // Dismiss update notification + const dismissUpdate = React.useCallback(() => { + setUpdateDismissed(true); + setShowUpdateNotification(false); + }, []); + + // Set up automatic checking interval + React.useEffect(() => { + if (!enableAutoCheck || !swManager) return; + + // Initial check after a short delay + const initialTimeout = setTimeout(() => { + checkForUpdates(); + }, 10000); // 10 seconds after component mount + + // Set up interval for periodic checks + const intervalId = setInterval(() => { + checkForUpdates(); + }, checkInterval); + + return () => { + clearTimeout(initialTimeout); + clearInterval(intervalId); + }; + }, [enableAutoCheck, swManager, checkInterval, checkForUpdates]); + + // Listen for visibility change to check for updates when user returns to tab + React.useEffect(() => { + if (!enableAutoCheck) return; + + const handleVisibilityChange = () => { + if (!document.hidden && swManager) { + // Check for updates when user returns to the tab + setTimeout(() => { + checkForUpdates(); + }, 2000); // 2 second delay + } + }; + + document.addEventListener('visibilitychange', handleVisibilityChange); + return () => { + document.removeEventListener('visibilitychange', handleVisibilityChange); + }; + }, [enableAutoCheck, swManager, checkForUpdates]); + + // Listen for focus events to check for updates + React.useEffect(() => { + if (!enableAutoCheck) return; + + const handleFocus = () => { + if (swManager && !isChecking) { + // Check for updates when window regains focus + setTimeout(() => { + checkForUpdates(); + }, 1000); // 1 second delay + } + }; + + window.addEventListener('focus', handleFocus); + return () => { + window.removeEventListener('focus', handleFocus); + }; + }, [enableAutoCheck, swManager, isChecking, checkForUpdates]); + + // Reset dismissed state when new update is found + React.useEffect(() => { + if (hasUpdate && updateDismissed) { + setUpdateDismissed(false); + } + }, [hasUpdate, updateDismissed]); + + return { + hasUpdate, + isChecking, + lastChecked, + checkForUpdates, + dismissUpdate, + showUpdateNotification, + setShowUpdateNotification + }; +} \ No newline at end of file diff --git a/worklenz-frontend/src/utils/serviceWorkerRegistration.ts b/worklenz-frontend/src/utils/serviceWorkerRegistration.ts index 1f70f785..e89f8230 100644 --- a/worklenz-frontend/src/utils/serviceWorkerRegistration.ts +++ b/worklenz-frontend/src/utils/serviceWorkerRegistration.ts @@ -198,6 +198,17 @@ export class ServiceWorkerManager { } } + // Check for updates + async checkForUpdates(): Promise { + try { + const response = await this.sendMessage('CHECK_FOR_UPDATES'); + return response.hasUpdates; + } catch (error) { + console.error('Failed to check for updates:', error); + return false; + } + } + // Force update service worker async forceUpdate(): Promise { if (!this.registration) return; @@ -212,6 +223,27 @@ export class ServiceWorkerManager { } } + // Perform hard reload (clear cache and reload) + async hardReload(): Promise { + try { + // Clear all caches first + await this.clearCache(); + + // Force update the service worker + if (this.registration) { + await this.registration.update(); + await this.sendMessage('SKIP_WAITING'); + } + + // Perform hard reload by clearing browser cache + window.location.reload(); + } catch (error) { + console.error('Failed to perform hard reload:', error); + // Fallback to regular reload + window.location.reload(); + } + } + // Check if app is running offline isOffline(): boolean { return !navigator.onLine; @@ -268,6 +300,8 @@ export function useServiceWorker() { swManager, clearCache: () => swManager?.clearCache(), forceUpdate: () => swManager?.forceUpdate(), + hardReload: () => swManager?.hardReload(), + checkForUpdates: () => swManager?.checkForUpdates(), getVersion: () => swManager?.getVersion(), }; } \ No newline at end of file