Skip to content

Commit 0f67a7b

Browse files
committed
Optimize continuosly typing message
1 parent 179f7f6 commit 0f67a7b

File tree

1 file changed

+70
-65
lines changed

1 file changed

+70
-65
lines changed

src/components/chat/bubbleParts/continuouslyTypingMessage.ts

Lines changed: 70 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,12 @@ type Result = {
2020
};
2121

2222
export function wrapContinuouslyTypingMessage({root, bubble, scrollable, isEnd = false, prevPosition = -1}: WrapContinuouslyTypingMessageArgs): Result {
23-
const maxPosition = getMaxPosition(root);
24-
25-
const {allNodes, currentNodeIdx} = hidePrevElements(root, prevPosition);
23+
const {
24+
maxPosition,
25+
nodeContents,
26+
allNodes,
27+
currentNodeIdx
28+
} = processNodeTree({root, prevPosition});
2629

2730
let
2831
lastTextNode: Node,
@@ -32,6 +35,7 @@ export function wrapContinuouslyTypingMessage({root, bubble, scrollable, isEnd =
3235

3336
function clean() {
3437
cleaned = true;
38+
allNodes.forEach(node => nodeContents.delete(node));
3539
};
3640

3741
function onEnd() {
@@ -52,10 +56,12 @@ export function wrapContinuouslyTypingMessage({root, bubble, scrollable, isEnd =
5256

5357
runAnimation({
5458
scrollable,
55-
typeNext: () => typeNext({
59+
typeNext: (length) => typeNext({
5660
result,
5761
setLastTextNode: (node) => lastTextNode = node,
58-
onEnd
62+
nodeContents,
63+
onEnd,
64+
length
5965
}),
6066
isCleaned: () => cleaned,
6167
maxPosition,
@@ -66,62 +72,41 @@ export function wrapContinuouslyTypingMessage({root, bubble, scrollable, isEnd =
6672
}
6773

6874

69-
function getMaxPosition(root: Node) {
70-
const initialTreeWalker = document.createTreeWalker(root, NodeFilter.SHOW_ALL);
71-
72-
const initialNodes: Node[] = [];
73-
74-
while(initialTreeWalker.nextNode()) {
75-
initialNodes.push(initialTreeWalker.currentNode);
76-
}
77-
78-
let position = -1;
79-
80-
for(const node of initialNodes) {
81-
if(node.nodeType === Node.TEXT_NODE) {
82-
const chars = node.textContent.split('').map(char => {
83-
const span = document.createElement('span');
84-
span.textContent = char;
85-
position++;
86-
return span;
87-
});
88-
const fragment = document.createDocumentFragment();
89-
fragment.append(...chars);
90-
node.parentNode?.replaceChild(fragment, node);
91-
}
92-
}
93-
94-
return position;
95-
}
96-
75+
type ProcessNodeTreeArgs = {
76+
root: Node;
77+
prevPosition: number;
78+
};
9779

98-
function hidePrevElements(root: Node, toPosition: number) {
99-
let currentNodeIdx = 0;
80+
function processNodeTree({root, prevPosition}: ProcessNodeTreeArgs) {
81+
const treeWalker = document.createTreeWalker(root, NodeFilter.SHOW_ALL);
10082

101-
const allNodesTreeWalker = document.createTreeWalker(root, NodeFilter.SHOW_ALL);
10283
const allNodes: Node[] = [];
84+
const nodeContents = new WeakMap<Node, string>();
10385

104-
let position = 0;
86+
while(treeWalker.nextNode()) allNodes.push(treeWalker.currentNode);
10587

106-
while(allNodesTreeWalker.nextNode()) {
107-
const node = allNodesTreeWalker.currentNode;
108-
allNodes.push(node);
88+
let
89+
position = -1,
90+
currentNodeIdx = 0
91+
;
10992

93+
for(const node of allNodes) {
11094
if(node.nodeType === Node.TEXT_NODE) {
111-
position += node.textContent.length;
112-
} else if(node instanceof Element && position > toPosition) {
95+
nodeContents.set(node, node.textContent);
96+
97+
node.textContent = node.textContent.slice(0, Math.max(0, prevPosition - position + 1));
98+
99+
position += nodeContents.get(node).length;
100+
} else if(node instanceof Element && position > prevPosition) {
113101
node.classList.add(styles.hidden);
114102
}
115103

116-
if(position <= toPosition) {
104+
if(position <= prevPosition) {
117105
currentNodeIdx++;
118106
}
119107
}
120108

121-
return {
122-
allNodes,
123-
currentNodeIdx
124-
};
109+
return {maxPosition: position, nodeContents, allNodes, currentNodeIdx};
125110
}
126111

127112

@@ -148,23 +133,35 @@ type TypeNextArgs = {
148133
result: Result;
149134
setLastTextNode: (node: Node) => void;
150135
onEnd: () => void;
136+
nodeContents: WeakMap<Node, string>;
137+
length: number;
151138
};
152139

153-
function typeNext({result, setLastTextNode, onEnd}: TypeNextArgs) {
140+
function typeNext({result, setLastTextNode, onEnd, nodeContents, length}: TypeNextArgs) {
154141
const {allNodes, clean} = result;
155-
result.currentPosition++;
156142

157-
let stillHere = true;
158-
for(; result.currentNodeIdx < allNodes.length && stillHere; result.currentNodeIdx++) {
143+
while(result.currentNodeIdx < allNodes.length && length) {
159144
const node = allNodes[result.currentNodeIdx];
160145

161146
if(node instanceof Element) {
147+
result.currentNodeIdx++;
162148
node.classList.remove(styles.hidden);
163-
}
149+
} else if(node.nodeType === Node.TEXT_NODE) {
150+
const typedLength = node.textContent.length;
151+
const finalContent = nodeContents.get(node);
152+
153+
const leftOverLength = Math.max(0, typedLength + length - finalContent.length);
154+
155+
const start = typedLength;
156+
const end = start + length - leftOverLength;
157+
158+
node.textContent += finalContent.slice(start, end);
164159

165-
if(node.nodeType === Node.TEXT_NODE && node.textContent.length) {
160+
length = leftOverLength;
161+
result.currentPosition += end - start;
162+
163+
if(leftOverLength) result.currentNodeIdx++;
166164
setLastTextNode(node);
167-
stillHere = false;
168165
}
169166
}
170167

@@ -176,10 +173,10 @@ function typeNext({result, setLastTextNode, onEnd}: TypeNextArgs) {
176173

177174

178175
const BASE_DELAY = 60 * 1_000 / (800 * 5); // 800wpm
179-
const MIN_DELAY = 60 * 1_000 / (3_000 * 5); // 3_000wpm
176+
const MIN_DELAY = 60 * 1_000 / (2_400 * 5); // 2_400wpm
180177
const DELAY_VARIATION = 0.3;
181178

182-
// Try to write it with the base speed of 800wpm or burst it in 5 seconds if it's a long message, maximum speed of 3_000wpm overall
179+
// Try to write it with the base speed of 800wpm or burst it in 5 seconds if it's a long message, maximum speed of 2_400wpm overall
183180
function getRandomDelay(targetDelay: number) {
184181
const delay = Math.max(MIN_DELAY, Math.min(BASE_DELAY, targetDelay));
185182
return delay + Math.random() * delay * DELAY_VARIATION;
@@ -188,7 +185,7 @@ function getRandomDelay(targetDelay: number) {
188185

189186
type RunAnimationArgs = {
190187
scrollable: HTMLElement;
191-
typeNext: () => void;
188+
typeNext: (length: number) => void;
192189
isCleaned: () => boolean;
193190
maxPosition: number;
194191
prevPosition: number;
@@ -198,8 +195,9 @@ const TARGET_TIME_TO_WRITE = 5000;
198195

199196
function runAnimation({scrollable, typeNext, isCleaned, maxPosition, prevPosition}: RunAnimationArgs) {
200197
const targetDelay = TARGET_TIME_TO_WRITE / (maxPosition - prevPosition);
198+
console.log('my-debug', {maxPosition, prevPosition, targetDelay});
201199

202-
let nextTime = performance.now();
200+
let prevTime = 0;
203201

204202
const animationInvalidation = registerAnimationInvalidation(scrollable);
205203

@@ -211,20 +209,27 @@ function runAnimation({scrollable, typeNext, isCleaned, maxPosition, prevPositio
211209
return true;
212210
};
213211

212+
let skip = -1;
213+
const skipFrames = 2;
214+
214215
animate(() => {
215-
if(checkCleaned()) {
216-
return false;
217-
}
216+
if(checkCleaned()) return false;
218217

218+
skip = (skip + 1) % skipFrames;
219+
if(skip) return true;
219220

220221
const now = performance.now();
222+
if(!prevTime) prevTime = now;
223+
224+
const length = Math.max(0, Math.round((now - prevTime) / getRandomDelay(targetDelay)));
225+
console.log('my-debug', {length});
221226

222-
while(now > nextTime && !checkCleaned()) {
223-
typeNext();
224-
nextTime = nextTime + getRandomDelay(targetDelay);
227+
if(length) {
228+
typeNext(length);
229+
prevTime = now;
225230
}
226231

227-
if(!animationInvalidation.isInvalidated()) {
232+
if(length && !animationInvalidation.isInvalidated()) {
228233
// value.aboutToScroll = true;
229234

230235
// animate(() => {

0 commit comments

Comments
 (0)