Add a notification modal when some users could not be selected from a list

This commit is contained in:
whimsical-c4lic0 2026-02-28 01:45:16 -06:00
parent d0de99a00c
commit 0af7066086
8 changed files with 170 additions and 10 deletions

View File

@ -294,8 +294,9 @@ public class Program(IServiceProvider serviceProvider)
}
else if (config.NonInteractiveMode && !string.IsNullOrEmpty(config.NonInteractiveModeListName))
{
Dictionary<string, long> selectedUsers =
ListUserSelectionResult listSelectionResult =
await orchestrationService.GetUsersForListAsync(config.NonInteractiveModeListName, users, lists);
Dictionary<string, long> selectedUsers = listSelectionResult.SelectedUsers;
hasSelectedUsersKVP = new KeyValuePair<bool, Dictionary<string, long>>(true, selectedUsers);
}
else

View File

@ -27,3 +27,10 @@ public class UserListResult
public string? IgnoredListError { get; set; }
}
public class ListUserSelectionResult
{
public Dictionary<string, long> SelectedUsers { get; set; } = new();
public List<string> UnavailableUsernames { get; set; } = [];
}

View File

@ -110,13 +110,26 @@ public class DownloadOrchestrationService(
/// <param name="allUsers">All available users.</param>
/// <param name="lists">Known lists keyed by name.</param>
/// <returns>The users that belong to the list.</returns>
public async Task<Dictionary<string, long>> GetUsersForListAsync(
public async Task<ListUserSelectionResult> GetUsersForListAsync(
string listName, Dictionary<string, long> allUsers, Dictionary<string, long> lists)
{
ListUserSelectionResult result = new();
long listId = lists[listName];
List<string> listUsernames = await apiService.GetListUsers($"/lists/{listId}/users") ?? [];
return allUsers.Where(x => listUsernames.Contains(x.Key))
HashSet<string> listUsernamesSet = listUsernames.ToHashSet(StringComparer.OrdinalIgnoreCase);
HashSet<string> allUsernamesSet = allUsers.Keys.ToHashSet(StringComparer.OrdinalIgnoreCase);
result.SelectedUsers = allUsers
.Where(x => listUsernamesSet.Contains(x.Key))
.ToDictionary(x => x.Key, x => x.Value);
result.UnavailableUsernames = listUsernames
.Where(username => !allUsernamesSet.Contains(username))
.Distinct(StringComparer.OrdinalIgnoreCase)
.OrderBy(username => username, StringComparer.OrdinalIgnoreCase)
.ToList();
return result;
}
/// <summary>

View File

@ -17,7 +17,7 @@ public interface IDownloadOrchestrationService
/// <summary>
/// Get users for a specific list by name.
/// </summary>
Task<Dictionary<string, long>> GetUsersForListAsync(
Task<ListUserSelectionResult> GetUsersForListAsync(
string listName, Dictionary<string, long> allUsers, Dictionary<string, long> lists);
/// <summary>

View File

@ -371,10 +371,23 @@ public partial class MainWindowViewModel(
[NotifyCanExecuteChangedFor(nameof(SubmitSinglePostOrMessageCommand))]
[NotifyCanExecuteChangedFor(nameof(DownloadSelectedCommand))]
[NotifyCanExecuteChangedFor(nameof(DownloadPurchasedTabCommand))]
[NotifyCanExecuteChangedFor(nameof(SelectUsersFromListCommand))]
private bool _isDownloadSelectionWarningModalOpen;
[ObservableProperty] private string _downloadSelectionWarningMessage = string.Empty;
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(OpenSinglePostOrMessageModalCommand))]
[NotifyCanExecuteChangedFor(nameof(SubmitSinglePostOrMessageCommand))]
[NotifyCanExecuteChangedFor(nameof(DownloadSelectedCommand))]
[NotifyCanExecuteChangedFor(nameof(DownloadPurchasedTabCommand))]
[NotifyCanExecuteChangedFor(nameof(SelectUsersFromListCommand))]
private bool _isListSelectionWarningModalOpen;
[ObservableProperty] private string _listSelectionWarningMessage = string.Empty;
public ObservableCollection<string> ListSelectionUnavailableUsers { get; } = [];
[ObservableProperty] [NotifyCanExecuteChangedFor(nameof(SubmitSinglePostOrMessageCommand))]
private string _singlePostOrMessageUrl = string.Empty;
@ -827,20 +840,24 @@ public partial class MainWindowViewModel(
user.IsSelected = false;
}
Dictionary<string, long> listUsers = await downloadOrchestrationService.GetUsersForListAsync(
ListUserSelectionResult listSelectionResult = await downloadOrchestrationService.GetUsersForListAsync(
SelectedListName,
_allUsers,
_allLists);
HashSet<string> selectedUsernames = listUsers.Keys.ToHashSet(StringComparer.Ordinal);
HashSet<string> selectedUsernames = listSelectionResult.SelectedUsers.Keys
.ToHashSet(StringComparer.OrdinalIgnoreCase);
foreach (SelectableUserViewModel user in AvailableUsers)
{
user.IsSelected = selectedUsernames.Contains(user.Username);
}
ShowListSelectionWarningIfNeeded(listSelectionResult.UnavailableUsernames);
OnPropertyChanged(nameof(SelectedUsersSummary));
OnPropertyChanged(nameof(AllUsersSelected));
DownloadSelectedCommand.NotifyCanExecuteChanged();
SelectUsersFromListCommand.NotifyCanExecuteChanged();
}
finally
{
@ -935,6 +952,13 @@ public partial class MainWindowViewModel(
EditConfig();
}
[RelayCommand]
private void CloseListSelectionWarning()
{
IsListSelectionWarningModalOpen = false;
ListSelectionUnavailableUsers.Clear();
}
[RelayCommand(CanExecute = nameof(CanStopWork))]
private void StopWork()
{
@ -1075,6 +1099,7 @@ public partial class MainWindowViewModel(
private bool CanApplySelectedList() =>
CurrentScreen == AppScreen.UserSelection &&
!string.IsNullOrWhiteSpace(SelectedListName) &&
!IsListSelectionWarningModalOpen &&
!IsDownloading;
private bool CanDownloadSelected() =>
@ -1083,6 +1108,7 @@ public partial class MainWindowViewModel(
!IsDownloadSelectionWarningModalOpen &&
!IsMissingCdmWarningModalOpen &&
!IsShowScrapeSizeWarningModalOpen &&
!IsListSelectionWarningModalOpen &&
!IsDownloading;
private bool CanDownloadPurchasedTab() =>
@ -1091,6 +1117,7 @@ public partial class MainWindowViewModel(
!IsDownloadSelectionWarningModalOpen &&
!IsMissingCdmWarningModalOpen &&
!IsShowScrapeSizeWarningModalOpen &&
!IsListSelectionWarningModalOpen &&
!IsDownloading;
private bool CanOpenSinglePostOrMessageModal() =>
@ -1099,6 +1126,7 @@ public partial class MainWindowViewModel(
!IsDownloadSelectionWarningModalOpen &&
!IsMissingCdmWarningModalOpen &&
!IsShowScrapeSizeWarningModalOpen &&
!IsListSelectionWarningModalOpen &&
!IsSinglePostOrMessageModalOpen;
private bool CanSubmitSinglePostOrMessage() =>
@ -1106,6 +1134,7 @@ public partial class MainWindowViewModel(
!IsDownloadSelectionWarningModalOpen &&
!IsMissingCdmWarningModalOpen &&
!IsShowScrapeSizeWarningModalOpen &&
!IsListSelectionWarningModalOpen &&
!IsDownloading &&
!string.IsNullOrWhiteSpace(SinglePostOrMessageUrl);
@ -1195,7 +1224,8 @@ public partial class MainWindowViewModel(
partial void OnSelectedListNameChanged(string? value)
{
SelectUsersFromListCommand.NotifyCanExecuteChanged();
if (_isApplyingListSelection || IsDownloading || CurrentScreen != AppScreen.UserSelection ||
if (_isApplyingListSelection || IsDownloading || IsListSelectionWarningModalOpen ||
CurrentScreen != AppScreen.UserSelection ||
string.IsNullOrWhiteSpace(value))
{
return;
@ -1533,6 +1563,42 @@ public partial class MainWindowViewModel(
return confirmed;
}
private void ShowListSelectionWarningIfNeeded(IReadOnlyCollection<string> unavailableUsers)
{
if (unavailableUsers.Count == 0)
{
return;
}
ListSelectionUnavailableUsers.Clear();
foreach (string username in unavailableUsers.OrderBy(name => name, StringComparer.OrdinalIgnoreCase))
{
ListSelectionUnavailableUsers.Add(username);
}
List<string> recommendations = [];
if (!configService.CurrentConfig.IncludeExpiredSubscriptions)
{
recommendations.Add("Include Expired Subscriptions");
}
if (!configService.CurrentConfig.IncludeRestrictedSubscriptions)
{
recommendations.Add("Include Restricted Subscriptions");
}
string recommendationText = recommendations.Count > 0
? $"Recommendation:\nEnable {string.Join(" and ", recommendations.Select(option => $"\"{option}\""))} in the subscriptions section of the configuration page. After doing so, select \"Refresh\" from the file menu."
: "These users may be unavailable or inaccessible with your current account.";
ListSelectionWarningMessage =
$"{unavailableUsers.Count} user(s) from \"{SelectedListName}\" could not be selected.\n\n" +
$"{recommendationText}\n\n" +
"Users that could not be selected:";
IsListSelectionWarningModalOpen = true;
}
private static bool TryParseSinglePostOrMessageUrl(
string url,
out SingleDownloadRequest request,

View File

@ -1573,6 +1573,58 @@
</Border>
</Grid>
<Grid Grid.Row="0" Grid.RowSpan="3"
IsVisible="{Binding IsListSelectionWarningModalOpen}"
Background="{DynamicResource OverlayBackgroundBrush}"
ZIndex="1002"
PointerPressed="OnModalOverlayClicked">
<Border Background="{DynamicResource ModalBackgroundBrush}"
BorderBrush="{DynamicResource ModalBorderBrush}"
BorderThickness="1"
CornerRadius="16"
Padding="28"
Width="720"
MaxHeight="560"
HorizontalAlignment="Center"
VerticalAlignment="Center"
BoxShadow="0 20 25 -5 #19000000, 0 10 10 -5 #0F000000"
PointerPressed="OnModalContentClicked">
<StackPanel Spacing="16">
<TextBlock FontSize="20"
FontWeight="Bold"
Foreground="{DynamicResource TextPrimaryBrush}"
Text="Some Users Could Not Be Selected" />
<TextBlock Foreground="{DynamicResource TextSecondaryBrush}"
TextWrapping="Wrap"
Text="{Binding ListSelectionWarningMessage}" />
<Border BorderBrush="{DynamicResource SurfaceBorderBrush}"
BorderThickness="1"
CornerRadius="8"
Background="{DynamicResource SurfaceBackgroundBrush}"
Padding="8"
MaxHeight="240">
<ListBox ItemsSource="{Binding ListSelectionUnavailableUsers}"
BorderThickness="0"
Background="Transparent">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding}" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Border>
<StackPanel Orientation="Horizontal" Spacing="10" HorizontalAlignment="Right">
<Button Content="Close"
Classes="primary"
Command="{Binding CloseListSelectionWarningCommand}" />
</StackPanel>
</StackPanel>
</Border>
</Grid>
<Grid Grid.Row="0" Grid.RowSpan="3"
IsVisible="{Binding IsMissingCdmWarningModalOpen}"
Background="{DynamicResource OverlayBackgroundBrush}"

View File

@ -283,6 +283,7 @@ public partial class MainWindow : Window
vm.CreatorConfigEditor.ModalViewModel.CancelCommand.Execute(null);
vm.CancelSinglePostOrMessageCommand.Execute(null);
vm.CancelDownloadSelectionWarningCommand.Execute(null);
vm.CloseListSelectionWarningCommand.Execute(null);
vm.CancelMissingCdmWarningCommand.Execute(null);
vm.CancelShowScrapeSizeWarningCommand.Execute(null);
}

View File

@ -79,10 +79,30 @@ public class DownloadOrchestrationServiceTests
Dictionary<string, long> allUsers = new() { { "alice", 1 }, { "bob", 2 } };
Dictionary<string, long> lists = new() { { "mylist", 5 } };
Dictionary<string, long> result = await service.GetUsersForListAsync("mylist", allUsers, lists);
ListUserSelectionResult result = await service.GetUsersForListAsync("mylist", allUsers, lists);
Assert.Single(result);
Assert.Equal(2, result["bob"]);
Assert.Single(result.SelectedUsers);
Assert.Equal(2, result.SelectedUsers["bob"]);
Assert.Empty(result.UnavailableUsernames);
}
[Fact]
public async Task GetUsersForListAsync_ReturnsUnavailableUsers()
{
FakeConfigService configService = new(CreateConfig());
ConfigurableApiService apiService = new()
{
ListUsersHandler = _ => Task.FromResult<List<string>?>(["bob", "carol"])
};
DownloadOrchestrationService service =
new(apiService, configService, new OrchestrationDownloadServiceStub(), new UserTrackingDbService());
Dictionary<string, long> allUsers = new() { { "alice", 1 }, { "bob", 2 } };
Dictionary<string, long> lists = new() { { "mylist", 5 } };
ListUserSelectionResult result = await service.GetUsersForListAsync("mylist", allUsers, lists);
Assert.Single(result.SelectedUsers);
Assert.Equal("carol", Assert.Single(result.UnavailableUsernames));
}
[Fact]