Refactor polls

This commit is contained in:
tyrrrz
2026-08-27 19:56:50 +03:00
parent b049fb2919
commit 33c9e77524
9 changed files with 73 additions and 79 deletions
@@ -28,6 +28,9 @@ public partial record Embed(
// but the client can render multiple images in some cases.
public EmbedImage? Image => Images.FirstOrDefault();
public EmbedField? TryGetField(string name) =>
Fields.FirstOrDefault(f => string.Equals(f.Name, name, StringComparison.Ordinal));
public PollResultEmbedProjection? TryGetPollResult() =>
PollResultEmbedProjection.TryResolve(this);
@@ -1,50 +1,37 @@
using System;
using System.Globalization;
using System.Linq;
using DiscordChatExporter.Core.Discord;
using DiscordChatExporter.Core.Discord.Data;
using PowerKit.Extensions;
namespace DiscordChatExporter.Core.Discord.Data.Embeds;
// https://docs.discord.com/developers/resources/message#embed-fields-by-embed-type-poll-result-embed-fields
public partial record PollResultEmbedProjection(
string QuestionText,
int WinningVoteCount,
string Question,
int TotalVoteCount,
int WinningVoteCount,
int? WinningAnswerId,
string? WinningAnswerText,
Emoji? WinningAnswerEmoji
)
{
public double WinningVotePercentage { get; } =
public double WinningVoteShare { get; } =
TotalVoteCount > 0 ? (double)WinningVoteCount / TotalVoteCount : 0;
}
public partial record PollResultEmbedProjection
{
private static string? TryGetFieldValue(Embed embed, string name) =>
embed
.Fields.FirstOrDefault(f => string.Equals(f.Name, name, StringComparison.Ordinal))
?.Value;
private static Emoji? TryParseWinningAnswerEmoji(Embed embed)
{
var name = TryGetFieldValue(embed, "victor_answer_emoji_name");
var name = embed.TryGetField("victor_answer_emoji_name")?.Value;
if (string.IsNullOrWhiteSpace(name))
return null;
var id =
TryGetFieldValue(embed, "victor_answer_emoji_id") is { } idValue
&& Snowflake.TryParse(idValue) is { } parsedId
? parsedId
: (Snowflake?)null;
var id = embed
.TryGetField("victor_answer_emoji_id")
?.Value?.Pipe(v => Snowflake.TryParse(v));
var isAnimated =
bool.TryParse(
TryGetFieldValue(embed, "victor_answer_emoji_animated"),
out var parsedIsAnimated
) && parsedIsAnimated;
embed.TryGetField("victor_answer_emoji_animated")?.Value?.Pipe(bool.ParseOrNull)
?? false;
return new Emoji(id, name, isAnimated);
}
@@ -54,27 +41,31 @@ public partial record PollResultEmbedProjection
if (embed.Kind != EmbedKind.PollResult)
return null;
var questionText = TryGetFieldValue(embed, "poll_question_text") ?? "";
var winningVoteCount = int.ParseOrDefault(
TryGetFieldValue(embed, "victor_answer_votes"),
CultureInfo.InvariantCulture
);
var totalVoteCount = int.ParseOrDefault(
TryGetFieldValue(embed, "total_votes"),
CultureInfo.InvariantCulture
);
var winningAnswerId = int.ParseOrNull(
TryGetFieldValue(embed, "victor_answer_id"),
CultureInfo.InvariantCulture
);
var question = embed.TryGetField("poll_question_text")?.Value ?? "";
var winningAnswerText = TryGetFieldValue(embed, "victor_answer_text");
var totalVoteCount =
embed
.TryGetField("total_votes")
?.Value?.Pipe(v => int.ParseOrNull(v, CultureInfo.InvariantCulture))
?? 0;
var winningVoteCount =
embed
.TryGetField("victor_answer_votes")
?.Value?.Pipe(v => int.ParseOrNull(v, CultureInfo.InvariantCulture))
?? 0;
var winningAnswerId = embed
.TryGetField("victor_answer_id")
?.Value?.Pipe(v => int.ParseOrNull(v, CultureInfo.InvariantCulture));
var winningAnswerText = embed.TryGetField("victor_answer_text")?.Value;
var winningAnswerEmoji = TryParseWinningAnswerEmoji(embed);
return new PollResultEmbedProjection(
questionText,
winningVoteCount,
question,
totalVoteCount,
winningVoteCount,
winningAnswerId,
winningAnswerText,
winningAnswerEmoji
@@ -41,8 +41,9 @@ public partial record Message(
&& Poll is null;
public bool IsSystemNotification { get; } =
Kind is >= MessageKind.RecipientAdd and <= MessageKind.ThreadCreated
|| Kind == MessageKind.PollResult;
Kind
is (>= MessageKind.RecipientAdd and <= MessageKind.ThreadCreated)
or MessageKind.PollResult;
public bool IsReply { get; } = Kind == MessageKind.Reply;
@@ -1,5 +1,4 @@
using System.Text.Json;
using DiscordChatExporter.Core.Discord.Data;
using JsonExtensions.Reading;
using PowerKit.Extensions;
@@ -11,9 +10,14 @@ public record PollAnswer(int Id, string Text, Emoji? Emoji)
public static PollAnswer Parse(JsonElement json)
{
var id = json.GetProperty("answer_id").GetInt32();
var media = json.GetProperty("poll_media");
var text = media.GetPropertyOrNull("text")?.GetStringOrNull() ?? "";
var emoji = media.GetPropertyOrNull("emoji")?.Pipe(Emoji.Parse);
var text =
json.GetPropertyOrNull("poll_media")?.GetPropertyOrNull("text")?.GetStringOrNull()
?? "";
var emoji = json.GetPropertyOrNull("poll_media")
?.GetPropertyOrNull("emoji")
?.Pipe(Emoji.Parse);
return new PollAnswer(id, text, emoji);
}
@@ -4,14 +4,14 @@ using JsonExtensions.Reading;
namespace DiscordChatExporter.Core.Discord.Data.Polls;
// https://discord.com/developers/docs/resources/poll#poll-answer-count-object
public record PollAnswerResult(int AnswerId, int Count, bool DidCurrentUserVote)
public record PollAnswerResult(int Id, int Count, bool DidCurrentUserVote)
{
public static PollAnswerResult Parse(JsonElement json)
{
var answerId = json.GetProperty("id").GetInt32();
var id = json.GetProperty("id").GetInt32();
var count = json.GetProperty("count").GetInt32();
var didCurrentUserVote = json.GetPropertyOrNull("me_voted")?.GetBooleanOrNull() ?? false;
return new PollAnswerResult(answerId, count, didCurrentUserVote);
return new PollAnswerResult(id, count, didCurrentUserVote);
}
}
@@ -13,7 +13,7 @@ public record PollResults(bool IsFinalized, IReadOnlyList<PollAnswerResult> Answ
public int WinningVoteCount { get; } = Answers.Select(a => a.Count).DefaultIfEmpty().Max();
public PollAnswerResult? TryGetAnswerResult(int answerId) =>
Answers.FirstOrDefault(a => a.AnswerId == answerId);
Answers.FirstOrDefault(a => a.Id == answerId);
public static PollResults Parse(JsonElement json)
{
@@ -22,13 +22,13 @@ internal class ContainsMessageFilter(string text) : MessageFilter
public override bool IsMatch(Message message) =>
IsMatch(message.Content)
|| IsMatch(message.Poll?.Question)
|| message.Poll?.Answers.Any(a => IsMatch(a.Text)) == true
|| message.Embeds.Any(e =>
IsMatch(e.Title)
|| IsMatch(e.Author?.Name)
|| IsMatch(e.Description)
|| IsMatch(e.Footer?.Text)
|| e.Fields.Any(f => IsMatch(f.Name) || IsMatch(f.Value))
);
)
|| IsMatch(message.Poll?.Question)
|| message.Poll?.Answers.Any(a => IsMatch(a.Text)) == true;
}
@@ -138,17 +138,17 @@
}
else if (message.Kind == MessageKind.PollResult && pollResult is not null)
{
@if (!string.IsNullOrWhiteSpace(pollResult.QuestionText))
@if (!string.IsNullOrWhiteSpace(pollResult.Question))
{
<span>'s poll </span>
@if (message.Reference?.MessageId is not null)
{
<a class="chatlog__system-notification-link" href="#chatlog__message-container-@message.Reference.MessageId">@pollResult.QuestionText</a>
<a class="chatlog__system-notification-link" href="#chatlog__message-container-@message.Reference.MessageId">@pollResult.Question</a>
}
else
{
<span class="chatlog__system-notification-link">@pollResult.QuestionText</span>
<span class="chatlog__system-notification-link">@pollResult.Question</span>
}
<span> has closed.</span>
@@ -175,8 +175,6 @@
@if (message.Kind == MessageKind.PollResult && pollResult is not null)
{
var hasWinningAnswer = pollResult.WinningAnswerId is not null;
<div class="chatlog__poll-result">
<div class="chatlog__poll-result-answer">
@if (pollResult.WinningAnswerEmoji is not null)
@@ -188,19 +186,19 @@
<div class="chatlog__poll-result-answer-info">
<div class="chatlog__poll-result-answer-text">
@(hasWinningAnswer ? pollResult.WinningAnswerText : "Poll closed")
@(pollResult.WinningAnswerId is not null ? pollResult.WinningAnswerText : "Poll closed")
@if (hasWinningAnswer)
@if (pollResult.WinningAnswerId is not null)
{
<span class="chatlog__poll-result-winner" title="Winning answer"><span aria-hidden="true">✓</span><span class="chatlog__poll-accessible-label">Winning answer</span></span>
}
</div>
<div class="chatlog__poll-result-answer-details">
@if (hasWinningAnswer)
@if (pollResult.WinningAnswerId is not null)
{
<span>Winning answer</span>
<span aria-hidden="true"> • </span>
<span>@pollResult.WinningVotePercentage.ToString("P0", Context.Request.CultureInfo)</span>
<span>@pollResult.WinningVoteShare.ToString("P0", Context.Request.CultureInfo)</span>
}
else
{
@@ -490,22 +488,19 @@
<div class="chatlog__poll-answers">
@foreach (var answer in poll.Answers)
{
var pollResults = poll.Results;
var answerResult = pollResults?.TryGetAnswerResult(answer.Id);
var isSelected = answerResult?.DidCurrentUserVote == true;
var isWinning =
pollResults is { IsFinalized: true, WinningVoteCount: > 0 }
&& answerResult?.Count == pollResults.WinningVoteCount;
var isActiveSelection = pollResults?.IsFinalized != true && isSelected;
var answerVotePercentage = pollResults is { TotalVoteCount: > 0 }
? (double)(answerResult?.Count ?? 0) / pollResults.TotalVoteCount
: 0;
var answerVotePercentageCss =
answerVotePercentage
.ToString("P2", CultureInfo.InvariantCulture)
.Replace(" ", "");
var answerResult = poll.Results?.TryGetAnswerResult(answer.Id);
<div class="chatlog__poll-answer @(isWinning ? "chatlog__poll-answer--winning" : isActiveSelection ? "chatlog__poll-answer--selected" : null)" style="--poll-answer-fill: @answerVotePercentageCss">
var isSelected = answerResult?.DidCurrentUserVote == true;
var isWinning =
poll.Results is { IsFinalized: true, WinningVoteCount: > 0 }
&& answerResult?.Count >= poll.Results.WinningVoteCount;
var answerVoteShare = poll.Results?.TotalVoteCount > 0
? (double)(answerResult?.Count ?? 0) / poll.Results.TotalVoteCount
: 0;
<div class="chatlog__poll-answer @(isWinning ? "chatlog__poll-answer--winning" : poll.Results?.IsFinalized != true && isSelected ? "chatlog__poll-answer--selected" : null)" style="--poll-answer-fill: @answerVoteShare.ToString("P2", CultureInfo.InvariantCulture).Replace(" ", "")">
<div class="chatlog__poll-answer-content">
@if (answer.Emoji is not null)
{
@@ -515,12 +510,12 @@
</div>
<div class="chatlog__poll-answer-details">
@if (pollResults is not null)
@if (poll.Results is not null)
{
var answerVoteCount = answerResult?.Count ?? 0;
<span class="chatlog__poll-answer-count">@answerVoteCount.ToString("N0", Context.Request.CultureInfo) @(answerVoteCount == 1 ? "vote" : "votes")</span>
<span class="chatlog__poll-answer-percentage">@answerVotePercentage.ToString("P0", Context.Request.CultureInfo)</span>
<span class="chatlog__poll-answer-percentage">@answerVoteShare.ToString("P0", Context.Request.CultureInfo)</span>
}
@if (isSelected)
@@ -42,9 +42,9 @@ internal static class PlainTextMessageExtensions
.WhereNotNull()
.FirstOrDefault()
is { } pollResult
? string.IsNullOrWhiteSpace(pollResult.QuestionText)
? string.IsNullOrWhiteSpace(pollResult.Question)
? $"{message.Author.DisplayName}'s poll has closed."
: $"{message.Author.DisplayName}'s poll {pollResult.QuestionText} has closed."
: $"{message.Author.DisplayName}'s poll {pollResult.Question} has closed."
: "A poll has closed.",
_ => message.Content,