diff --git a/package-lock.json b/package-lock.json
index ccff6e09..42f0e378 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -32,6 +32,7 @@
"react-intersection-observer": "~9.5.3",
"react-quick-pinch-zoom": "~5.1.0",
"react-router-dom": "6.6.2",
+ "runes2": "~1.1.3",
"string-length": "5.0.1",
"swiped-events": "~1.1.9",
"toastify-js": "~1.12.0",
@@ -6057,6 +6058,11 @@
"queue-microtask": "^1.2.2"
}
},
+ "node_modules/runes2": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/runes2/-/runes2-1.1.3.tgz",
+ "integrity": "sha512-sJ/0iVFLne4f2S7cMB1OckBtC9lqkzP5a/wPnDIkbrWzgUsJ+JMQv6y7hk76U7zvbua+je5GltfpsZazUhG05w=="
+ },
"node_modules/safe-buffer": {
"version": "5.2.1",
"dev": true,
diff --git a/package.json b/package.json
index bce70b0e..a090fe30 100644
--- a/package.json
+++ b/package.json
@@ -34,6 +34,7 @@
"react-intersection-observer": "~9.5.3",
"react-quick-pinch-zoom": "~5.1.0",
"react-router-dom": "6.6.2",
+ "runes2": "~1.1.3",
"string-length": "5.0.1",
"swiped-events": "~1.1.9",
"toastify-js": "~1.12.0",
diff --git a/src/components/compose.css b/src/components/compose.css
index 504ee99a..4096c8e4 100644
--- a/src/components/compose.css
+++ b/src/components/compose.css
@@ -653,6 +653,11 @@
white-space: pre-wrap;
min-height: 5em;
max-height: 50vh;
+ scrollbar-width: none;
+
+ &::-webkit-scrollbar {
+ display: none;
+ }
/* Follow textarea styles */
@media (min-width: 40em) {
diff --git a/src/components/compose.jsx b/src/components/compose.jsx
index bb6542c6..41919a3b 100644
--- a/src/components/compose.jsx
+++ b/src/components/compose.jsx
@@ -5,6 +5,7 @@ import equal from 'fast-deep-equal';
import { forwardRef } from 'preact/compat';
import { useEffect, useMemo, useRef, useState } from 'preact/hooks';
import { useHotkeys } from 'react-hotkeys-hook';
+import { substring } from 'runes2';
import stringLength from 'string-length';
import { uid } from 'uid/single';
import { useDebouncedCallback, useThrottledCallback } from 'use-debounce';
@@ -131,16 +132,20 @@ function highlightText(text, { maxCharacters = Infinity }) {
const { composerCharacterCount } = states;
let leftoverHTML = '';
if (composerCharacterCount > maxCharacters) {
- const leftoverCount = composerCharacterCount - maxCharacters;
+ // NOTE: runes2 substring considers surrogate pairs
+ // const leftoverCount = composerCharacterCount - maxCharacters;
// Highlight exceeded characters
leftoverHTML =
'' +
- html.slice(-leftoverCount) +
+ // html.slice(-leftoverCount) +
+ substring(html, maxCharacters) +
'';
- html = html.slice(0, -leftoverCount);
+ // html = html.slice(0, -leftoverCount);
+ html = substring(html, 0, maxCharacters);
+ return html + leftoverHTML;
}
- html = html
+ return html
.replace(urlRegexObj, '$2$3') // URLs
.replace(MENTION_RE, '$1$2') // Mentions
.replace(HASHTAG_RE, '$1$2') // Hashtags
@@ -148,8 +153,6 @@ function highlightText(text, { maxCharacters = Infinity }) {
SCAN_RE,
'$1$2',
); // Emoji shortcodes
-
- return html + leftoverHTML;
}
function Compose({
@@ -1538,6 +1541,7 @@ const Textarea = forwardRef((props, ref) => {
target.setRangeText('', pos, selectionStart);
}
autoResizeTextarea(target);
+ target.dispatchEvent(new Event('input'));
}
}
} catch (e) {
@@ -1545,6 +1549,7 @@ const Textarea = forwardRef((props, ref) => {
console.error(e);
}
}
+ composeHighlightRef.current.scrollTop = target.scrollTop;
}}
onInput={(e) => {
const { target } = e;