@inherits LocalizableComponentBase @using System.Globalization @* What the server says of itself (/clientapi/admin/settings): its name, its short description and its Markdown description, its thumbnail, who answers for it (an address and a persona of this server, picked by searching the local accounts or from the administrator's own personas), its privacy policy and terms of service, and whether its weekly activity is published. The thumbnail is saved as soon as it is picked, streamed from the file. Saving starts from the settings PrivaPub has now (InstanceAdminService.SaveSettings), so what Rules or Media limits saved stays. Registrations are the configuration's and only shown. *@
@if (IsLoading) {

@Localizer["loading"]

} else if (settings is null) {

@Localizer["The server's settings could not be read."]

@Localizer["Retry"]
} else {
@Localizer["Thumbnail"] @Localizer[
@if (isUploading) { } @if (!settings.ThumbnailIsDefault) { @Localizer["Back to the default picture"] }

@Localizer["Saved as soon as it is picked: at most {0}, fitted within 1200×630 and stripped of its metadata.", MaximumThumbnail.GetFileSize(Localizer)]

@Localizer["Contact"] @Localizer["Contact persona"] @if (form.ContactAccountId is { Length: > 0 }) {
@(contactName ?? form.ContactAccountId) @if (contactHandle is { Length: > 0 }) { @contactHandle } @Localizer["No contact persona"]
} else {

@Localizer["None: the server names nobody."]

} @* its form attribute names no form, so Enter in the search never saves the settings *@
@if (contactQuery is { Length: > 0 }) { @foreach (var account in contactMatches) { @account.Name@("@" + account.Acct) } @if (contactMatches.Count == 0 && !isSearching) { @Localizer["No persona of this server matches."] } } else { @foreach (var persona in CascadingState.Personas) { @(persona.Name ?? persona.UserName) } }

@Localizer["The contact persona is shown to everyone as the one who runs this server."]

@Localizer["Empty, the server has no terms. Without a date, new terms take effect today."]

@Localizer["Twelve weeks of local posts and new logins, for anyone to read. On a small server those counts tell who joined and when people write: it is off by default."]

@Localizer["Sign-ups"] @(settings.RegistrationMode == "Open" ? Localizer["open to anyone"] : Localizer["by invitation only"]) @Localizer["(set in the server's configuration, Registrations:Mode)"]

@if (settings.UpdatedAt is { } savedAt) { @Localizer["Saved {0} ago by {1}", savedAt.GetPassedTime(Localizer), SavedBy] } @Localizer["Discard changes"] @if (isSaving) { } @Localizer["Save"]
}
@code { [Inject] InstanceAdminService InstanceAdminService { get; set; } [Inject] AccountService AccountService { get; set; } [Inject] MediaService MediaService { get; set; } [Inject] ConfirmService Confirm { get; set; } ViewServerSettings settings; ServerSettingsForm form = new(); // the terms' date as the date input holds it (yyyy-MM-dd), empty for none string termsDate; // the contact persona as shown: what the settings name, or the one just picked string contactName; string contactHandle; string contactQuery; List contactMatches = []; int searching; bool isSearching; bool isSaving; bool isUploading; List pictureTypes = []; List thumbnailErrors = []; string PictureTypes => string.Join(',', pictureTypes); // what PrivaPub takes for a picture now, within its hard cap long MaximumThumbnail => settings?.Media.EffectiveMaxImageBytes is long effective and > 0 ? Math.Min(effective, ServerMediaLimitsForm.HardMaxImageBytes) : ServerMediaLimitsForm.HardMaxImageBytes; string SavedBy => settings?.UpdatedBy switch { null => Localizer["someone"], "cli" => Localizer["the command line"], var name => name }; protected override async Task OnInitializedAsync() { if (CascadingState.IsOffline) { WhenOnline(LoadData); StartOnlinePolling(); IsLoading = false; return; } await LoadData(); IsLoading = false; } async Task LoadData() { pictureTypes = [.. (await MediaService.Limits()).SupportedMimeTypes.Where(t => t.StartsWith("image/", StringComparison.Ordinal))]; settings = await InstanceAdminService.Settings(); Reset(); } async Task Retry() { IsLoading = true; await LoadData(); IsLoading = false; } void Reset() { if (settings is null) return; form = InstanceAdminService.FormOf(settings); termsDate = settings.TermsEffectiveAt?.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture); contactName = settings.ContactAccount?.Name ?? settings.ContactAccount?.UserName; contactHandle = settings.ContactAccount?.Handle; contactQuery = null; contactMatches = []; } async Task SearchContact(string query) { contactQuery = query; var lookup = ++searching; if (string.IsNullOrWhiteSpace(query)) { contactMatches = []; isSearching = false; return; } isSearching = true; await Task.Delay(300); if (lookup != searching) return; var found = await AccountService.Suggest(query.Trim(), false); if (lookup != searching) return; // the server's own personas only: a local account's acct has no host contactMatches = [.. found.Accounts.Where(a => !a.Group && a.Acct is { Length: > 0 } acct && !acct.Contains('@'))]; isSearching = false; } void PickContact(string accountId, string name, string handle) { form.ContactAccountId = accountId; contactName = name; contactHandle = handle; contactQuery = null; contactMatches = []; } void RemoveContact() { form.ContactAccountId = null; contactName = null; contactHandle = null; } async Task PickThumbnail(InputFileChangeEventArgs eventArgs) { thumbnailErrors = []; var file = eventArgs.File; if (!pictureTypes.Contains(file.ContentType)) { thumbnailErrors.Add(Localizer["\"{0}\" is not a kind of file this server takes.", file.Name]); return; } if (file.Size > MaximumThumbnail) { thumbnailErrors.Add(Localizer["Supplied file \"{0}\" with size {1} exceeds the maximum of {2}.", file.Name, file.Size.GetFileSize(Localizer), MaximumThumbnail.GetFileSize(Localizer)]); return; } isUploading = true; var result = await InstanceAdminService.SaveThumbnail(file, MaximumThumbnail); isUploading = false; if (result.IsInvalid) { Toast.AddResponseError(result); return; } // the form keeps what is being written; only the picture and the dates come from the answer settings = (ViewServerSettings)result.Data; Toast.AddResponseSuccess(Localizer["Thumbnail saved."]); } async Task RemoveThumbnail() { if (!await Confirm.Ask(Localizer["Remove the thumbnail?"], Localizer["The picture uploaded is deleted, and the server shows its default picture again."], Localizer["Remove"], true)) return; isUploading = true; var result = await InstanceAdminService.RemoveThumbnail(); isUploading = false; if (result.IsInvalid) { Toast.AddResponseError(result); return; } settings = (ViewServerSettings)result.Data; Toast.AddResponseSuccess(Localizer["Thumbnail removed."]); } // the value written here when it differs from the one shown, else the one PrivaPub has now static string Changed(string written, string shown, string latest) => written == shown ? latest : written; async Task Save() { var effective = default(DateTime?); if (termsDate is { Length: > 0 }) { if (!DateTime.TryParseExact(termsDate, "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out var on)) { Toast.AddResponseError(Localizer["\"{0}\" is not a date.", termsDate]); return; } effective = on; } // only what was changed here: what another administrator saved meanwhile to the rest stays var shown = InstanceAdminService.FormOf(settings); var dateChanged = termsDate != settings.TermsEffectiveAt?.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture); isSaving = true; var result = await InstanceAdminService.SaveSettings(latest => { latest.Title = Changed(form.Title?.Trim(), shown.Title, latest.Title); latest.ShortDescription = Changed(form.ShortDescription, shown.ShortDescription, latest.ShortDescription); latest.Description = Changed(form.Description, shown.Description, latest.Description); latest.ContactEmail = Changed(form.ContactEmail?.Trim(), shown.ContactEmail, latest.ContactEmail); latest.ContactAccountId = Changed(form.ContactAccountId, shown.ContactAccountId, latest.ContactAccountId); latest.PrivacyPolicy = Changed(form.PrivacyPolicy, shown.PrivacyPolicy, latest.PrivacyPolicy); latest.TermsOfService = Changed(form.TermsOfService, shown.TermsOfService, latest.TermsOfService); latest.TermsEffectiveAt = dateChanged ? effective : latest.TermsEffectiveAt; latest.PublishActivity = form.PublishActivity != shown.PublishActivity ? form.PublishActivity : latest.PublishActivity; }); isSaving = false; if (result.IsInvalid) { Toast.AddResponseError(result); return; } settings = (ViewServerSettings)result.Data; Reset(); Toast.AddResponseSuccess(Localizer["The server's settings are saved."]); } }