Fixed the caching issue that causes mails to be not removed. Improved drag/drop.
This commit is contained in:
@@ -233,6 +233,11 @@ public class WinoMailCollection : ObservableRecipient, IRecipient<SelectedItemsC
|
||||
return !string.IsNullOrEmpty(threadId) && _threadIdToItemsMap.ContainsKey(threadId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether a mail with the given UniqueId currently exists in this collection.
|
||||
/// </summary>
|
||||
public bool ContainsMailUniqueId(Guid uniqueId) => MailCopyIdHashSet.ContainsKey(uniqueId);
|
||||
|
||||
/// <summary>
|
||||
/// Finds a MailItemViewModel by its UniqueId, searching through all items including those inside threads.
|
||||
/// </summary>
|
||||
@@ -444,7 +449,14 @@ public class WinoMailCollection : ObservableRecipient, IRecipient<SelectedItemsC
|
||||
// Try cache first
|
||||
if (_itemToGroupMap.TryGetValue(item, out var cachedGroup))
|
||||
{
|
||||
return cachedGroup;
|
||||
// Cache can become stale during concurrent list refreshes/moves.
|
||||
// Validate before returning so we don't mutate a detached group.
|
||||
if (_mailItemSource.Contains(cachedGroup) && cachedGroup.Contains(item))
|
||||
{
|
||||
return cachedGroup;
|
||||
}
|
||||
|
||||
_itemToGroupMap.TryRemove(item, out _);
|
||||
}
|
||||
|
||||
// Fallback to search if not in cache
|
||||
@@ -487,6 +499,8 @@ public class WinoMailCollection : ObservableRecipient, IRecipient<SelectedItemsC
|
||||
{
|
||||
MailCopyIdHashSet.Clear();
|
||||
_threadIdToItemsMap.Clear();
|
||||
_itemToGroupMap.Clear();
|
||||
_uniqueIdToMailItemMap.Clear();
|
||||
_uniqueIdToThreadMap.Clear();
|
||||
}
|
||||
|
||||
@@ -741,9 +755,6 @@ public class WinoMailCollection : ObservableRecipient, IRecipient<SelectedItemsC
|
||||
/// <returns></returns>
|
||||
public Task UpdateMailCopy(MailCopy updatedMailCopy, MailUpdateSource mailUpdateSource)
|
||||
{
|
||||
// This item doesn't exist in the list.
|
||||
if (!MailCopyIdHashSet.ContainsKey(updatedMailCopy.UniqueId)) return Task.CompletedTask;
|
||||
|
||||
return ExecuteUIThread(() =>
|
||||
{
|
||||
var itemContainer = GetMailItemContainer(updatedMailCopy.UniqueId);
|
||||
@@ -762,6 +773,12 @@ public class WinoMailCollection : ObservableRecipient, IRecipient<SelectedItemsC
|
||||
itemContainer.ItemViewModel.IsBusy = mailUpdateSource == MailUpdateSource.ClientUpdated;
|
||||
|
||||
UpdateUniqueIdHashes(itemContainer.ItemViewModel, true);
|
||||
|
||||
// Keep thread membership cache in sync for items rendered inside thread containers.
|
||||
if (itemContainer.ThreadViewModel != null)
|
||||
{
|
||||
_uniqueIdToThreadMap[itemContainer.ItemViewModel.MailCopy.UniqueId] = itemContainer.ThreadViewModel;
|
||||
}
|
||||
}
|
||||
|
||||
// Trigger thread property notifications if this item is in a thread
|
||||
@@ -836,17 +853,19 @@ public class WinoMailCollection : ObservableRecipient, IRecipient<SelectedItemsC
|
||||
|
||||
public async Task RemoveAsync(MailCopy removeItem)
|
||||
{
|
||||
// This item doesn't exist in the list.
|
||||
if (!MailCopyIdHashSet.ContainsKey(removeItem.UniqueId)) return;
|
||||
var itemContainer = GetMailItemContainer(removeItem.UniqueId);
|
||||
|
||||
if (_uniqueIdToThreadMap.TryGetValue(removeItem.UniqueId, out var threadMailItemViewModel))
|
||||
// This item doesn't exist in the list.
|
||||
if (itemContainer?.ItemViewModel == null) return;
|
||||
|
||||
if (itemContainer.ThreadViewModel != null)
|
||||
{
|
||||
// Item is inside a thread - use cached lookups instead of scanning all groups.
|
||||
var threadMailItemViewModel = itemContainer.ThreadViewModel;
|
||||
var group = FindGroupContainingItem(threadMailItemViewModel);
|
||||
if (group == null) return;
|
||||
|
||||
var removalItem = threadMailItemViewModel.ThreadEmails.FirstOrDefault(e => e.MailCopy.UniqueId == removeItem.UniqueId);
|
||||
if (removalItem == null) return;
|
||||
var removalItem = itemContainer.ItemViewModel;
|
||||
|
||||
// Update ThreadId cache before modifying the thread
|
||||
UpdateThreadIdCache(threadMailItemViewModel, false);
|
||||
@@ -889,36 +908,11 @@ public class WinoMailCollection : ObservableRecipient, IRecipient<SelectedItemsC
|
||||
}
|
||||
else
|
||||
{
|
||||
// Standalone item - use cached lookup.
|
||||
IMailListItem mailItem = null;
|
||||
ObservableGroup<object, IMailListItem> group = null;
|
||||
// Standalone item.
|
||||
IMailListItem mailItem = itemContainer.ItemViewModel;
|
||||
var group = FindGroupContainingItem(mailItem);
|
||||
|
||||
if (_uniqueIdToMailItemMap.TryGetValue(removeItem.UniqueId, out var cachedItem))
|
||||
{
|
||||
mailItem = cachedItem;
|
||||
group = FindGroupContainingItem(mailItem);
|
||||
}
|
||||
|
||||
// Fallback to scan if not in cache
|
||||
if (mailItem == null || group == null)
|
||||
{
|
||||
for (int i = 0; i < _mailItemSource.Count; i++)
|
||||
{
|
||||
var g = _mailItemSource[i];
|
||||
for (int k = 0; k < g.Count; k++)
|
||||
{
|
||||
if (g[k] is MailItemViewModel mvm && mvm.MailCopy.UniqueId == removeItem.UniqueId)
|
||||
{
|
||||
mailItem = mvm;
|
||||
group = g;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (mailItem != null) break;
|
||||
}
|
||||
}
|
||||
|
||||
if (mailItem != null && group != null)
|
||||
if (group != null)
|
||||
{
|
||||
await RemoveItemInternalAsync(group, mailItem);
|
||||
}
|
||||
|
||||
@@ -99,6 +99,16 @@ public partial class MailListPageViewModel : MailBaseViewModel,
|
||||
[ObservableProperty]
|
||||
private bool isMultiSelectionModeEnabled;
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(SelectedMessageText))]
|
||||
[NotifyPropertyChangedFor(nameof(DraggingMessageText))]
|
||||
public partial bool IsDragInProgress { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(SelectedMessageText))]
|
||||
[NotifyPropertyChangedFor(nameof(DraggingMessageText))]
|
||||
public partial int DraggingItemsCount { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial string SearchQuery { get; set; }
|
||||
|
||||
@@ -291,7 +301,13 @@ public partial class MailListPageViewModel : MailBaseViewModel,
|
||||
public bool IsFolderSynchronizationEnabled => ActiveFolder?.IsSynchronizationEnabled ?? false;
|
||||
public bool IsArchiveSpecialFolder => ActiveFolder?.SpecialFolderType == SpecialFolderType.Archive;
|
||||
|
||||
public string SelectedMessageText => MailCollection.SelectedItemsCount > 0 ? string.Format(Translator.MailsSelected, MailCollection.SelectedItemsCount) : Translator.NoMailSelected;
|
||||
public string SelectedMessageText => IsDragInProgress
|
||||
? string.Format(Translator.MailsDragging, DraggingItemsCount)
|
||||
: MailCollection.SelectedItemsCount > 0
|
||||
? string.Format(Translator.MailsSelected, MailCollection.SelectedItemsCount)
|
||||
: Translator.NoMailSelected;
|
||||
|
||||
public string DraggingMessageText => string.Format(Translator.MailsDragging, DraggingItemsCount);
|
||||
|
||||
/// <summary>
|
||||
/// Indicates current state of the mail list. Doesn't matter it's loading or no.
|
||||
@@ -361,6 +377,12 @@ public partial class MailListPageViewModel : MailBaseViewModel,
|
||||
SelectedFolderPivot?.SelectedItemCount = MailCollection.SelectedItemsCount;
|
||||
}
|
||||
|
||||
public void SetDragState(bool isDragInProgress, int draggingItemsCount = 0)
|
||||
{
|
||||
IsDragInProgress = isDragInProgress;
|
||||
DraggingItemsCount = isDragInProgress ? Math.Max(1, draggingItemsCount) : 0;
|
||||
}
|
||||
|
||||
private void NotifyItemFoundState()
|
||||
{
|
||||
OnPropertyChanged(nameof(IsEmpty));
|
||||
@@ -718,7 +740,15 @@ public partial class MailListPageViewModel : MailBaseViewModel,
|
||||
{
|
||||
base.OnMailUpdated(updatedMail, source);
|
||||
|
||||
await MailCollection.UpdateMailCopy(updatedMail, source);
|
||||
try
|
||||
{
|
||||
await listManipulationSemepahore.WaitAsync();
|
||||
await MailCollection.UpdateMailCopy(updatedMail, source);
|
||||
}
|
||||
finally
|
||||
{
|
||||
listManipulationSemepahore.Release();
|
||||
}
|
||||
|
||||
// await ExecuteUIThread(() => { SetupTopBarActions(); });
|
||||
}
|
||||
@@ -727,56 +757,60 @@ public partial class MailListPageViewModel : MailBaseViewModel,
|
||||
{
|
||||
base.OnMailRemoved(removedMail);
|
||||
|
||||
if (removedMail.AssignedAccount == null || removedMail.AssignedFolder == null) return;
|
||||
if (removedMail.AssignedAccount == null) return;
|
||||
|
||||
// We should delete the items only if:
|
||||
// 1. They are deleted from the active folder.
|
||||
// 2. Deleted from draft or sent folder.
|
||||
// 3. Removal is not caused by Gmail Unread folder action.
|
||||
// Delete/sent are special folders that can list their items in other folders.
|
||||
|
||||
bool removedFromActiveFolder = ActiveFolder.HandlingFolders.Any(a => a.Id == removedMail.AssignedFolder.Id);
|
||||
bool removedFromDraftOrSent = removedMail.AssignedFolder.SpecialFolderType == SpecialFolderType.Draft ||
|
||||
removedMail.AssignedFolder.SpecialFolderType == SpecialFolderType.Sent;
|
||||
|
||||
bool isDeletedByGmailUnreadFolderAction = ActiveFolder.SpecialFolderType == SpecialFolderType.Unread &&
|
||||
gmailUnreadFolderMarkedAsReadUniqueIds.Contains(removedMail.UniqueId);
|
||||
|
||||
if ((removedFromActiveFolder || removedFromDraftOrSent) && !isDeletedByGmailUnreadFolderAction)
|
||||
try
|
||||
{
|
||||
bool isDeletedMailSelected = MailCollection.SelectedItems.Any(a => a.MailCopy.UniqueId == removedMail.UniqueId);
|
||||
await listManipulationSemepahore.WaitAsync();
|
||||
|
||||
// Automatically select the next item in the list if the setting is enabled.
|
||||
MailItemViewModel nextItem = null;
|
||||
// Remove only if this specific mail copy currently exists in this list.
|
||||
// Using AssignedFolder-based checks is unreliable for move flows because the
|
||||
// same MailCopy instance can be updated before this message is handled.
|
||||
bool removedItemExistsInCurrentList = MailCollection.ContainsMailUniqueId(removedMail.UniqueId);
|
||||
|
||||
if (isDeletedMailSelected && PreferencesService.AutoSelectNextItem)
|
||||
bool isDeletedByGmailUnreadFolderAction = ActiveFolder?.SpecialFolderType == SpecialFolderType.Unread &&
|
||||
gmailUnreadFolderMarkedAsReadUniqueIds.Contains(removedMail.UniqueId);
|
||||
|
||||
if (removedItemExistsInCurrentList && !isDeletedByGmailUnreadFolderAction)
|
||||
{
|
||||
await ExecuteUIThread(() =>
|
||||
bool isDeletedMailSelected = MailCollection.SelectedItems.Any(a => a.MailCopy.UniqueId == removedMail.UniqueId);
|
||||
|
||||
// Automatically select the next item in the list if the setting is enabled.
|
||||
MailItemViewModel nextItem = null;
|
||||
|
||||
if (isDeletedMailSelected && PreferencesService.AutoSelectNextItem)
|
||||
{
|
||||
nextItem = MailCollection.GetNextItem(removedMail);
|
||||
});
|
||||
await ExecuteUIThread(() =>
|
||||
{
|
||||
nextItem = MailCollection.GetNextItem(removedMail);
|
||||
});
|
||||
}
|
||||
|
||||
// RemoveAsync already handles UI threading internally
|
||||
await MailCollection.RemoveAsync(removedMail);
|
||||
|
||||
if (nextItem != null)
|
||||
WeakReferenceMessenger.Default.Send(new SelectMailItemContainerEvent(nextItem.UniqueId, ScrollToItem: true));
|
||||
else if (isDeletedMailSelected)
|
||||
{
|
||||
// There are no next item to select, but we removed the last item which was selected.
|
||||
// Clearing selected item will dispose rendering page.
|
||||
|
||||
// UnselectAllAsync already handles UI threading internally
|
||||
await MailCollection.UnselectAllAsync();
|
||||
}
|
||||
|
||||
await ExecuteUIThread(() => { NotifyItemFoundState(); });
|
||||
}
|
||||
|
||||
// RemoveAsync already handles UI threading internally
|
||||
await MailCollection.RemoveAsync(removedMail);
|
||||
|
||||
if (nextItem != null)
|
||||
WeakReferenceMessenger.Default.Send(new SelectMailItemContainerEvent(nextItem.UniqueId, ScrollToItem: true));
|
||||
else if (isDeletedMailSelected)
|
||||
else if (isDeletedByGmailUnreadFolderAction)
|
||||
{
|
||||
// There are no next item to select, but we removed the last item which was selected.
|
||||
// Clearing selected item will dispose rendering page.
|
||||
|
||||
// UnselectAllAsync already handles UI threading internally
|
||||
await MailCollection.UnselectAllAsync();
|
||||
// Remove the entry from the set so we can listen to actual deletes next time.
|
||||
gmailUnreadFolderMarkedAsReadUniqueIds.Remove(removedMail.UniqueId);
|
||||
}
|
||||
|
||||
await ExecuteUIThread(() => { NotifyItemFoundState(); });
|
||||
}
|
||||
else if (isDeletedByGmailUnreadFolderAction)
|
||||
finally
|
||||
{
|
||||
// Remove the entry from the set so we can listen to actual deletes next time.
|
||||
gmailUnreadFolderMarkedAsReadUniqueIds.Remove(removedMail.UniqueId);
|
||||
listManipulationSemepahore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user