fuzzyworld

north-star / commit

changing how default names are calculated

bf1c19e23cf17fa96b10c7b9b883479561781e99 · Pavel Safronov · 2026-03-15 10:57:19 -0700 · parents b79e1020b090

changing how default names are calculated
 src/app.js | 45 +++++++++++++++++++++++++++++++++++++++------
 1 file changed, 39 insertions(+), 6 deletions(-)

diff --git a/src/app.js b/src/app.js
index 2ede552..ddab320 100644
--- a/src/app.js
+++ b/src/app.js
@@ -510,7 +510,7 @@ if (calibrateHeadingLink) {
     });
 }
 
-const NAME_STORAGE_KEY = 'north-star-self-name';
+const NAME_STORAGE_KEY = 'north-star-self-name-v2';
 const COLOR_STORAGE_KEY = 'north-star-self-color';
 const PARTICIPANT_VISIBILITY_STORAGE_KEY = 'north-star-participant-visibility';
 const PARTICIPANTS_PANEL_VISIBLE_STORAGE_KEY = 'north-star-participants-panel-visible';
@@ -533,14 +533,47 @@ const getReadableTextColor = (hexColor) => {
 
     return luminance > 145 ? '#0f172a' : '#f8fafc';
 };
-const createRandomUserName = () => `User ${Math.floor(10000 + Math.random() * 90000)}`;
+
+const getDeviceHint = async () => {
+    // Try high-entropy UA data first (Chromium only).
+    if (navigator.userAgentData) {
+        try {
+            const data = await navigator.userAgentData.getHighEntropyValues(['model']);
+            if (data.model) return data.model;
+        } catch {
+            // Fall through to UA string parsing.
+        }
+    }
+
+    const ua = navigator.userAgent;
+    // iPhone / iPad
+    const iosMatch = ua.match(/\(([^)]+)\)/);
+    if (/iPhone/.test(ua)) return 'iPhone';
+    if (/iPad/.test(ua)) return 'iPad';
+    // Named Android device (e.g. "SM-G991B Build/..." or "Pixel 7 Build/...")
+    const androidModel = ua.match(/;\s*([^;]+?)\s+Build\//);
+    if (androidModel) return androidModel[1];
+    // OS family fallback
+    if (/Android/.test(ua)) return 'Android';
+    if (/Mac/.test(ua)) return 'Mac';
+    if (/Windows/.test(ua)) return 'PC';
+    if (/Linux/.test(ua)) return 'Linux';
+    return null;
+};
+
+const createRandomUserName = async () => {
+    const hint = await getDeviceHint();
+    const suffix = Math.floor(1000 + Math.random() * 9000);
+    return hint ? `${hint} ${suffix}` : `User ${suffix}`;
+};
+
 const createRandomColor = () => `#${Math.floor(Math.random() * 0x1000000).toString(16).padStart(6, '0')}`;
-const resolveInitialSelfName = () => {
+const resolveInitialSelfName = async () => {
     const savedName = normalizeName(localStorage.getItem(NAME_STORAGE_KEY));
 
     if (savedName) return savedName;
 
-    const generatedName = createRandomUserName();
+    const generatedName = await createRandomUserName();
     localStorage.setItem(NAME_STORAGE_KEY, generatedName);
     return generatedName;
 };
@@ -554,7 +587,7 @@ const resolveInitialSelfColor = () => {
     return generatedColor;
 };
 
-const initialSelfName = resolveInitialSelfName();
+const initialSelfName = await resolveInitialSelfName();
 const initialSelfColor = resolveInitialSelfColor();
 const participantVisibility = (() => {
     try {
@@ -844,7 +877,7 @@ if (selfColorInput) {
 }
 
 const applySelfName = async (rawName) => {
-    const nextName = normalizeName(rawName) || createRandomUserName();
+    const nextName = normalizeName(rawName) || await createRandomUserName();
 
     if (nextName === selfParticipant.name) {
         if (selfNameInput) selfNameInput.value = nextName;