BlueSky & more on desktop
lazurite.stormlightlabs.org/
tauri
rust
typescript
bluesky
appview
atproto
solid
1import type { LogEntry, Maybe } from "$/lib/types";
2
3const MAX_JSON_PREVIEW_CHARS = 6000;
4
5export function escapeForRegex(value: string) {
6 return value.replaceAll(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`);
7}
8
9export function formatCount(value: Maybe<number>) {
10 if (!value) {
11 return "0";
12 }
13
14 if (value >= 1000) {
15 return `${(value / 1000).toFixed(value >= 10_000 ? 0 : 1)}K`;
16 }
17
18 return value.toString();
19}
20
21export function normalizeError(err: unknown): string {
22 if (err instanceof Error) {
23 return err.message;
24 } else {
25 return String(err);
26 }
27}
28
29export function formatEtaSeconds(value: number) {
30 if (value < 60) {
31 return `${value}s`;
32 }
33
34 const minutes = Math.floor(value / 60);
35 const seconds = value % 60;
36 return `${minutes}m ${seconds}s`;
37}
38
39export function formatProgress(value: number | null | undefined) {
40 if (typeof value !== "number" || Number.isNaN(value)) {
41 return "Pending";
42 }
43
44 return `${Math.round(value)}%`;
45}
46
47export function formatBytes(bytes: number): string {
48 if (bytes === 0) return "0 B";
49 const k = 1024;
50 const sizes = ["B", "KB", "MB", "GB"];
51 const i = Math.floor(Math.log(bytes) / Math.log(k));
52 return `${Number.parseFloat((bytes / Math.pow(k, i)).toFixed(1))} ${sizes[i]}`;
53}
54
55export function formatLogTimestamp(timestamp: string | null) {
56 if (!timestamp) {
57 return "--";
58 }
59
60 const parsed = new Date(timestamp);
61 if (Number.isNaN(parsed.getTime())) {
62 return timestamp;
63 }
64
65 return parsed.toLocaleString(undefined, {
66 day: "2-digit",
67 hour: "2-digit",
68 minute: "2-digit",
69 month: "short",
70 second: "2-digit",
71 });
72}
73
74export function formatLogCopyLine(log: LogEntry) {
75 const prefix = [formatLogTimestamp(log.timestamp), log.level, log.target ?? "app"].join(" ");
76 return `${prefix} ${log.message}`;
77}
78
79export function formatJoinedDate(value?: string | null) {
80 if (!value) {
81 return null;
82 }
83
84 const parsed = new Date(value);
85 if (Number.isNaN(parsed.getTime())) {
86 return null;
87 }
88
89 return parsed.toLocaleDateString(undefined, { month: "long", year: "numeric" });
90}
91
92export function initials(name: string) {
93 return name.trim().slice(0, 1).toUpperCase() || "?";
94}
95
96export function formatHandle(handle: string | null | undefined, did: string | null | undefined) {
97 if (!handle) {
98 return did ?? "Unknown";
99 }
100
101 return handle.startsWith("did:") || handle.startsWith("@") ? handle : `@${handle}`;
102}
103
104export function clamp(value: number, min: number, max: number) {
105 return Math.min(Math.max(value, min), max);
106}
107
108export function hashString(value: string) {
109 let hash = 0x81_1C_9D_C5;
110 for (let index = 0; index < value.length; index += 1) {
111 hash ^= value.codePointAt(index)!;
112 hash = Math.imul(hash, 0x01_00_01_93);
113 }
114
115 return (hash >>> 0).toString(16).padStart(8, "0");
116}
117
118export function stringifyUnknown(value: unknown) {
119 const seen = new WeakSet<object>();
120
121 try {
122 const json = JSON.stringify(value, (_, current) => {
123 if (typeof current !== "object" || current === null) {
124 return current;
125 }
126
127 if (seen.has(current)) {
128 return "[Circular]";
129 }
130 seen.add(current);
131 return current;
132 }, 2);
133
134 if (!json) {
135 return "null";
136 }
137
138 if (json.length <= MAX_JSON_PREVIEW_CHARS) {
139 return json;
140 }
141
142 return `${json.slice(0, MAX_JSON_PREVIEW_CHARS)}\n...`;
143 } catch {
144 return String(value);
145 }
146}
147
148export function formatRelativeTime(value: string) {
149 const timestamp = new Date(value).getTime();
150 if (Number.isNaN(timestamp)) {
151 return "";
152 }
153
154 const deltaSeconds = Math.round((timestamp - Date.now()) / 1000);
155 const formatter = new Intl.RelativeTimeFormat(undefined, { numeric: "auto" });
156 const ranges = [
157 ["year", 60 * 60 * 24 * 365],
158 ["month", 60 * 60 * 24 * 30],
159 ["day", 60 * 60 * 24],
160 ["hour", 60 * 60],
161 ["minute", 60],
162 ] as const;
163
164 for (const [unit, seconds] of ranges) {
165 if (Math.abs(deltaSeconds) >= seconds) {
166 return formatter.format(Math.round(deltaSeconds / seconds), unit);
167 }
168 }
169
170 return formatter.format(deltaSeconds, "second");
171}