Add local mail pinning support

This commit is contained in:
Burak Kaan Köse
2026-04-21 23:17:08 +02:00
parent c0023614ad
commit 09820dda71
19 changed files with 531 additions and 53 deletions
@@ -12,6 +12,7 @@ using Serilog;
using Wino.Core.Domain.Entities.Mail;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.MailItem;
using Wino.Mail.ViewModels.Data;
using Wino.Messaging.Client.Mails;
using Wino.Messaging.UI;
@@ -139,10 +140,24 @@ public class WinoMailCollection : ObservableRecipient, IRecipient<SelectedItemsC
private object GetGroupingKey(IMailListItem mailItem)
{
if (mailItem.IsPinned)
return MailListGroupKey.Pinned;
if (SortingType == SortingOptionType.ReceiveDate)
return mailItem.CreationDate.ToLocalTime().Date;
else
return mailItem.FromName;
return new MailListGroupKey(false, mailItem.CreationDate.ToLocalTime().Date);
return new MailListGroupKey(false, mailItem.FromName);
}
private bool ShouldReinsertForChanges(MailCopyChangeFlags changedProperties)
{
if ((changedProperties & (MailCopyChangeFlags.ThreadId | MailCopyChangeFlags.IsPinned)) != 0)
return true;
if (SortingType == SortingOptionType.ReceiveDate)
return (changedProperties & MailCopyChangeFlags.CreationDate) != 0;
return (changedProperties & (MailCopyChangeFlags.FromName | MailCopyChangeFlags.FromAddress)) != 0;
}
private void UpdateUniqueIdHashes(IMailHashContainer itemContainer, bool isAdd)
@@ -608,7 +623,7 @@ public class WinoMailCollection : ObservableRecipient, IRecipient<SelectedItemsC
}
});
if ((appliedChanges & MailCopyChangeFlags.ThreadId) != 0)
if (ShouldReinsertForChanges(appliedChanges))
{
await ReinsertUpdatedItemAsync(updatedItem, wasSelected, existingItem.IsBusy);
return;
@@ -993,6 +1008,16 @@ public class WinoMailCollection : ObservableRecipient, IRecipient<SelectedItemsC
if (updates.Count == 0)
return;
if (changedProperties == MailCopyChangeFlags.None || ShouldReinsertForChanges(changedProperties))
{
foreach (var update in updates)
{
await UpdateExistingItemAsync(update.ItemContainer, update.UpdatedMail, mailUpdateSource, changedProperties);
}
return;
}
await ExecuteUIThread(() =>
{
foreach (var update in updates)
+23 -3
View File
@@ -17,6 +17,7 @@ public partial class MailItemViewModel(MailCopy mailCopy) : ObservableRecipient,
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(CreationDate))]
[NotifyPropertyChangedFor(nameof(IsFlagged))]
[NotifyPropertyChangedFor(nameof(IsPinned))]
[NotifyPropertyChangedFor(nameof(FromName))]
[NotifyPropertyChangedFor(nameof(IsFocused))]
[NotifyPropertyChangedFor(nameof(IsRead))]
@@ -82,6 +83,12 @@ public partial class MailItemViewModel(MailCopy mailCopy) : ObservableRecipient,
set => SetProperty(MailCopy.IsFlagged, value, MailCopy, (u, n) => u.IsFlagged = n);
}
public bool IsPinned
{
get => MailCopy.IsPinned;
set => SetProperty(MailCopy.IsPinned, value, MailCopy, (u, n) => u.IsPinned = n);
}
public string FromName
{
get => string.IsNullOrEmpty(MailCopy.FromName) ? MailCopy.FromAddress : MailCopy.FromName;
@@ -233,6 +240,7 @@ public partial class MailItemViewModel(MailCopy mailCopy) : ObservableRecipient,
{
nameof(CreationDate) or nameof(SortingDate) => MailCopyChangeFlags.CreationDate,
nameof(IsFlagged) => MailCopyChangeFlags.IsFlagged,
nameof(IsPinned) => MailCopyChangeFlags.IsPinned,
nameof(FromName) or nameof(SortingName) => MailCopyChangeFlags.FromName,
nameof(IsFocused) => MailCopyChangeFlags.IsFocused,
nameof(IsRead) => MailCopyChangeFlags.IsRead,
@@ -293,12 +301,13 @@ public partial class MailItemViewModel(MailCopy mailCopy) : ObservableRecipient,
changedFlags |= SetIfChanged(MailCopy.Importance, source.Importance, value => MailCopy.Importance = value, MailCopyChangeFlags.Importance);
changedFlags |= SetIfChanged(MailCopy.IsRead, source.IsRead, value => MailCopy.IsRead = value, MailCopyChangeFlags.IsRead);
changedFlags |= SetIfChanged(MailCopy.IsFlagged, source.IsFlagged, value => MailCopy.IsFlagged = value, MailCopyChangeFlags.IsFlagged);
changedFlags |= SetIfChanged(MailCopy.IsPinned, source.IsPinned, value => MailCopy.IsPinned = value, MailCopyChangeFlags.IsPinned);
changedFlags |= SetIfChanged(MailCopy.IsFocused, source.IsFocused, value => MailCopy.IsFocused = value, MailCopyChangeFlags.IsFocused);
changedFlags |= SetIfChanged(MailCopy.FileId, source.FileId, value => MailCopy.FileId = value, MailCopyChangeFlags.FileId);
changedFlags |= SetIfChanged(MailCopy.ItemType, source.ItemType, value => MailCopy.ItemType = value, MailCopyChangeFlags.ItemType);
changedFlags |= SetIfChanged(MailCopy.SenderContact, source.SenderContact, value => MailCopy.SenderContact = value, MailCopyChangeFlags.SenderContact);
changedFlags |= SetIfChanged(MailCopy.AssignedAccount, source.AssignedAccount, value => MailCopy.AssignedAccount = value, MailCopyChangeFlags.AssignedAccount);
changedFlags |= SetIfChanged(MailCopy.AssignedFolder, source.AssignedFolder, value => MailCopy.AssignedFolder = value, MailCopyChangeFlags.AssignedFolder);
changedFlags |= SetIfChangedIfNotNull(MailCopy.SenderContact, source.SenderContact, value => MailCopy.SenderContact = value, MailCopyChangeFlags.SenderContact);
changedFlags |= SetIfChangedIfNotNull(MailCopy.AssignedAccount, source.AssignedAccount, value => MailCopy.AssignedAccount = value, MailCopyChangeFlags.AssignedAccount);
changedFlags |= SetIfChangedIfNotNull(MailCopy.AssignedFolder, source.AssignedFolder, value => MailCopy.AssignedFolder = value, MailCopyChangeFlags.AssignedFolder);
changedFlags |= SetIfChanged(MailCopy.UniqueId, source.UniqueId, value => MailCopy.UniqueId = value, MailCopyChangeFlags.UniqueId);
changedFlags |= SetIfChanged(MailCopy.IsReadReceiptRequested, source.IsReadReceiptRequested, value => MailCopy.IsReadReceiptRequested = value, MailCopyChangeFlags.ReadReceiptState);
changedFlags |= SetIfChanged(MailCopy.ReadReceiptStatus, source.ReadReceiptStatus, value => MailCopy.ReadReceiptStatus = value, MailCopyChangeFlags.ReadReceiptState);
@@ -353,6 +362,14 @@ public partial class MailItemViewModel(MailCopy mailCopy) : ObservableRecipient,
return flag;
}
private static MailCopyChangeFlags SetIfChangedIfNotNull<T>(T currentValue, T newValue, Action<T> setter, MailCopyChangeFlags flag) where T : class
{
if (newValue == null)
return MailCopyChangeFlags.None;
return SetIfChanged(currentValue, newValue, setter, flag);
}
private void RaisePropertyChanges(MailCopyChangeFlags changedFlags)
{
if (changedFlags == MailCopyChangeFlags.None)
@@ -377,6 +394,9 @@ public partial class MailItemViewModel(MailCopy mailCopy) : ObservableRecipient,
if ((changedFlags & MailCopyChangeFlags.IsFlagged) != 0)
Queue(nameof(IsFlagged));
if ((changedFlags & MailCopyChangeFlags.IsPinned) != 0)
Queue(nameof(IsPinned));
if ((changedFlags & MailCopyChangeFlags.FromName) != 0)
{
Queue(nameof(FromName));
@@ -91,6 +91,11 @@ public partial class ThreadMailItemViewModel : ObservableRecipient, IMailListIte
/// </summary>
public bool IsFlagged => ThreadEmails.Any(e => e.IsFlagged);
/// <summary>
/// Gets whether any email in this thread is pinned.
/// </summary>
public bool IsPinned => ThreadEmails.Any(e => e.IsPinned);
/// <summary>
/// Gets whether the latest email is focused
/// </summary>
@@ -182,6 +187,7 @@ public partial class ThreadMailItemViewModel : ObservableRecipient, IMailListIte
[NotifyPropertyChangedFor(nameof(HasAttachments))]
[NotifyPropertyChangedFor(nameof(IsCalendarEvent))]
[NotifyPropertyChangedFor(nameof(IsFlagged))]
[NotifyPropertyChangedFor(nameof(IsPinned))]
[NotifyPropertyChangedFor(nameof(IsFocused))]
[NotifyPropertyChangedFor(nameof(IsRead))]
[NotifyPropertyChangedFor(nameof(HasReadReceiptTracking))]
@@ -473,6 +479,9 @@ public partial class ThreadMailItemViewModel : ObservableRecipient, IMailListIte
if ((changedFlags & MailCopyChangeFlags.IsFlagged) != 0 || changedFlags == MailCopyChangeFlags.All)
Queue(nameof(IsFlagged));
if ((changedFlags & MailCopyChangeFlags.IsPinned) != 0 || changedFlags == MailCopyChangeFlags.All)
Queue(nameof(IsPinned));
if ((changedFlags & MailCopyChangeFlags.IsRead) != 0 || changedFlags == MailCopyChangeFlags.All)
Queue(nameof(IsRead));
+28 -1
View File
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Collections.Concurrent;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Linq;
@@ -760,6 +761,17 @@ public partial class MailListPageViewModel : MailBaseViewModel,
await _winoRequestDelegator.ExecuteAsync(accountId, requests).ConfigureAwait(false);
}
public Task ChangePinnedStatusAsync(IEnumerable<MailItemViewModel> targetItems, bool isPinned)
{
var uniqueIds = targetItems?
.Where(a => a?.MailCopy != null)
.Select(a => a.MailCopy.UniqueId)
.Distinct()
.ToList() ?? [];
return _mailService.ChangePinnedStatusAsync(uniqueIds, isPinned);
}
private bool ShouldPreventItemAdd(MailCopy mailItem)
{
bool condition = mailItem.IsRead
@@ -1553,13 +1565,28 @@ public partial class MailListPageViewModel : MailBaseViewModel,
}
}
var initialExistingIds = new ConcurrentDictionary<Guid, bool>(MailCollection.MailCopyIdHashSet);
var localPinnedItems = new List<MailCopy>();
if (!isDoingOnlineSearch)
{
var pinnedOptions = CreateInitializationOptions(SearchQuery, MailCollection.MailCopyIdHashSet);
localPinnedItems = await _mailService.FetchPinnedMailsAsync(pinnedOptions, cancellationToken).ConfigureAwait(false);
foreach (var pinnedItem in localPinnedItems)
{
initialExistingIds.TryAdd(pinnedItem.UniqueId, true);
}
}
var initializationOptions = CreateInitializationOptions(
isDoingOnlineSearch ? string.Empty : SearchQuery,
MailCollection.MailCopyIdHashSet,
initialExistingIds,
onlineSearchItems,
isDoingOnlineSearch);
items = await _mailService.FetchMailsAsync(initializationOptions, cancellationToken).ConfigureAwait(false);
items = localPinnedItems.Count > 0 ? [.. localPinnedItems, .. items] : items;
if (!listManipulationCancellationTokenSource.IsCancellationRequested)
{
@@ -604,6 +604,15 @@ public partial class MailRenderingPageViewModel : MailBaseViewModel,
if (initializedMailItemViewModel == null)
return;
var assignedFolder = initializedMailItemViewModel.MailCopy.AssignedFolder;
if (assignedFolder == null)
{
Log.Warning("Skipping folder-specific mail commands because AssignedFolder is missing for {MailUniqueId}",
initializedMailItemViewModel.MailCopy.UniqueId);
return;
}
MenuItems.Add(MailOperationMenuItem.Create(MailOperation.Seperator));
// You can't do these to draft items.
@@ -625,7 +634,7 @@ public partial class MailRenderingPageViewModel : MailBaseViewModel,
}
// Archive - Unarchive
if (initializedMailItemViewModel.MailCopy.AssignedFolder.SpecialFolderType == SpecialFolderType.Archive)
if (assignedFolder.SpecialFolderType == SpecialFolderType.Archive)
MenuItems.Add(MailOperationMenuItem.Create(MailOperation.UnArchive));
else
MenuItems.Add(MailOperationMenuItem.Create(MailOperation.Archive));
@@ -647,10 +656,10 @@ public partial class MailRenderingPageViewModel : MailBaseViewModel,
else
MenuItems.Add(MailOperationMenuItem.Create(MailOperation.MarkAsRead, true, false));
if (initializedMailItemViewModel.MailCopy.AssignedFolder.SpecialFolderType == SpecialFolderType.Junk)
if (assignedFolder.SpecialFolderType == SpecialFolderType.Junk)
MenuItems.Add(MailOperationMenuItem.Create(MailOperation.MarkAsNotJunk, true, true));
else if (!initializedMailItemViewModel.IsDraft &&
initializedMailItemViewModel.MailCopy.AssignedFolder.SpecialFolderType != SpecialFolderType.Sent)
assignedFolder.SpecialFolderType != SpecialFolderType.Sent)
MenuItems.Add(MailOperationMenuItem.Create(MailOperation.MoveToJunk, true, true));
}