Skip to content

Commit 5be38bf

Browse files
committed
fix websocket, tts and config
1 parent df99c32 commit 5be38bf

File tree

3 files changed

+115
-34
lines changed

3 files changed

+115
-34
lines changed

apps/self-hosted/src/features/auth/utils/hive-auth.ts

Lines changed: 63 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,16 @@ export async function loginWithHiveAuth(
8787
const key = generateKey();
8888
let uuid: string | null = null;
8989
let authTimeout: ReturnType<typeof setTimeout> | null = null;
90+
let settled = false;
91+
92+
// Create challenge once to ensure consistent timestamps between auth_req and QR data
93+
const challenge: HiveAuthChallenge = {
94+
key_type: 'posting',
95+
challenge: JSON.stringify({
96+
login: true,
97+
ts: Date.now(),
98+
}),
99+
};
90100

91101
const cleanup = () => {
92102
if (authTimeout) {
@@ -98,16 +108,22 @@ export async function loginWithHiveAuth(
98108
}
99109
};
100110

101-
ws.onopen = () => {
102-
// Send auth request
103-
const challenge: HiveAuthChallenge = {
104-
key_type: 'posting',
105-
challenge: JSON.stringify({
106-
login: true,
107-
ts: Date.now(),
108-
}),
109-
};
111+
const safeReject = (error: Error) => {
112+
if (!settled) {
113+
settled = true;
114+
reject(error);
115+
}
116+
};
110117

118+
const safeResolve = () => {
119+
if (!settled) {
120+
settled = true;
121+
resolve();
122+
}
123+
};
124+
125+
ws.onopen = () => {
126+
// Send auth request using the pre-created challenge
111127
const authReq = {
112128
cmd: 'auth_req',
113129
account: username,
@@ -131,13 +147,7 @@ export async function loginWithHiveAuth(
131147
case 'auth_wait':
132148
if (msg.uuid) {
133149
uuid = msg.uuid;
134-
const challenge: HiveAuthChallenge = {
135-
key_type: 'posting',
136-
challenge: JSON.stringify({
137-
login: true,
138-
ts: Date.now(),
139-
}),
140-
};
150+
// Reuse the same challenge for QR data
141151
const qrData = generateQRData(username, uuid, key, challenge);
142152
callbacks.onQRCode?.(qrData);
143153
callbacks.onWaiting?.();
@@ -154,33 +164,33 @@ export async function loginWithHiveAuth(
154164
};
155165
callbacks.onSuccess?.(session);
156166
cleanup();
157-
resolve();
167+
safeResolve();
158168
}
159169
break;
160170

161171
case 'auth_nack':
162172
callbacks.onError?.('Authentication rejected');
163173
cleanup();
164-
reject(new Error('Authentication rejected'));
174+
safeReject(new Error('Authentication rejected'));
165175
break;
166176

167177
case 'auth_err':
168178
callbacks.onError?.(msg.error || 'Authentication error');
169179
cleanup();
170-
reject(new Error(msg.error || 'Authentication error'));
180+
safeReject(new Error(msg.error || 'Authentication error'));
171181
break;
172182
}
173183
} catch (error) {
174184
callbacks.onError?.('Failed to parse response');
175185
cleanup();
176-
reject(error);
186+
safeReject(error instanceof Error ? error : new Error('Failed to parse response'));
177187
}
178188
};
179189

180190
ws.onerror = () => {
181191
callbacks.onError?.('WebSocket connection error');
182192
cleanup();
183-
reject(new Error('WebSocket connection error'));
193+
safeReject(new Error('WebSocket connection error'));
184194
};
185195

186196
ws.onclose = () => {
@@ -189,14 +199,19 @@ export async function loginWithHiveAuth(
189199
clearTimeout(authTimeout);
190200
authTimeout = null;
191201
}
202+
// Reject if the Promise hasn't been settled yet
203+
if (!settled) {
204+
callbacks.onError?.('WebSocket closed before authentication completed');
205+
safeReject(new Error('WebSocket closed before authentication completed'));
206+
}
192207
};
193208

194209
// Timeout after 5 minutes
195210
authTimeout = setTimeout(() => {
196211
if (ws.readyState === WebSocket.OPEN) {
197212
callbacks.onError?.('Authentication timeout');
198213
cleanup();
199-
reject(new Error('Authentication timeout'));
214+
safeReject(new Error('Authentication timeout'));
200215
}
201216
}, 5 * 60 * 1000);
202217
});
@@ -213,6 +228,7 @@ export async function broadcastWithHiveAuth(
213228
return new Promise((resolve, reject) => {
214229
const ws = new WebSocket(HIVEAUTH_API);
215230
let signTimeout: ReturnType<typeof setTimeout> | null = null;
231+
let settled = false;
216232

217233
const cleanup = () => {
218234
if (signTimeout) {
@@ -224,6 +240,20 @@ export async function broadcastWithHiveAuth(
224240
}
225241
};
226242

243+
const safeReject = (error: Error) => {
244+
if (!settled) {
245+
settled = true;
246+
reject(error);
247+
}
248+
};
249+
250+
const safeResolve = () => {
251+
if (!settled) {
252+
settled = true;
253+
resolve();
254+
}
255+
};
256+
227257
ws.onopen = () => {
228258
const signReq = {
229259
cmd: 'sign_req',
@@ -252,32 +282,32 @@ export async function broadcastWithHiveAuth(
252282
case 'sign_ack':
253283
callbacks?.onSuccess?.(msg.data);
254284
cleanup();
255-
resolve();
285+
safeResolve();
256286
break;
257287

258288
case 'sign_nack':
259289
callbacks?.onError?.('Transaction rejected');
260290
cleanup();
261-
reject(new Error('Transaction rejected'));
291+
safeReject(new Error('Transaction rejected'));
262292
break;
263293

264294
case 'sign_err':
265295
callbacks?.onError?.(msg.error || 'Signing error');
266296
cleanup();
267-
reject(new Error(msg.error || 'Signing error'));
297+
safeReject(new Error(msg.error || 'Signing error'));
268298
break;
269299
}
270300
} catch (error) {
271301
callbacks?.onError?.('Failed to parse response');
272302
cleanup();
273-
reject(error);
303+
safeReject(error instanceof Error ? error : new Error('Failed to parse response'));
274304
}
275305
};
276306

277307
ws.onerror = () => {
278308
callbacks?.onError?.('WebSocket connection error');
279309
cleanup();
280-
reject(new Error('WebSocket connection error'));
310+
safeReject(new Error('WebSocket connection error'));
281311
};
282312

283313
ws.onclose = () => {
@@ -286,14 +316,19 @@ export async function broadcastWithHiveAuth(
286316
clearTimeout(signTimeout);
287317
signTimeout = null;
288318
}
319+
// Reject if the Promise hasn't been settled yet
320+
if (!settled) {
321+
callbacks?.onError?.('WebSocket closed before signing completed');
322+
safeReject(new Error('WebSocket closed before signing completed'));
323+
}
289324
};
290325

291326
// Timeout after 2 minutes for signing
292327
signTimeout = setTimeout(() => {
293328
if (ws.readyState === WebSocket.OPEN) {
294329
callbacks?.onError?.('Signing timeout');
295330
cleanup();
296-
reject(new Error('Signing timeout'));
331+
safeReject(new Error('Signing timeout'));
297332
}
298333
}, 2 * 60 * 1000);
299334
});

apps/self-hosted/src/features/blog/components/text-to-speech-button.tsx

Lines changed: 51 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@ export function TextToSpeechButton({ text, title, className }: Props) {
136136
onClick={handleStop}
137137
className="p-1 text-theme-muted hover:text-red-500 transition-colors rounded"
138138
title={t('stop')}
139+
aria-label={t('stop')}
139140
>
140141
<UilStopCircle className="w-4 h-4" />
141142
</button>
@@ -158,6 +159,7 @@ export function TextToSpeechButton({ text, title, className }: Props) {
158159
onClick={handleStop}
159160
className="p-1 text-theme-muted hover:text-red-500 transition-colors rounded"
160161
title={t('stop')}
162+
aria-label={t('stop')}
161163
>
162164
<UilStopCircle className="w-4 h-4" />
163165
</button>
@@ -169,24 +171,68 @@ export function TextToSpeechButton({ text, title, className }: Props) {
169171

170172
// Helper to split text into chunks for speech synthesis
171173
function splitIntoChunks(text: string, maxLength: number): string[] {
172-
const sentences = text.match(/[^.!?]+[.!?]+/g) || [text];
174+
// Match sentences with punctuation, and also capture trailing text without punctuation
175+
const sentenceRegex = /[^.!?]+[.!?]+|[^.!?]+$/g;
176+
const sentences = text.match(sentenceRegex) || [text];
173177
const chunks: string[] = [];
174178
let currentChunk = '';
175179

180+
// Helper to split a long sentence into word-boundary chunks
181+
const splitLongSentence = (sentence: string): string[] => {
182+
const words = sentence.split(/\s+/);
183+
const subChunks: string[] = [];
184+
let subChunk = '';
185+
186+
for (const word of words) {
187+
if (subChunk.length + word.length + 1 > maxLength) {
188+
if (subChunk) {
189+
subChunks.push(subChunk.trim());
190+
}
191+
// If a single word is longer than maxLength, add it anyway
192+
subChunk = word;
193+
} else {
194+
subChunk += (subChunk ? ' ' : '') + word;
195+
}
196+
}
197+
198+
if (subChunk) {
199+
subChunks.push(subChunk.trim());
200+
}
201+
202+
return subChunks;
203+
};
204+
176205
for (const sentence of sentences) {
177-
if ((currentChunk + sentence).length > maxLength) {
206+
const trimmedSentence = sentence.trim();
207+
if (!trimmedSentence) continue;
208+
209+
// If the sentence itself is too long, split it at word boundaries
210+
if (trimmedSentence.length > maxLength) {
211+
// First, flush the current chunk if any
212+
if (currentChunk) {
213+
chunks.push(currentChunk.trim());
214+
currentChunk = '';
215+
}
216+
// Split the long sentence and add sub-chunks
217+
const subChunks = splitLongSentence(trimmedSentence);
218+
for (const subChunk of subChunks) {
219+
chunks.push(subChunk);
220+
}
221+
} else if ((currentChunk + ' ' + trimmedSentence).length > maxLength) {
222+
// Adding this sentence would exceed the limit
178223
if (currentChunk) {
179224
chunks.push(currentChunk.trim());
180225
}
181-
currentChunk = sentence;
226+
currentChunk = trimmedSentence;
182227
} else {
183-
currentChunk += sentence;
228+
currentChunk += (currentChunk ? ' ' : '') + trimmedSentence;
184229
}
185230
}
186231

232+
// Always push the final chunk
187233
if (currentChunk) {
188234
chunks.push(currentChunk.trim());
189235
}
190236

191-
return chunks;
237+
return chunks.filter(chunk => chunk.length > 0);
192238
}

apps/self-hosted/src/features/floating-menu/components/config-editor.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ function ArrayFieldEditor({
101101
className={`${inputClassName} font-mono`}
102102
style={{
103103
...inputStyle,
104-
borderColor: isValid ? inputStyle.border : '#ef4444',
104+
borderColor: isValid ? FLOATING_MENU_THEME.borderColor : '#ef4444',
105105
}}
106106
rows={4}
107107
aria-label={field.label}

0 commit comments

Comments
 (0)