399 lines
16 KiB
Plaintext
399 lines
16 KiB
Plaintext
@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. *@
|
|||
|
|
<div class="flex flex-col gap-4 w-full" data-instance>
|
|||
|
|
@if (IsLoading)
|
|||
|
|
{
|
|||
|
|
<p class="flex w-full items-center justify-center gap-2">
|
|||
|
|
<DLoading Variant="DLoadingVariant.Dots" LabelText="@Localizer["loading"]" />
|
|||
|
|
@Localizer["loading"]
|
|||
|
|
</p>
|
|||
|
|
}
|
|||
|
|
else if (settings is null)
|
|||
|
|
{
|
|||
|
|
<div class="flex flex-wrap items-center gap-2">
|
|||
|
|
<p class="text-sm text-error">@Localizer["The server's settings could not be read."]</p>
|
|||
|
|
<DButton Size="DSize.Sm" Disabled="IsDefaultDisabled" OnClick="Retry">@Localizer["Retry"]</DButton>
|
|||
|
|
</div>
|
|||
|
|
}
|
|||
|
|
else
|
|||
|
|
{
|
|||
|
|
<div class="flex flex-col gap-2">
|
|||
|
|
<span class="text-sm font-semibold">@Localizer["Thumbnail"]</span>
|
|||
|
|
<img src="@settings.ThumbnailUrl" alt="@Localizer["Thumbnail"]" class="w-full max-w-sm aspect-[40/21] object-cover rounded-lg neo-inset neo-2xs"
|
|||
|
|
data-instance-thumbnail-image="@(settings.ThumbnailIsDefault ? "default" : "uploaded")" />
|
|||
|
|
<div class="flex flex-wrap items-center gap-2 pointer-coarse:gap-2">
|
|||
|
|
<DFileInput Size="DSize.Sm" Accept="@PictureTypes" OnChange="PickThumbnail" disabled="@(IsDefaultDisabled || isUploading)"
|
|||
|
|
aria-label="@Localizer["Thumbnail"]" data-instance-thumbnail />
|
|||
|
|
@if (isUploading)
|
|||
|
|
{
|
|||
|
|
<DLoading Size="DSize.Sm" LabelText="@Localizer["loading"]" />
|
|||
|
|
}
|
|||
|
|
@if (!settings.ThumbnailIsDefault)
|
|||
|
|
{
|
|||
|
|
<DButton Size="DSize.Sm" Disabled="IsDefaultDisabled || isUploading" OnClick="RemoveThumbnail" data-instance-thumbnail-remove>
|
|||
|
|
<i class="ion-md-trash text-error" aria-hidden="true"></i><span>@Localizer["Back to the default picture"]</span>
|
|||
|
|
</DButton>
|
|||
|
|
}
|
|||
|
|
</div>
|
|||
|
|
<p class="text-xs opacity-70">@Localizer["Saved as soon as it is picked: at most {0}, fitted within 1200×630 and stripped of its metadata.", MaximumThumbnail.GetFileSize(Localizer)]</p>
|
|||
|
|
<ErrorViewer AdditionalErrors="thumbnailErrors" />
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<EditForm Model="form" OnValidSubmit="Save" class="flex flex-col gap-4">
|
|||
|
|
<DataAnnotationsValidator />
|
|||
|
|
|
|||
|
|
<label class="flex flex-col gap-1">
|
|||
|
|
<span class="text-sm font-semibold">@Localizer["Server name"]</span>
|
|||
|
|
<DInput TValue="string" class="w-full" @bind-Value="form.Title" Immediate="true" autocomplete="off" maxlength="64" data-instance-title />
|
|||
|
|
</label>
|
|||
|
|
<label class="flex flex-col gap-1">
|
|||
|
|
<span class="text-sm font-semibold">@Localizer["Short description"]</span>
|
|||
|
|
<DTextarea TValue="string" class="w-full" @bind-Value="form.ShortDescription" Immediate="true" rows="2" autocomplete="off" maxlength="500"
|
|||
|
|
data-instance-short-description />
|
|||
|
|
<span class="text-xs opacity-70">@Localizer["Plain text, as the instance API, NodeInfo and the server's own actor give it."]</span>
|
|||
|
|
</label>
|
|||
|
|
<label class="flex flex-col gap-1">
|
|||
|
|
<span class="text-sm font-semibold">@Localizer["Description (Markdown)"]</span>
|
|||
|
|
<DTextarea TValue="string" class="w-full font-mono text-sm" @bind-Value="form.Description" Immediate="true" rows="8" autocomplete="off"
|
|||
|
|
maxlength="@ServerSettingsForm.MaxText" data-instance-description />
|
|||
|
|
@if (settings.DescriptionUpdatedAt is { } described)
|
|||
|
|
{
|
|||
|
|
<span class="text-xs opacity-70" title="@described.ToLocalTime().ToString(SUtility.dateTimeFormatExt)">@Localizer["Changed {0} ago", described.GetPassedTime(Localizer)]</span>
|
|||
|
|
}
|
|||
|
|
</label>
|
|||
|
|
|
|||
|
|
<fieldset class="flex flex-col gap-2 p-3 rounded-xl neo-inset neo-2xs">
|
|||
|
|
<legend class="text-sm font-semibold px-1">@Localizer["Contact"]</legend>
|
|||
|
|
<label class="flex flex-col gap-1">
|
|||
|
|
<span class="text-sm">@Localizer["Contact address"]</span>
|
|||
|
|
<DInput TValue="string" Type="email" class="w-full" @bind-Value="form.ContactEmail" Immediate="true" autocomplete="off" maxlength="254"
|
|||
|
|
data-instance-email>
|
|||
|
|
<Prefix><i class="ion-md-mail" aria-hidden="true"></i></Prefix>
|
|||
|
|
</DInput>
|
|||
|
|
</label>
|
|||
|
|
<span class="text-sm">@Localizer["Contact persona"]</span>
|
|||
|
|
@if (form.ContactAccountId is { Length: > 0 })
|
|||
|
|
{
|
|||
|
|
<div class="flex flex-wrap items-center gap-2 p-2 rounded-lg neo-raised neo-2xs" data-instance-contact="@form.ContactAccountId">
|
|||
|
|
<i class="ion-md-person text-lg" aria-hidden="true"></i>
|
|||
|
|
<span class="flex flex-col min-w-0 flex-1">
|
|||
|
|
<b class="truncate">@(contactName ?? form.ContactAccountId)</b>
|
|||
|
|
@if (contactHandle is { Length: > 0 })
|
|||
|
|
{
|
|||
|
|
<span class="text-xs opacity-70 truncate">@contactHandle</span>
|
|||
|
|
}
|
|||
|
|
</span>
|
|||
|
|
<DButton Size="DSize.Sm" Disabled="IsDefaultDisabled || isSaving" OnClick="RemoveContact" data-instance-contact-remove>
|
|||
|
|
<i class="ion-md-close" aria-hidden="true"></i><span>@Localizer["No contact persona"]</span>
|
|||
|
|
</DButton>
|
|||
|
|
</div>
|
|||
|
|
}
|
|||
|
|
else
|
|||
|
|
{
|
|||
|
|
<p class="text-xs opacity-70">@Localizer["None: the server names nobody."]</p>
|
|||
|
|
}
|
|||
|
|
@* its form attribute names no form, so Enter in the search never saves the settings *@
|
|||
|
|
<DInput TValue="string" class="w-full" Value="contactQuery" ValueChanged="SearchContact" ValueExpression="() => contactQuery" Immediate="true"
|
|||
|
|
autocomplete="off" form="instance-contact-search" placeholder="@Localizer["Search this server's personas"]"
|
|||
|
|
aria-label="@Localizer["Search this server's personas"]" data-instance-contact-search>
|
|||
|
|
<Prefix><i class="ion-md-search" aria-hidden="true"></i></Prefix>
|
|||
|
|
</DInput>
|
|||
|
|
<div class="flex flex-wrap gap-2" data-instance-contact-options>
|
|||
|
|
@if (contactQuery is { Length: > 0 })
|
|||
|
|
{
|
|||
|
|
@foreach (var account in contactMatches)
|
|||
|
|
{
|
|||
|
|
<DButton Size="DSize.Sm" class="max-w-full" Disabled="IsDefaultDisabled || isSaving" OnClick="() => PickContact(account.Id, account.Name, account.Acct)"
|
|||
|
|
data-instance-contact-option="@account.Id">
|
|||
|
|
<span class="truncate">@account.Name</span><span class="text-xs opacity-70 truncate">@("@" + account.Acct)</span>
|
|||
|
|
</DButton>
|
|||
|
|
}
|
|||
|
|
@if (contactMatches.Count == 0 && !isSearching)
|
|||
|
|
{
|
|||
|
|
<span class="text-xs opacity-70">@Localizer["No persona of this server matches."]</span>
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
else
|
|||
|
|
{
|
|||
|
|
@foreach (var persona in CascadingState.Personas)
|
|||
|
|
{
|
|||
|
|
<DButton Size="DSize.Sm" class="max-w-full" Disabled="IsDefaultDisabled || isSaving"
|
|||
|
|
OnClick="() => PickContact(persona.Id, persona.Name ?? persona.UserName, persona.Handle ?? persona.UserName)" data-instance-contact-option="@persona.Id">
|
|||
|
|
<span class="truncate">@(persona.Name ?? persona.UserName)</span>
|
|||
|
|
</DButton>
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
</div>
|
|||
|
|
<p class="text-xs opacity-70">@Localizer["The contact persona is shown to everyone as the one who runs this server."]</p>
|
|||
|
|
</fieldset>
|
|||
|
|
|
|||
|
|
<label class="flex flex-col gap-1">
|
|||
|
|
<span class="text-sm font-semibold">@Localizer["Privacy policy (Markdown)"]</span>
|
|||
|
|
<DTextarea TValue="string" class="w-full font-mono text-sm" @bind-Value="form.PrivacyPolicy" Immediate="true" rows="8" autocomplete="off"
|
|||
|
|
maxlength="@ServerSettingsForm.MaxText" data-instance-privacy />
|
|||
|
|
@if (settings.PrivacyPolicyUpdatedAt is { } policyAt)
|
|||
|
|
{
|
|||
|
|
<span class="text-xs opacity-70" title="@policyAt.ToLocalTime().ToString(SUtility.dateTimeFormatExt)">@Localizer["Changed {0} ago", policyAt.GetPassedTime(Localizer)]</span>
|
|||
|
|
}
|
|||
|
|
</label>
|
|||
|
|
<div class="flex flex-col gap-1">
|
|||
|
|
<label class="flex flex-col gap-1">
|
|||
|
|
<span class="text-sm font-semibold">@Localizer["Terms of service (Markdown)"]</span>
|
|||
|
|
<DTextarea TValue="string" class="w-full font-mono text-sm" @bind-Value="form.TermsOfService" Immediate="true" rows="8" autocomplete="off"
|
|||
|
|
maxlength="@ServerSettingsForm.MaxText" placeholder="@Localizer["None"]" data-instance-terms />
|
|||
|
|
</label>
|
|||
|
|
<label class="flex flex-wrap items-center gap-2">
|
|||
|
|
<span class="text-sm">@Localizer["In effect from"]</span>
|
|||
|
|
<DInput TValue="string" Type="date" class="w-auto" @bind-Value="termsDate" autocomplete="off" data-instance-terms-date />
|
|||
|
|
</label>
|
|||
|
|
<span class="text-xs opacity-70">@Localizer["Empty, the server has no terms. Without a date, new terms take effect today."]</span>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="flex flex-col gap-2">
|
|||
|
|
<DToggle LabelText="@Localizer["Publish the weekly activity"]" @bind-Value="form.PublishActivity" data-instance-publish-activity />
|
|||
|
|
<p class="text-xs opacity-70">@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."]</p>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<p class="text-sm" data-instance-registrations="@settings.RegistrationMode">
|
|||
|
|
<b>@Localizer["Sign-ups"]</b>
|
|||
|
|
@(settings.RegistrationMode == "Open" ? Localizer["open to anyone"] : Localizer["by invitation only"])
|
|||
|
|
<span class="text-xs opacity-70">@Localizer["(set in the server's configuration, Registrations:Mode)"]</span>
|
|||
|
|
</p>
|
|||
|
|
|
|||
|
|
<ErrorViewer />
|
|||
|
|
<div class="flex flex-wrap items-center justify-end gap-2 pointer-coarse:gap-2">
|
|||
|
|
@if (settings.UpdatedAt is { } savedAt)
|
|||
|
|
{
|
|||
|
|
<span class="me-auto text-xs opacity-70" title="@savedAt.ToLocalTime().ToString(SUtility.dateTimeFormatExt)" data-instance-saved>
|
|||
|
|
@Localizer["Saved {0} ago by {1}", savedAt.GetPassedTime(Localizer), SavedBy]
|
|||
|
|
</span>
|
|||
|
|
}
|
|||
|
|
<DButton Disabled="IsDefaultDisabled || isSaving" OnClick="Reset" data-instance-reset>@Localizer["Discard changes"]</DButton>
|
|||
|
|
<DButton Type="submit" Color="DColor.Primary" Disabled="IsDefaultDisabled || isSaving" data-instance-save>
|
|||
|
|
@if (isSaving)
|
|||
|
|
{
|
|||
|
|
<DLoading Size="DSize.Sm" LabelText="@Localizer["loading"]" />
|
|||
|
|
}
|
|||
|
|
<span>@Localizer["Save"]</span>
|
|||
|
|
</DButton>
|
|||
|
|
</div>
|
|||
|
|
</EditForm>
|
|||
|
|
}
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
@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<Account> contactMatches = [];
|
|||
|
|
int searching;
|
|||
|
|
bool isSearching;
|
|||
|
|
bool isSaving;
|
|||
|
|
bool isUploading;
|
|||
|
|
List<string> pictureTypes = [];
|
|||
|
|
List<string> 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."]);
|
|||
|
|
}
|
|||
|
|
}
|