@page "/filters" @attribute [Authorize] @inherits LocalizableComponentBase @Localizer["Filters"] @* The current persona's filters (/api/v2/filters): words, phrases and posts it would rather not see, where (home and lists, notifications, the public feeds, threads, profiles), how (folded behind the filter's title, left out, or with media blurred) and until when. PrivaPub marks every post with the filters it matches; the cards apply them (Content.FilterIn). Filters are the persona's own and never leave PrivaPub. *@

@Localizer["Filters"]

@Localizer["Make a filter"] @if (IsLoading) {

@Localizer["loading"]

} else if (filters.Count == 0) {

@Localizer["This persona has no filter yet."]

}
@foreach (var filter in filters) {
@filter.Title @ActionName(filter.FilterAction) @if (!filter.IsActive) { @Localizer["Expired"] } else if (filter.ExpiresAt is { } expires) { @Localizer["until {0}", expires.ToLocalTime().ToString(SUtility.dateTimeFormatExt)] } @Localizer["{0} words, {1} posts", filter.Keywords.Count, filter.Statuses.Count]
@if (open?.Id == filter.Id) {
@Localizer["Where"] @foreach (var context in Contexts) { }
@foreach (var action in Actions) { } @foreach (var (seconds, label) in Expiries) { }

@Localizer["Words"]

@foreach (var row in edit.Keywords.Where(k => !k.Removed)) {
@row.Keyword
}
@Localizer["Add"]
@if (filter.Statuses.Count > 0) {

@Localizer["Posts filtered one by one"]

@foreach (var item in filter.Statuses) {
@item.StatusId
}
}
@Localizer["Save"] @if (isConfirmingDelete) { @Localizer["Delete it for good"] @Localizer["Cancel"] } else { @Localizer["Delete"] }
}
}
@code { const int KeepExpiry = -1; static readonly FilterContext[] Contexts = [FilterContext.Home, FilterContext.Notifications, FilterContext.Public, FilterContext.Thread, FilterContext.Account]; static readonly FilterAction[] Actions = [FilterAction.Warn, FilterAction.Hide, FilterAction.Blur]; [Inject] FilterService FilterService { get; set; } List filters = []; TitleForm draft = new(); FilterEdit edit = new(); Filter open; string loadedPersonaId; bool isSaving; bool isConfirmingDelete; (int Seconds, string Label)[] Expiries => [ (KeepExpiry, Localizer["Keep its expiry"]), (0, Localizer["Never expires"]), (1800, Localizer["Expires in 30 minutes"]), (3600, Localizer["Expires in 1 hour"]), (43200, Localizer["Expires in 12 hours"]), (86400, Localizer["Expires in 1 day"]), (604800, Localizer["Expires in 1 week"]) ]; string ContextName(FilterContext context) => context switch { FilterContext.Home => Localizer["Home and lists"], FilterContext.Notifications => Localizer["Notifications"], FilterContext.Public => Localizer["Public feeds"], FilterContext.Thread => Localizer["Threads"], _ => Localizer["Profiles"] }; string ActionName(FilterAction action) => action switch { FilterAction.Hide => Localizer["Hide"], FilterAction.Blur => Localizer["Blur"], _ => Localizer["Warn"] }; string ActionText(FilterAction action) => action switch { FilterAction.Hide => Localizer["Leave the post out"], FilterAction.Blur => Localizer["Blur its media"], _ => Localizer["Fold the post behind the filter's title"] }; // The persona changed (NavMenu's switcher): its filters are another persona's. protected override async Task OnParametersSetAsync() { if (CascadingState.PersonaId is null) { IsLoading = false; return; } if (CascadingState.PersonaId == loadedPersonaId) return; loadedPersonaId = CascadingState.PersonaId; if (CascadingState.IsOffline) { WhenOnline(LoadData); StartOnlinePolling(); IsLoading = false; return; } await LoadData(); } async Task LoadData() { IsLoading = true; open = null; filters = await FilterService.All(); IsLoading = false; } // a new filter applies everywhere and warns until it is told otherwise async Task Create() { isSaving = true; var result = await FilterService.Save(null, new FilterForm { Title = draft.Title.Trim(), Context = [.. Contexts], FilterAction = FilterAction.Warn }); isSaving = false; if (result.IsInvalid) { Toast.AddResponseError(result); return; } var filter = (Filter)result.Data; filters.Add(filter); draft = new(); Toast.AddResponseSuccess(Localizer["Filter {0} made.", filter.Title]); Toggle(filter); } void Toggle(Filter filter) { isConfirmingDelete = false; if (open?.Id == filter.Id) { open = null; return; } open = filter; edit = new FilterEdit { Title = filter.Title, Contexts = [.. filter.Context], Action = filter.FilterAction, ExpiresIn = KeepExpiry, Keywords = [.. filter.Keywords.Select(k => new KeywordRow { Id = k.Id, Keyword = k.Keyword, WholeWord = k.WholeWord, WasWholeWord = k.WholeWord })] }; } void ToggleContext(FilterContext context, bool on) { if (on) edit.Contexts.Add(context); else edit.Contexts.Remove(context); } void AddKeyword() { var keyword = edit.NewKeyword?.Trim(); if (string.IsNullOrEmpty(keyword) || edit.Keywords.Any(k => !k.Removed && string.Equals(k.Keyword, keyword, StringComparison.OrdinalIgnoreCase))) return; edit.Keywords.Add(new KeywordRow { Keyword = keyword, WholeWord = true }); edit.NewKeyword = null; } void RemoveKeyword(KeywordRow row) { if (row.Id is null) edit.Keywords.Remove(row); else row.Removed = true; } async Task Save() { AddKeyword(); var form = new FilterForm { Title = edit.Title.Trim(), Context = [.. Contexts.Where(edit.Contexts.Contains)], FilterAction = edit.Action, ExpiresIn = edit.ExpiresIn == KeepExpiry ? null : edit.ExpiresIn, KeywordsAttributes = [ .. edit.Keywords.Where(k => k.Id is not null && k.Removed).Select(k => new FilterKeywordForm { Id = k.Id, Destroy = true }), .. edit.Keywords.Where(k => k.Id is not null && !k.Removed && k.WholeWord != k.WasWholeWord) .Select(k => new FilterKeywordForm { Id = k.Id, Keyword = k.Keyword, WholeWord = k.WholeWord }), .. edit.Keywords.Where(k => k.Id is null).Select(k => new FilterKeywordForm { Keyword = k.Keyword, WholeWord = k.WholeWord }) ] }; isSaving = true; var result = await FilterService.Save(open.Id, form); isSaving = false; if (result.IsInvalid) { Toast.AddResponseError(result); return; } var saved = (Filter)result.Data; var index = filters.FindIndex(f => f.Id == saved.Id); if (index >= 0) filters[index] = saved; open = null; Toast.AddResponseSuccess(Localizer["Filter {0} saved.", saved.Title]); } async Task Delete() { isSaving = true; var result = await FilterService.Delete(open.Id); isSaving = false; isConfirmingDelete = false; if (result.IsInvalid) { Toast.AddResponseError(result); return; } filters.RemoveAll(f => f.Id == open.Id); Toast.AddResponseSuccess(Localizer["Filter {0} deleted.", open.Title]); open = null; } async Task RemoveStatus(Filter filter, FilterStatus item) { isSaving = true; var result = await FilterService.RemoveStatus(item.Id); isSaving = false; if (result.IsInvalid) { Toast.AddResponseError(result); return; } filter.Statuses.Remove(item); } sealed class TitleForm { public string Title { get; set; } } sealed class FilterEdit { public string Title { get; set; } public HashSet Contexts { get; set; } = []; public FilterAction Action { get; set; } = FilterAction.Warn; public int ExpiresIn { get; set; } = KeepExpiry; public List Keywords { get; set; } = []; public string NewKeyword { get; set; } } sealed class KeywordRow { public string Id { get; set; } public string Keyword { get; set; } public bool WholeWord { get; set; } public bool WasWholeWord { get; set; } public bool Removed { get; set; } } }