GUI: double-clicking a child-less server auto-opens export setup

Co-authored-by: Tyrrrz <1935960+Tyrrrz@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot] 2026-02-27 16:15:04 +00:00
parent 6879fca6fd
commit c2ee6f047d
9 changed files with 78 additions and 25 deletions

View file

@ -180,7 +180,7 @@ public partial record Message
// Currently Discord only supports 1 snapshot per forward
var forwardedMessage = json.GetPropertyOrNull("message_snapshots")
?.EnumerateArrayOrNull()
?.Select(j => j.GetPropertyOrNull("message"))
?.Select(j => j.GetPropertyOrNull("message"))
.WhereNotNull()
.Select(MessageSnapshot.Parse)
.FirstOrDefault();

View file

@ -9,14 +9,15 @@ public record MessageReference(
MessageReferenceKind Kind,
Snowflake? MessageId,
Snowflake? ChannelId,
Snowflake? GuildId)
Snowflake? GuildId
)
{
public static MessageReference Parse(JsonElement json)
{
var kind =
json.GetPropertyOrNull("type")?.GetInt32OrNull()?.Pipe(t => (MessageReferenceKind)t)
?? MessageReferenceKind.Default;
var messageId = json.GetPropertyOrNull("message_id")
?.GetNonWhiteSpaceStringOrNull()
?.Pipe(Snowflake.Parse);

View file

@ -15,7 +15,8 @@ public record MessageSnapshot(
string Content,
IReadOnlyList<Attachment> Attachments,
IReadOnlyList<Embed> Embeds,
IReadOnlyList<Sticker> Stickers)
IReadOnlyList<Sticker> Stickers
)
{
public static MessageSnapshot Parse(JsonElement json)
{
@ -23,37 +24,35 @@ public record MessageSnapshot(
json.GetPropertyOrNull("timestamp")?.GetDateTimeOffsetOrNull()
?? DateTimeOffset.MinValue;
var editedTimestamp = json
.GetPropertyOrNull("edited_timestamp")
?.GetDateTimeOffsetOrNull();
var editedTimestamp = json.GetPropertyOrNull("edited_timestamp")?.GetDateTimeOffsetOrNull();
var content = json.GetPropertyOrNull("content")?.GetStringOrNull() ?? "";
var attachments =
json
.GetPropertyOrNull("attachments")
json.GetPropertyOrNull("attachments")
?.EnumerateArrayOrNull()
?.Select(Attachment.Parse)
.ToArray()
?? [];
var embeds =
json
.GetPropertyOrNull("embeds")
?.EnumerateArrayOrNull()
?.Select(Embed.Parse)
.ToArray()
json.GetPropertyOrNull("embeds")?.EnumerateArrayOrNull()?.Select(Embed.Parse).ToArray()
?? [];
var stickers =
json
.GetPropertyOrNull("sticker_items")
json.GetPropertyOrNull("sticker_items")
?.EnumerateArrayOrNull()
?.Select(Sticker.Parse)
.ToArray()
?? [];
return new MessageSnapshot(timestamp,
editedTimestamp, content, attachments, embeds, stickers);
return new MessageSnapshot(
timestamp,
editedTimestamp,
content,
attachments,
embeds,
stickers
);
}
}

View file

@ -352,7 +352,7 @@ internal class JsonMessageWriter(Stream stream, ExportContext context)
_writer.WriteEndObject();
await _writer.FlushAsync(cancellationToken);
}
private async ValueTask WriteStickerAsync(
Sticker sticker,
CancellationToken cancellationToken = default
@ -553,14 +553,14 @@ internal class JsonMessageWriter(Stream stream, ExportContext context)
_writer.WriteString(
"timestamp",
Context.NormalizeDate(message.ForwardedMessage.Timestamp)
Context.NormalizeDate(message.ForwardedMessage.Timestamp)
);
_writer.WriteString(
"timestampEdited",
message.ForwardedMessage.EditedTimestamp?.Pipe(Context.NormalizeDate)
);
_writer.WriteString(
"content",
await FormatMarkdownAsync(message.ForwardedMessage.Content, cancellationToken)

View file

@ -239,9 +239,9 @@ internal class PlainTextMessageWriter(Stream stream, ExportContext context)
);
}
await _writer.WriteLineAsync(
$"Originally sent: {Context.FormatDate(forwardedMessage.Timestamp)}"
);
await _writer.WriteLineAsync(
$"Originally sent: {Context.FormatDate(forwardedMessage.Timestamp)}"
);
await WriteAttachmentsAsync(forwardedMessage.Attachments, cancellationToken);
await WriteEmbedsAsync(forwardedMessage.Embeds, cancellationToken);

View file

@ -24,7 +24,7 @@ public static class CollectionExtensions
}
}
}
extension<T>(IEnumerable<T?> source)
where T : struct
{

View file

@ -223,6 +223,54 @@ public partial class DashboardViewModel : ViewModelBase
}
}
[RelayCommand]
private async Task ShowSetupIfSingleChannelAsync()
{
// Wait for any in-progress channel loading to complete
if (IsBusy)
{
var tcs = new TaskCompletionSource();
void Handler(object? _, System.ComponentModel.PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(IsBusy) && !IsBusy)
tcs.TrySetResult();
}
PropertyChanged += Handler;
// Re-check after subscribing to avoid missing the transition
if (!IsBusy)
{
PropertyChanged -= Handler;
}
else
{
try
{
await tcs.Task;
}
finally
{
PropertyChanged -= Handler;
}
}
}
// Only auto-export when there is exactly one child-less (non-category) channel
if (
AvailableChannels is not { Count: 1 }
|| AvailableChannels[0].Children.Count != 0
|| AvailableChannels[0].Channel.IsCategory
)
return;
SelectedChannels.Clear();
SelectedChannels.Add(AvailableChannels[0]);
await ExportAsync();
}
private bool CanExport() =>
!IsBusy && _discord is not null && SelectedGuild is not null && SelectedChannels.Any();

View file

@ -99,6 +99,7 @@
BorderThickness="0,0,1,0"
Grid.Column="0">
<ListBox
DoubleTapped="AvailableGuildsListBox_OnDoubleTapped"
ItemsSource="{Binding AvailableGuilds}"
ScrollViewer.VerticalScrollBarVisibility="Hidden"
SelectedItem="{Binding SelectedGuild}"

View file

@ -1,5 +1,6 @@
using System.Linq;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity;
using DiscordChatExporter.Core.Discord.Data;
using DiscordChatExporter.Gui.Framework;
@ -22,6 +23,9 @@ public partial class DashboardView : UserControl<DashboardViewModel>
SelectionChangedEventArgs args
) => DataContext.PullChannelsCommand.Execute(null);
private void AvailableGuildsListBox_OnDoubleTapped(object? sender, TappedEventArgs args) =>
DataContext.ShowSetupIfSingleChannelCommand.Execute(null);
private void AvailableChannelsTreeView_OnSelectionChanged(
object? sender,
SelectionChangedEventArgs args