diff --git a/DiscordChatExporter.Cli/Verbs/Options/ExportOptions.cs b/DiscordChatExporter.Cli/Verbs/Options/ExportOptions.cs index cd121b1..a95978a 100644 --- a/DiscordChatExporter.Cli/Verbs/Options/ExportOptions.cs +++ b/DiscordChatExporter.Cli/Verbs/Options/ExportOptions.cs @@ -12,9 +12,11 @@ namespace DiscordChatExporter.Cli.Verbs.Options [Option('o', "output", Default = null, HelpText = "Output file or directory path.")] public string OutputPath { get; set; } + // HACK: CommandLineParser doesn't support DateTimeOffset [Option("after", Default = null, HelpText = "Limit to messages sent after this date.")] public DateTime? After { get; set; } + // HACK: CommandLineParser doesn't support DateTimeOffset [Option("before", Default = null, HelpText = "Limit to messages sent before this date.")] public DateTime? Before { get; set; } diff --git a/DiscordChatExporter.Core.Models/ChatLog.cs b/DiscordChatExporter.Core.Models/ChatLog.cs index d8dc2ca..5d18a2c 100644 --- a/DiscordChatExporter.Core.Models/ChatLog.cs +++ b/DiscordChatExporter.Core.Models/ChatLog.cs @@ -9,21 +9,21 @@ namespace DiscordChatExporter.Core.Models public Channel Channel { get; } - public DateTime? From { get; } + public DateTimeOffset? After { get; } - public DateTime? To { get; } + public DateTimeOffset? Before { get; } public IReadOnlyList Messages { get; } public Mentionables Mentionables { get; } - public ChatLog(Guild guild, Channel channel, DateTime? from, DateTime? to, + public ChatLog(Guild guild, Channel channel, DateTimeOffset? after, DateTimeOffset? before, IReadOnlyList messages, Mentionables mentionables) { Guild = guild; Channel = channel; - From = from; - To = to; + After = after; + Before = before; Messages = messages; Mentionables = mentionables; } diff --git a/DiscordChatExporter.Core.Models/Embed.cs b/DiscordChatExporter.Core.Models/Embed.cs index 340f8d6..5246ccd 100644 --- a/DiscordChatExporter.Core.Models/Embed.cs +++ b/DiscordChatExporter.Core.Models/Embed.cs @@ -12,7 +12,7 @@ namespace DiscordChatExporter.Core.Models public string Url { get; } - public DateTime? Timestamp { get; } + public DateTimeOffset? Timestamp { get; } public Color Color { get; } @@ -28,7 +28,7 @@ namespace DiscordChatExporter.Core.Models public EmbedFooter Footer { get; } - public Embed(string title, string url, DateTime? timestamp, Color color, EmbedAuthor author, string description, + public Embed(string title, string url, DateTimeOffset? timestamp, Color color, EmbedAuthor author, string description, IReadOnlyList fields, EmbedImage thumbnail, EmbedImage image, EmbedFooter footer) { Title = title; diff --git a/DiscordChatExporter.Core.Models/Message.cs b/DiscordChatExporter.Core.Models/Message.cs index 538090b..5b35b0d 100644 --- a/DiscordChatExporter.Core.Models/Message.cs +++ b/DiscordChatExporter.Core.Models/Message.cs @@ -15,9 +15,9 @@ namespace DiscordChatExporter.Core.Models public User Author { get; } - public DateTime Timestamp { get; } + public DateTimeOffset Timestamp { get; } - public DateTime? EditedTimestamp { get; } + public DateTimeOffset? EditedTimestamp { get; } public string Content { get; } @@ -29,8 +29,8 @@ namespace DiscordChatExporter.Core.Models public IReadOnlyList MentionedUsers { get; } - public Message(string id, string channelId, MessageType type, User author, DateTime timestamp, - DateTime? editedTimestamp, string content, IReadOnlyList attachments, + public Message(string id, string channelId, MessageType type, User author, DateTimeOffset timestamp, + DateTimeOffset? editedTimestamp, string content, IReadOnlyList attachments, IReadOnlyList embeds, IReadOnlyList reactions, IReadOnlyList mentionedUsers) { Id = id; diff --git a/DiscordChatExporter.Core.Rendering/CsvChatLogRenderer.cs b/DiscordChatExporter.Core.Rendering/CsvChatLogRenderer.cs index b8afe18..96f6e6c 100644 --- a/DiscordChatExporter.Core.Rendering/CsvChatLogRenderer.cs +++ b/DiscordChatExporter.Core.Rendering/CsvChatLogRenderer.cs @@ -22,7 +22,8 @@ namespace DiscordChatExporter.Core.Rendering _dateFormat = dateFormat; } - private string FormatDate(DateTime date) => date.ToString(_dateFormat, CultureInfo.InvariantCulture); + private string FormatDate(DateTimeOffset date) => + date.ToLocalTime().ToString(_dateFormat, CultureInfo.InvariantCulture); private string FormatMarkdown(Node node) { diff --git a/DiscordChatExporter.Core.Rendering/HtmlChatLogRenderer.MessageGroup.cs b/DiscordChatExporter.Core.Rendering/HtmlChatLogRenderer.MessageGroup.cs index e560955..2809af3 100644 --- a/DiscordChatExporter.Core.Rendering/HtmlChatLogRenderer.MessageGroup.cs +++ b/DiscordChatExporter.Core.Rendering/HtmlChatLogRenderer.MessageGroup.cs @@ -10,11 +10,11 @@ namespace DiscordChatExporter.Core.Rendering { public User Author { get; } - public DateTime Timestamp { get; } + public DateTimeOffset Timestamp { get; } public IReadOnlyList Messages { get; } - public MessageGroup(User author, DateTime timestamp, IReadOnlyList messages) + public MessageGroup(User author, DateTimeOffset timestamp, IReadOnlyList messages) { Author = author; Timestamp = timestamp; diff --git a/DiscordChatExporter.Core.Rendering/HtmlChatLogRenderer.cs b/DiscordChatExporter.Core.Rendering/HtmlChatLogRenderer.cs index 530b825..a2daa02 100644 --- a/DiscordChatExporter.Core.Rendering/HtmlChatLogRenderer.cs +++ b/DiscordChatExporter.Core.Rendering/HtmlChatLogRenderer.cs @@ -29,7 +29,8 @@ namespace DiscordChatExporter.Core.Rendering private string HtmlEncode(string s) => WebUtility.HtmlEncode(s); - private string FormatDate(DateTime date) => date.ToString(_dateFormat, CultureInfo.InvariantCulture); + private string FormatDate(DateTimeOffset date) => + date.ToLocalTime().ToString(_dateFormat, CultureInfo.InvariantCulture); private IEnumerable GroupMessages(IEnumerable messages) => messages.GroupContiguous((buffer, message) => @@ -182,7 +183,7 @@ namespace DiscordChatExporter.Core.Rendering var model = new ScriptObject(); model.SetValue("Model", _chatLog, true); model.Import(nameof(GroupMessages), new Func, IEnumerable>(GroupMessages)); - model.Import(nameof(FormatDate), new Func(FormatDate)); + model.Import(nameof(FormatDate), new Func(FormatDate)); model.Import(nameof(FormatMarkdown), new Func(FormatMarkdown)); context.PushGlobal(model); diff --git a/DiscordChatExporter.Core.Rendering/PlainTextChatLogRenderer.cs b/DiscordChatExporter.Core.Rendering/PlainTextChatLogRenderer.cs index 31b6a3b..1f6837f 100644 --- a/DiscordChatExporter.Core.Rendering/PlainTextChatLogRenderer.cs +++ b/DiscordChatExporter.Core.Rendering/PlainTextChatLogRenderer.cs @@ -22,21 +22,22 @@ namespace DiscordChatExporter.Core.Rendering _dateFormat = dateFormat; } - private string FormatDate(DateTime date) => date.ToString(_dateFormat, CultureInfo.InvariantCulture); + private string FormatDate(DateTimeOffset date) => + date.ToLocalTime().ToString(_dateFormat, CultureInfo.InvariantCulture); - private string FormatDateRange(DateTime? from, DateTime? to) + private string FormatDateRange(DateTimeOffset? after, DateTimeOffset? before) { - // Both 'from' and 'to' - if (from.HasValue && to.HasValue) - return $"{FormatDate(from.Value)} to {FormatDate(to.Value)}"; + // Both 'after' and 'before' + if (after != null && before != null) + return $"{FormatDate(after.Value)} to {FormatDate(before.Value)}"; - // Just 'from' - if (from.HasValue) - return $"after {FormatDate(from.Value)}"; + // Just 'after' + if (after != null) + return $"after {FormatDate(after.Value)}"; - // Just 'to' - if (to.HasValue) - return $"before {FormatDate(to.Value)}"; + // Just 'before' + if (before != null) + return $"before {FormatDate(before.Value)}"; // Neither return null; @@ -113,7 +114,7 @@ namespace DiscordChatExporter.Core.Rendering await writer.WriteLineAsync($"Channel: {_chatLog.Channel.Name}"); await writer.WriteLineAsync($"Topic: {_chatLog.Channel.Topic}"); await writer.WriteLineAsync($"Messages: {_chatLog.Messages.Count:N0}"); - await writer.WriteLineAsync($"Range: {FormatDateRange(_chatLog.From, _chatLog.To)}"); + await writer.WriteLineAsync($"Range: {FormatDateRange(_chatLog.After, _chatLog.Before)}"); await writer.WriteLineAsync('='.Repeat(62)); await writer.WriteLineAsync(); diff --git a/DiscordChatExporter.Core.Rendering/Resources/HtmlShared.html b/DiscordChatExporter.Core.Rendering/Resources/HtmlShared.html index a2a3fc5..f5b33b5 100644 --- a/DiscordChatExporter.Core.Rendering/Resources/HtmlShared.html +++ b/DiscordChatExporter.Core.Rendering/Resources/HtmlShared.html @@ -43,14 +43,14 @@
{{ Model.Messages | array.size | object.format "N0" }} messages
- {{~ if Model.From || Model.To ~}} + {{~ if Model.After || Model.Before ~}}
- {{~ if Model.From && Model.To ~}} - Between {{ Model.From | FormatDate | html.escape }} and {{ Model.To | FormatDate | html.escape }} - {{~ else if Model.From ~}} - After {{ Model.From | FormatDate | html.escape }} - {{~ else if Model.To ~}} - Before {{ Model.To | FormatDate | html.escape }} + {{~ if Model.After && Model.Before ~}} + Between {{ Model.After | FormatDate | html.escape }} and {{ Model.Before | FormatDate | html.escape }} + {{~ else if Model.After ~}} + After {{ Model.After | FormatDate | html.escape }} + {{~ else if Model.Before ~}} + Before {{ Model.Before | FormatDate | html.escape }} {{~ end ~}}
{{~ end ~}} diff --git a/DiscordChatExporter.Core.Services/DataService.Parsers.cs b/DiscordChatExporter.Core.Services/DataService.Parsers.cs index 67e9aba..5b4cac4 100644 --- a/DiscordChatExporter.Core.Services/DataService.Parsers.cs +++ b/DiscordChatExporter.Core.Services/DataService.Parsers.cs @@ -117,7 +117,7 @@ namespace DiscordChatExporter.Core.Services var title = json["title"]?.Value(); var description = json["description"]?.Value(); var url = json["url"]?.Value(); - var timestamp = json["timestamp"]?.Value(); + var timestamp = json["timestamp"]?.Value(); // Get color var color = json["color"] != null @@ -164,8 +164,8 @@ namespace DiscordChatExporter.Core.Services // Get basic data var id = json["id"].Value(); var channelId = json["channel_id"].Value(); - var timestamp = json["timestamp"].Value(); - var editedTimestamp = json["edited_timestamp"]?.Value(); + var timestamp = json["timestamp"].Value(); + var editedTimestamp = json["edited_timestamp"]?.Value(); var content = json["content"].Value(); var type = (MessageType) json["type"].Value(); diff --git a/DiscordChatExporter.Core.Services/DataService.cs b/DiscordChatExporter.Core.Services/DataService.cs index 76bb571..cec862e 100644 --- a/DiscordChatExporter.Core.Services/DataService.cs +++ b/DiscordChatExporter.Core.Services/DataService.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Net.Http; using System.Net.Http.Headers; @@ -8,6 +9,7 @@ using DiscordChatExporter.Core.Models; using DiscordChatExporter.Core.Services.Exceptions; using DiscordChatExporter.Core.Services.Internal; using Failsafe; +using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Tyrrrz.Extensions; @@ -64,7 +66,12 @@ namespace DiscordChatExporter.Core.Services var raw = await response.Content.ReadAsStringAsync(); // Parse - return JToken.Parse(raw); + using (var reader = new JsonTextReader(new StringReader(raw))) + { + reader.DateParseHandling = DateParseHandling.DateTimeOffset; + + return JToken.Load(reader); + } } } }); @@ -123,24 +130,24 @@ namespace DiscordChatExporter.Core.Services } public async Task> GetChannelMessagesAsync(AuthToken token, string channelId, - DateTime? from = null, DateTime? to = null, IProgress progress = null) + DateTimeOffset? after = null, DateTimeOffset? before = null, IProgress progress = null) { var result = new List(); // Get the last message var response = await GetApiResponseAsync(token, "channels", $"{channelId}/messages", - "limit=1", $"before={to?.ToSnowflake()}"); + "limit=1", $"before={before?.ToSnowflake()}"); var lastMessage = response.Select(ParseMessage).FirstOrDefault(); // If the last message doesn't exist or it's outside of range - return - if (lastMessage == null || lastMessage.Timestamp < from) + if (lastMessage == null || lastMessage.Timestamp < after) { progress?.Report(1); return result; } // Get other messages - var offsetId = from?.ToSnowflake() ?? "0"; + var offsetId = after?.ToSnowflake() ?? "0"; while (true) { // Get message batch @@ -215,19 +222,19 @@ namespace DiscordChatExporter.Core.Services } public async Task GetChatLogAsync(AuthToken token, Guild guild, Channel channel, - DateTime? from = null, DateTime? to = null, IProgress progress = null) + DateTimeOffset? after = null, DateTimeOffset? before = null, IProgress progress = null) { // Get messages - var messages = await GetChannelMessagesAsync(token, channel.Id, from, to, progress); + var messages = await GetChannelMessagesAsync(token, channel.Id, after, before, progress); // Get mentionables var mentionables = await GetMentionablesAsync(token, guild.Id, messages); - return new ChatLog(guild, channel, from, to, messages, mentionables); + return new ChatLog(guild, channel, after, before, messages, mentionables); } public async Task GetChatLogAsync(AuthToken token, Channel channel, - DateTime? from = null, DateTime? to = null, IProgress progress = null) + DateTimeOffset? after = null, DateTimeOffset? before = null, IProgress progress = null) { // Get guild var guild = channel.GuildId == Guild.DirectMessages.Id @@ -235,17 +242,17 @@ namespace DiscordChatExporter.Core.Services : await GetGuildAsync(token, channel.GuildId); // Get the chat log - return await GetChatLogAsync(token, guild, channel, from, to, progress); + return await GetChatLogAsync(token, guild, channel, after, before, progress); } public async Task GetChatLogAsync(AuthToken token, string channelId, - DateTime? from = null, DateTime? to = null, IProgress progress = null) + DateTimeOffset? after = null, DateTimeOffset? before = null, IProgress progress = null) { // Get channel var channel = await GetChannelAsync(token, channelId); // Get the chat log - return await GetChatLogAsync(token, channel, from, to, progress); + return await GetChatLogAsync(token, channel, after, before, progress); } public void Dispose() diff --git a/DiscordChatExporter.Core.Services/ExportService.cs b/DiscordChatExporter.Core.Services/ExportService.cs index 320f84d..85d544d 100644 --- a/DiscordChatExporter.Core.Services/ExportService.cs +++ b/DiscordChatExporter.Core.Services/ExportService.cs @@ -58,7 +58,7 @@ namespace DiscordChatExporter.Core.Services { // Create partitions by grouping up to X contiguous messages into separate chat logs var partitions = chatLog.Messages.GroupContiguous(g => g.Count < partitionLimit.Value) - .Select(g => new ChatLog(chatLog.Guild, chatLog.Channel, chatLog.From, chatLog.To, g, chatLog.Mentionables)) + .Select(g => new ChatLog(chatLog.Guild, chatLog.Channel, chatLog.After, chatLog.Before, g, chatLog.Mentionables)) .ToArray(); // Split file path into components diff --git a/DiscordChatExporter.Core.Services/Helpers/ExportHelper.cs b/DiscordChatExporter.Core.Services/Helpers/ExportHelper.cs index e4896d1..9ea3bbb 100644 --- a/DiscordChatExporter.Core.Services/Helpers/ExportHelper.cs +++ b/DiscordChatExporter.Core.Services/Helpers/ExportHelper.cs @@ -14,7 +14,7 @@ namespace DiscordChatExporter.Core.Services.Helpers Path.GetExtension(path) == null; public static string GetDefaultExportFileName(ExportFormat format, Guild guild, Channel channel, - DateTime? from = null, DateTime? to = null) + DateTimeOffset? after = null, DateTimeOffset? before = null) { var result = new StringBuilder(); @@ -22,24 +22,24 @@ namespace DiscordChatExporter.Core.Services.Helpers result.Append($"{guild.Name} - {channel.Name} [{channel.Id}]"); // Append date range - if (from != null || to != null) + if (after != null || before != null) { result.Append(" ("); - // Both 'from' and 'to' are set - if (from != null && to != null) + // Both 'after' and 'before' are set + if (after != null && before != null) { - result.Append($"{from:yyyy-MM-dd} to {to:yyyy-MM-dd}"); + result.Append($"{after:yyyy-MM-dd} to {before:yyyy-MM-dd}"); } - // Only 'from' is set - else if (from != null) + // Only 'after' is set + else if (after != null) { - result.Append($"after {from:yyyy-MM-dd}"); + result.Append($"after {after:yyyy-MM-dd}"); } - // Only 'to' is set + // Only 'before' is set else { - result.Append($"before {to:yyyy-MM-dd}"); + result.Append($"before {before:yyyy-MM-dd}"); } result.Append(")"); diff --git a/DiscordChatExporter.Core.Services/Internal/Extensions.cs b/DiscordChatExporter.Core.Services/Internal/Extensions.cs index 80b86fe..c722bc6 100644 --- a/DiscordChatExporter.Core.Services/Internal/Extensions.cs +++ b/DiscordChatExporter.Core.Services/Internal/Extensions.cs @@ -5,10 +5,10 @@ namespace DiscordChatExporter.Core.Services.Internal { internal static class Extensions { - public static string ToSnowflake(this DateTime dateTime) + public static string ToSnowflake(this DateTimeOffset date) { const long epoch = 62135596800000; - var unixTime = dateTime.ToUniversalTime().Ticks / TimeSpan.TicksPerMillisecond - epoch; + var unixTime = date.ToUniversalTime().Ticks / TimeSpan.TicksPerMillisecond - epoch; var value = ((ulong) unixTime - 1420070400000UL) << 22; return value.ToString(); } diff --git a/DiscordChatExporter.Gui/Converters/DateTimeOffsetToDateTimeConverter.cs b/DiscordChatExporter.Gui/Converters/DateTimeOffsetToDateTimeConverter.cs new file mode 100644 index 0000000..d5b9ef3 --- /dev/null +++ b/DiscordChatExporter.Gui/Converters/DateTimeOffsetToDateTimeConverter.cs @@ -0,0 +1,28 @@ +using System; +using System.Globalization; +using System.Windows.Data; + +namespace DiscordChatExporter.Gui.Converters +{ + [ValueConversion(typeof(DateTimeOffset), typeof(DateTime))] + public class DateTimeOffsetToDateTimeConverter : IValueConverter + { + public static DateTimeOffsetToDateTimeConverter Instance { get; } = new DateTimeOffsetToDateTimeConverter(); + + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value is DateTimeOffset date) + return date.DateTime; + + return null; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value is DateTime date) + return new DateTimeOffset(date); + + return null; + } + } +} \ No newline at end of file diff --git a/DiscordChatExporter.Gui/Converters/ExportFormatToStringConverter.cs b/DiscordChatExporter.Gui/Converters/ExportFormatToStringConverter.cs index 6a5ddbb..9309cfd 100644 --- a/DiscordChatExporter.Gui/Converters/ExportFormatToStringConverter.cs +++ b/DiscordChatExporter.Gui/Converters/ExportFormatToStringConverter.cs @@ -12,8 +12,10 @@ namespace DiscordChatExporter.Gui.Converters public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { - var format = value as ExportFormat?; - return format?.GetDisplayName(); + if (value is ExportFormat format) + return format.GetDisplayName(); + + return null; } public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) diff --git a/DiscordChatExporter.Gui/DiscordChatExporter.Gui.csproj b/DiscordChatExporter.Gui/DiscordChatExporter.Gui.csproj index 859cd28..805d860 100644 --- a/DiscordChatExporter.Gui/DiscordChatExporter.Gui.csproj +++ b/DiscordChatExporter.Gui/DiscordChatExporter.Gui.csproj @@ -59,6 +59,7 @@ + diff --git a/DiscordChatExporter.Gui/ViewModels/Dialogs/ExportSetupViewModel.cs b/DiscordChatExporter.Gui/ViewModels/Dialogs/ExportSetupViewModel.cs index c8441d4..4858d49 100644 --- a/DiscordChatExporter.Gui/ViewModels/Dialogs/ExportSetupViewModel.cs +++ b/DiscordChatExporter.Gui/ViewModels/Dialogs/ExportSetupViewModel.cs @@ -27,9 +27,9 @@ namespace DiscordChatExporter.Gui.ViewModels.Dialogs public ExportFormat SelectedFormat { get; set; } = ExportFormat.HtmlDark; - public DateTime? From { get; set; } + public DateTimeOffset? After { get; set; } - public DateTime? To { get; set; } + public DateTimeOffset? Before { get; set; } public int? PartitionLimit { get; set; } @@ -54,11 +54,11 @@ namespace DiscordChatExporter.Gui.ViewModels.Dialogs _settingsService.LastExportFormat = SelectedFormat; _settingsService.LastPartitionLimit = PartitionLimit; - // Clamp 'from' and 'to' values - if (From > To) - From = To; - if (To < From) - To = From; + // Clamp 'after' and 'before' values + if (After > Before) + After = Before; + if (Before < After) + Before = After; // If single channel - prompt file path if (IsSingleChannel) @@ -67,7 +67,7 @@ namespace DiscordChatExporter.Gui.ViewModels.Dialogs var channel = Channels.Single(); // Generate default file name - var defaultFileName = ExportHelper.GetDefaultExportFileName(SelectedFormat, Guild, channel, From, To); + var defaultFileName = ExportHelper.GetDefaultExportFileName(SelectedFormat, Guild, channel, After, Before); // Generate filter var ext = SelectedFormat.GetFileExtension(); diff --git a/DiscordChatExporter.Gui/ViewModels/RootViewModel.cs b/DiscordChatExporter.Gui/ViewModels/RootViewModel.cs index 52c37fa..e5b2408 100644 --- a/DiscordChatExporter.Gui/ViewModels/RootViewModel.cs +++ b/DiscordChatExporter.Gui/ViewModels/RootViewModel.cs @@ -267,7 +267,7 @@ namespace DiscordChatExporter.Gui.ViewModels { // Generate default file name var fileName = ExportHelper.GetDefaultExportFileName(dialog.SelectedFormat, dialog.Guild, - channel, dialog.From, dialog.To); + channel, dialog.After, dialog.Before); // Combine paths filePath = Path.Combine(filePath, fileName); @@ -275,7 +275,7 @@ namespace DiscordChatExporter.Gui.ViewModels // Get chat log var chatLog = await _dataService.GetChatLogAsync(token, dialog.Guild, channel, - dialog.From, dialog.To, operation); + dialog.After, dialog.Before, operation); // Export await _exportService.ExportChatLogAsync(chatLog, filePath, dialog.SelectedFormat, diff --git a/DiscordChatExporter.Gui/Views/Dialogs/ExportSetupView.xaml b/DiscordChatExporter.Gui/Views/Dialogs/ExportSetupView.xaml index 848dedf..57a8df7 100644 --- a/DiscordChatExporter.Gui/Views/Dialogs/ExportSetupView.xaml +++ b/DiscordChatExporter.Gui/Views/Dialogs/ExportSetupView.xaml @@ -86,8 +86,8 @@ Margin="16,8" materialDesign:HintAssist.Hint="From (optional)" materialDesign:HintAssist.IsFloating="True" - DisplayDateEnd="{Binding To}" - SelectedDate="{Binding From}" + DisplayDateEnd="{Binding Before, Converter={x:Static converters:DateTimeOffsetToDateTimeConverter.Instance}}" + SelectedDate="{Binding After, Converter={x:Static converters:DateTimeOffsetToDateTimeConverter.Instance}}" ToolTip="If this is set, only messages sent after this date will be exported" />