feat(community): „von @name“ statt Anzeigename, Benutzername vor dem Hochladen und Veröffentlichen
Logo-Kacheln, Logo-Infozeile und die Detailseite einer Community-Karte zeigen den Benutzernamen des Kontos (Hilfsfunktion byline: Profil, sonst alter Anzeigename, sonst „Anonym“). Die Namen kommen je geladener Seite in einer Abfrage nach, ein Fehler dabei bleibt ohne Fehlertext, ein eigenes Umbenennen aktualisiert die eigenen Einträge sofort. Die Felder „Anzeigename“ in den Dialogen „Logo hochladen“ und „Veröffentlichen“ fallen weg, ebenso account/display-name.ts und der localStorage-Schlüssel; der Client schickt displayName: ''. Vor dem Hochladen und Veröffentlichen verlangt requireUsername() einen Namen („Abbrechen“: es passiert nichts); antwortet der Dienst mit 403 usernameRequired, öffnet sich der Namensdialog, danach ist ein neuer Versuch möglich. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
3440eb46c3
commit
56cb67be4b
26 files changed
+1247
-407
No files matched your search
@@ -1,14 +1,16 @@
|
||||
// Veröffentlichen-Dialog von „Meine Karten“ (Spec § 6): Titel, Herausgeber/Bank, Tags, Anzeigename und die Pflicht-Checkbox.
|
||||
// Veröffentlichen-Dialog von „Meine Karten“ (Spec § 6): Titel, Herausgeber/Bank, Tags und die Pflicht-Checkbox. Einen Anzeigenamen gibt
|
||||
// es nicht mehr: die Detailseite zeigt den Benutzernamen des Kontos (Spec § 5). Hat das Konto keinen, antwortet der Dienst mit 403
|
||||
// `usernameRequired`: dann öffnet sich der Namensdialog, und mit dem gespeicherten Namen lässt es sich noch einmal versuchen.
|
||||
// Eigenes <dialog> wie report-dialog.ts; alle Texte gehen als Text in den DOM, nie als HTML. Übersetzt wird beim Aufbau mit t().
|
||||
import '../account/account.css';
|
||||
import './community.css';
|
||||
import { rememberDisplayName, rememberedDisplayName } from '../account/display-name';
|
||||
import { errorMessage } from '../account/errors';
|
||||
import { usernameDialog } from '../account/username-dialog';
|
||||
import { LOGIN_NEEDED, LoginCancelled } from '../account/with-login';
|
||||
import { el, setFieldInvalid } from '../editor/dom';
|
||||
import { PIXELS_MESSAGE, READ_MESSAGE } from '../editor/upload';
|
||||
import { msg, t } from '../i18n/i18n';
|
||||
import { ApiError, apiErrorText, isAlreadyPublished } from './api';
|
||||
import { ApiError, apiErrorText, isAlreadyPublished, isUsernameRequired } from './api';
|
||||
import { LOGO_MISSING_PUBLISH, NOT_RENDERED, TOO_LARGE, normalizeTags, type PublishForm } from './publish';
|
||||
|
||||
/** Wortlaut aus Spec § 6. */
|
||||
@@ -20,8 +22,6 @@ const RUNNING = msg('Wird veröffentlicht …');
|
||||
/** Grenzen wie der Server (deploy/api/app.js): Länge in UTF-16-Einheiten, nach dem Trimmen. */
|
||||
const TITLE_MAX = 60;
|
||||
const ISSUER_MAX = 40;
|
||||
/** Auch für den Anzeigenamen im Dialog „Logo hochladen“ (logos/logo-dialog.ts). */
|
||||
export const DISPLAY_NAME_MAX = 30;
|
||||
const TAGS_MAX = 5;
|
||||
const TAG_MAX = 20;
|
||||
/** Die Fehlerzeile des Dialogs: ein ungültiges Feld verweist mit `aria-describedby` auf sie. */
|
||||
@@ -29,7 +29,7 @@ const ERROR_ID = 'publish-error';
|
||||
/**
|
||||
* Steuerzeichen (C0, DEL, C1), Nullbreiten- und Bidi-Zeichen (U+200B-200F, U+202A-202E, U+2060-206F) und das BOM (U+FEFF):
|
||||
* unsichtbar oder umordnend. Dasselbe Muster prüft der Server (deploy/api/app.js, CONTROL_CHARACTERS).
|
||||
* Auch für Name und Anzeigename im Dialog „Logo hochladen“ (logos/logo-dialog.ts).
|
||||
* Auch für den Namen im Dialog „Logo hochladen“ (logos/logo-dialog.ts).
|
||||
*/
|
||||
export const CONTROL_CHARACTERS = /[\u0000-\u001f\u007f-\u009f\u200b-\u200f\u202a-\u202e\u2060-\u206f\ufeff]/;
|
||||
|
||||
@@ -37,7 +37,7 @@ export interface PublishFormValues extends PublishForm {
|
||||
consent: boolean;
|
||||
}
|
||||
|
||||
export type PublishField = 'title' | 'issuer' | 'tags' | 'displayName' | 'consent';
|
||||
export type PublishField = 'title' | 'issuer' | 'tags' | 'consent';
|
||||
|
||||
/** Das erste Problem in Formularreihenfolge (mit Text in der Sprache der Seite) oder null, wenn alles passt. Die Grenzen prüft der Server noch einmal. */
|
||||
export function validatePublishForm(values: PublishFormValues): { field: PublishField; message: string } | null {
|
||||
@@ -61,17 +61,6 @@ export function validatePublishForm(values: PublishFormValues): { field: Publish
|
||||
}
|
||||
if (tags.length > TAGS_MAX) return { field: 'tags', message: t('Bitte gib höchstens {max} Tags an.', { max: TAGS_MAX }) };
|
||||
|
||||
const displayName = values.displayName.trim();
|
||||
if (CONTROL_CHARACTERS.test(displayName)) {
|
||||
return { field: 'displayName', message: t('Der Anzeigename enthält ungültige Zeichen.') };
|
||||
}
|
||||
if (displayName.length > DISPLAY_NAME_MAX) {
|
||||
return {
|
||||
field: 'displayName',
|
||||
message: t('Der Anzeigename ist zu lang (höchstens {max} Zeichen).', { max: DISPLAY_NAME_MAX }),
|
||||
};
|
||||
}
|
||||
|
||||
if (!values.consent) return { field: 'consent', message: t('Bitte bestätige die Erklärung zum Bild.') };
|
||||
return null;
|
||||
}
|
||||
@@ -126,14 +115,11 @@ export function publishDialog(opts: PublishDialogOptions): Promise<PublishOutcom
|
||||
const titleInput = text('title', { 'aria-required': 'true' });
|
||||
const issuerInput = text('issuer');
|
||||
const tagsInput = text('tags', { 'aria-describedby': 'publish-tags-hint' });
|
||||
const nameInput = text('displayName', { placeholder: t('Anonym'), 'aria-describedby': 'publish-displayName-hint' });
|
||||
nameInput.value = rememberedDisplayName(); // der zuletzt benutzte Name, auch aus dem Dialog „Logo hochladen“
|
||||
const consentInput = el('input', { type: 'checkbox', name: 'consent', id: 'publish-consent' });
|
||||
const inputs: Record<PublishField, HTMLInputElement> = {
|
||||
title: titleInput,
|
||||
issuer: issuerInput,
|
||||
tags: tagsInput,
|
||||
displayName: nameInput,
|
||||
consent: consentInput,
|
||||
};
|
||||
|
||||
@@ -159,7 +145,6 @@ export function publishDialog(opts: PublishDialogOptions): Promise<PublishOutcom
|
||||
field(t('Titel (Pflicht)'), titleInput),
|
||||
field(t('Herausgeber / Bank (optional)'), issuerInput),
|
||||
field(t('Tags (optional)'), tagsInput, t('Bis zu 5, mit Komma getrennt, z. B. „visa, gold“.')),
|
||||
field(t('Anzeigename (optional)'), nameInput, t('Leer = „Anonym“. Deine E-Mail-Adresse wird nie angezeigt.')),
|
||||
el('label', { class: 'publish-consent' }, consentInput, el('span', {}, t(CONSENT_TEXT))),
|
||||
);
|
||||
const errorEl = el('p', { class: 'auth-msg auth-msg-error', id: ERROR_ID, role: 'alert' });
|
||||
@@ -222,12 +207,27 @@ export function publishDialog(opts: PublishDialogOptions): Promise<PublishOutcom
|
||||
close.focus();
|
||||
}
|
||||
|
||||
/**
|
||||
* Der Dienst verlangt einen Benutzernamen: der Namensdialog öffnet sich über dem Dialog. Mit gespeichertem Namen ist der Hinweis erledigt
|
||||
* und der Fokus liegt auf „Veröffentlichen“ (erneut versuchen); bei „Abbrechen“ oder wenn der Namensdialog nicht aufgeht, bleibt der Grund stehen.
|
||||
*/
|
||||
async function askForUsername(): Promise<void> {
|
||||
let saved: string | null = null;
|
||||
try {
|
||||
saved = await usernameDialog({ mode: 'required', signal: opts.signal });
|
||||
} catch {
|
||||
// der Namensdialog lässt sich nicht öffnen: der Grund oben steht, der Nutzer kann den Namen im Konto-Menü wählen
|
||||
}
|
||||
if (closed || saved === null) return;
|
||||
errorEl.textContent = '';
|
||||
send.focus();
|
||||
}
|
||||
|
||||
async function submit(): Promise<void> {
|
||||
const values: PublishFormValues = {
|
||||
title: titleInput.value.trim(),
|
||||
issuer: issuerInput.value.trim(),
|
||||
tags: tagsInput.value,
|
||||
displayName: nameInput.value.trim(),
|
||||
consent: consentInput.checked,
|
||||
};
|
||||
clearInvalid();
|
||||
@@ -241,7 +241,7 @@ export function publishDialog(opts: PublishDialogOptions): Promise<PublishOutcom
|
||||
}
|
||||
setBusy(true);
|
||||
try {
|
||||
await opts.submit({ title: values.title, issuer: values.issuer, tags: values.tags, displayName: values.displayName });
|
||||
await opts.submit({ title: values.title, issuer: values.issuer, tags: values.tags });
|
||||
} catch (err) {
|
||||
if (closed) return;
|
||||
if (isAlreadyPublished(err)) {
|
||||
@@ -253,10 +253,10 @@ export function publishDialog(opts: PublishDialogOptions): Promise<PublishOutcom
|
||||
setBusy(false);
|
||||
errorEl.textContent = publishFailureText(err);
|
||||
reopen(); // war der Dialog zwischendurch zu, soll der Fehler zu sehen sein
|
||||
if (isUsernameRequired(err)) await askForUsername();
|
||||
return;
|
||||
}
|
||||
if (closed) return;
|
||||
rememberDisplayName(values.displayName);
|
||||
outcome = 'published';
|
||||
finish();
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user