Fixing outlook attachments, re-using compose page and some additional fixes on the mime headers for outlook.

This commit is contained in:
Burak Kaan Köse
2026-02-07 13:10:57 +01:00
parent 1ec8d5bbf2
commit d28de50ec6
10 changed files with 234 additions and 145 deletions
@@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using Microsoft.Graph.Models; using Microsoft.Graph.Models;
@@ -138,37 +139,38 @@ public static class OutlookIntegratorExtensions
var bccAddresses = GetRecipients(mime.Bcc).ToList(); var bccAddresses = GetRecipients(mime.Bcc).ToList();
var replyToAddresses = GetRecipients(mime.ReplyTo).ToList(); var replyToAddresses = GetRecipients(mime.ReplyTo).ToList();
// Prefer HTML body, fall back to plain text.
var (bodyContent, bodyType) = mime.HtmlBody != null
? (mime.HtmlBody, BodyType.Html)
: (mime.TextBody ?? string.Empty, BodyType.Text);
var message = new Message() var message = new Message()
{ {
Subject = mime.Subject, Subject = mime.Subject,
Importance = GetImportance(mime.Importance), Importance = GetImportance(mime.Importance),
Body = new ItemBody() { ContentType = BodyType.Html, Content = mime.HtmlBody }, Body = new ItemBody() { ContentType = bodyType, Content = bodyContent },
IsDraft = false, IsDraft = false,
IsRead = true, // Sent messages are always read. IsRead = true,
ToRecipients = toAddresses, ToRecipients = toAddresses,
CcRecipients = ccAddresses, CcRecipients = ccAddresses,
BccRecipients = bccAddresses, BccRecipients = bccAddresses,
From = fromAddress, From = fromAddress,
InternetMessageId = mime.MessageId, InternetMessageId = mime.MessageId,
ReplyTo = replyToAddresses, ReplyTo = replyToAddresses,
Attachments = []
}; };
// Set ConversationId if provided to maintain threading
if (!string.IsNullOrEmpty(conversationId)) if (!string.IsNullOrEmpty(conversationId))
{ {
message.ConversationId = conversationId; message.ConversationId = conversationId;
} }
// Headers are only included when creating the draft. // Headers are only included when creating the draft.
// When sending, they are not included. Graph will throw an error. // Graph API throws an error if headers are included in send/patch operations.
if (includeInternetHeaders) if (includeInternetHeaders)
{ {
message.InternetMessageHeaders = GetHeaderList(mime); message.InternetMessageHeaders = GetHeaderList(mime);
} }
return message; return message;
} }
@@ -367,6 +369,40 @@ public static class OutlookIntegratorExtensions
#region Mime to Outlook Message Helpers #region Mime to Outlook Message Helpers
/// <summary>
/// Extracts all attachments (inline and regular) from a MimeMessage
/// and returns them as Graph SDK FileAttachment objects.
/// </summary>
public static List<FileAttachment> ExtractAttachments(this MimeMessage mime)
{
var attachments = new List<FileAttachment>();
foreach (var part in mime.BodyParts)
{
bool isInline = part.ContentDisposition?.Disposition == "inline";
if (!part.IsAttachment && !isInline)
continue;
if (part is not MimePart mimePart || mimePart.Content == null)
continue;
using var memory = new MemoryStream();
mimePart.Content.DecodeTo(memory);
attachments.Add(new FileAttachment()
{
Name = part.ContentDisposition?.FileName ?? part.ContentType.Name,
ContentBytes = memory.ToArray(),
ContentType = part.ContentType.MimeType,
ContentId = part.ContentId,
IsInline = isInline
});
}
return attachments;
}
private static IEnumerable<Recipient> GetRecipients(this InternetAddressList internetAddresses) private static IEnumerable<Recipient> GetRecipients(this InternetAddressList internetAddresses)
{ {
foreach (var address in internetAddresses) foreach (var address in internetAddresses)
@@ -399,46 +435,46 @@ public static class OutlookIntegratorExtensions
private static List<InternetMessageHeader> GetHeaderList(this MimeMessage mime) private static List<InternetMessageHeader> GetHeaderList(this MimeMessage mime)
{ {
// Graph API only allows max of 5 headers. // Graph API only allows max of 5 headers.
// Here we'll try to ignore some headers that are not neccessary. // Prioritize threading headers to keep reply grouping intact.
// Outlook API will generate them automatically.
// Some headers also require to start with X- or x-.
const int headerLimit = 5;
string[] headersToIgnore = ["Date", "To", "Cc", "Bcc", "MIME-Version", "From", "Subject", "Message-Id"]; string[] headersToIgnore = ["Date", "To", "Cc", "Bcc", "MIME-Version", "From", "Subject", "Message-Id"];
string[] headersToModify = ["In-Reply-To", "Reply-To", "References", "Thread-Topic"];
var headers = new List<InternetMessageHeader>(); var headers = new List<InternetMessageHeader>();
// PRIORITY: Always include WinoLocalDraftHeader first if it exists void AddHeader(string name, string value)
// This is critical for draft mapping functionality
var winoDraftHeader = mime.Headers.FirstOrDefault(h => h.Field == Domain.Constants.WinoLocalDraftHeader);
int includedHeaderCount = 0;
if (winoDraftHeader != null)
{ {
var headerValue = winoDraftHeader.Value.Length >= 995 ? winoDraftHeader.Value.Substring(0, 995) : winoDraftHeader.Value; if (string.IsNullOrWhiteSpace(name) || string.IsNullOrWhiteSpace(value)) return;
headers.Add(new InternetMessageHeader() { Name = winoDraftHeader.Field, Value = headerValue }); if (headers.Count >= headerLimit) return;
includedHeaderCount++; if (headers.Any(h => string.Equals(h.Name, name, StringComparison.OrdinalIgnoreCase))) return;
// No header value should exceed 995 characters.
var headerValue = value.Length >= 995 ? value.Substring(0, 995) : value;
headers.Add(new InternetMessageHeader() { Name = name, Value = headerValue });
} }
// Include other headers up to the limit (excluding the already added WinoLocalDraftHeader) // PRIORITY: Always include WinoLocalDraftHeader first if it exists.
var winoDraftHeader = mime.Headers.FirstOrDefault(h => h.Field == Domain.Constants.WinoLocalDraftHeader);
if (winoDraftHeader != null)
AddHeader(winoDraftHeader.Field, winoDraftHeader.Value);
// Threading headers must be preserved with their real RFC names.
AddHeader("In-Reply-To", mime.Headers[HeaderId.InReplyTo]);
AddHeader("References", mime.Headers[HeaderId.References]);
// Fill remaining slots with custom headers only (avoid Graph restrictions).
foreach (var header in mime.Headers) foreach (var header in mime.Headers)
{ {
if (header.Field == Domain.Constants.WinoLocalDraftHeader) if (headers.Count >= headerLimit) break;
continue; // Already processed above if (header.Field == Domain.Constants.WinoLocalDraftHeader) continue;
if (headersToIgnore.Contains(header.Field)) continue;
if (string.Equals(header.Field, "In-Reply-To", StringComparison.OrdinalIgnoreCase)) continue;
if (string.Equals(header.Field, "References", StringComparison.OrdinalIgnoreCase)) continue;
if (!headersToIgnore.Contains(header.Field)) // Only include custom headers beyond the core threading ones.
{ if (!header.Field.StartsWith("X-", StringComparison.OrdinalIgnoreCase)) continue;
var headerName = headersToModify.Contains(header.Field) ? $"X-{header.Field}" : header.Field;
// No header value should exceed 995 characters. AddHeader(header.Field, header.Value);
var headerValue = header.Value.Length >= 995 ? header.Value.Substring(0, 995) : header.Value;
headers.Add(new InternetMessageHeader() { Name = headerName, Value = headerValue });
includedHeaderCount++;
}
if (includedHeaderCount >= 5) break;
} }
return headers; return headers;
-24
View File
@@ -1,24 +0,0 @@
using System.Text.Json.Serialization;
namespace Wino.Core.Misc;
public class OutlookFileAttachment
{
[JsonPropertyName("@odata.type")]
public string OdataType { get; } = "#microsoft.graph.fileAttachment";
[JsonPropertyName("name")]
public string FileName { get; set; }
[JsonPropertyName("contentBytes")]
public string Base64EncodedContentBytes { get; set; }
[JsonPropertyName("contentType")]
public string ContentType { get; set; }
[JsonPropertyName("contentId")]
public string ContentId { get; set; }
[JsonPropertyName("isInline")]
public bool IsInline { get; set; }
}
+20 -75
View File
@@ -36,7 +36,6 @@ using Wino.Core.Domain.Models.Synchronization;
using Wino.Core.Extensions; using Wino.Core.Extensions;
using Wino.Core.Http; using Wino.Core.Http;
using Wino.Core.Integration.Processors; using Wino.Core.Integration.Processors;
using Wino.Core.Misc;
using Wino.Core.Requests.Bundles; using Wino.Core.Requests.Bundles;
using Wino.Core.Requests.Calendar; using Wino.Core.Requests.Calendar;
using Wino.Core.Requests.Folder; using Wino.Core.Requests.Folder;
@@ -45,7 +44,6 @@ using Wino.Core.Requests.Mail;
namespace Wino.Core.Synchronizers.Mail; namespace Wino.Core.Synchronizers.Mail;
[JsonSerializable(typeof(Microsoft.Graph.Me.Messages.Item.Move.MovePostRequestBody))] [JsonSerializable(typeof(Microsoft.Graph.Me.Messages.Item.Move.MovePostRequestBody))]
[JsonSerializable(typeof(OutlookFileAttachment))]
public partial class OutlookSynchronizerJsonContext : JsonSerializerContext; public partial class OutlookSynchronizerJsonContext : JsonSerializerContext;
/// <summary> /// <summary>
@@ -1382,96 +1380,43 @@ public class OutlookSynchronizer : WinoSynchronizer<RequestInformation, Message,
public override List<IRequestBundle<RequestInformation>> SendDraft(SendDraftRequest request) public override List<IRequestBundle<RequestInformation>> SendDraft(SendDraftRequest request)
{ {
var sendDraftPreparationRequest = request.Request; var sendDraftPreparationRequest = request.Request;
// 1. Delete draft
// 2. Create new Message with new MIME.
// 3. Make sure that conversation id is tagged correctly for replies.
var mailCopyId = sendDraftPreparationRequest.MailItem.Id; var mailCopyId = sendDraftPreparationRequest.MailItem.Id;
var mimeMessage = sendDraftPreparationRequest.Mime; var mimeMessage = sendDraftPreparationRequest.Mime;
// Convert mime message to Outlook message. // Graph API ignores the From header in direct MIME uploads, so we must convert
// Outlook synchronizer does not send MIME messages directly anymore. // to a JSON Message object to properly support sending from aliases.
// Alias support is lacking with direct MIMEs.
// Therefore we convert the MIME message to Outlook message and use proper APIs.
// Pass the ConversationId (ThreadId) to maintain threading for replies/forwards
var conversationId = sendDraftPreparationRequest.MailItem.ThreadId; var conversationId = sendDraftPreparationRequest.MailItem.ThreadId;
var outlookMessage = mimeMessage.AsOutlookMessage(false, conversationId); var outlookMessage = mimeMessage.AsOutlookMessage(false, conversationId);
// Create attachment requests. // Build the request sequence: upload attachments -> patch draft -> send.
// TODO: We need to support large file attachments with sessioned upload at some point. // These execute serially via batch DependsOn (see ConfigureSerialExecution).
var attachmentBundles = CreateAttachmentUploadBundles(mimeMessage, mailCopyId);
var attachmentRequestList = CreateAttachmentUploadBundles(mimeMessage, mailCopyId, request).ToList();
// Update draft.
var patchDraftRequest = _graphClient.Me.Messages[mailCopyId].ToPatchRequestInformation(outlookMessage); var patchDraftRequest = _graphClient.Me.Messages[mailCopyId].ToPatchRequestInformation(outlookMessage);
var patchDraftRequestBundle = new HttpRequestBundle<RequestInformation>(patchDraftRequest, request); var patchDraftBundle = new HttpRequestBundle<RequestInformation>(patchDraftRequest, request);
// Send draft. var sendRequest = PreparePostRequestInformation(_graphClient.Me.Messages[mailCopyId].Send.ToPostRequestInformation());
var sendBundle = new HttpRequestBundle<RequestInformation>(sendRequest, request);
var sendDraftRequest = PreparePostRequestInformation(_graphClient.Me.Messages[mailCopyId].Send.ToPostRequestInformation()); return [.. attachmentBundles, patchDraftBundle, sendBundle];
var sendDraftRequestBundle = new HttpRequestBundle<RequestInformation>(sendDraftRequest, request);
return [.. attachmentRequestList, patchDraftRequestBundle, sendDraftRequestBundle];
} }
private List<IRequestBundle<RequestInformation>> CreateAttachmentUploadBundles(MimeMessage mime, string mailCopyId, IRequestBase sourceRequest) /// <summary>
/// Extracts attachments from the MIME message and creates individual
/// Graph API upload requests using the SDK's FileAttachment type.
/// </summary>
private List<IRequestBundle<RequestInformation>> CreateAttachmentUploadBundles(MimeMessage mime, string mailCopyId)
{ {
var allAttachments = new List<OutlookFileAttachment>(); var attachments = mime.ExtractAttachments();
var bundles = new List<IRequestBundle<RequestInformation>>(attachments.Count);
foreach (var part in mime.BodyParts) foreach (var attachment in attachments)
{ {
var isAttachmentOrInline = part.IsAttachment ? true : part.ContentDisposition?.Disposition == "inline"; var uploadRequest = _graphClient.Me.Messages[mailCopyId].Attachments.ToPostRequestInformation(attachment);
bundles.Add(new HttpRequestBundle<RequestInformation>(uploadRequest, null));
if (!isAttachmentOrInline) continue;
using var memory = new MemoryStream();
((MimePart)part).Content.DecodeTo(memory);
var base64String = Convert.ToBase64String(memory.ToArray());
var attachment = new OutlookFileAttachment()
{
Base64EncodedContentBytes = base64String,
FileName = part.ContentDisposition?.FileName ?? part.ContentType.Name,
ContentId = part.ContentId,
ContentType = part.ContentType.MimeType,
IsInline = part.ContentDisposition?.Disposition == "inline"
};
allAttachments.Add(attachment);
} }
static RequestInformation PrepareUploadAttachmentRequest(RequestInformation requestInformation, OutlookFileAttachment outlookFileAttachment) return bundles;
{
requestInformation.Headers.Clear();
string contentJson = JsonSerializer.Serialize(outlookFileAttachment, OutlookSynchronizerJsonContext.Default.OutlookFileAttachment);
requestInformation.Content = new MemoryStream(Encoding.UTF8.GetBytes(contentJson));
requestInformation.HttpMethod = Method.POST;
requestInformation.Headers.Add("Content-Type", "application/json");
return requestInformation;
}
var retList = new List<IRequestBundle<RequestInformation>>();
// Prepare attachment upload requests.
foreach (var attachment in allAttachments)
{
var emptyPostRequest = _graphClient.Me.Messages[mailCopyId].Attachments.ToPostRequestInformation(new Attachment());
var modifiedAttachmentUploadRequest = PrepareUploadAttachmentRequest(emptyPostRequest, attachment);
var bundle = new HttpRequestBundle<RequestInformation>(modifiedAttachmentUploadRequest, null);
retList.Add(bundle);
}
return retList;
} }
public override List<IRequestBundle<RequestInformation>> Archive(BatchArchiveRequest request) public override List<IRequestBundle<RequestInformation>> Archive(BatchArchiveRequest request)
+34 -1
View File
@@ -21,11 +21,13 @@ using Wino.Core.Domain.Models.Navigation;
using Wino.Core.Extensions; using Wino.Core.Extensions;
using Wino.Core.Services; using Wino.Core.Services;
using Wino.Mail.ViewModels.Data; using Wino.Mail.ViewModels.Data;
using Wino.Mail.ViewModels.Messages;
using Wino.Messaging.Client.Mails; using Wino.Messaging.Client.Mails;
namespace Wino.Mail.ViewModels; namespace Wino.Mail.ViewModels;
public partial class ComposePageViewModel : MailBaseViewModel public partial class ComposePageViewModel : MailBaseViewModel,
IRecipient<NewComposeDraftItemRequestedEvent>
{ {
public Func<Task<string>> GetHTMLBodyFunction; public Func<Task<string>> GetHTMLBodyFunction;
@@ -432,6 +434,35 @@ public partial class ComposePageViewModel : MailBaseViewModel
} }
} }
public async void Receive(NewComposeDraftItemRequestedEvent message)
{
// Save current draft before switching.
await UpdateMimeChangesAsync();
// Reset state for the new draft.
isUpdatingMimeBlocked = false;
ComposingAccount = null;
IncludedAttachments.Clear();
// Set the new draft item and prepare it.
CurrentMailDraftItem = message.MailItemViewModel;
await TryPrepareComposeAsync(true);
}
protected override void RegisterRecipients()
{
base.RegisterRecipients();
Messenger.Register<NewComposeDraftItemRequestedEvent>(this);
}
protected override void UnregisterRecipients()
{
base.UnregisterRecipients();
Messenger.Unregister<NewComposeDraftItemRequestedEvent>(this);
}
private async Task<bool> InitializeComposerAccountAsync() private async Task<bool> InitializeComposerAccountAsync()
{ {
if (CurrentMailDraftItem == null) return false; if (CurrentMailDraftItem == null) return false;
@@ -550,6 +581,8 @@ public partial class ComposePageViewModel : MailBaseViewModel
{ {
if (CurrentMimeMessage == null) return; if (CurrentMimeMessage == null) return;
IncludedAttachments.Clear();
foreach (var attachment in CurrentMimeMessage.Attachments) foreach (var attachment in CurrentMimeMessage.Attachments)
{ {
if (attachment.IsAttachment && attachment is MimePart attachmentPart) if (attachment.IsAttachment && attachment is MimePart attachmentPart)
@@ -635,6 +635,56 @@ public partial class MailListPageViewModel : MailBaseViewModel,
if (isFromDraftOrSentFolder) if (isFromDraftOrSentFolder)
{ {
// Fix for draft duplication: When a draft is created for reply/forward, it's first added as local draft.
// Then the server sync fetches it back. We should skip adding remote drafts if a local draft already exists
// with the same ThreadId. The mapping system (DraftMapped) will handle updating the existing local draft.
if (addedMail.IsDraft && !addedMail.IsLocalDraft && !string.IsNullOrEmpty(addedMail.ThreadId))
{
// Check if collection already has a local draft with the same ThreadId in the same folder
bool hasLocalDraftInSameThread = false;
foreach (var group in MailCollection.MailItems)
{
foreach (var item in group)
{
if (item is MailItemViewModel mailItem)
{
if (mailItem.IsDraft &&
mailItem.MailCopy.IsLocalDraft &&
mailItem.MailCopy.ThreadId == addedMail.ThreadId &&
mailItem.MailCopy.FolderId == addedMail.FolderId)
{
hasLocalDraftInSameThread = true;
break;
}
}
else if (item is ThreadMailItemViewModel threadItem)
{
foreach (var threadEmail in threadItem.ThreadEmails)
{
if (threadEmail.IsDraft &&
threadEmail.MailCopy.IsLocalDraft &&
threadEmail.MailCopy.ThreadId == addedMail.ThreadId &&
threadEmail.MailCopy.FolderId == addedMail.FolderId)
{
hasLocalDraftInSameThread = true;
break;
}
}
if (hasLocalDraftInSameThread) break;
}
}
if (hasLocalDraftInSameThread) break;
}
if (hasLocalDraftInSameThread)
{
// Local draft exists in the same thread - skip adding remote duplicate
// The mapping system will update the local draft with remote IDs when DraftMapped message is received
return;
}
}
// Only add if the ThreadId exists in the collection (can be threaded with existing items) // Only add if the ThreadId exists in the collection (can be threaded with existing items)
if (!ThreadIdExistsInCollection(addedMail)) return; if (!ThreadIdExistsInCollection(addedMail)) return;
} }
@@ -757,6 +807,17 @@ public partial class MailListPageViewModel : MailBaseViewModel,
} }
} }
protected override void OnDraftMapped(string localDraftCopyId, string remoteDraftCopyId)
{
base.OnDraftMapped(localDraftCopyId, remoteDraftCopyId);
// When a draft is mapped from local to remote, the database has been updated
// but the UI collection still references the MailCopy object with old IDs.
// The MailCollection.AddAsync method checks UniqueId (which doesn't change during mapping)
// so if mapping worked correctly, no duplicate should appear.
// This method is here for future enhancements if additional UI updates are needed.
}
private async Task<List<MailItemViewModel>> PrepareMailViewModelsAsync(IEnumerable<MailCopy> mailItems, CancellationToken cancellationToken = default) private async Task<List<MailItemViewModel>> PrepareMailViewModelsAsync(IEnumerable<MailCopy> mailItems, CancellationToken cancellationToken = default)
{ {
// Run ViewModel creation on background thread to avoid blocking UI // Run ViewModel creation on background thread to avoid blocking UI
@@ -0,0 +1,10 @@
using Wino.Mail.ViewModels.Data;
namespace Wino.Mail.ViewModels.Messages;
/// <summary>
/// When the compose page is already active, but a different draft item is selected.
/// To not trigger navigation again and re-use existing WebView2 editor.
/// </summary>
/// <param name="MailItemViewModel">The new draft mail item to compose.</param>
public record NewComposeDraftItemRequestedEvent(MailItemViewModel MailItemViewModel);
@@ -185,6 +185,15 @@ public class NavigationService : NavigationServiceBase, INavigationService
{ {
WeakReferenceMessenger.Default.Send(new NewMailItemRenderingRequestedEvent(mailItemViewModel)); WeakReferenceMessenger.Default.Send(new NewMailItemRenderingRequestedEvent(mailItemViewModel));
} }
else if (listingFrame.Content != null
&& listingFrame.Content.GetType() == GetPageType(WinoPage.ComposePage)
&& page == WinoPage.ComposePage
&& parameter is MailItemViewModel composeDraftViewModel)
{
// ComposePage is already active and we're switching to another draft.
// Reuse existing ComposePage and WebView2 instead of navigating.
WeakReferenceMessenger.Default.Send(new NewComposeDraftItemRequestedEvent(composeDraftViewModel));
}
else if (listingFrame.Content != null else if (listingFrame.Content != null
&& listingFrame.Content.GetType() == GetPageType(WinoPage.IdlePage) && listingFrame.Content.GetType() == GetPageType(WinoPage.IdlePage)
&& pageType == typeof(IdlePage)) && pageType == typeof(IdlePage))
+11 -1
View File
@@ -21,6 +21,7 @@ using Wino.Core.Domain;
using Wino.Core.Domain.Entities.Shared; using Wino.Core.Domain.Entities.Shared;
using Wino.Core.Domain.Models.Reader; using Wino.Core.Domain.Models.Reader;
using Wino.Mail.ViewModels.Data; using Wino.Mail.ViewModels.Data;
using Wino.Mail.ViewModels.Messages;
using Wino.Mail.WinUI.Extensions; using Wino.Mail.WinUI.Extensions;
using Wino.Messaging.Client.Mails; using Wino.Messaging.Client.Mails;
using Wino.Messaging.Client.Shell; using Wino.Messaging.Client.Shell;
@@ -30,7 +31,8 @@ namespace Wino.Views.Mail;
public sealed partial class ComposePage : ComposePageAbstract, public sealed partial class ComposePage : ComposePageAbstract,
IRecipient<CreateNewComposeMailRequested>, IRecipient<CreateNewComposeMailRequested>,
IRecipient<ApplicationThemeChanged> IRecipient<ApplicationThemeChanged>,
IRecipient<NewComposeDraftItemRequestedEvent>
{ {
public WebView2 GetWebView() => WebViewEditor.GetUnderlyingWebView(); public WebView2 GetWebView() => WebViewEditor.GetUnderlyingWebView();
@@ -300,6 +302,12 @@ public sealed partial class ComposePage : ComposePageAbstract,
WebViewEditor.IsEditorDarkMode = message.IsUnderlyingThemeDark; WebViewEditor.IsEditorDarkMode = message.IsUnderlyingThemeDark;
} }
void IRecipient<NewComposeDraftItemRequestedEvent>.Receive(NewComposeDraftItemRequestedEvent message)
{
// Reset the initial focus flag so ToBox gets focus for the new draft.
isInitialFocusHandled = false;
}
private void ImportanceClicked(object sender, RoutedEventArgs e) private void ImportanceClicked(object sender, RoutedEventArgs e)
{ {
ImportanceFlyout.Hide(); ImportanceFlyout.Hide();
@@ -415,6 +423,7 @@ public sealed partial class ComposePage : ComposePageAbstract,
WeakReferenceMessenger.Default.Register<CreateNewComposeMailRequested>(this); WeakReferenceMessenger.Default.Register<CreateNewComposeMailRequested>(this);
WeakReferenceMessenger.Default.Register<ApplicationThemeChanged>(this); WeakReferenceMessenger.Default.Register<ApplicationThemeChanged>(this);
WeakReferenceMessenger.Default.Register<NewComposeDraftItemRequestedEvent>(this);
} }
protected override void UnregisterRecipients() protected override void UnregisterRecipients()
@@ -423,6 +432,7 @@ public sealed partial class ComposePage : ComposePageAbstract,
WeakReferenceMessenger.Default.Unregister<CreateNewComposeMailRequested>(this); WeakReferenceMessenger.Default.Unregister<CreateNewComposeMailRequested>(this);
WeakReferenceMessenger.Default.Unregister<ApplicationThemeChanged>(this); WeakReferenceMessenger.Default.Unregister<ApplicationThemeChanged>(this);
WeakReferenceMessenger.Default.Unregister<NewComposeDraftItemRequestedEvent>(this);
} }
// TODO: Save mime on closing the app. // TODO: Save mime on closing the app.
@@ -262,8 +262,9 @@ public sealed partial class MailListPage : MailListPageAbstract,
} }
else if (IsComposingPageActive()) else if (IsComposingPageActive())
{ {
// Composer is already active. Prepare composer WebView2 animation. // Composer is already active. Skip connected animation since the page
PrepareComposePageWebViewTransition(); // will be reused in-place (no navigation occurs).
// NavigationService will send NewComposeDraftItemRequestedEvent instead.
} }
else else
composerPageTransition = NavigationTransitionType.DrillIn; composerPageTransition = NavigationTransitionType.DrillIn;
+15 -7
View File
@@ -1056,12 +1056,14 @@ public class MailService : BaseDatabaseService, IMailService
bool isIdChanging = localDraftCopy.Id != newMailCopyId; bool isIdChanging = localDraftCopy.Id != newMailCopyId;
localDraftCopy.Id = newMailCopyId; localDraftCopy.Id = newMailCopyId;
localDraftCopy.DraftId = newDraftId; if (!string.IsNullOrEmpty(newDraftId))
localDraftCopy.ThreadId = newThreadId; localDraftCopy.DraftId = newDraftId;
if (!string.IsNullOrEmpty(newThreadId))
localDraftCopy.ThreadId = newThreadId;
await UpdateMailAsync(localDraftCopy).ConfigureAwait(false); await UpdateMailAsync(localDraftCopy).ConfigureAwait(false);
ReportUIChange(new DraftMapped(oldLocalDraftId, newDraftId)); ReportUIChange(new DraftMapped(oldLocalDraftId, localDraftCopy.DraftId));
return true; return true;
} }
@@ -1070,14 +1072,20 @@ public class MailService : BaseDatabaseService, IMailService
{ {
return UpdateAllMailCopiesAsync(mailCopyId, (item) => return UpdateAllMailCopiesAsync(mailCopyId, (item) =>
{ {
if (item.ThreadId != newThreadId || item.DraftId != newDraftId) var shouldUpdateThreadId = !string.IsNullOrEmpty(newThreadId);
var shouldUpdateDraftId = !string.IsNullOrEmpty(newDraftId);
if ((shouldUpdateThreadId && item.ThreadId != newThreadId) ||
(shouldUpdateDraftId && item.DraftId != newDraftId))
{ {
var oldDraftId = item.DraftId; var oldDraftId = item.DraftId;
item.DraftId = newDraftId; if (shouldUpdateDraftId)
item.ThreadId = newThreadId; item.DraftId = newDraftId;
if (shouldUpdateThreadId)
item.ThreadId = newThreadId;
ReportUIChange(new DraftMapped(oldDraftId, newDraftId)); ReportUIChange(new DraftMapped(oldDraftId, item.DraftId));
return true; return true;
} }