From 288c549f205777f5d3ebac7a0924155f78ba4122 Mon Sep 17 00:00:00 2001 From: pancakes Date: Mon, 30 Dec 2024 22:11:13 +1000 Subject: [PATCH] [frontend/components] Add polls to notes and allow voting --- .../Components/Note/NoteBody.razor | 6 +- .../Components/Note/NotePoll.razor | 92 +++++++++++++++++++ .../Components/Note/NotePoll.razor.css | 85 +++++++++++++++++ .../ControllerModels/NoteControllerModel.cs | 4 + Iceshrimp.Frontend/wwwroot/css/app.css | 6 +- Iceshrimp.Shared/Schemas/Web/NoteResponse.cs | 10 +- 6 files changed, 195 insertions(+), 8 deletions(-) create mode 100644 Iceshrimp.Frontend/Components/Note/NotePoll.razor create mode 100644 Iceshrimp.Frontend/Components/Note/NotePoll.razor.css diff --git a/Iceshrimp.Frontend/Components/Note/NoteBody.razor b/Iceshrimp.Frontend/Components/Note/NoteBody.razor index 33965996..93da0ff2 100644 --- a/Iceshrimp.Frontend/Components/Note/NoteBody.razor +++ b/Iceshrimp.Frontend/Components/Note/NoteBody.razor @@ -50,6 +50,10 @@ } + @if (NoteBase.Poll != null) + { + + } @if (NoteBase.Attachments.Count > 0) { @@ -144,4 +148,4 @@ { Controller?.Unregister(this); } -} \ No newline at end of file +} diff --git a/Iceshrimp.Frontend/Components/Note/NotePoll.razor b/Iceshrimp.Frontend/Components/Note/NotePoll.razor new file mode 100644 index 00000000..7acd205d --- /dev/null +++ b/Iceshrimp.Frontend/Components/Note/NotePoll.razor @@ -0,0 +1,92 @@ +@using Iceshrimp.Frontend.Core.Services +@using Iceshrimp.Frontend.Localization +@using Iceshrimp.Shared.Schemas.Web +@using Microsoft.Extensions.Localization +@inject ApiService Api; +@inject IStringLocalizer Loc; + +
+ @if (CanVote()) + { +
+ @foreach (var (choice, i) in Poll.Choices.Select((c, i) => (c, i))) + { + + @if (Poll.Multiple) + { + + } + else + { + + } + + + } + +
+ } + else + { +
+ @{ var total = Poll.Choices.Sum(p => p.Votes); } + @foreach (var choice in Poll.Choices) + { + + + + + @Loc["{0} votes", choice.Votes]@(choice.Votes / total * 100)% + + } +
+ } + @{ + List footerText = []; + @if (Poll.Multiple) + footerText.Add(Loc["Multiple choice"]); + @if (Poll.VotersCount != null) + footerText.Add(Loc["{0} users voted", Poll.VotersCount]); + @if (Poll.ExpiresAt != null) + footerText.Add(Poll.ExpiresAt <= DateTime.UtcNow ? Loc["Ended"] : Loc["Ends at {0}", Poll.ExpiresAt.Value.ToLocalTime().ToString("G")]); + } + @string.Join(" - ", footerText) +
+ +@code { + [Parameter, EditorRequired] public required NotePollSchema Poll { get; set; } + [Parameter, EditorRequired] public required List Emoji { get; set; } + private List Choices { get; set; } = []; + + private bool CanVote() + { + return (Poll.Multiple && !Poll.Choices.All(p => p.Voted) || !Poll.Choices.Any(p => p.Voted)) && (Poll.ExpiresAt == null || Poll.ExpiresAt > DateTime.UtcNow); + } + + private void SelectMultiple(int choice) + { + if (Choices.Contains(choice)) + Choices.RemoveAll(p => p == choice); + else + Choices.Add(choice); + } + + private void SelectSingle(int choice) + { + Choices = [choice]; + } + + private async Task Vote() + { + if (!CanVote()) return; + + var res = await Api.Notes.AddPollVoteAsync(Poll.NoteId, Choices); + if (res != null) + { + Poll = res; + StateHasChanged(); + } + } +} \ No newline at end of file diff --git a/Iceshrimp.Frontend/Components/Note/NotePoll.razor.css b/Iceshrimp.Frontend/Components/Note/NotePoll.razor.css new file mode 100644 index 00000000..af43d55b --- /dev/null +++ b/Iceshrimp.Frontend/Components/Note/NotePoll.razor.css @@ -0,0 +1,85 @@ +.poll-options { + display: flex; + flex-direction: column; + gap: 0.5rem; + width: 100%; + margin-top: 0.5em; +} + +.poll-option input { + display: none; +} + +.poll-option label { + display: inline-block; + width: 100%; + padding: 0.2rem 0.5rem; + border-radius: 0.5rem; + background-color: var(--highlight-color); + text-wrap: wrap; + word-break: break-word; + cursor: pointer; +} + +.poll-option label:hover { + background-color: var(--hover-color); +} + +.poll-option input:checked ~ label { + background: var(--accent-color); +} + +.poll-results { + display: flex; + flex-direction: column; + gap: 0.5rem; + width: 100%; + margin-top: 0.5em; +} + +.poll-result { + --percentage: 0%; + display: flex; + align-items: center; + gap: 0.25rem; + width: 100%; + padding: 0.2rem 0.5rem; + border-radius: 0.5rem; + background: linear-gradient(to right, var(--notice-color) var(--percentage), var(--highlight-color) var(--percentage), var(--highlight-color)); +} + +.poll-result.voted { + background: linear-gradient(to right, var(--accent-primary-color), var(--accent-secondary-color) var(--percentage), var(--highlight-color) var(--percentage), var(--highlight-color)); +} + +.poll-value { + flex-grow: 1; + text-wrap: wrap; + word-break: break-word; +} + +.poll-info { + flex-shrink: 0; +} + +.vote-count, .vote-percentage { + display: inline-block; + vertical-align: middle; + text-wrap: nowrap; +} + +.vote-count { + margin-right: 0.5rem; + font-size: 0.7em; +} + +@media (max-width: 768px) { + .poll-result { + flex-direction: column; + align-items: start; + } + + .vote-count { + font-size: 1em; + } +} \ No newline at end of file diff --git a/Iceshrimp.Frontend/Core/ControllerModels/NoteControllerModel.cs b/Iceshrimp.Frontend/Core/ControllerModels/NoteControllerModel.cs index cdf6f010..c5e3ddb6 100644 --- a/Iceshrimp.Frontend/Core/ControllerModels/NoteControllerModel.cs +++ b/Iceshrimp.Frontend/Core/ControllerModels/NoteControllerModel.cs @@ -71,6 +71,10 @@ internal class NoteControllerModel(ApiClient api) public Task RefetchNoteAsync(string id) => api.CallNullableAsync(HttpMethod.Get, $"/notes/{id}/refetch"); + public Task AddPollVoteAsync(string id, List choices) => + api.CallNullableAsync(HttpMethod.Post, $"/notes/{id}/vote", + data: new NotePollRequest { Choices = choices }); + public Task MuteNoteAsync(string id) => api.CallAsync(HttpMethod.Post, $"/notes/{id}/mute"); } \ No newline at end of file diff --git a/Iceshrimp.Frontend/wwwroot/css/app.css b/Iceshrimp.Frontend/wwwroot/css/app.css index c5e87de6..4ab76333 100644 --- a/Iceshrimp.Frontend/wwwroot/css/app.css +++ b/Iceshrimp.Frontend/wwwroot/css/app.css @@ -17,7 +17,9 @@ --foreground-color: #17151e; --highlight-color: #2e2b4c; --hover-color: #3d3a66; - --accent-color: linear-gradient(to right, #9A92FF, #8372F5); + --accent-primary-color: #9A92FF; + --accent-secondary-color: #8372F5; + --accent-color: linear-gradient(to right, var(--accent-primary-color), var(--accent-secondary-color)); --notice-color: #92c1ff; --font-color: #e7edff; --link: #9E9EFF; @@ -330,4 +332,4 @@ pre:has(code){ --height: 0px; min-height: var(--height); } -} \ No newline at end of file +} diff --git a/Iceshrimp.Shared/Schemas/Web/NoteResponse.cs b/Iceshrimp.Shared/Schemas/Web/NoteResponse.cs index 9a2191ac..7557fea7 100644 --- a/Iceshrimp.Shared/Schemas/Web/NoteResponse.cs +++ b/Iceshrimp.Shared/Schemas/Web/NoteResponse.cs @@ -61,11 +61,11 @@ public class NoteAttachment public class NotePollSchema { - [JI] public required string NoteId; - public required DateTime? ExpiresAt { get; set; } - public required bool Multiple { get; set; } - public required List Choices { get; set; } - public required int? VotersCount { get; set; } + public required string NoteId { get; set; } + public required DateTime? ExpiresAt { get; set; } + public required bool Multiple { get; set; } + public required List Choices { get; set; } + public required int? VotersCount { get; set; } } public class NotePollChoice