fuzzyworld

north-star / mobile / files / src / core / utils.ts

Branch mobile · default branch · all branches

utils.ts

3.5 KB · history · blame

1type FriendLike = {
2 timestamp?: number;
3 lastSeen?: number;
4};
5
6type UserAgentDataWithModel = Navigator['userAgentData'] & {
7 getHighEntropyValues: (hints: string[]) => Promise<{ model?: string }>;
8};
9
10export const getShortestDelta = (fromAngle: number, toAngle: number) => ((toAngle - fromAngle + 540) % 360) - 180;
11
12export const normalizeAngle = (angle: number) => ((angle % 360) + 360) % 360;
13
14export const normalizeSignedAngle = (angle: number) => {
15 const normalized = normalizeAngle(angle);
16 return normalized > 180 ? normalized - 360 : normalized;
17};
18
19export const normalizeName = (value: string | null | undefined) => (value || '').trim().slice(0, 32);
20
21export const normalizeColor = (value: string | null | undefined) => {
22 const candidate = (value || '').trim().toLowerCase();
23 return /^#[0-9a-f]{6}$/.test(candidate) ? candidate : '';
24};
25
26export const getReadableTextColor = (hexColor: string) => {
27 const normalized = normalizeColor(hexColor);
28 if (!normalized) return '#0f172a';
29
30 const r = parseInt(normalized.slice(1, 3), 16);
31 const g = parseInt(normalized.slice(3, 5), 16);
32 const b = parseInt(normalized.slice(5, 7), 16);
33 const luminance = 0.2126 * r + 0.7152 * g + 0.0722 * b;
34
35 return luminance > 145 ? '#0f172a' : '#f8fafc';
36};
37
38export const getArrowDisplayColor = (hexColor: string) => {
39 if (!document.body.classList.contains('dark')) return hexColor;
40 const normalized = normalizeColor(hexColor);
41 if (!normalized) return '#ffffff';
42
43 const r = parseInt(normalized.slice(1, 3), 16) / 255;
44 const g = parseInt(normalized.slice(3, 5), 16) / 255;
45 const b = parseInt(normalized.slice(5, 7), 16) / 255;
46 const luminance = 0.2126 * r + 0.7152 * g + 0.0722 * b;
47 return luminance < 0.2 ? '#ffffff' : hexColor;
48};
49
50export type PresenceThresholds = {
51 onlineGraceMs: number;
52 recentlyOfflineMs: number;
53};
54
55export const getFriendPresenceState = (friend: FriendLike, now: number, thresholds: PresenceThresholds): 'online' | 'recent-offline' | 'stale-offline' => {
56 const friendTimestamp = Number(friend.lastSeen ?? friend.timestamp);
57 if (!Number.isFinite(friendTimestamp)) return 'stale-offline';
58 const ageMs = Math.max(0, now - friendTimestamp);
59 if (ageMs <= thresholds.onlineGraceMs) return 'online';
60 if (ageMs <= thresholds.recentlyOfflineMs) return 'recent-offline';
61 return 'stale-offline';
62};
63
64export const getDeviceHint = async () => {
65 const userAgentData = (navigator as Navigator & { userAgentData?: UserAgentDataWithModel }).userAgentData;
66 if (userAgentData) {
67 try {
68 const data = await userAgentData.getHighEntropyValues(['model']);
69 if (data.model) return data.model;
70 } catch {
71 // Fall through to UA parsing.
72 }
73 }
74
75 const ua = navigator.userAgent;
76 if (/iPhone/.test(ua)) return 'iPhone';
77 if (/iPad/.test(ua)) return 'iPad';
78
79 const androidModel = ua.match(/;\s*([^;]+?)\s+Build\//);
80 if (androidModel) return androidModel[1];
81
82 if (/Android/.test(ua)) return 'Android';
83 if (/Mac/.test(ua)) return 'Mac';
84 if (/Windows/.test(ua)) return 'PC';
85 if (/Linux/.test(ua)) return 'Linux';
86 return null;
87};
88
89export const createRandomUserName = async () => {
90 const hint = await getDeviceHint();
91 const suffix = Math.floor(1000 + Math.random() * 9000);
92 return hint ? `${hint} ${suffix}` : `User ${suffix}`;
93};
94
95export const createRandomColor = () => `#${Math.floor(Math.random() * 0x1000000).toString(16).padStart(6, '0')}`;