diff --git a/apps/mobile/src/host/text-playback.ts b/apps/mobile/src/host/text-playback.ts index 62b3ce195f..400c1ba2b5 100644 --- a/apps/mobile/src/host/text-playback.ts +++ b/apps/mobile/src/host/text-playback.ts @@ -71,15 +71,10 @@ export function createTextPlayback(initial = '', now = Date.now()) { const oldLength = ends.length; const shown = body.length; const from = next.startsWith(target) ? (ends.at(-2) ?? 0) : 0; - const kept = from === 0 ? [] : ends.filter((end) => end <= from); - ends = [ - ...kept, - ...[...graphemeSegments(next.slice(from))].map( - (part) => from + part.index + part.segment.length, - ), - ]; - cursor = ends.findIndex((end) => end > shown); - if (cursor < 0) cursor = ends.length; + ends.length = from === 0 ? 0 : ends.length - 1; + for (const part of graphemeSegments(next.slice(from))) + ends.push(from + part.index + part.segment.length); + cursor = firstEndAfter(ends, shown); target = next; const added = Math.max(0, ends.length - oldLength); if (added > 0) { @@ -159,4 +154,15 @@ export function createTextPlayback(initial = '', now = Date.now()) { }; } +function firstEndAfter(ends: number[], offset: number) { + let low = 0; + let high = ends.length; + while (low < high) { + const middle = (low + high) >>> 1; + if (ends[middle]! > offset) high = middle; + else low = middle + 1; + } + return low; +} + export type TextPlayback = ReturnType; diff --git a/apps/mobile/src/ui/chat/message-bubble.tsx b/apps/mobile/src/ui/chat/message-bubble.tsx index c9b24113f9..f6e67bdfb2 100644 --- a/apps/mobile/src/ui/chat/message-bubble.tsx +++ b/apps/mobile/src/ui/chat/message-bubble.tsx @@ -452,14 +452,14 @@ function PressableBubble({ if (anchor.current !== undefined) onLongPress(anchor.current); else measure(onLongPress); }; - const hold = - onLongPress === undefined - ? undefined - : () => { - haptic('menu'); - if (shrinks) scale.value = withSpring(1, spring.spatial.fast); - open(); - }; + const holdRef = useRef<() => void>(undefined); + holdRef.current = () => { + haptic('menu'); + if (shrinks) scale.value = withSpring(1, spring.spatial.fast); + open(); + }; + const holds = onLongPress !== undefined; + const hold = useMemo(() => (holds ? () => holdRef.current?.() : undefined), [holds]); return ( 2 * (HEADER + CHUNK + 17) + 1 || - !/^[hd][0-9a-f]+$/u.test(wire) || + (wire[0] !== 'h' && wire[0] !== 'd') || + wire.length < 3 || wire.length % 2 !== 1 ) throw new Error('channel-frame-invalid'); @@ -83,8 +98,7 @@ export class ConnectChannel { (this.role === 'initiator' && this.stage !== 1) ) throw new Error('channel-handshake-invalid'); - if (this.noise.read(hexToBytes(wire.slice(1))).length !== 0) - throw new Error('channel-early-payload'); + if (this.noise.read(wireBytes(wire)).length !== 0) throw new Error('channel-early-payload'); if (this.role === 'responder' && this.stage === 0) { this.stage = 1; return { reply: `h${bytesToHex(this.noise.write())}` }; @@ -94,7 +108,7 @@ export class ConnectChannel { return { ...(reply === undefined ? {} : { reply }), ready: true }; } if (wire[0] !== 'd') throw new Error('channel-frame-invalid'); - const frame = hexToBytes(wire.slice(1)); + const frame = wireBytes(wire); if (frame.length < HEADER + 17) throw new Error('channel-length-invalid'); const header = frame.subarray(0, HEADER); const view = new DataView(header.buffer, header.byteOffset, header.byteLength);