a fork of iceshrimp.net but a tweaked frontend to my personal liking. waow
fediverse social-media social iceshrimp fedi
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

[frontend] Add update checking and banner notifications

authored by

Lilian and committed by
Laura Hausmann
1da774b8 aac01b6a

+343 -35
+7 -1
Iceshrimp.Frontend/App.razor
··· 1 1 @using Iceshrimp.Frontend.Components 2 + @using Iceshrimp.Frontend.Core.Services 2 3 @using Microsoft.AspNetCore.Components.Authorization 4 + @* ReSharper disable once UnusedMember.Local *@ 5 + @inject UpdateService UpdateSvc; 3 6 <ErrorBoundary> 4 7 <ChildContent> 5 8 <Router AppAssembly="@typeof(App).Assembly"> ··· 23 26 <ErrorContent Context="Exception"> 24 27 <ErrorUi Exception="Exception"></ErrorUi> 25 28 </ErrorContent> 26 - </ErrorBoundary> 29 + </ErrorBoundary> 30 + 31 + @code { 32 + }
+24
Iceshrimp.Frontend/Components/BannerContainer.razor
··· 1 + @using Iceshrimp.Assets.PhosphorIcons 2 + @using Microsoft.Extensions.Localization 3 + @inject IStringLocalizer<BannerContainer> Loc; 4 + @if (CurrentBanners.Count > 0) 5 + { 6 + <div class="banner-container"> 7 + @foreach (var banner in CurrentBanners) 8 + { 9 + <div class="banner"> 10 + <div class="banner-body" @onclick="() => { banner.OnTap?.Invoke();}"> 11 + @banner.Text 12 + </div> 13 + <button class="button" @onclick="() => { Close(banner); }"> 14 + <Icon Name="Icons.X"/> 15 + </button> 16 + </div> 17 + } 18 + @if (Banners.Count > 0 && CurrentBanners.Count >= 5) 19 + { 20 + <div class="more">@Loc["and {0} more", Banners.Count]</div> 21 + } 22 + </div> 23 + } 24 +
+50
Iceshrimp.Frontend/Components/BannerContainer.razor.cs
··· 1 + using Iceshrimp.Frontend.Core.Services; 2 + using Microsoft.AspNetCore.Components; 3 + 4 + namespace Iceshrimp.Frontend.Components; 5 + 6 + public partial class BannerContainer : ComponentBase 7 + { 8 + [Inject] private GlobalComponentSvc GlobalComponentSvc { get; set; } = null!; 9 + private List<Banner> CurrentBanners { get; set; } = []; 10 + private List<Banner> Banners { get; } = []; 11 + 12 + public void AddBanner(Banner newBanner) 13 + { 14 + Banners.Add(newBanner); 15 + FillBanners(); 16 + } 17 + 18 + private void FillBanners() 19 + { 20 + if (Banners.Count > 0) 21 + { 22 + while (CurrentBanners.Count < 5 && Banners.Count > 0) 23 + { 24 + CurrentBanners.Add(Banners.First()); 25 + Banners.Remove(Banners.First()); 26 + } 27 + } 28 + 29 + StateHasChanged(); 30 + } 31 + 32 + private void Close(Banner banner) 33 + { 34 + banner.OnClose?.Invoke(); 35 + CurrentBanners.Remove(banner); 36 + FillBanners(); 37 + } 38 + 39 + protected override void OnInitialized() 40 + { 41 + GlobalComponentSvc.BannerComponent = this; 42 + } 43 + 44 + public class Banner 45 + { 46 + public required string Text { get; set; } 47 + public Action? OnClose { get; set; } 48 + public Action? OnTap { get; set; } 49 + } 50 + }
+27
Iceshrimp.Frontend/Components/BannerContainer.razor.css
··· 1 + .banner-container { 2 + position: fixed; 3 + bottom: 5rem; 4 + right: 5rem; 5 + background-color: var(--background-color); 6 + border: solid var(--highlight-color) 0.1rem; 7 + border-radius: 1rem; 8 + padding: 0.5rem 1rem; 9 + z-index: +1; 10 + display: flex; 11 + flex-direction: column; 12 + align-items: center; 13 + width: 15rem; 14 + } 15 + .banner-body { 16 + display: flex; 17 + width: 100%; 18 + } 19 + .banner { 20 + border: solid var(--highlight-color) 0.1rem; 21 + border-radius: 1rem; 22 + padding: 0.25rem 0.5rem; 23 + display: flex; 24 + width: 100%; 25 + align-items: center; 26 + margin: 0.25rem; 27 + }
+2 -2
Iceshrimp.Frontend/Components/GlobalComponents.razor
··· 1 - <EmojiPicker></EmojiPicker> 2 - 1 + <EmojiPicker /> 2 + <BannerContainer /> 3 3 @code { 4 4 }
+2 -1
Iceshrimp.Frontend/Core/Services/GlobalComponentSvc.cs
··· 4 4 5 5 public class GlobalComponentSvc 6 6 { 7 - public EmojiPicker? EmojiPicker { get; set; } 7 + public EmojiPicker? EmojiPicker { get; set; } 8 + public BannerContainer? BannerComponent { get; set; } 8 9 }
+106
Iceshrimp.Frontend/Core/Services/UpdateService.cs
··· 1 + using Iceshrimp.Frontend.Components; 2 + using Iceshrimp.Shared.Helpers; 3 + using Iceshrimp.Shared.Schemas.Web; 4 + using Microsoft.AspNetCore.Components; 5 + using Microsoft.JSInterop; 6 + 7 + namespace Iceshrimp.Frontend.Core.Services; 8 + 9 + internal class UpdateService 10 + { 11 + private readonly ApiService _api; 12 + private readonly ILogger<UpdateService> _logger; 13 + private readonly GlobalComponentSvc _globalComponentSvc; 14 + private readonly Lazy<Task<IJSObjectReference>> _moduleTask; 15 + private readonly NavigationManager _nav; 16 + 17 + private VersionInfo FrontendVersion { get; } = VersionHelpers.VersionInfo.Value; 18 + public VersionResponse? BackendVersion { get; private set; } 19 + 20 + // ReSharper disable once UnusedAutoPropertyAccessor.Local 21 + private Timer Timer { get; set; } 22 + 23 + public UpdateService( 24 + ApiService api, ILogger<UpdateService> logger, GlobalComponentSvc globalComponentSvc, IJSRuntime js, 25 + NavigationManager nav 26 + ) 27 + { 28 + _api = api; 29 + _logger = logger; 30 + _globalComponentSvc = globalComponentSvc; 31 + _nav = nav; 32 + 33 + _moduleTask = new Lazy<Task<IJSObjectReference>>(() => js.InvokeAsync<IJSObjectReference>( 34 + "import", 35 + "./Core/Services/UpdateService.cs.js") 36 + .AsTask()); 37 + Timer = new Timer(Callback, null, TimeSpan.Zero, TimeSpan.FromSeconds(60)); 38 + _ = RegisterUpdateCallback(); 39 + } 40 + 41 + private async Task RegisterUpdateCallback() 42 + { 43 + var module = await _moduleTask.Value; 44 + var objRef = DotNetObjectReference.Create(this); 45 + await module.InvokeAsync<string>("RegisterUpdateCallback", objRef); 46 + } 47 + 48 + [JSInvokable] 49 + public void OnUpdateFound() 50 + { 51 + var banner = new BannerContainer.Banner 52 + { 53 + Text = "New version available", OnTap = () => { _nav.NavigateTo("/settings/about"); } 54 + }; 55 + _globalComponentSvc.BannerComponent?.AddBanner(banner); 56 + } 57 + 58 + public async Task<bool> ServiceWorkerCheckWaiting() 59 + { 60 + var module = await _moduleTask.Value; 61 + return await module.InvokeAsync<bool>("ServiceWorkerCheckWaiting"); 62 + } 63 + 64 + public async Task ServiceWorkerUpdate() 65 + { 66 + var module = await _moduleTask.Value; 67 + await module.InvokeVoidAsync("ServiceWorkerUpdate"); 68 + } 69 + 70 + public async Task<bool> ServiceWorkerSkipWaiting() 71 + { 72 + var module = await _moduleTask.Value; 73 + return await module.InvokeAsync<bool>("ServiceWorkerSkipWaiting"); 74 + } 75 + 76 + private async void Callback(object? _) 77 + { 78 + await CheckVersion(); 79 + } 80 + 81 + private async Task<VersionResponse?> GetVersion() 82 + { 83 + try 84 + { 85 + var backendVersion = await _api.Version.GetVersion(); 86 + _logger.LogInformation("Successfully fetched backend version."); 87 + return backendVersion; 88 + } 89 + catch (Exception e) 90 + { 91 + _logger.LogError(e, "Failed to fetch backend version."); 92 + return null; 93 + } 94 + } 95 + 96 + private async Task CheckVersion() 97 + { 98 + var version = await GetVersion(); 99 + if (version is null) return; 100 + BackendVersion = version; 101 + if (version.Version != FrontendVersion.Version) 102 + { 103 + await ServiceWorkerUpdate(); 104 + } 105 + } 106 + }
+76 -4
Iceshrimp.Frontend/Pages/Settings/About.razor
··· 1 1 @page "/settings/about" 2 2 @using System.Text 3 3 @using Iceshrimp.Assets.PhosphorIcons 4 + @using Iceshrimp.Frontend.Components 4 5 @using Iceshrimp.Frontend.Core.InMemoryLogger 5 6 @using Iceshrimp.Frontend.Core.Services 6 7 @using Iceshrimp.Frontend.Localization 8 + @using Iceshrimp.Shared.Schemas.Web 7 9 @using Microsoft.AspNetCore.Authorization 8 10 @using Microsoft.Extensions.Localization 9 11 @attribute [Authorize] ··· 11 13 @inject VersionService Version; 12 14 @inject IStringLocalizer<Localization> Loc; 13 15 @inject IJSRuntime Js; 14 - @inject InMemoryLogService LogService; 16 + @inject InMemoryLogService LogSvc; 17 + @inject UpdateService UpdateSvc; 18 + @inject NavigationManager Nav; 19 + @inject GlobalComponentSvc GlobalComponentSvc; 15 20 16 21 <div class="body"> 17 22 <div class="version"> ··· 36 41 <code>@Environment.Version</code> 37 42 </span> 38 43 </div> 44 + <div class="update"> 45 + <button class="button" @onclick="@CheckUpdate">@Loc["Check for updates"]</button> 46 + @if (_updateAvailable == CheckState.True) 47 + { 48 + <div>New version!</div> 49 + <div class="version"> 50 + <span class="name">Iceshrimp.NET</span> 51 + <span class="value"> 52 + <code>@_newVersion?.Version</code> 53 + </span> 54 + <span class="name">Codename</span> 55 + <span class="value"> 56 + <code>@_newVersion?.Codename</code> 57 + </span> 58 + @if (_newVersion?.CommitHash != null) 59 + { 60 + <span class="name">Commit</span> 61 + <span class="value"> 62 + <code>@_newVersion?.CommitHash</code> 63 + </span> 64 + } 65 + </div> 66 + <button class="button" @onclick="@SkipWaiting">@Loc["Load new version"]</button> 67 + @if (_skipWaitingRes == CheckState.False) 68 + { 69 + <div>@Loc["Something went wrong, please check the logs."]</div> 70 + } 71 + } 72 + @if (_updateAvailable == CheckState.False) 73 + { 74 + <div>@Loc["Already on the newest version!"]</div> 75 + } 76 + 77 + </div> 39 78 <div class="logs"> 40 79 <h1>@Loc["Logs"]</h1> 41 80 @Loc["These logs may contain sensitive information, please do not post them publicly.\n" + "Providing them to developers upon request may help with debugging."] 42 - <button class="button" @onclick="DownloadLogs"><Icon Name="Icons.DownloadSimple"/><span>@Loc["Download Logs"]</span></button> 81 + <button class="button" @onclick="DownloadLogs"> 82 + <Icon Name="Icons.DownloadSimple"/> 83 + <span>@Loc["Download Logs"]</span></button> 43 84 </div> 85 + 44 86 </div> 87 + 45 88 @code { 46 - private IJSInProcessObjectReference _module = null!; 89 + private IJSInProcessObjectReference _module = null!; 90 + private CheckState _updateAvailable = CheckState.Unknown; 91 + private CheckState _skipWaitingRes = CheckState.Unknown; 92 + private VersionResponse? _newVersion; 47 93 48 94 protected override async Task OnInitializedAsync() 49 95 { 50 96 _module = (IJSInProcessObjectReference)await Js.InvokeAsync<IJSObjectReference>("import", "./Components/ErrorUi.razor.js"); 51 97 } 52 98 99 + private enum CheckState 100 + { 101 + Unknown, 102 + True, 103 + False 104 + } 105 + 106 + private async void CheckUpdate() 107 + { 108 + await UpdateSvc.ServiceWorkerUpdate(); 109 + var res = await UpdateSvc.ServiceWorkerCheckWaiting(); 110 + _updateAvailable = res ? CheckState.True : CheckState.False; 111 + _newVersion = UpdateSvc.BackendVersion; 112 + StateHasChanged(); 113 + } 114 + 115 + private async void SkipWaiting() 116 + { 117 + var res = await UpdateSvc.ServiceWorkerSkipWaiting(); 118 + _skipWaitingRes = res ? CheckState.True : CheckState.False; 119 + if (res) 120 + { 121 + Nav.NavigateTo("/", true); 122 + } 123 + } 124 + 53 125 private void DownloadLogs() 54 126 { 55 - var logBytes = LogService.GetLogs().SelectMany(p => Encoding.UTF8.GetBytes(p)).ToArray(); 127 + var logBytes = LogSvc.GetLogs().SelectMany(p => Encoding.UTF8.GetBytes(p)).ToArray(); 56 128 _module.InvokeVoid("DownloadFile", "log.txt", "text/plain", logBytes); 57 129 } 58 130 }
+2 -2
Iceshrimp.Frontend/Pages/Settings/About.razor.js
··· 1 1 export function DownloadFile(filename, contentType, content) { 2 - const file = new File([content], filename, { type: contentType}); 2 + const file = new File([content], filename, {type: contentType}); 3 3 const exportUrl = URL.createObjectURL(file); 4 4 5 5 const a = document.createElement("a"); ··· 10 10 a.click(); 11 11 12 12 URL.revokeObjectURL(exportUrl); 13 - } 13 + }
+1
Iceshrimp.Frontend/Startup.cs
··· 28 28 builder.Services.AddSingleton<VersionService>(); 29 29 builder.Services.AddSingleton<MessageService>(); 30 30 builder.Services.AddSingleton<GlobalComponentSvc>(); 31 + builder.Services.AddSingleton<UpdateService>(); 31 32 builder.Services.AddAuthorizationCore(); 32 33 builder.Services.AddCascadingAuthenticationState(); 33 34 builder.Services.AddBlazoredLocalStorageAsSingleton();
+32
Iceshrimp.Frontend/wwwroot/Core/Services/UpdateService.cs.js
··· 1 + export async function RegisterUpdateCallback(dotNetHelper) { 2 + const registration = await navigator.serviceWorker.getRegistration(); 3 + if (registration) { 4 + registration.addEventListener("updatefound", () => { 5 + dotNetHelper.invokeMethodAsync('OnUpdateFound'); 6 + }) 7 + } 8 + else { 9 + console.error("Failed to get service worker registration") 10 + } 11 + } 12 + 13 + export async function ServiceWorkerCheckWaiting(){ 14 + const registration = await navigator.serviceWorker.getRegistration(); 15 + return registration.waiting ? true : false; 16 + } 17 + 18 + export async function ServiceWorkerUpdate(){ 19 + const registration = await navigator.serviceWorker.getRegistration(); 20 + await registration.update(); 21 + } 22 + 23 + export async function ServiceWorkerSkipWaiting(){ 24 + const registration = await navigator.serviceWorker.getRegistration(); 25 + if (registration.waiting){ 26 + registration.waiting.postMessage({ type: 'SKIP_WAITING' }) 27 + return true; 28 + } 29 + else { 30 + return false; 31 + } 32 + }
+3 -3
Iceshrimp.Frontend/wwwroot/css/app.css
··· 111 111 padding: 0.5em; 112 112 padding-inline: 1em; 113 113 align-items: center; 114 - i { 115 - padding: 0 0.5em 0 0; 116 - } 114 + /*i {*/ 115 + /* padding: 0 0.5em 0 0;*/ 116 + /*}*/ 117 117 } 118 118 .button:hover { 119 119 background-color: var(--hover-color);
-22
Iceshrimp.Frontend/wwwroot/manifest.webmanifest
··· 1 - { 2 - "name": "Iceshrimp.Frontend", 3 - "short_name": "Iceshrimp.Frontend", 4 - "id": "./", 5 - "start_url": "./", 6 - "display": "standalone", 7 - "background_color": "#ffffff", 8 - "theme_color": "#03173d", 9 - "prefer_related_applications": false, 10 - "icons": [ 11 - { 12 - "src": "icon-512.png", 13 - "type": "image/png", 14 - "sizes": "512x512" 15 - }, 16 - { 17 - "src": "icon-192.png", 18 - "type": "image/png", 19 - "sizes": "192x192" 20 - } 21 - ] 22 - }
+5
Iceshrimp.Frontend/wwwroot/service-worker.js
··· 2 2 // This is because caching would make development more difficult (changes would not 3 3 // be reflected on the first load after each change). 4 4 self.addEventListener('fetch', () => { }); 5 + self.addEventListener('message', (event) => { 6 + if (event.data && event.data.type === 'SKIP_WAITING') { 7 + self.skipWaiting(); 8 + } 9 + });
+6
Iceshrimp.Frontend/wwwroot/service-worker.published.js
··· 53 53 54 54 return cachedResponse || fetch(event.request); 55 55 } 56 + 57 + self.addEventListener('message', (event) => { 58 + if (event.data && event.data.type === 'SKIP_WAITING') { 59 + self.skipWaiting(); 60 + } 61 + });