From 2a76092566e26934543829ba987846f44bf23a7e Mon Sep 17 00:00:00 2001 From: Joseph Milazzo Date: Thu, 19 Aug 2021 16:49:53 -0700 Subject: [PATCH] Update Notification Refactor (#511) * Replaced profile links to anchors so we can open in new tab if we like * Refactored how update checking works. We now explicitly check and send back on the same API. We have a weekly job that will push an update to the user. * Implemented a changelog tab * Ported over a GA fix for using ' in PR bodies. * Don't check cert for Github --- .github/workflows/sonar-scan.yml | 1 + API/Controllers/ServerController.cs | 20 +++-- API/DTOs/Update/UpdateNotificationDto.cs | 38 ++++++++ API/Interfaces/ITaskScheduler.cs | 1 - .../Services/IVersionUpdaterService.cs | 8 +- API/Services/TaskScheduler.cs | 12 ++- API/Services/Tasks/VersionUpdaterService.cs | 90 ++++++++++++++----- API/SignalR/MessageHub.cs | 17 ++-- API/SignalR/PresenceHub.cs | 3 + .../src/app/_services/message-hub.service.ts | 13 +++ UI/Web/src/app/_services/server.service.ts | 7 +- UI/Web/src/app/admin/admin.module.ts | 4 +- .../admin/changelog/changelog.component.html | 14 +++ .../admin/changelog/changelog.component.scss | 5 ++ .../admin/changelog/changelog.component.ts | 23 +++++ .../admin/dashboard/dashboard.component.html | 3 + .../admin/dashboard/dashboard.component.ts | 3 +- .../manage-system.component.html | 4 +- .../manage-system/manage-system.component.ts | 19 ++-- .../app/nav-header/nav-header.component.html | 13 +-- .../app/nav-header/nav-header.component.ts | 4 +- 21 files changed, 246 insertions(+), 56 deletions(-) create mode 100644 API/DTOs/Update/UpdateNotificationDto.cs create mode 100644 UI/Web/src/app/admin/changelog/changelog.component.html create mode 100644 UI/Web/src/app/admin/changelog/changelog.component.scss create mode 100644 UI/Web/src/app/admin/changelog/changelog.component.ts diff --git a/.github/workflows/sonar-scan.yml b/.github/workflows/sonar-scan.yml index e66510d8b..fa42c9b5b 100644 --- a/.github/workflows/sonar-scan.yml +++ b/.github/workflows/sonar-scan.yml @@ -237,6 +237,7 @@ jobs: id: parse-body run: | body='${{ steps.findPr.outputs.body }}' + body=${body//\'/} body="${body//'%'/'%25'}" body="${body//$'\n'/'%0A'}" body="${body//$'\r'/'%0D'}" diff --git a/API/Controllers/ServerController.cs b/API/Controllers/ServerController.cs index 40686162c..97aa8bf46 100644 --- a/API/Controllers/ServerController.cs +++ b/API/Controllers/ServerController.cs @@ -1,7 +1,9 @@ using System; +using System.Collections.Generic; using System.IO; using System.Threading.Tasks; using API.DTOs.Stats; +using API.DTOs.Update; using API.Extensions; using API.Interfaces; using API.Interfaces.Services; @@ -25,9 +27,11 @@ namespace API.Controllers private readonly IArchiveService _archiveService; private readonly ICacheService _cacheService; private readonly ITaskScheduler _taskScheduler; + private readonly IVersionUpdaterService _versionUpdaterService; public ServerController(IHostApplicationLifetime applicationLifetime, ILogger logger, IConfiguration config, - IBackupService backupService, IArchiveService archiveService, ICacheService cacheService, ITaskScheduler taskScheduler) + IBackupService backupService, IArchiveService archiveService, ICacheService cacheService, ITaskScheduler taskScheduler, + IVersionUpdaterService versionUpdaterService) { _applicationLifetime = applicationLifetime; _logger = logger; @@ -36,6 +40,7 @@ namespace API.Controllers _archiveService = archiveService; _cacheService = cacheService; _taskScheduler = taskScheduler; + _versionUpdaterService = versionUpdaterService; } /// @@ -102,11 +107,16 @@ namespace API.Controllers } } - [HttpPost("check-update")] - public ActionResult CheckForUpdates() + [HttpGet("check-update")] + public async Task> CheckForUpdates() { - _taskScheduler.CheckForUpdate(); - return Ok(); + return Ok(await _versionUpdaterService.CheckForUpdate()); + } + + [HttpGet("changelog")] + public async Task>> GetChangelog() + { + return Ok(await _versionUpdaterService.GetAllReleases()); } } } diff --git a/API/DTOs/Update/UpdateNotificationDto.cs b/API/DTOs/Update/UpdateNotificationDto.cs new file mode 100644 index 000000000..7a16fa18e --- /dev/null +++ b/API/DTOs/Update/UpdateNotificationDto.cs @@ -0,0 +1,38 @@ +namespace API.DTOs.Update +{ + /// + /// Update Notification denoting a new release available for user to update to + /// + public class UpdateNotificationDto + { + /// + /// Current installed Version + /// + public string CurrentVersion { get; init; } + /// + /// Semver of the release version + /// 0.4.3 + /// + public string UpdateVersion { get; init; } + /// + /// Release body in HTML + /// + public string UpdateBody { get; init; } + /// + /// Title of the release + /// + public string UpdateTitle { get; init; } + /// + /// Github Url + /// + public string UpdateUrl { get; init; } + /// + /// If this install is within Docker + /// + public bool IsDocker { get; init; } + /// + /// Is this a pre-release + /// + public bool IsPrerelease { get; init; } + } +} diff --git a/API/Interfaces/ITaskScheduler.cs b/API/Interfaces/ITaskScheduler.cs index ea674d155..b75282fd5 100644 --- a/API/Interfaces/ITaskScheduler.cs +++ b/API/Interfaces/ITaskScheduler.cs @@ -15,6 +15,5 @@ void RefreshSeriesMetadata(int libraryId, int seriesId); void ScanSeries(int libraryId, int seriesId, bool forceUpdate = false); void CancelStatsTasks(); - void CheckForUpdate(); } } diff --git a/API/Interfaces/Services/IVersionUpdaterService.cs b/API/Interfaces/Services/IVersionUpdaterService.cs index a47de72cf..c47cf08b4 100644 --- a/API/Interfaces/Services/IVersionUpdaterService.cs +++ b/API/Interfaces/Services/IVersionUpdaterService.cs @@ -1,11 +1,15 @@ using System; +using System.Collections.Generic; using System.Threading.Tasks; +using API.DTOs.Update; +using API.Services.Tasks; namespace API.Interfaces.Services { public interface IVersionUpdaterService { - public Task CheckForUpdate(); - + Task CheckForUpdate(); + Task PushUpdate(UpdateNotificationDto update); + Task> GetAllReleases(); } } diff --git a/API/Services/TaskScheduler.cs b/API/Services/TaskScheduler.cs index faa171a3e..1a96014ac 100644 --- a/API/Services/TaskScheduler.cs +++ b/API/Services/TaskScheduler.cs @@ -70,6 +70,8 @@ namespace API.Services } RecurringJob.AddOrUpdate("cleanup", () => _cleanupService.Cleanup(), Cron.Daily); + + RecurringJob.AddOrUpdate("check-for-updates", () => _scannerService.ScanLibraries(), Cron.Daily); } #region StatsTasks @@ -104,7 +106,7 @@ namespace API.Services public void ScheduleUpdaterTasks() { _logger.LogInformation("Scheduling Auto-Update tasks"); - RecurringJob.AddOrUpdate("check-updates", () => _versionUpdaterService.CheckForUpdate(), Cron.Daily); + RecurringJob.AddOrUpdate("check-updates", () => CheckForUpdate(), Cron.Weekly); } #endregion @@ -152,9 +154,13 @@ namespace API.Services BackgroundJob.Enqueue(() => _backupService.BackupDatabase()); } - public void CheckForUpdate() + /// + /// Not an external call. Only public so that we can call this for a Task + /// + public async Task CheckForUpdate() { - BackgroundJob.Enqueue(() => _versionUpdaterService.CheckForUpdate()); + var update = await _versionUpdaterService.CheckForUpdate(); + await _versionUpdaterService.PushUpdate(update); } } } diff --git a/API/Services/Tasks/VersionUpdaterService.cs b/API/Services/Tasks/VersionUpdaterService.cs index 9f65f136b..bed687539 100644 --- a/API/Services/Tasks/VersionUpdaterService.cs +++ b/API/Services/Tasks/VersionUpdaterService.cs @@ -1,10 +1,14 @@ using System; using System.Collections.Generic; +using System.Linq; +using System.Net.Http; using System.Threading.Tasks; +using API.DTOs.Update; using API.Interfaces.Services; using API.SignalR; using API.SignalR.Presence; using Flurl.Http; +using Flurl.Http.Configuration; using Kavita.Common.EnvironmentInfo; using MarkdownDeep; using Microsoft.AspNetCore.SignalR; @@ -32,36 +36,81 @@ namespace API.Services.Tasks /// Url of the release on Github /// public string Html_Url { get; init; } - } + + public class UntrustedCertClientFactory : DefaultHttpClientFactory + { + public override HttpMessageHandler CreateMessageHandler() { + return new HttpClientHandler { + ServerCertificateCustomValidationCallback = (a, b, c, d) => true + }; + } + } + public class VersionUpdaterService : IVersionUpdaterService { private readonly ILogger _logger; private readonly IHubContext _messageHub; private readonly IPresenceTracker _tracker; private readonly Markdown _markdown = new MarkdownDeep.Markdown(); + private static readonly string GithubLatestReleasesUrl = "https://api.github.com/repos/Kareadita/Kavita/releases/latest"; + private static readonly string GithubAllReleasesUrl = "https://api.github.com/repos/Kareadita/Kavita/releases"; public VersionUpdaterService(ILogger logger, IHubContext messageHub, IPresenceTracker tracker) { _logger = logger; _messageHub = messageHub; _tracker = tracker; + + FlurlHttp.ConfigureClient(GithubLatestReleasesUrl, cli => + cli.Settings.HttpClientFactory = new UntrustedCertClientFactory()); + FlurlHttp.ConfigureClient(GithubAllReleasesUrl, cli => + cli.Settings.HttpClientFactory = new UntrustedCertClientFactory()); } /// - /// Scheduled Task that checks if a newer version is available. If it is, will check if User is currently connected and push - /// a message. + /// Fetches the latest release from Github /// - public async Task CheckForUpdate() + public async Task CheckForUpdate() { - var update = await GetGithubRelease(); + return CreateDto(update); + } - if (update == null || string.IsNullOrEmpty(update.Tag_Name)) return; + /// + /// + /// + /// + public async Task> GetAllReleases() + { + var updates = await GetGithubReleases(); + return updates.Select(CreateDto); + } - var admins = await _tracker.GetOnlineAdmins(); + private UpdateNotificationDto? CreateDto(GithubReleaseMetadata update) + { + if (update == null || string.IsNullOrEmpty(update.Tag_Name)) return null; var version = update.Tag_Name.Replace("v", string.Empty); var updateVersion = new Version(version); + + return new UpdateNotificationDto() + { + CurrentVersion = version, + UpdateVersion = updateVersion.ToString(), + UpdateBody = _markdown.Transform(update.Body.Trim()), + UpdateTitle = update.Name, + UpdateUrl = update.Html_Url, + IsDocker = new OsInfo(Array.Empty()).IsDocker + }; + } + + public async Task PushUpdate(UpdateNotificationDto update) + { + if (update == null) return; + + var admins = await _tracker.GetOnlineAdmins(); + var updateVersion = new Version(update.CurrentVersion); + if (BuildInfo.Version < updateVersion) { _logger.LogInformation("Server is out of date. Current: {CurrentVersion}. Available: {AvailableUpdate}", BuildInfo.Version, updateVersion); @@ -74,10 +123,8 @@ namespace API.Services.Tasks } } - private async Task SendEvent(GithubReleaseMetadata update, IReadOnlyList admins) + private async Task SendEvent(UpdateNotificationDto update, IReadOnlyList admins) { - var version = update.Tag_Name.Replace("v", string.Empty); - var updateVersion = new Version(version); var connections = new List(); foreach (var admin in admins) { @@ -87,26 +134,29 @@ namespace API.Services.Tasks await _messageHub.Clients.Users(admins).SendAsync("UpdateAvailable", new SignalRMessage { Name = "UpdateAvailable", - Body = new - { - CurrentVersion = version, - UpdateVersion = updateVersion.ToString(), - UpdateBody = _markdown.Transform(update.Body.Trim()), - UpdateTitle = update.Name, - UpdateUrl = update.Html_Url, - IsDocker = new OsInfo(Array.Empty()).IsDocker - } + Body = update }); } + private static async Task GetGithubRelease() { - var update = await "https://api.github.com/repos/Kareadita/Kavita/releases/latest" + var update = await GithubLatestReleasesUrl .WithHeader("Accept", "application/json") .WithHeader("User-Agent", "Kavita") .GetJsonAsync(); return update; } + + private static async Task> GetGithubReleases() + { + var update = await GithubAllReleasesUrl + .WithHeader("Accept", "application/json") + .WithHeader("User-Agent", "Kavita") + .GetJsonAsync>(); + + return update; + } } } diff --git a/API/SignalR/MessageHub.cs b/API/SignalR/MessageHub.cs index 2ec9cc469..262e5b3bb 100644 --- a/API/SignalR/MessageHub.cs +++ b/API/SignalR/MessageHub.cs @@ -7,27 +7,30 @@ using Microsoft.AspNetCore.SignalR; namespace API.SignalR { + /// + /// Generic hub for sending messages to UI + /// [Authorize] public class MessageHub : Hub { - private static readonly HashSet _connections = new HashSet(); + private static readonly HashSet Connections = new HashSet(); public static bool IsConnected { get { - lock (_connections) + lock (Connections) { - return _connections.Count != 0; + return Connections.Count != 0; } } } public override async Task OnConnectedAsync() { - lock (_connections) + lock (Connections) { - _connections.Add(Context.ConnectionId); + Connections.Add(Context.ConnectionId); } await base.OnConnectedAsync(); @@ -35,9 +38,9 @@ namespace API.SignalR public override async Task OnDisconnectedAsync(Exception exception) { - lock (_connections) + lock (Connections) { - _connections.Remove(Context.ConnectionId); + Connections.Remove(Context.ConnectionId); } await base.OnDisconnectedAsync(exception); diff --git a/API/SignalR/PresenceHub.cs b/API/SignalR/PresenceHub.cs index 93aa5d59b..bb700e88a 100644 --- a/API/SignalR/PresenceHub.cs +++ b/API/SignalR/PresenceHub.cs @@ -6,6 +6,9 @@ using Microsoft.AspNetCore.SignalR; namespace API.SignalR { + /// + /// Keeps track of who is logged into the app + /// public class PresenceHub : Hub { private readonly IPresenceTracker _tracker; diff --git a/UI/Web/src/app/_services/message-hub.service.ts b/UI/Web/src/app/_services/message-hub.service.ts index 8683164c2..9b959795d 100644 --- a/UI/Web/src/app/_services/message-hub.service.ts +++ b/UI/Web/src/app/_services/message-hub.service.ts @@ -2,6 +2,7 @@ import { Injectable } from '@angular/core'; import { HubConnection, HubConnectionBuilder } from '@microsoft/signalr'; import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; import { User } from '@sentry/angular'; +import { BehaviorSubject, ReplaySubject } from 'rxjs'; import { environment } from 'src/environments/environment'; import { UpdateNotificationModalComponent } from '../shared/update-notification/update-notification-modal.component'; @@ -9,6 +10,11 @@ export enum EVENTS { UpdateAvailable = 'UpdateAvailable' } +export interface Message { + event: EVENTS; + payload: T; +} + @Injectable({ providedIn: 'root' }) @@ -17,6 +23,9 @@ export class MessageHubService { private hubConnection!: HubConnection; private updateNotificationModalRef: NgbModalRef | null = null; + private messagesSource = new ReplaySubject>(1); + public messages$ = this.messagesSource.asObservable(); + constructor(private modalService: NgbModal) { } createHubConnection(user: User) { @@ -36,6 +45,10 @@ export class MessageHubService { }); this.hubConnection.on(EVENTS.UpdateAvailable, resp => { + this.messagesSource.next({ + event: EVENTS.UpdateAvailable, + payload: resp.body + }); // Ensure only 1 instance of UpdateNotificationModal can be open at once if (this.updateNotificationModalRef != null) { return; } this.updateNotificationModalRef = this.modalService.open(UpdateNotificationModalComponent, { scrollable: true, size: 'lg' }); diff --git a/UI/Web/src/app/_services/server.service.ts b/UI/Web/src/app/_services/server.service.ts index 5274b48ea..4538d17db 100644 --- a/UI/Web/src/app/_services/server.service.ts +++ b/UI/Web/src/app/_services/server.service.ts @@ -2,6 +2,7 @@ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { environment } from 'src/environments/environment'; import { ServerInfo } from '../admin/_models/server-info'; +import { UpdateVersionEvent } from '../_models/events/update-version-event'; @Injectable({ providedIn: 'root' @@ -29,6 +30,10 @@ export class ServerService { } checkForUpdate() { - return this.httpClient.post(this.baseUrl + 'server/check-update', {}); + return this.httpClient.get(this.baseUrl + 'server/check-update', {}); + } + + getChangelog() { + return this.httpClient.get(this.baseUrl + 'server/changelog', {}); } } diff --git a/UI/Web/src/app/admin/admin.module.ts b/UI/Web/src/app/admin/admin.module.ts index 13a59e48b..fb7d5d297 100644 --- a/UI/Web/src/app/admin/admin.module.ts +++ b/UI/Web/src/app/admin/admin.module.ts @@ -15,6 +15,7 @@ import { ManageSettingsComponent } from './manage-settings/manage-settings.compo import { FilterPipe } from './filter.pipe'; import { EditRbsModalComponent } from './_modals/edit-rbs-modal/edit-rbs-modal.component'; import { ManageSystemComponent } from './manage-system/manage-system.component'; +import { ChangelogComponent } from './changelog/changelog.component'; @@ -31,7 +32,8 @@ import { ManageSystemComponent } from './manage-system/manage-system.component'; ManageSettingsComponent, FilterPipe, EditRbsModalComponent, - ManageSystemComponent + ManageSystemComponent, + ChangelogComponent ], imports: [ CommonModule, diff --git a/UI/Web/src/app/admin/changelog/changelog.component.html b/UI/Web/src/app/admin/changelog/changelog.component.html new file mode 100644 index 000000000..073b72b95 --- /dev/null +++ b/UI/Web/src/app/admin/changelog/changelog.component.html @@ -0,0 +1,14 @@ + +
+
+
{{update.updateTitle}} Installed
+

+          Download
+        
+
+
+ + +
+ +
\ No newline at end of file diff --git a/UI/Web/src/app/admin/changelog/changelog.component.scss b/UI/Web/src/app/admin/changelog/changelog.component.scss new file mode 100644 index 000000000..0f737bca4 --- /dev/null +++ b/UI/Web/src/app/admin/changelog/changelog.component.scss @@ -0,0 +1,5 @@ +.update-body { + width: 100%; + word-wrap: break-word; + white-space: pre-wrap; +} \ No newline at end of file diff --git a/UI/Web/src/app/admin/changelog/changelog.component.ts b/UI/Web/src/app/admin/changelog/changelog.component.ts new file mode 100644 index 000000000..e24ad9845 --- /dev/null +++ b/UI/Web/src/app/admin/changelog/changelog.component.ts @@ -0,0 +1,23 @@ +import { Component, OnInit } from '@angular/core'; +import { UpdateVersionEvent } from 'src/app/_models/events/update-version-event'; +import { ServerService } from 'src/app/_services/server.service'; + +@Component({ + selector: 'app-changelog', + templateUrl: './changelog.component.html', + styleUrls: ['./changelog.component.scss'] +}) +export class ChangelogComponent implements OnInit { + + updates: Array = []; + isLoading: boolean = true; + + constructor(private serverService: ServerService) { } + + ngOnInit(): void { + this.serverService.getChangelog().subscribe(updates => { + this.updates = updates; + this.isLoading = false; + }); + } +} diff --git a/UI/Web/src/app/admin/dashboard/dashboard.component.html b/UI/Web/src/app/admin/dashboard/dashboard.component.html index dbc9e03b8..26886dd7f 100644 --- a/UI/Web/src/app/admin/dashboard/dashboard.component.html +++ b/UI/Web/src/app/admin/dashboard/dashboard.component.html @@ -16,6 +16,9 @@ + + + diff --git a/UI/Web/src/app/admin/dashboard/dashboard.component.ts b/UI/Web/src/app/admin/dashboard/dashboard.component.ts index aabe57829..33549db13 100644 --- a/UI/Web/src/app/admin/dashboard/dashboard.component.ts +++ b/UI/Web/src/app/admin/dashboard/dashboard.component.ts @@ -17,7 +17,8 @@ export class DashboardComponent implements OnInit { {title: 'General', fragment: ''}, {title: 'Users', fragment: 'users'}, {title: 'Libraries', fragment: 'libraries'}, - {title: 'System', fragment: 'system'} + {title: 'System', fragment: 'system'}, + {title: 'Changelog', fragment: 'changelog'}, ]; counter = this.tabs.length + 1; active = this.tabs[0]; diff --git a/UI/Web/src/app/admin/manage-system/manage-system.component.html b/UI/Web/src/app/admin/manage-system/manage-system.component.html index ce1c0e342..fe3549a76 100644 --- a/UI/Web/src/app/admin/manage-system/manage-system.component.html +++ b/UI/Web/src/app/admin/manage-system/manage-system.component.html @@ -3,7 +3,7 @@
-
diff --git a/UI/Web/src/app/admin/manage-system/manage-system.component.ts b/UI/Web/src/app/admin/manage-system/manage-system.component.ts index 54f787474..86c1f1599 100644 --- a/UI/Web/src/app/admin/manage-system/manage-system.component.ts +++ b/UI/Web/src/app/admin/manage-system/manage-system.component.ts @@ -1,7 +1,9 @@ import { Component, OnInit } from '@angular/core'; import { FormControl, FormGroup, Validators } from '@angular/forms'; +import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { ToastrService } from 'ngx-toastr'; import { finalize, take, takeWhile } from 'rxjs/operators'; +import { UpdateNotificationModalComponent } from 'src/app/shared/update-notification/update-notification-modal.component'; import { DownloadService } from 'src/app/shared/_services/download.service'; import { ServerService } from 'src/app/_services/server.service'; import { SettingsService } from '../settings.service'; @@ -21,11 +23,12 @@ export class ManageSystemComponent implements OnInit { clearCacheInProgress: boolean = false; backupDBInProgress: boolean = false; - hasCheckedForUpdate: boolean = false; + isCheckingForUpdate: boolean = false; downloadLogsInProgress: boolean = false; constructor(private settingsService: SettingsService, private toastr: ToastrService, - private serverService: ServerService, public downloadService: DownloadService) { } + private serverService: ServerService, public downloadService: DownloadService, + private modalService: NgbModal) { } ngOnInit(): void { @@ -82,9 +85,15 @@ export class ManageSystemComponent implements OnInit { } checkForUpdates() { - this.hasCheckedForUpdate = true; - this.serverService.checkForUpdate().subscribe(() => { - this.toastr.info('This might take a few minutes. If an update is available, the server will notify you.'); + this.isCheckingForUpdate = true; + this.serverService.checkForUpdate().subscribe((update) => { + this.isCheckingForUpdate = false; + if (update === null) { + this.toastr.info('No updates available'); + return; + } + const modalRef = this.modalService.open(UpdateNotificationModalComponent, { scrollable: true, size: 'lg' }); + modalRef.componentInstance.updateData = update; }); } diff --git a/UI/Web/src/app/nav-header/nav-header.component.html b/UI/Web/src/app/nav-header/nav-header.component.html index 870dc6f1f..28da75c1e 100644 --- a/UI/Web/src/app/nav-header/nav-header.component.html +++ b/UI/Web/src/app/nav-header/nav-header.component.html @@ -61,13 +61,14 @@
- diff --git a/UI/Web/src/app/nav-header/nav-header.component.ts b/UI/Web/src/app/nav-header/nav-header.component.ts index 8d03f56bb..1216af274 100644 --- a/UI/Web/src/app/nav-header/nav-header.component.ts +++ b/UI/Web/src/app/nav-header/nav-header.component.ts @@ -4,7 +4,6 @@ import { Router } from '@angular/router'; import { Subject } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; import { ScrollService } from '../scroll.service'; -import { UtilityService } from '../shared/_services/utility.service'; import { SearchResult } from '../_models/search-result'; import { AccountService } from '../_services/account.service'; import { ImageService } from '../_services/image.service'; @@ -31,7 +30,8 @@ export class NavHeaderComponent implements OnInit, OnDestroy { private readonly onDestroy = new Subject(); constructor(public accountService: AccountService, private router: Router, public navService: NavService, - private libraryService: LibraryService, public imageService: ImageService, @Inject(DOCUMENT) private document: Document, private scrollService: ScrollService) { } + private libraryService: LibraryService, public imageService: ImageService, @Inject(DOCUMENT) private document: Document, + private scrollService: ScrollService) { } ngOnInit(): void { this.navService.darkMode$.pipe(takeUntil(this.onDestroy)).subscribe(res => {