diff --git a/Phonebook.Frontend/.dockerignore b/Phonebook.Frontend/.dockerignore index 045d816fd..54c04e4a5 100644 --- a/Phonebook.Frontend/.dockerignore +++ b/Phonebook.Frontend/.dockerignore @@ -11,6 +11,8 @@ !tslint.json !nginx !substitute_variables.sh +!index_rehash.sh !globals.js !version.js -!opensearch.xml \ No newline at end of file +!opensearch.xml +!ngsw-config.json \ No newline at end of file diff --git a/Phonebook.Frontend/Dockerfile b/Phonebook.Frontend/Dockerfile index 494ed65ac..26a25bf12 100644 --- a/Phonebook.Frontend/Dockerfile +++ b/Phonebook.Frontend/Dockerfile @@ -11,6 +11,7 @@ COPY ./package.json /usr/local/app/package.json RUN npm ci # Because: https://stackoverflow.com/questions/37715224/copy-multiple-directories-with-one-command COPY ./src/ ./src/ +COPY ./ngsw-config.json /usr/local/app/ngsw-config.json COPY ["angular.json", "tsconfig.json", "tslint.json", "./"] RUN npm run build:de @@ -38,9 +39,12 @@ COPY ./nginx/ ./ COPY ./substitute_variables.sh ./substitute_variables.sh RUN chmod +x ./substitute_variables.sh +COPY ./index_rehash.sh ./index_rehash.sh +RUN chmod +x ./index_rehash.sh + RUN rm /usr/share/nginx/html/index.html COPY --from=builder /usr/local/app/dist /usr/share/nginx/html COPY ./opensearch.xml /usr/share/nginx/html/opensearch.xml -ENTRYPOINT ["./substitute_variables.sh", "/usr/share/nginx/html", "./substitute_variables.sh", "/etc/nginx"] +ENTRYPOINT ["./substitute_variables.sh", "/usr/share/nginx/html", "./substitute_variables.sh", "/etc/nginx", "./index_rehash.sh", "/usr/share/nginx/html"] CMD ["nginx"] \ No newline at end of file diff --git a/Phonebook.Frontend/angular.json b/Phonebook.Frontend/angular.json index 2397e6065..55cf1f1e9 100644 --- a/Phonebook.Frontend/angular.json +++ b/Phonebook.Frontend/angular.json @@ -107,12 +107,8 @@ "extractLicenses": true, "vendorChunk": false, "buildOptimizer": true, - "fileReplacements": [ - { - "replace": "src/environments/environment.ts", - "with": "src/environments/environment.prod.ts" - } - ] + "serviceWorker": true, + "ngswConfigPath": "ngsw-config.json" }, "configurations": { "en": { @@ -121,7 +117,17 @@ "i18nLocale": "en", "i18nMissingTranslation": "error", "outputPath": "dist/en", - "baseHref": "/en/" + "baseHref": "/en/", + "fileReplacements": [ + { + "replace": "src/manifest.webmanifest", + "with": "src/manifest.en.webmanifest" + }, + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ] }, "de": { "i18nFile": "src/i18n/messages.de.xlf", @@ -129,7 +135,17 @@ "i18nLocale": "de", "i18nMissingTranslation": "error", "outputPath": "dist/de", - "baseHref": "/de/" + "baseHref": "/de/", + "fileReplacements": [ + { + "replace": "src/manifest.webmanifest", + "with": "src/manifest.de.webmanifest" + }, + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ] }, "preview": { "i18nFile": "src/i18n/messages.de.xlf", diff --git a/Phonebook.Frontend/index_rehash.sh b/Phonebook.Frontend/index_rehash.sh new file mode 100644 index 000000000..4fe589114 --- /dev/null +++ b/Phonebook.Frontend/index_rehash.sh @@ -0,0 +1,34 @@ +#!/bin/bash + +# This updates the hash of the index.html in the corresponding ngsw.json +# Execute it after replacing the environment variables + +# Paths to folders containing the index.html files (where the hash should be recalculated) +paths=( "de" "en") + +# The first parameter has to be the path to the directory with the language folders +if [[ -z $1 ]]; then + echo 'ERROR: No target file or directory given.' + exit 1 +fi + + + +# Go to all folders containing index.html and ngsw.json +for i in "${paths[@]}" +do + # Calculate hash of index.html + replaceString=($(sha1sum $1/$i/index.html)) + + if [ "$DEBUG" = true ] + then + # If DEBUG=true in order to log the replaced files + sed -i "s|\"\/$i\/index\.html\":\s\"\(.*\)\"|\"/""$i""/index.html\": \"""$replaceString""\"|Ig;w /dev/stdout" "$1/$i/ngsw.json" + else + # If DEBUG=false do it without logging + sed -i "s|\"\/$i\/index\.html\":\s\"\(.*\)\"|\"/""$i""/index.html\": \"""$replaceString""\"|Ig" "$1/$i/ngsw.json" + fi +done + +# Execute all other parameters +exec "${@:2}" diff --git a/Phonebook.Frontend/nginx/nginx.conf b/Phonebook.Frontend/nginx/nginx.conf index bbec8c1ba..43293ca72 100644 --- a/Phonebook.Frontend/nginx/nginx.conf +++ b/Phonebook.Frontend/nginx/nginx.conf @@ -73,18 +73,6 @@ http { alias /usr/share/nginx/html/opensearch.xml; } - location = /ngsw.json { - return 404; - } - - location = /de/ngsw.json { - return 404; - } - - location = /en/ngsw.json { - return 404; - } - location /de/ { alias /usr/share/nginx/html/de/; add_header Set-Cookie "lang=de;Domain=$host;Path=/;Max-Age=31536000"; diff --git a/Phonebook.Frontend/ngsw-config.json b/Phonebook.Frontend/ngsw-config.json new file mode 100644 index 000000000..933d5811b --- /dev/null +++ b/Phonebook.Frontend/ngsw-config.json @@ -0,0 +1,59 @@ +{ + "index": "/index.html", + "assetGroups": [ + { + "name": "app", + "installMode": "prefetch", + "resources": { + "files": [ + "/favicon.ico", + "/index.html", + "/manifest.webmanifest", + "/*.css", + "/*.js", + "/*.woff2", + "/*.woff", + "/*.svg", + "/*.ttf", + "/*.eot" + ] + } + }, + { + "name": "assets", + "installMode": "prefetch", + "updateMode": "prefetch", + "resources": { + "files": [ + "/assets/**", + "/*.(eot|svg|cur|jpg|png|webp|gif|otf|ttf|woff|woff2|ani)" + ] + } + } + ], + "dataGroups": [ + { + "name": "api", + "urls": [ + "/api/**" + ], + "cacheConfig": { + "strategy": "performance", + "maxSize": 1, + "maxAge": "1h" + } + }, + { + "name": "pictures", + "urls": [ + "*.(jpg|png|webp|gif|svg)" + ], + "cacheConfig": { + "strategy": "freshness", + "timeout": "4s", + "maxSize": 1, + "maxAge": "1h" + } + } + ] +} \ No newline at end of file diff --git a/Phonebook.Frontend/package-lock.json b/Phonebook.Frontend/package-lock.json index 7a5ef7d7c..6a3792fd9 100644 --- a/Phonebook.Frontend/package-lock.json +++ b/Phonebook.Frontend/package-lock.json @@ -9311,8 +9311,7 @@ "ansi-regex": { "version": "2.1.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "aproba": { "version": "1.2.0", @@ -9355,8 +9354,7 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "concat-map": { "version": "0.0.1", @@ -9367,8 +9365,7 @@ "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "core-util-is": { "version": "1.0.2", @@ -9485,8 +9482,7 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "ini": { "version": "1.3.5", @@ -9498,7 +9494,6 @@ "version": "1.0.0", "bundled": true, "dev": true, - "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -9528,7 +9523,6 @@ "version": "2.3.5", "bundled": true, "dev": true, - "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -9547,7 +9541,6 @@ "version": "0.5.1", "bundled": true, "dev": true, - "optional": true, "requires": { "minimist": "0.0.8" } @@ -9641,7 +9634,6 @@ "version": "1.4.0", "bundled": true, "dev": true, - "optional": true, "requires": { "wrappy": "1" } @@ -9727,8 +9719,7 @@ "safe-buffer": { "version": "5.1.2", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "safer-buffer": { "version": "2.1.2", @@ -9764,7 +9755,6 @@ "version": "1.0.2", "bundled": true, "dev": true, - "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -9784,7 +9774,6 @@ "version": "3.0.1", "bundled": true, "dev": true, - "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -9828,14 +9817,12 @@ "wrappy": { "version": "1.0.2", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "yallist": { "version": "3.0.3", "bundled": true, - "dev": true, - "optional": true + "dev": true } } }, @@ -16872,8 +16859,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, - "optional": true + "dev": true } } }, diff --git a/Phonebook.Frontend/package.json b/Phonebook.Frontend/package.json index 4d1817860..aa380b01a 100644 --- a/Phonebook.Frontend/package.json +++ b/Phonebook.Frontend/package.json @@ -115,4 +115,4 @@ "path": "Phonebook.Frontend/node_modules/cz-customizable" } } -} \ No newline at end of file +} diff --git a/Phonebook.Frontend/src/app/app.component.ts b/Phonebook.Frontend/src/app/app.component.ts index c578b235c..5cc39751c 100644 --- a/Phonebook.Frontend/src/app/app.component.ts +++ b/Phonebook.Frontend/src/app/app.component.ts @@ -1,18 +1,19 @@ import { Platform } from '@angular/cdk/platform'; -import { Component, OnInit, OnDestroy } from '@angular/core'; +import { Component, OnDestroy, OnInit } from '@angular/core'; import { MatDialog } from '@angular/material/dialog'; import { MatSnackBar } from '@angular/material/snack-bar'; -import { NavigationError, Router, ActivatedRoute } from '@angular/router'; +import { ActivatedRoute, NavigationError, Router } from '@angular/router'; +import { SwUpdate } from '@angular/service-worker'; import { I18n } from '@ngx-translate/i18n-polyfill'; import { Store } from '@ngxs/store'; +import { untilComponentDestroyed } from 'ng2-rx-componentdestroyed'; import { filter } from 'rxjs/operators'; import { BugReportConsentComponent } from 'src/app/shared/dialogs/bug-report-consent/bug-report-consent.component'; import { DisplayNotificationDialog } from 'src/app/shared/dialogs/display-notification-dialog/display-notification.dialog'; import { IeWarningComponent } from 'src/app/shared/dialogs/ie-warning/ie-warning.component'; -import { AppState, InitTheme, SetSendFeedback, SetDisplayedNotificationVersion } from 'src/app/shared/states/App.state'; -import { ReleaseInfoService } from './services/release-info.service'; +import { AppState, InitTheme, ServiceWorkerNotificationDisplayed, SetDisplayedNotificationVersion, SetSendFeedback } from 'src/app/shared/states/App.state'; import { runtimeEnvironment } from 'src/environments/runtime-environment'; -import { untilComponentDestroyed } from 'ng2-rx-componentdestroyed'; +import { ReleaseInfoService } from './services/release-info.service'; @Component({ selector: 'app-root', @@ -27,9 +28,7 @@ export class AppComponent implements OnInit, OnDestroy { private snackBar: MatSnackBar, private releaseMigrationService: ReleaseInfoService, private store: Store, - // Commented as long as serviceWorker is reinstalled - // Issue: https://github.com/T-Systems-MMS/phonebook/issues/87 - // private swUpdates: SwUpdate, + private swUpdates: SwUpdate, private matDialog: MatDialog, private platform: Platform, private router: Router, @@ -38,66 +37,64 @@ export class AppComponent implements OnInit, OnDestroy { ) {} public ngOnInit() { this.store.dispatch(new InitTheme()); - // Commented as long as serviceWorker is reinstalled - // Issue: https://github.com/T-Systems-MMS/phonebook/issues/87 - // //Checking if the Service Worker was installed correctly. - // if (!this.store.selectSnapshot(AppState.serviceWorkerNotificationDisplayed)) { - // if ('serviceWorker' in navigator) { - // this.swUpdates.activated.subscribe(active => { - // if (active.current) { - // this.snackBar.open( - // this.i18n({ - // value: 'Hurray! You can use this Website offline.', - // description: 'Message indicating the service worker was successfully installed', - // id: 'AppComponentServiceWorkerSuccessMessage', - // meaning: 'AppComponent' - // }), - // '', - // { duration: 3000 } - // ); - // this.store.dispatch(new ServiceWorkerNotificationDisplayed()); - // } - // }); - // } else { - // this.snackBar.open( - // this.i18n({ - // value: 'Your Browser does not support Offline Apps!', - // description: 'Message indicating the service worker was not installed', - // id: 'AppComponentServiceWorkerErrorMessage', - // meaning: 'AppComponent' - // }), - // '', - // { - // duration: 3000 - // } - // ); - // this.store.dispatch(new ServiceWorkerNotificationDisplayed()); - // } - // } + //Checking if the Service Worker was installed correctly. + if (!this.store.selectSnapshot(AppState.serviceWorkerNotificationDisplayed)) { + if ('serviceWorker' in navigator) { + this.swUpdates.activated.subscribe(active => { + if (active.current) { + this.snackBar.open( + this.i18n({ + value: 'Hurray! You can use this Website offline.', + description: 'Message indicating the service worker was successfully installed', + id: 'AppComponentServiceWorkerSuccessMessage', + meaning: 'AppComponent' + }), + '', + { duration: 3000 } + ); + this.store.dispatch(new ServiceWorkerNotificationDisplayed()); + } + }); + } else { + this.snackBar.open( + this.i18n({ + value: 'Your Browser does not support Offline Apps!', + description: 'Message indicating the service worker was not installed', + id: 'AppComponentServiceWorkerErrorMessage', + meaning: 'AppComponent' + }), + '', + { + duration: 3000 + } + ); + this.store.dispatch(new ServiceWorkerNotificationDisplayed()); + } + } - // // Check if a new Update for the Service worker is available - // if (this.swUpdates.isEnabled) { - // this.swUpdates.available.subscribe(() => { - // const updateNotification = this.snackBar.open( - // this.i18n({ - // value: 'A newer version is available. Do you want to update straight away?', - // description: 'Message indicating the app can be updated and question if it should be updated', - // id: 'AppComponentServiceWorkerUpdateMessage', - // meaning: 'AppComponent' - // }), - // this.i18n({ - // value: 'Update!', - // description: 'Button Text for updating the app', - // id: 'AppComponentServiceWorkerUpdateButtonMessage', - // meaning: 'AppComponent' - // }), - // { duration: 4000 } - // ); - // updateNotification.onAction().subscribe(onAction => { - // window.location.reload(); - // }); - // }); - // } + // Check if a new Update for the Service worker is available + if (this.swUpdates.isEnabled) { + this.swUpdates.available.subscribe(() => { + const updateNotification = this.snackBar.open( + this.i18n({ + value: 'A newer version is available. Do you want to update straight away?', + description: 'Message indicating the app can be updated and question if it should be updated', + id: 'AppComponentServiceWorkerUpdateMessage', + meaning: 'AppComponent' + }), + this.i18n({ + value: 'Update!', + description: 'Button Text for updating the app', + id: 'AppComponentServiceWorkerUpdateButtonMessage', + meaning: 'AppComponent' + }), + { duration: 4000 } + ); + updateNotification.onAction().subscribe(onAction => { + window.location.reload(); + }); + }); + } //if skip_cookies is set, dont show dialogs if (this.skippedDialogs) { diff --git a/Phonebook.Frontend/src/app/app.module.ts b/Phonebook.Frontend/src/app/app.module.ts index 8adba3b09..4e4105087 100644 --- a/Phonebook.Frontend/src/app/app.module.ts +++ b/Phonebook.Frontend/src/app/app.module.ts @@ -3,9 +3,10 @@ import { DragDropModule } from '@angular/cdk/drag-drop'; import { PlatformModule } from '@angular/cdk/platform'; import { HttpClientModule } from '@angular/common/http'; import { LOCALE_ID, NgModule, TRANSLATIONS } from '@angular/core'; -import { MatBadgeModule } from '@angular/material'; +import { MatBadgeModule, MAT_DIALOG_DEFAULT_OPTIONS } from '@angular/material'; import { BrowserModule } from '@angular/platform-browser'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { ServiceWorkerModule } from '@angular/service-worker'; import { I18n } from '@ngx-translate/i18n-polyfill'; import { NgxsReduxDevtoolsPluginModule } from '@ngxs/devtools-plugin'; import { NgxsLoggerPluginModule } from '@ngxs/logger-plugin'; @@ -41,12 +42,18 @@ import { ErrorHandlerModule } from 'src/app/shared/error/error.module'; // Modules import { MaterialModule } from 'src/app/shared/material.module'; import { WINDOW_PROVIDER } from 'src/app/shared/providers/window.provider'; -import { AppState, BookmarksState, CommonPersonsState, LastPersonsState, SearchState, TableState } from 'src/app/shared/states'; +import { + AppState, + BookmarksState, + CommonPersonsState, + LastPersonsState, + SearchState, + TableState +} from 'src/app/shared/states'; import { environment } from 'src/environments/environment'; // Services import { FloorplanService } from './services/floorplan.service'; import { SearchComponent } from './shared/components/search/search.component'; -import {MAT_DIALOG_DEFAULT_OPTIONS} from '@angular/material'; declare const require; @@ -55,6 +62,7 @@ declare const require; imports: [ BrowserModule, AppRoutingModule, + ServiceWorkerModule.register('ngsw-worker.js', { enabled: environment.production }), BrowserAnimationsModule, HttpClientModule, ErrorHandlerModule.forRoot(), @@ -99,7 +107,7 @@ declare const require; }, deps: [LOCALE_ID] }, - {provide: MAT_DIALOG_DEFAULT_OPTIONS, useValue: {panelClass: 'mat-dialog-override'}}, + { provide: MAT_DIALOG_DEFAULT_OPTIONS, useValue: { panelClass: 'mat-dialog-override' } }, WINDOW_PROVIDER, ServiceWorkerService, WindowRef, diff --git a/Phonebook.Frontend/src/manifest.de.webmanifest b/Phonebook.Frontend/src/manifest.de.webmanifest new file mode 100644 index 000000000..b6c33990c --- /dev/null +++ b/Phonebook.Frontend/src/manifest.de.webmanifest @@ -0,0 +1,51 @@ +{ + "name": "Phonebook - Deutsch", + "short_name": "Phonebook", + "theme_color": "#1976d2", + "background_color": "#fafafa", + "display": "standalone", + "scope": "/de/", + "start_url": "/de/", + "icons": [ + { + "src": "assets/icons/icon-72x72.png", + "sizes": "72x72", + "type": "image/png" + }, + { + "src": "assets/icons/icon-96x96.png", + "sizes": "96x96", + "type": "image/png" + }, + { + "src": "assets/icons/icon-128x128.png", + "sizes": "128x128", + "type": "image/png" + }, + { + "src": "assets/icons/icon-144x144.png", + "sizes": "144x144", + "type": "image/png" + }, + { + "src": "assets/icons/icon-152x152.png", + "sizes": "152x152", + "type": "image/png" + }, + { + "src": "assets/icons/icon-192x192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "assets/icons/icon-384x384.png", + "sizes": "384x384", + "type": "image/png" + }, + { + "src": "assets/icons/icon-512x512.png", + "sizes": "512x512", + "type": "image/png" + } + ] +} \ No newline at end of file diff --git a/Phonebook.Frontend/src/manifest.en.webmanifest b/Phonebook.Frontend/src/manifest.en.webmanifest new file mode 100644 index 000000000..fb1676804 --- /dev/null +++ b/Phonebook.Frontend/src/manifest.en.webmanifest @@ -0,0 +1,51 @@ +{ + "name": "Phonebook - English", + "short_name": "Phonebook", + "theme_color": "#1976d2", + "background_color": "#fafafa", + "display": "standalone", + "scope": "/en/", + "start_url": "/en/", + "icons": [ + { + "src": "assets/icons/icon-72x72.png", + "sizes": "72x72", + "type": "image/png" + }, + { + "src": "assets/icons/icon-96x96.png", + "sizes": "96x96", + "type": "image/png" + }, + { + "src": "assets/icons/icon-128x128.png", + "sizes": "128x128", + "type": "image/png" + }, + { + "src": "assets/icons/icon-144x144.png", + "sizes": "144x144", + "type": "image/png" + }, + { + "src": "assets/icons/icon-152x152.png", + "sizes": "152x152", + "type": "image/png" + }, + { + "src": "assets/icons/icon-192x192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "assets/icons/icon-384x384.png", + "sizes": "384x384", + "type": "image/png" + }, + { + "src": "assets/icons/icon-512x512.png", + "sizes": "512x512", + "type": "image/png" + } + ] +} \ No newline at end of file