diff options
author | Rory& <root@rory.gay> | 2024-04-20 23:48:43 +0200 |
---|---|---|
committer | Rory& <root@rory.gay> | 2024-04-20 23:48:43 +0200 |
commit | 68fe1a2284045908d92ef06c1c26cd937ded784e (patch) | |
tree | 71144f404f060888dcc5e4036e038abe857adc4e | |
parent | Start adding pages (diff) | |
download | BugMine-68fe1a2284045908d92ef06c1c26cd937ded784e.tar.xz |
Add basic project management
32 files changed, 708 insertions, 65 deletions
diff --git a/BugMine.Sdk/BugMine.Sdk.csproj b/BugMine.Sdk/BugMine.Sdk.csproj new file mode 100644 index 0000000..092683c --- /dev/null +++ b/BugMine.Sdk/BugMine.Sdk.csproj @@ -0,0 +1,19 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <TargetFramework>net8.0</TargetFramework> + <ImplicitUsings>enable</ImplicitUsings> + <Nullable>enable</Nullable> + </PropertyGroup> + + <ItemGroup> + <Reference Include="Microsoft.Extensions.DependencyInjection.Abstractions"> + <HintPath>..\..\..\..\.nuget\packages\microsoft.extensions.dependencyinjection.abstractions\8.0.0\lib\net8.0\Microsoft.Extensions.DependencyInjection.Abstractions.dll</HintPath> + </Reference> + </ItemGroup> + + <ItemGroup> + <ProjectReference Include="..\LibMatrix\LibMatrix\LibMatrix.csproj" /> + </ItemGroup> + +</Project> diff --git a/BugMine.Sdk/BugMineClient.cs b/BugMine.Sdk/BugMineClient.cs new file mode 100644 index 0000000..be80c3a --- /dev/null +++ b/BugMine.Sdk/BugMineClient.cs @@ -0,0 +1,55 @@ +using System.Text.RegularExpressions; +using ArcaneLibs.Extensions; +using LibMatrix.Homeservers; +using LibMatrix.Responses; + +namespace BugMine.Web.Classes; + +public class BugMineClient(AuthenticatedHomeserverGeneric homeserver) { + public AuthenticatedHomeserverGeneric Homeserver { get; } = homeserver; + + public async IAsyncEnumerable<BugMineProject> GetProjects() { + List<Task<BugMineProject>> tasks = []; + await foreach (var room in homeserver.GetJoinedRoomsByType(BugMineProject.RoomType)) { + tasks.Add(room.AsBugMineProject()); + } + + var results = tasks.ToAsyncEnumerable(); + await foreach (var result in results) { + yield return result; + } + } + + public async Task<BugMineProject> CreateProject(ProjectInfo request) { + var alias = string.Join('_', Regex.Matches(request.Name, @"[a-zA-Z0-9]+").Select(x => x.Value))+"-bugmine"; + + var crr = new CreateRoomRequest() { + CreationContent = new() { + ["type"] = "gay.rory.bugmine.project" + }, + Name = $"{request.Name} (BugMine project)", + RoomAliasName = alias + }; + + var response = await Homeserver.CreateRoom(crr); + await response.SendStateEventAsync(ProjectInfo.EventId, request); + + return await response.AsBugMineProject(); + } + + public async Task<BugMineProject?> GetProject(string projectSlug) { + if (projectSlug.StartsWith('!')) { + var room = homeserver.GetRoom(projectSlug); + if (room == null) return null; + + return await (await room.AsBugMineProject()).InitializeAsync(); + } + else { + var alias = $"#{projectSlug}"; + var resolveResult = await Homeserver.ResolveRoomAliasAsync(alias); + if (string.IsNullOrEmpty(resolveResult?.RoomId)) return null; //TODO: fallback to finding via joined rooms' canonical alias event? + + return await (await homeserver.GetRoom(resolveResult.RoomId).AsBugMineProject()).InitializeAsync(); + } + } +} \ No newline at end of file diff --git a/BugMine.Sdk/BugMineIssue.cs b/BugMine.Sdk/BugMineIssue.cs new file mode 100644 index 0000000..6cf3409 --- /dev/null +++ b/BugMine.Sdk/BugMineIssue.cs @@ -0,0 +1,12 @@ +using LibMatrix; +using LibMatrix.RoomTypes; + +namespace BugMine.Web.Classes; + +public class BugMineIssue(GenericRoom room, StateEventResponse data) { + public GenericRoom Room { get; } = room; + public StateEventResponse Data { get; } = data; + // public async IAsyncEnumerable<StateEventResponse> GetRelatedEventsAsync() { + // + // } +} \ No newline at end of file diff --git a/BugMine.Sdk/BugMineProject.cs b/BugMine.Sdk/BugMineProject.cs new file mode 100644 index 0000000..c90ba6e --- /dev/null +++ b/BugMine.Sdk/BugMineProject.cs @@ -0,0 +1,43 @@ +using System.Text.Json.Nodes; +using LibMatrix.Homeservers; +using LibMatrix.RoomTypes; + +namespace BugMine.Web.Classes; + +public class BugMineProject(GenericRoom room) { + public const string RoomType = "gay.rory.bugmine.project"; + public GenericRoom Room { get; } = room; + public ProjectInfo Info { get; set; } + public string ProjectSlug { get; set; } + + public async Task<BugMineProject> InitializeAsync() { + Info = (await Room.GetStateAsync<ProjectInfo>(ProjectInfo.EventId))!; + var alias = await room.GetCanonicalAliasAsync(); + + if (alias != null) + ProjectSlug = alias.Alias?[1..] ?? room.RoomId; + else ProjectSlug = room.RoomId; + + return this; + } + + public async Task<BugMineIssue> CreateIssue(BugMineIssueData issue) { + // add relation to room creation event + issue.RelatesTo = new() { + EventId = (await Room.GetStateEventAsync("m.room.create")).EventId, + RelationType = "gay.rory.bugmine.issue" + }; + var eventId = await Room.SendTimelineEventAsync(BugMineIssueData.EventId, issue); + + // return new BugMineIssueAccessor(Room, await Room.GetEventAsync<>(eventId)); + var evt = await room.GetEventAsync(eventId.EventId); + Console.WriteLine(evt); + return new BugMineIssue(Room, evt); + } +} + +public static class ProjectRoomExtensions { + public static async Task<BugMineProject> AsBugMineProject(this GenericRoom room) { + return await new BugMineProject(room).InitializeAsync(); + } +} \ No newline at end of file diff --git a/BugMine.Sdk/Events/State/ProjectInfo.cs b/BugMine.Sdk/Events/State/ProjectInfo.cs new file mode 100644 index 0000000..2d15bff --- /dev/null +++ b/BugMine.Sdk/Events/State/ProjectInfo.cs @@ -0,0 +1,11 @@ +using LibMatrix.EventTypes; + +namespace BugMine.Web.Classes; + +[MatrixEvent(EventName = EventId)] +public class ProjectInfo : EventContent { + public const string EventId = "gay.rory.bugmine.project_info"; + public string? Name { get; set; } + public string? ProjectIcon { get; set; } + public string? Repository { get; set; } +} \ No newline at end of file diff --git a/BugMine.Sdk/Events/Timeline/BugMineIssueComment.cs b/BugMine.Sdk/Events/Timeline/BugMineIssueComment.cs new file mode 100644 index 0000000..50c73a1 --- /dev/null +++ b/BugMine.Sdk/Events/Timeline/BugMineIssueComment.cs @@ -0,0 +1,11 @@ +using LibMatrix.EventTypes; + +namespace BugMine.Web.Classes; + +[MatrixEvent(EventName = EventId)] +public class BugMineIssueComment : TimelineEventContent { + public const string EventId = "gay.rory.bugmine.comment"; + public string Comment { get; set; } + public string Author { get; set; } + public DateTime Timestamp { get; set; } +} \ No newline at end of file diff --git a/BugMine.Sdk/Events/Timeline/BugMineIssueData.cs b/BugMine.Sdk/Events/Timeline/BugMineIssueData.cs new file mode 100644 index 0000000..480102a --- /dev/null +++ b/BugMine.Sdk/Events/Timeline/BugMineIssueData.cs @@ -0,0 +1,12 @@ +using LibMatrix.EventTypes; + +namespace BugMine.Web.Classes; + +[MatrixEvent(EventName = ProjectInfo.EventId)] +public class BugMineIssueData : TimelineEventContent { + public const string EventId = "gay.rory.bugmine.issue"; + public string Name { get; set; } + public string? AssignedTo { get; set; } + public string? Status { get; set; } + public string? Priority { get; set; } +} \ No newline at end of file diff --git a/BugMine.Sdk/ServiceInstaller.cs b/BugMine.Sdk/ServiceInstaller.cs new file mode 100644 index 0000000..06309ac --- /dev/null +++ b/BugMine.Sdk/ServiceInstaller.cs @@ -0,0 +1,25 @@ +using LibMatrix.Services; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +public static class ServiceInstaller { + /// <summary> + /// Adds BugMine SDK services to the service collection. + /// </summary> + /// <param name="services">Service collection</param> + /// <param name="config">Optional BugMine SDK configuration</param> + /// <returns>Input service collection</returns> + public static IServiceCollection AddBugMine(this IServiceCollection services, BugMineSdkConfiguration? config = null) { + services.AddRoryLibMatrixServices(new() { + AppName = config?.AppName ?? "BugMine SDK app" + }); + return services; + } +} + +/// <summary> +/// Configuration for the BugMine SDK. +/// </summary> +public class BugMineSdkConfiguration { + public string AppName { get; set; } = "BugMine SDK app"; +} \ No newline at end of file diff --git a/BugMine.Web/BugMine.Web.csproj b/BugMine.Web/BugMine.Web.csproj index fc79e51..851a47d 100644 --- a/BugMine.Web/BugMine.Web.csproj +++ b/BugMine.Web/BugMine.Web.csproj @@ -24,6 +24,7 @@ </ItemGroup> <ItemGroup> + <ProjectReference Include="..\BugMine.Sdk\BugMine.Sdk.csproj" /> <ProjectReference Condition="Exists('..\LibMatrix\ArcaneLibs\ArcaneLibs.Blazor.Components\ArcaneLibs.Blazor.Components.csproj')" Include="..\LibMatrix\ArcaneLibs\ArcaneLibs.Blazor.Components\ArcaneLibs.Blazor.Components.csproj"/> <PackageReference Condition="!Exists('..\LibMatrix\ArcaneLibs\ArcaneLibs.Blazor.Components\ArcaneLibs.Blazor.Components.csproj')" Include="ArcaneLibs.Blazor.Components" Version="*-preview*"/> <ProjectReference Include="..\LibMatrix\LibMatrix\LibMatrix.csproj"/> @@ -67,6 +68,7 @@ <_ContentIncludedByDefault Remove="wwwroot\tmp\css\jetbrains-mono\webfonts\JetBrainsMono-SemiBoldItalic.woff2" /> <_ContentIncludedByDefault Remove="wwwroot\tmp\css\jetbrains-mono\webfonts\JetBrainsMono-Thin.woff2" /> <_ContentIncludedByDefault Remove="wwwroot\tmp\css\jetbrains-mono\webfonts\JetBrainsMono-ThinItalic.woff2" /> + <_ContentIncludedByDefault Remove="wwwroot\sample-data\weather.json" /> </ItemGroup> diff --git a/BugMine.Web/Classes/BugMineStorage.cs b/BugMine.Web/Classes/BugMineStorage.cs index b45e253..73b46b5 100644 --- a/BugMine.Web/Classes/BugMineStorage.cs +++ b/BugMine.Web/Classes/BugMineStorage.cs @@ -1,20 +1,25 @@ using LibMatrix; using LibMatrix.Homeservers; +using LibMatrix.Interfaces.Services; using LibMatrix.Services; using Microsoft.AspNetCore.Components; namespace BugMine.Web.Classes; -public class BugMineStorage(ILogger<BugMineStorage> logger, TieredStorageService storageService, HomeserverProviderService homeserverProviderService, NavigationManager navigationManager) { +public class BugMineStorage( + ILogger<BugMineStorage> logger, + IStorageProvider localStorage, + HomeserverProviderService homeserverProviderService, + NavigationManager navigationManager) { public async Task<List<UserAuth>?> GetAllTokens() { logger.LogTrace("Getting all tokens."); - return await storageService.DataStorageProvider!.LoadObjectAsync<List<UserAuth>>("bugmine.tokens") ?? + return await localStorage.LoadObjectAsync<List<UserAuth>>("bugmine.tokens") ?? new List<UserAuth>(); } public async Task<UserAuth?> GetCurrentToken() { logger.LogTrace("Getting current token."); - var currentToken = await storageService.DataStorageProvider!.LoadObjectAsync<UserAuth>("bugmine.token"); + var currentToken = await localStorage.LoadObjectAsync<UserAuth>("bugmine.token"); var allTokens = await GetAllTokens(); if (allTokens is null or { Count: 0 }) { await SetCurrentToken(null); @@ -37,27 +42,40 @@ public class BugMineStorage(ILogger<BugMineStorage> logger, TieredStorageService var tokens = await GetAllTokens() ?? new List<UserAuth>(); tokens.Add(UserAuth); - await storageService.DataStorageProvider!.SaveObjectAsync("bugmine.tokens", tokens); + await localStorage!.SaveObjectAsync("bugmine.tokens", tokens); } - private async Task<AuthenticatedHomeserverGeneric?> GetCurrentSession() { + private async Task<BugMineClient?> GetCurrentSession() { logger.LogTrace("Getting current session."); var token = await GetCurrentToken(); if (token == null) { return null; } - return await homeserverProviderService.GetAuthenticatedWithToken(token.Homeserver, token.AccessToken, token.Proxy); + var hc = await homeserverProviderService.GetAuthenticatedWithToken(token.Homeserver, token.AccessToken, token.Proxy, useGeneric: true); + return new BugMineClient(hc); } - public async Task<AuthenticatedHomeserverGeneric?> GetSession(UserAuth userAuth) { + public async Task<BugMineClient?> GetSession(UserAuth userAuth) { logger.LogTrace("Getting session."); - return await homeserverProviderService.GetAuthenticatedWithToken(userAuth.Homeserver, userAuth.AccessToken, userAuth.Proxy); + var hc = await homeserverProviderService.GetAuthenticatedWithToken(userAuth.Homeserver, userAuth.AccessToken, userAuth.Proxy, useGeneric: true); + return new BugMineClient(hc); } - public async Task<AuthenticatedHomeserverGeneric?> GetCurrentSessionOrNavigate() { + public async Task<BugMineClient?> GetCurrentSessionOrNavigate() { logger.LogTrace("Getting current session or navigating."); - AuthenticatedHomeserverGeneric? session = null; + var session = await GetCurrentSessionOrNull(); + + if (session is null) { + logger.LogInformation("No session found. Navigating to login."); + navigationManager.NavigateTo("/Login"); + } + + return session; + } + + public async Task<BugMineClient?> GetCurrentSessionOrNull() { + BugMineClient? session = null; try { //catch if the token is invalid @@ -74,11 +92,6 @@ public class BugMineStorage(ILogger<BugMineStorage> logger, TieredStorageService throw; } - if (session is null) { - logger.LogInformation("No session found. Navigating to login."); - navigationManager.NavigateTo("/Login"); - } - return session; } @@ -90,11 +103,11 @@ public class BugMineStorage(ILogger<BugMineStorage> logger, TieredStorageService } tokens.RemoveAll(x => x.AccessToken == auth.AccessToken); - await storageService.DataStorageProvider.SaveObjectAsync("bugmine.tokens", tokens); + await localStorage.SaveObjectAsync("bugmine.tokens", tokens); } public async Task SetCurrentToken(UserAuth? auth) { logger.LogTrace("Setting current token."); - await storageService.DataStorageProvider.SaveObjectAsync("bugmine.token", auth); + await localStorage.SaveObjectAsync("bugmine.token", auth); } -} +} \ No newline at end of file diff --git a/BugMine.Web/Components/IssueImportWorker.razor b/BugMine.Web/Components/IssueImportWorker.razor new file mode 100644 index 0000000..bc72156 --- /dev/null +++ b/BugMine.Web/Components/IssueImportWorker.razor @@ -0,0 +1,25 @@ +@using LibMatrix.Homeservers +@inject ILogger<IssueImportWorker> Logger +@if(Client == null) { + <span>Not logged in.</span> + <a href="/Auth/Login">Login</a> +} else { + + <span>Logged in as @Client.Homeserver.UserId</span> + <a href="/Auth/Logout">Logout</a> +} + +@code { + + private BugMineClient? Client { get; set; } + private string Status { get; set; } = ""; + protected override async Task OnInitializedAsync() { + while(Client == null) { + Client = await BugMineStorage.GetCurrentSessionOrNull(); + if(Client == null) { + await Task.Delay(1000); + } + } + } + +} \ No newline at end of file diff --git a/BugMine.Web/Components/ProgressLog.razor b/BugMine.Web/Components/ProgressLog.razor new file mode 100644 index 0000000..f149ac6 --- /dev/null +++ b/BugMine.Web/Components/ProgressLog.razor @@ -0,0 +1,16 @@ +@using System.Collections.ObjectModel + + +@code { + private ObservableCollection<string> _messages = new ObservableCollection<string>(); + + private ObservableCollection<string> Messages { + get => _messages; + set { + _messages = value; + _messages.CollectionChanged += (_, _) => StateHasChanged(); + StateHasChanged(); + } + } + +} \ No newline at end of file diff --git a/BugMine.Web/Components/SimpleSpinner.razor b/BugMine.Web/Components/SimpleSpinner.razor new file mode 100644 index 0000000..920247d --- /dev/null +++ b/BugMine.Web/Components/SimpleSpinner.razor @@ -0,0 +1,18 @@ +<pre style="width: fit-content; height: fit-content; background-color: transparent; color: white; rotate: @(i)deg;">|</pre> + +@code { + int i = 0; + protected override void OnInitialized() + { + base.OnInitialized(); + Task.Run(async () => + { + while (true) + { + await Task.Delay(12); + i+=10; + StateHasChanged(); + } + }); + } +} \ No newline at end of file diff --git a/BugMine.Web/Components/UpdateAvailableDetector.razor b/BugMine.Web/Components/UpdateAvailableDetector.razor new file mode 100644 index 0000000..5197a6f --- /dev/null +++ b/BugMine.Web/Components/UpdateAvailableDetector.razor @@ -0,0 +1,38 @@ +@* Source: https://whuysentruit.medium.com/blazor-wasm-pwa-adding-a-new-update-available-notification-d9f65c4ad13 *@ +@inject IJSRuntime _jsRuntime + +@if (_newVersionAvailable) +{ + <button type="button" class="btn btn-warning shadow floating-update-button" onclick="window.location.reload()"> + A new version of the application is available. Click here to reload. + </button> +} + +@code { + + private bool _newVersionAvailable = false; + + protected override async Task OnInitializedAsync() + { + await RegisterForUpdateAvailableNotification(); + } + + private async Task RegisterForUpdateAvailableNotification() + { + await _jsRuntime.InvokeAsync<object>( + identifier: "registerForUpdateAvailableNotification", + DotNetObjectReference.Create(this), + nameof(OnUpdateAvailable)); + } + + [JSInvokable(nameof(OnUpdateAvailable))] + public Task OnUpdateAvailable() + { + _newVersionAvailable = true; + + StateHasChanged(); + + return Task.CompletedTask; + } + +} \ No newline at end of file diff --git a/BugMine.Web/Components/UpdateAvailableDetector.razor.css b/BugMine.Web/Components/UpdateAvailableDetector.razor.css new file mode 100644 index 0000000..047ea8d --- /dev/null +++ b/BugMine.Web/Components/UpdateAvailableDetector.razor.css @@ -0,0 +1,15 @@ +.floating-update-button { + position: fixed; + + right: 2rem; + bottom: 2rem; + + padding: 1rem 1.5rem; + + animation: fadein 2s ease-out; +} + +@keyframes fadein { + from { right: -100%; } + to { right: 2rem; } +} diff --git a/BugMine.Web/Layout/MainLayout.razor b/BugMine.Web/Layout/MainLayout.razor index e7554be..5e907e3 100644 --- a/BugMine.Web/Layout/MainLayout.razor +++ b/BugMine.Web/Layout/MainLayout.razor @@ -6,6 +6,7 @@ <main> <div class="top-row px-4"> + <IssueImportWorker/> <a href="https://learn.microsoft.com/aspnet/core/" target="_blank">About</a> </div> @@ -13,4 +14,6 @@ @Body </article> </main> -</div> \ No newline at end of file +</div> + +<UpdateAvailableDetector/> \ No newline at end of file diff --git a/BugMine.Web/Layout/NavMenu.razor b/BugMine.Web/Layout/NavMenu.razor index 9f5af9d..4174dfd 100644 --- a/BugMine.Web/Layout/NavMenu.razor +++ b/BugMine.Web/Layout/NavMenu.razor @@ -19,6 +19,13 @@ <span class="bi bi-list-nested-nav-menu" aria-hidden="true"></span> Projects </NavLink> </div> + @if (Constants.Debug) { + <div class="nav-item px-3"> + <NavLink class="nav-link" href="/DevTools"> + <span class="bi bi-list-nested-nav-menu" aria-hidden="true"></span> Devtools + </NavLink> + </div> + } </nav> </div> diff --git a/BugMine.Web/Pages/Auth/Auth.razor b/BugMine.Web/Pages/Auth/Auth.razor new file mode 100644 index 0000000..e58f47b --- /dev/null +++ b/BugMine.Web/Pages/Auth/Auth.razor @@ -0,0 +1,6 @@ +@page "/Auth" +<h3>Auth</h3> + +@code { + +} \ No newline at end of file diff --git a/BugMine.Web/Pages/Auth/LegacyLogin.razor b/BugMine.Web/Pages/Auth/LegacyLogin.razor new file mode 100644 index 0000000..5257028 --- /dev/null +++ b/BugMine.Web/Pages/Auth/LegacyLogin.razor @@ -0,0 +1,71 @@ +@page "/Auth/LegacyLogin" +@using System.Text.Json.Serialization +@using LibMatrix.Services +@inject HomeserverProviderService hsProvider +<h3>Login</h3> +<hr/> + +<span style="display: block;"> + <label>User ID:</label> + <span>@@</span><!-- + --><FancyTextBox @bind-Value="@authData.Username"></FancyTextBox><!-- + --><span>:</span><!-- + --><FancyTextBox @bind-Value="@authData.Homeserver"></FancyTextBox> +</span> +<span style="display: block;"> + <label>Password:</label> + <FancyTextBox @bind-Value="@authData.Password" IsPassword="true"></FancyTextBox> +</span> +<span style="display: block"> + <label>Proxy (<a href="https://cgit.rory.gay/matrix/MxApiExtensions.git">MxApiExtensions</a> or similar):</label> + <FancyTextBox @bind-Value="@authData.Proxy"></FancyTextBox> +</span> +<br/> +<LinkButton OnClick="@(() => LoginWithAuth(authData))">Log in</LinkButton> + +<h4>Continue as guest</h4> +<hr/> +<LinkButton OnClick="@(() => LoginWithAuth(new LoginStruct { Homeserver = "matrix.org", Username = "guest", Password = "guest" }))">Log in as guest</LinkButton> + +@code { + private LoginStruct authData = new(); + + List<UserAuth>? LoggedInSessions { get; set; } = new(); + + async Task LoginWithAuth(LoginStruct record) { + if (LoggedInSessions.Any(x => x.UserId == $"@{record.Username}:{record.Homeserver}" && x.Proxy == record.Proxy)) return; + StateHasChanged(); + try { + var result = new UserAuth(await hsProvider.Login(record.Homeserver, record.Username, record.Password, record.Proxy)) { + Proxy = record.Proxy + }; + if (result == null) { + Console.WriteLine($"Failed to login to {record.Homeserver} as {record.Username}!"); + return; + } + + Console.WriteLine($"Obtained access token for {result.UserId}!"); + + await BugMineStorage.AddToken(result); + await BugMineStorage.SetCurrentToken(result); + } + catch (Exception e) { + Console.WriteLine($"Failed to login to {record.Homeserver} as {record.Username}!"); + Console.WriteLine(e); + record.Exception = e; + } + + StateHasChanged(); + } + + private class LoginStruct { + public string? Homeserver { get; set; } = ""; + public string? Username { get; set; } = ""; + public string? Password { get; set; } = ""; + public string? Proxy { get; set; } + + [JsonIgnore] + internal Exception? Exception { get; set; } + } + +} \ No newline at end of file diff --git a/BugMine.Web/Pages/Auth/Login.razor b/BugMine.Web/Pages/Auth/Login.razor new file mode 100644 index 0000000..7b457ec --- /dev/null +++ b/BugMine.Web/Pages/Auth/Login.razor @@ -0,0 +1,69 @@ +@page "/Auth/Login" +@using ArcaneLibs.Extensions +@using LibMatrix.Homeservers +@using LibMatrix.Services +@inject HomeserverProviderService hsProvider +<h3>Login</h3> +<hr/> +<p>Notice: this doesn't work yet, please use <a href="/Auth/LegacyLogin">legacy login</a>.</p> +<span style="display: block;"> + <span>Homeserver: </span> + <FancyTextBox @bind-Value="@HomeserverName"></FancyTextBox> +</span> + +@* <span style="display: block;"> *@ +@* <label>User ID:</label> *@ +@* <span>@@</span><!-- *@ +@* --><FancyTextBox @bind-Value="@authData.Username"></FancyTextBox><!-- *@ +@* --><span>:</span><!-- *@ +@* --><FancyTextBox @bind-Value="@authData.Homeserver"></FancyTextBox> *@ +@* </span> *@ +@* <span style="display: block;"> *@ +@* <label>Password:</label> *@ +@* <FancyTextBox @bind-Value="@authData.Password" IsPassword="true"></FancyTextBox> *@ +@* </span> *@ +@* <br/> *@ +@* <LinkButton OnClick="@(() => LoginWithAuth(authData))">Log in</LinkButton> *@ + +@if (Constants.Debug) { + <br/> + <span>Auth client state:</span> + <pre> + @Homeserver?.Auth.ToJson() + </pre> + <span>Current stage:</span> + <pre> + @CurrentStage?.ToJson() + </pre> +} + +@code { + private string? _homeserverName = null; + + private string? HomeserverName { + get => _homeserverName; + set { + _homeserverName = value; + HomeserverChanged(); + } + } + + public RemoteHomeserver? Homeserver { get; set; } + public UserInteractiveAuthClient.IUIAStage CurrentStage { get; set; } = null!; + + //oninit + protected override async Task OnInitializedAsync() { + HomeserverName = "matrixunittests.rory.gay"; + } + + public async Task HomeserverChanged() { + if (string.IsNullOrWhiteSpace(HomeserverName)) return; + Homeserver = await hsProvider.GetRemoteHomeserver(HomeserverName); + CurrentStage = await Homeserver.Auth.GetAvailableFlowsAsync(enableRegister: true, enableGuest: true); + + + StateHasChanged(); + } + +} + diff --git a/BugMine.Web/Pages/Auth/Logout.razor b/BugMine.Web/Pages/Auth/Logout.razor new file mode 100644 index 0000000..dd019a1 --- /dev/null +++ b/BugMine.Web/Pages/Auth/Logout.razor @@ -0,0 +1,18 @@ +@page "/Auth/Logout" +<p>Logging out...</p> + +@code { + + protected override async Task OnInitializedAsync() { + var client = await BugMineStorage.GetCurrentSessionOrNull(); + if (client != null) { + await client.Homeserver.Logout(); + } + + await BugMineStorage.RemoveToken(await BugMineStorage.GetCurrentToken()); + await BugMineStorage.SetCurrentToken(null); + + NavigationManager.NavigateTo("/", true); + } + +} \ No newline at end of file diff --git a/BugMine.Web/Pages/DevTools.razor b/BugMine.Web/Pages/DevTools.razor new file mode 100644 index 0000000..f8fc408 --- /dev/null +++ b/BugMine.Web/Pages/DevTools.razor @@ -0,0 +1,54 @@ +@page "/DevTools" +@using LibMatrix.Homeservers +@using LibMatrix.EventTypes.Spec.State +<h3>DevTools</h3> + +<LinkButton OnClick="@MassCreateProjects">Mass create projects</LinkButton> +<LinkButton OnClick="@DestroyAllProjects">Destroy all projects</LinkButton> + +@code { + + private BugMineClient? Client { get; set; } + + protected override async Task OnInitializedAsync() { + Client = await BugMineStorage.GetCurrentSessionOrNavigate(); + } + + private async Task DestroyAllProjects() { + var ss = new SemaphoreSlim(16, 16); + await foreach (var proj in Client.Homeserver.GetJoinedRoomsByType(BugMineProject.RoomType)) { + Task.Run(async () => { + await ss.WaitAsync(); + await proj.SendStateEventAsync(RoomNameEventContent.EventId, new RoomNameEventContent() { + Name = "Disbanded BugMine project." + }); + await proj.SendStateEventAsync(RoomJoinRulesEventContent.EventId, new RoomJoinRulesEventContent() { + JoinRule = RoomJoinRulesEventContent.JoinRules.Private + }); + await proj.SendStateEventAsync(RoomCanonicalAliasEventContent.EventId, new RoomCanonicalAliasEventContent() { + Alias = null + }); + await proj.LeaveAsync("Disbanded room."); + ss.Release(); + }); + } + } + + private async Task MassCreateProjects() { + // var rooms = await Client.Homeserver.GetJoinedRooms(); + // List<string> roomNames = (await Task.WhenAll(rooms.Select(x => x.GetNameAsync()))).Where(x => x != null).ToList(); + for (int i = 0; i < 5; i++) { + Task.Run(async () => { + // var randomName = roomNames[Random.Shared.Next(roomNames.Count)]; + var proj = await Client.CreateProject(new() { + Name = /*randomName + */Guid.NewGuid().ToString()[..8] + }); + + await proj.CreateIssue(new() { + Name = "meow" + }); + }); + } + } + +} \ No newline at end of file diff --git a/BugMine.Web/Pages/Projects/Index.razor b/BugMine.Web/Pages/Projects/Index.razor index a24747c..eaf8dc1 100644 --- a/BugMine.Web/Pages/Projects/Index.razor +++ b/BugMine.Web/Pages/Projects/Index.razor @@ -1,22 +1,52 @@ @page "/Projects" +@using LibMatrix.Homeservers <h3>Projects</h3> -@if (true) { +@if (Projects.Count == 0) { <p>There are no projects to display.</p> } +else { + <div class="projects"> + @foreach (var project in Projects) { + <div class="card project-card" @onclick="@(()=>Navigate(project))"> + @if (string.IsNullOrWhiteSpace(project.Info.ProjectIcon)) { + <img class="project-icon" src="/icon-512.png"> + } + else { + <img class="project-icon" src="/icon-512.png"> + } + <span class="project-name">@project.Info.Name</span> + </div> + @* <p>@project.Info.Name</p> *@ + } + </div> +} <p>Did not find the project board you are looking for?</p> <LinkButton href="/Projects/New">Create new board</LinkButton> @code { - private List<Project> projects = new List<Project>(); - - + private BugMineClient? Client { get; set; } + private List<BugMineProject> Projects { get; set; } = []; + private CancellationTokenSource? _cts = new(); protected override async Task OnInitializedAsync() { - + Client = await BugMineStorage.GetCurrentSessionOrNavigate(); + if (Client == null) { + return; + } + + await foreach (var project in Client.GetProjects()) { + Projects.Add(project); + StateHasChanged(); + // await Task.Delay(100); + } } - - private class Project { } + + private async Task Navigate(BugMineProject project) { + Console.WriteLine($"Navigating to {project.ProjectSlug}"); + NavigationManager.NavigateTo($"/Projects/{project.ProjectSlug}/"); + } + } \ No newline at end of file diff --git a/BugMine.Web/Pages/Projects/Index.razor.css b/BugMine.Web/Pages/Projects/Index.razor.css new file mode 100644 index 0000000..20d8c4d --- /dev/null +++ b/BugMine.Web/Pages/Projects/Index.razor.css @@ -0,0 +1,26 @@ +.projects { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(20rem, 1fr)); + gap: 1rem; + padding-bottom: 1rem; +} + +.project-card { + display: flex; + padding: 1rem; + border-radius: 0.5rem; + background-color: var(--bs-dark); + box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.5); + transition: background-color 1s, box-shadow 3s; +} + +.project-icon { + border-radius: 50%; + width: 2rem; + height: 2rem; + margin-right: 1rem; +} + +.project-name { + display: inline; +} \ No newline at end of file diff --git a/BugMine.Web/Pages/Projects/NewProject.razor b/BugMine.Web/Pages/Projects/NewProject.razor index 529813e..00b7b21 100644 --- a/BugMine.Web/Pages/Projects/NewProject.razor +++ b/BugMine.Web/Pages/Projects/NewProject.razor @@ -1,25 +1,47 @@ @page "/Projects/New" -@using LibMatrix.Responses -@using BugMine.Web.Classes @using ArcaneLibs.Extensions <h3>New project</h3> <span>Project name: </span> -<FancyTextBox bind-Value="@request.Name"></FancyTextBox> +<FancyTextBox @bind-Value="@_request.Name"></FancyTextBox> <br/> -<span>Shortname: </span> -<FancyTextBox bind-Value="@request.RoomAliasName"></FancyTextBox> +<span>Project repository: </span> +<FancyTextBox @bind-Value="@_request.Repository"></FancyTextBox> <br/> +@* <span>Room alias: </span> *@ +@* <FancyTextBox @bind-Value="@_request."></FancyTextBox> *@ +@* <br/> *@ @if (Constants.Debug) { <span>Debug: </span> <pre> - @request.ToJson() + @_request.ToJson() </pre> <br/> } +<LinkButton OnClick="@CreateProject">Create project</LinkButton> + @code { - private CreateRoomRequest request = new CreateRoomRequest(); + + private BugMineClient? Client { get; set; } + + private readonly ProjectInfo _request = new(); + + protected override async Task OnInitializedAsync() { + Client = await BugMineStorage.GetCurrentSessionOrNavigate(); + if (Client == null) { + return; + } + } + + private async Task CreateProject() { + if (Client == null) { + return; + } + + var proj = await Client.CreateProject(_request); + NavigationManager.NavigateTo($"/Projects/{proj.ProjectSlug}/"); + } } \ No newline at end of file diff --git a/BugMine.Web/Pages/Projects/ViewProject.razor b/BugMine.Web/Pages/Projects/ViewProject.razor new file mode 100644 index 0000000..de94c79 --- /dev/null +++ b/BugMine.Web/Pages/Projects/ViewProject.razor @@ -0,0 +1,33 @@ +@page "/Projects/{ProjectSlug}/" + +<ProgressLog ></ProgressLog> + +@if (Client is null) { + <p>Authenticating</p> +} +else if(Project is null) { + <p>Loading</p> +} +else { + <h1>@Project.Info.Name</h1> + @* <p>@Project.Description</p> *@ +} + +@code { + [Parameter] public string ProjectSlug { get; set; } + + private BugMineClient? Client { get; set; } + + private BugMineProject? Project { get; set; } + + protected override async Task OnInitializedAsync() { + Client = await BugMineStorage.GetCurrentSessionOrNavigate(); + if (Client == null) { + return; + } + StateHasChanged(); + + Project = await Client.GetProject(ProjectSlug); + } + +} \ No newline at end of file diff --git a/BugMine.Web/Program.cs b/BugMine.Web/Program.cs index 535f022..c862ee0 100644 --- a/BugMine.Web/Program.cs +++ b/BugMine.Web/Program.cs @@ -6,6 +6,8 @@ using Blazored.SessionStorage; using Microsoft.AspNetCore.Components.Web; using Microsoft.AspNetCore.Components.WebAssembly.Hosting; using BugMine.Web; +using BugMine.Web.Classes; +using LibMatrix.Interfaces.Services; using LibMatrix.Services; var builder = WebAssemblyHostBuilder.CreateDefault(args); @@ -52,5 +54,9 @@ builder.Services.AddBlazoredSessionStorage(config => { config.JsonSerializerOptions.WriteIndented = false; }); -builder.Services.AddRoryLibMatrixServices(); +// builder.Services.AddRoryLibMatrixServices(); +builder.Services.AddBugMine(); +builder.Services.AddScoped<IStorageProvider, LocalStorageProviderService>(); +builder.Services.AddScoped<BugMineStorage>(); + await builder.Build().RunAsync(); \ No newline at end of file diff --git a/BugMine.Web/_Imports.razor b/BugMine.Web/_Imports.razor index a65f12e..a94e865 100644 --- a/BugMine.Web/_Imports.razor +++ b/BugMine.Web/_Imports.razor @@ -8,4 +8,8 @@ @using Microsoft.JSInterop @using BugMine.Web @using BugMine.Web.Layout -@using ArcaneLibs.Blazor.Components \ No newline at end of file +@using BugMine.Web.Components +@using BugMine.Web.Classes +@using ArcaneLibs.Blazor.Components +@inject BugMineStorage BugMineStorage +@inject NavigationManager NavigationManager \ No newline at end of file diff --git a/BugMine.Web/wwwroot/bugmine.webmanifest b/BugMine.Web/wwwroot/bugmine.webmanifest index 27088c0..fd51a9d 100644 --- a/BugMine.Web/wwwroot/bugmine.webmanifest +++ b/BugMine.Web/wwwroot/bugmine.webmanifest @@ -1,7 +1,7 @@ { - "name": "Rory&::MatrixUtils", - "short_name": "RMU", - "description": "A collection of Matrix utilities.", + "name": "BugMine", + "short_name": "BugMine", + "description": "A [Matrix] based, decentralised issue tracker", "icons": [ { "src": "icon-192.png", diff --git a/BugMine.Web/wwwroot/sample-data/weather.json b/BugMine.Web/wwwroot/sample-data/weather.json deleted file mode 100644 index b745973..0000000 --- a/BugMine.Web/wwwroot/sample-data/weather.json +++ /dev/null @@ -1,27 +0,0 @@ -[ - { - "date": "2022-01-06", - "temperatureC": 1, - "summary": "Freezing" - }, - { - "date": "2022-01-07", - "temperatureC": 14, - "summary": "Bracing" - }, - { - "date": "2022-01-08", - "temperatureC": -13, - "summary": "Freezing" - }, - { - "date": "2022-01-09", - "temperatureC": -16, - "summary": "Balmy" - }, - { - "date": "2022-01-10", - "temperatureC": -2, - "summary": "Chilly" - } -] diff --git a/BugMine.sln b/BugMine.sln index c400b62..5ad7b35 100644 --- a/BugMine.sln +++ b/BugMine.sln @@ -14,6 +14,8 @@ EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BugMine.Web", "BugMine.Web\BugMine.Web.csproj", "{20F5268C-C2C9-4820-8046-C2800E5DE119}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BugMine.Sdk", "BugMine.Sdk\BugMine.Sdk.csproj", "{EC02CDCE-8DDD-42AF-80AE-92A15A9B5AC4}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -36,6 +38,10 @@ Global {1CAA2B6D-0365-4C8B-96EE-26026514FEE2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {1CAA2B6D-0365-4C8B-96EE-26026514FEE2}.Release|Any CPU.ActiveCfg = Release|Any CPU {1CAA2B6D-0365-4C8B-96EE-26026514FEE2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EC02CDCE-8DDD-42AF-80AE-92A15A9B5AC4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EC02CDCE-8DDD-42AF-80AE-92A15A9B5AC4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EC02CDCE-8DDD-42AF-80AE-92A15A9B5AC4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EC02CDCE-8DDD-42AF-80AE-92A15A9B5AC4}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {F4E241C3-0300-4B87-8707-BCBDEF1F0185} = {8F4F6BEC-0C66-486B-A21A-1C35B2EDAD33} diff --git a/LibMatrix b/LibMatrix -Subproject 37b97d65c0a5262539a5de560e911048166b8bb +Subproject 440807e02393410327cd86d5ffa007dee98f895 |