@@ -95,7 +112,7 @@ export default function ProfileSettings() {
Public at qrypt.chat/u/{user.username}, and as{' '}
- OpenProfile.md. Leave a field empty to hide it.
+ OpenProfile.md. Leave a field empty to hide it (an empty display name keeps the current one).
{message &&
{message.text}
}
diff --git a/src/lib/profile/fields.js b/src/lib/profile/fields.js
index 68e87e97..1c358937 100644
--- a/src/lib/profile/fields.js
+++ b/src/lib/profile/fields.js
@@ -28,6 +28,22 @@ export function cleanEmoji(input) {
return { value: parts[0].segment };
}
+/**
+ * The display name shown in chats, on /u/
and in OpenProfile.md.
+ * Control and bidi-override characters are stripped and whitespace collapsed.
+ * `users.display_name` is NOT NULL, so empty means "keep the current name".
+ * @returns {{ value: string | null } | { error: string }}
+ */
+export function cleanDisplayName(input) {
+ if (input === null || input === undefined) return { value: null };
+ if (typeof input !== 'string') return { error: 'Display name must be text' };
+ const text = input.replace(/[\p{Cc}\u200B\u200E\u200F\u202A-\u202E\u2060\u2066-\u2069\uFEFF]/gu, ' ').trim().replace(/\s+/g, ' ');
+ if (!text) return { value: null };
+ if ([...text].length > 50) return { error: 'Display name can be at most 50 characters' };
+ if (/[<>]/.test(text)) return { error: 'Display name contains characters that are not allowed' };
+ return { value: text };
+}
+
/**
* Pronouns as the person writes them (she/her, they/them, any), or empty to clear.
* @returns {{ value: string | null } | { error: string }}
diff --git a/src/lib/profile/fields.test.js b/src/lib/profile/fields.test.js
index 7908ba87..bc34c111 100644
--- a/src/lib/profile/fields.test.js
+++ b/src/lib/profile/fields.test.js
@@ -1,6 +1,6 @@
import { describe, expect, it } from 'vitest';
import { emoji, parseOpenProfile, pronouns, web, accounts } from '@profullstack/openprofile';
-import { cleanEmoji, cleanPronouns, cleanWebsite, PUBLIC_PROFILE_COLUMNS, toOpenProfile } from './fields.js';
+import { cleanDisplayName, cleanEmoji, cleanPronouns, cleanWebsite, PUBLIC_PROFILE_COLUMNS, toOpenProfile } from './fields.js';
describe('profile fields', () => {
it('emoji: exactly one emoji grapheme, empty clears', () => {
@@ -14,6 +14,19 @@ describe('profile fields', () => {
expect(cleanEmoji('