[backend/razor] Add polls to public preview notes

This commit is contained in:
pancakes 2025-01-07 17:38:38 +10:00 committed by Lilian
parent 5f99f1d055
commit 66f3b23e46
No known key found for this signature in database
4 changed files with 110 additions and 3 deletions

View file

@ -28,13 +28,15 @@ public class NoteRenderer(
var emoji = await GetEmojiAsync(allNotes); var emoji = await GetEmojiAsync(allNotes);
var users = await GetUsersAsync(allNotes); var users = await GetUsersAsync(allNotes);
var attachments = await GetAttachmentsAsync(allNotes); var attachments = await GetAttachmentsAsync(allNotes);
var polls = await GetPollsAsync(allNotes);
return await RenderAsync(note, users, mentions, emoji, attachments); return await RenderAsync(note, users, mentions, emoji, attachments, polls);
} }
private async Task<PreviewNote> RenderAsync( private async Task<PreviewNote> RenderAsync(
Note note, List<PreviewUser> users, Dictionary<string, List<Note.MentionedUser>> mentions, Note note, List<PreviewUser> users, Dictionary<string, List<Note.MentionedUser>> mentions,
Dictionary<string, List<Emoji>> emoji, Dictionary<string, List<PreviewAttachment>?> attachments Dictionary<string, List<Emoji>> emoji, Dictionary<string, List<PreviewAttachment>?> attachments,
Dictionary<string, PreviewPoll> polls
) )
{ {
var renderedText = await mfm.RenderAsync(note.Text, note.User.Host, mentions[note.Id], emoji[note.Id], "span", attachments[note.Id]); var renderedText = await mfm.RenderAsync(note.Text, note.User.Host, mentions[note.Id], emoji[note.Id], "span", attachments[note.Id]);
@ -49,6 +51,7 @@ public class NoteRenderer(
QuoteUrl = note.Renote?.Url ?? note.Renote?.Uri ?? note.Renote?.GetPublicUriOrNull(instance.Value), QuoteUrl = note.Renote?.Url ?? note.Renote?.Uri ?? note.Renote?.GetPublicUriOrNull(instance.Value),
QuoteInaccessible = note.Renote?.VisibilityIsPublicOrHome == false, QuoteInaccessible = note.Renote?.VisibilityIsPublicOrHome == false,
Attachments = attachments[note.Id]?.Where(p => !inlineMediaUrls.Contains(p.Url)).ToList(), Attachments = attachments[note.Id]?.Where(p => !inlineMediaUrls.Contains(p.Url)).ToList(),
Poll = polls.GetValueOrDefault(note.Id),
CreatedAt = note.CreatedAt.ToDisplayStringTz(), CreatedAt = note.CreatedAt.ToDisplayStringTz(),
UpdatedAt = note.UpdatedAt?.ToDisplayStringTz() UpdatedAt = note.UpdatedAt?.ToDisplayStringTz()
}; };
@ -112,6 +115,24 @@ public class NoteRenderer(
.ToList()); .ToList());
} }
private async Task<Dictionary<string, PreviewPoll>> GetPollsAsync(List<Note> notes)
{
if (notes is []) return new Dictionary<string, PreviewPoll>();
var ids = notes.Select(p => p.Id).ToList();
var polls = await db.Polls
.Where(p => ids.Contains(p.NoteId))
.ToListAsync();
return polls.ToDictionary(p => p.NoteId,
p => new PreviewPoll
{
ExpiresAt = p.ExpiresAt,
Multiple = p.Multiple,
Choices = p.Choices.Zip(p.Votes).Select(c => (c.First, c.Second)).ToList(),
VotersCount = p.VotersCount
});
}
public async Task<List<PreviewNote>> RenderManyAsync(List<Note> notes) public async Task<List<PreviewNote>> RenderManyAsync(List<Note> notes)
{ {
if (notes is []) return []; if (notes is []) return [];
@ -120,7 +141,8 @@ public class NoteRenderer(
var mentions = await GetMentionsAsync(allNotes); var mentions = await GetMentionsAsync(allNotes);
var emoji = await GetEmojiAsync(allNotes); var emoji = await GetEmojiAsync(allNotes);
var attachments = await GetAttachmentsAsync(allNotes); var attachments = await GetAttachmentsAsync(allNotes);
return await notes.Select(p => RenderAsync(p, users, mentions, emoji, attachments)) var polls = await GetPollsAsync(allNotes);
return await notes.Select(p => RenderAsync(p, users, mentions, emoji, attachments, polls))
.AwaitAllAsync() .AwaitAllAsync()
.ToListAsync(); .ToListAsync();
} }

View file

@ -11,6 +11,7 @@ public class PreviewNote
public required string? QuoteUrl; public required string? QuoteUrl;
public required bool QuoteInaccessible; public required bool QuoteInaccessible;
public required List<PreviewAttachment>? Attachments; public required List<PreviewAttachment>? Attachments;
public required PreviewPoll? Poll;
public required string CreatedAt; public required string CreatedAt;
public required string? UpdatedAt; public required string? UpdatedAt;
} }
@ -23,3 +24,11 @@ public class PreviewAttachment
public required string? Alt; public required string? Alt;
public required bool Sensitive; public required bool Sensitive;
} }
public class PreviewPoll
{
public required DateTime? ExpiresAt { get; set; }
public required bool Multiple { get; set; }
public required List<(string Value, int Votes)> Choices { get; set; }
public required int? VotersCount { get; set; }
}

View file

@ -30,6 +30,33 @@ else
</p> </p>
} }
if (_note.Poll != null)
{
var total = _note.Poll.Choices.Sum(p => p.Votes);
<div class="poll-results">
@foreach (var choice in _note.Poll.Choices)
{
var percentage = Math.Floor(choice.Votes / (double)total * 100);
<span class="poll-result" style="--percentage: @percentage%;">
<span class="poll-value">@choice.Value</span>
<span class="poll-info"><span class="vote-count">@choice.Votes votes</span><span class="vote-percentage">@percentage%</span></span>
</span>
}
</div>
List<string> footerText = [];
@if (_note.Poll.Multiple)
footerText.Add("Multiple choice");
@if (_note.Poll.VotersCount != null)
footerText.Add($"{_note.Poll.VotersCount} users voted");
@if (_note.Poll.ExpiresAt != null)
footerText.Add(_note.Poll.ExpiresAt <= DateTime.UtcNow ? "Ended" : $"Ends at {_note.Poll.ExpiresAt.Value.ToLocalTime():G}");
<span>@string.Join(" - ", footerText)</span>
}
if (!ShowMedia && _note.Attachments != null) if (!ShowMedia && _note.Attachments != null)
{ {
<p> <p>

View file

@ -0,0 +1,49 @@
.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;
padding: 0.2rem 0.5rem;
border-radius: 0.5rem;
background: linear-gradient(to right, var(--selection) var(--percentage), var(--background) var(--percentage), var(--background));
}
.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;
}
}