mirror of
https://github.com/cheeaun/phanpy.git
synced 2025-02-18 00:01:55 +03:00
Refactor textarea and chars count meter
It won't re-render on every key press anymore
This commit is contained in:
parent
fea7145ac9
commit
1f12c53ee1
2 changed files with 259 additions and 195 deletions
|
@ -1,14 +1,17 @@
|
||||||
import './compose.css';
|
import './compose.css';
|
||||||
|
|
||||||
import '@github/text-expander-element';
|
import '@github/text-expander-element';
|
||||||
|
import { forwardRef } from 'preact/compat';
|
||||||
import { useEffect, useMemo, useRef, useState } from 'preact/hooks';
|
import { useEffect, useMemo, useRef, useState } from 'preact/hooks';
|
||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
import stringLength from 'string-length';
|
import stringLength from 'string-length';
|
||||||
|
import { useSnapshot } from 'valtio';
|
||||||
|
|
||||||
import supportedLanguages from '../data/status-supported-languages';
|
import supportedLanguages from '../data/status-supported-languages';
|
||||||
import urlRegex from '../data/url-regex';
|
import urlRegex from '../data/url-regex';
|
||||||
import emojifyText from '../utils/emojify-text';
|
import emojifyText from '../utils/emojify-text';
|
||||||
import openCompose from '../utils/open-compose';
|
import openCompose from '../utils/open-compose';
|
||||||
|
import states from '../utils/states';
|
||||||
import store from '../utils/store';
|
import store from '../utils/store';
|
||||||
import visibilityIconsMap from '../utils/visibility-icons-map';
|
import visibilityIconsMap from '../utils/visibility-icons-map';
|
||||||
|
|
||||||
|
@ -54,6 +57,16 @@ menu.className = 'text-expander-menu';
|
||||||
|
|
||||||
const DEFAULT_LANG = 'en';
|
const DEFAULT_LANG = 'en';
|
||||||
|
|
||||||
|
// https://github.com/mastodon/mastodon/blob/c4a429ed47e85a6bbf0d470a41cc2f64cf120c19/app/javascript/mastodon/features/compose/util/counter.js
|
||||||
|
const urlRegexObj = new RegExp(urlRegex.source, urlRegex.flags);
|
||||||
|
const usernameRegex = /(^|[^\/\w])@(([a-z0-9_]+)@[a-z0-9\.\-]+[a-z0-9]+)/gi;
|
||||||
|
const urlPlaceholder = '$2xxxxxxxxxxxxxxxxxxxxxxx';
|
||||||
|
function countableText(inputText) {
|
||||||
|
return inputText
|
||||||
|
.replace(urlRegexObj, urlPlaceholder)
|
||||||
|
.replace(usernameRegex, '$1@$3');
|
||||||
|
}
|
||||||
|
|
||||||
function Compose({
|
function Compose({
|
||||||
onClose,
|
onClose,
|
||||||
replyToStatus,
|
replyToStatus,
|
||||||
|
@ -62,6 +75,7 @@ function Compose({
|
||||||
standalone,
|
standalone,
|
||||||
hasOpener,
|
hasOpener,
|
||||||
}) {
|
}) {
|
||||||
|
console.warn('RENDER COMPOSER');
|
||||||
const [uiState, setUIState] = useState('default');
|
const [uiState, setUIState] = useState('default');
|
||||||
|
|
||||||
const accounts = store.local.getJSON('accounts');
|
const accounts = store.local.getJSON('accounts');
|
||||||
|
@ -223,130 +237,6 @@ function Compose({
|
||||||
}
|
}
|
||||||
}, [draftStatus, editStatus, replyToStatus]);
|
}, [draftStatus, editStatus, replyToStatus]);
|
||||||
|
|
||||||
const textExpanderRef = useRef();
|
|
||||||
const textExpanderTextRef = useRef('');
|
|
||||||
useEffect(() => {
|
|
||||||
if (textExpanderRef.current) {
|
|
||||||
const handleChange = (e) => {
|
|
||||||
// console.log('text-expander-change', e);
|
|
||||||
const { key, provide, text } = e.detail;
|
|
||||||
textExpanderTextRef.current = text;
|
|
||||||
|
|
||||||
if (text === '') {
|
|
||||||
provide(
|
|
||||||
Promise.resolve({
|
|
||||||
matched: false,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (key === ':') {
|
|
||||||
// const emojis = customEmojis.current.filter((emoji) =>
|
|
||||||
// emoji.shortcode.startsWith(text),
|
|
||||||
// );
|
|
||||||
const emojis = filterShortcodes(customEmojis.current, text);
|
|
||||||
let html = '';
|
|
||||||
emojis.forEach((emoji) => {
|
|
||||||
const { shortcode, url } = emoji;
|
|
||||||
html += `
|
|
||||||
<li role="option" data-value="${encodeHTML(shortcode)}">
|
|
||||||
<img src="${encodeHTML(
|
|
||||||
url,
|
|
||||||
)}" width="16" height="16" alt="" loading="lazy" />
|
|
||||||
:${encodeHTML(shortcode)}:
|
|
||||||
</li>`;
|
|
||||||
});
|
|
||||||
// console.log({ emojis, html });
|
|
||||||
menu.innerHTML = html;
|
|
||||||
provide(
|
|
||||||
Promise.resolve({
|
|
||||||
matched: emojis.length > 0,
|
|
||||||
fragment: menu,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const type = {
|
|
||||||
'@': 'accounts',
|
|
||||||
'#': 'hashtags',
|
|
||||||
}[key];
|
|
||||||
provide(
|
|
||||||
new Promise((resolve) => {
|
|
||||||
const searchResults = masto.v2.search({
|
|
||||||
type,
|
|
||||||
q: text,
|
|
||||||
limit: 5,
|
|
||||||
});
|
|
||||||
searchResults.then((value) => {
|
|
||||||
if (text !== textExpanderTextRef.current) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
console.log({ value, type, v: value[type] });
|
|
||||||
const results = value[type];
|
|
||||||
console.log('RESULTS', value, results);
|
|
||||||
let html = '';
|
|
||||||
results.forEach((result) => {
|
|
||||||
const {
|
|
||||||
name,
|
|
||||||
avatarStatic,
|
|
||||||
displayName,
|
|
||||||
username,
|
|
||||||
acct,
|
|
||||||
emojis,
|
|
||||||
} = result;
|
|
||||||
const displayNameWithEmoji = emojifyText(displayName, emojis);
|
|
||||||
// const item = menuItem.cloneNode();
|
|
||||||
if (acct) {
|
|
||||||
html += `
|
|
||||||
<li role="option" data-value="${encodeHTML(acct)}">
|
|
||||||
<span class="avatar">
|
|
||||||
<img src="${encodeHTML(
|
|
||||||
avatarStatic,
|
|
||||||
)}" width="16" height="16" alt="" loading="lazy" />
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
<b>${displayNameWithEmoji || username}</b>
|
|
||||||
<br>@${encodeHTML(acct)}
|
|
||||||
</span>
|
|
||||||
</li>
|
|
||||||
`;
|
|
||||||
} else {
|
|
||||||
html += `
|
|
||||||
<li role="option" data-value="${encodeHTML(name)}">
|
|
||||||
<span>#<b>${encodeHTML(name)}</b></span>
|
|
||||||
</li>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
menu.innerHTML = html;
|
|
||||||
});
|
|
||||||
console.log('MENU', results, menu);
|
|
||||||
resolve({
|
|
||||||
matched: results.length > 0,
|
|
||||||
fragment: menu,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
textExpanderRef.current.addEventListener(
|
|
||||||
'text-expander-change',
|
|
||||||
handleChange,
|
|
||||||
);
|
|
||||||
|
|
||||||
textExpanderRef.current.addEventListener('text-expander-value', (e) => {
|
|
||||||
const { key, item } = e.detail;
|
|
||||||
if (key === ':') {
|
|
||||||
e.detail.value = `:${item.dataset.value}:`;
|
|
||||||
} else {
|
|
||||||
e.detail.value = `${key}${item.dataset.value}`;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const formRef = useRef();
|
const formRef = useRef();
|
||||||
|
|
||||||
const beforeUnloadCopy =
|
const beforeUnloadCopy =
|
||||||
|
@ -432,19 +322,16 @@ function Compose({
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const [charCount, setCharCount] = useState(
|
|
||||||
textareaRef.current?.value?.length +
|
|
||||||
spoilerTextRef.current?.value?.length || 0,
|
|
||||||
);
|
|
||||||
const leftChars = maxCharacters - charCount;
|
|
||||||
const getCharCount = () => {
|
const getCharCount = () => {
|
||||||
const { value } = textareaRef.current;
|
const { value } = textareaRef.current;
|
||||||
const { value: spoilerText } = spoilerTextRef.current;
|
const { value: spoilerText } = spoilerTextRef.current;
|
||||||
return stringLength(countableText(value)) + stringLength(spoilerText);
|
return stringLength(countableText(value)) + stringLength(spoilerText);
|
||||||
};
|
};
|
||||||
const updateCharCount = () => {
|
const updateCharCount = () => {
|
||||||
setCharCount(getCharCount());
|
const count = getCharCount();
|
||||||
|
states.composerCharacterCount = count;
|
||||||
};
|
};
|
||||||
|
useEffect(updateCharCount, []);
|
||||||
|
|
||||||
useHotkeys(
|
useHotkeys(
|
||||||
'esc',
|
'esc',
|
||||||
|
@ -818,41 +705,22 @@ function Compose({
|
||||||
</select>
|
</select>
|
||||||
</label>{' '}
|
</label>{' '}
|
||||||
</div>
|
</div>
|
||||||
<text-expander ref={textExpanderRef} keys="@ # :">
|
<Textarea
|
||||||
<textarea
|
ref={textareaRef}
|
||||||
ref={textareaRef}
|
placeholder={
|
||||||
placeholder={
|
replyToStatus
|
||||||
replyToStatus
|
? 'Post your reply'
|
||||||
? 'Post your reply'
|
: editStatus
|
||||||
: editStatus
|
? 'Edit your status'
|
||||||
? 'Edit your status'
|
: 'What are you doing?'
|
||||||
: 'What are you doing?'
|
}
|
||||||
}
|
required={mediaAttachments.length === 0}
|
||||||
required={mediaAttachments.length === 0}
|
disabled={uiState === 'loading'}
|
||||||
autoCapitalize="sentences"
|
onInput={() => {
|
||||||
autoComplete="on"
|
updateCharCount();
|
||||||
autoCorrect="on"
|
}}
|
||||||
spellCheck="true"
|
maxCharacters={maxCharacters}
|
||||||
dir="auto"
|
/>
|
||||||
rows="6"
|
|
||||||
cols="50"
|
|
||||||
name="status"
|
|
||||||
disabled={uiState === 'loading'}
|
|
||||||
onInput={(e) => {
|
|
||||||
const { scrollHeight, offsetHeight, clientHeight, value } =
|
|
||||||
e.target;
|
|
||||||
const offset = offsetHeight - clientHeight;
|
|
||||||
e.target.style.height = value
|
|
||||||
? scrollHeight + offset + 'px'
|
|
||||||
: null;
|
|
||||||
updateCharCount();
|
|
||||||
}}
|
|
||||||
style={{
|
|
||||||
maxHeight: `${maxCharacters / 50}em`,
|
|
||||||
'--text-weight': (1 + charCount / 140).toFixed(1) || 1,
|
|
||||||
}}
|
|
||||||
></textarea>
|
|
||||||
</text-expander>
|
|
||||||
{mediaAttachments.length > 0 && (
|
{mediaAttachments.length > 0 && (
|
||||||
<div class="media-attachments">
|
<div class="media-attachments">
|
||||||
{mediaAttachments.map((attachment, i) => {
|
{mediaAttachments.map((attachment, i) => {
|
||||||
|
@ -957,26 +825,8 @@ function Compose({
|
||||||
</button>{' '}
|
</button>{' '}
|
||||||
<div class="spacer" />
|
<div class="spacer" />
|
||||||
{uiState === 'loading' && <Loader abrupt />}{' '}
|
{uiState === 'loading' && <Loader abrupt />}{' '}
|
||||||
{uiState !== 'loading' && charCount > maxCharacters / 2 && (
|
{uiState !== 'loading' && (
|
||||||
<>
|
<CharCountMeter maxCharacters={maxCharacters} />
|
||||||
<meter
|
|
||||||
class={`donut ${
|
|
||||||
leftChars <= -10
|
|
||||||
? 'explode'
|
|
||||||
: leftChars <= 0
|
|
||||||
? 'danger'
|
|
||||||
: leftChars <= 20
|
|
||||||
? 'warning'
|
|
||||||
: ''
|
|
||||||
}`}
|
|
||||||
value={charCount}
|
|
||||||
max={maxCharacters}
|
|
||||||
data-left={leftChars}
|
|
||||||
style={{
|
|
||||||
'--percentage': (charCount / maxCharacters) * 100,
|
|
||||||
}}
|
|
||||||
/>{' '}
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
<label class="toolbar-button">
|
<label class="toolbar-button">
|
||||||
<span class="icon-text">
|
<span class="icon-text">
|
||||||
|
@ -1012,6 +862,229 @@ function Compose({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const Textarea = forwardRef((props, ref) => {
|
||||||
|
const [text, setText] = useState(ref.current?.value || '');
|
||||||
|
const { maxCharacters, ...textareaProps } = props;
|
||||||
|
const snapStates = useSnapshot(states);
|
||||||
|
const charCount = snapStates.composerCharacterCount;
|
||||||
|
|
||||||
|
const textExpanderRef = useRef();
|
||||||
|
const textExpanderTextRef = useRef('');
|
||||||
|
useEffect(() => {
|
||||||
|
let handleChange, handleValue, handleCommited;
|
||||||
|
if (textExpanderRef.current) {
|
||||||
|
handleChange = (e) => {
|
||||||
|
// console.log('text-expander-change', e);
|
||||||
|
const { key, provide, text } = e.detail;
|
||||||
|
textExpanderTextRef.current = text;
|
||||||
|
|
||||||
|
if (text === '') {
|
||||||
|
provide(
|
||||||
|
Promise.resolve({
|
||||||
|
matched: false,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key === ':') {
|
||||||
|
// const emojis = customEmojis.current.filter((emoji) =>
|
||||||
|
// emoji.shortcode.startsWith(text),
|
||||||
|
// );
|
||||||
|
const emojis = filterShortcodes(customEmojis.current, text);
|
||||||
|
let html = '';
|
||||||
|
emojis.forEach((emoji) => {
|
||||||
|
const { shortcode, url } = emoji;
|
||||||
|
html += `
|
||||||
|
<li role="option" data-value="${encodeHTML(shortcode)}">
|
||||||
|
<img src="${encodeHTML(
|
||||||
|
url,
|
||||||
|
)}" width="16" height="16" alt="" loading="lazy" />
|
||||||
|
:${encodeHTML(shortcode)}:
|
||||||
|
</li>`;
|
||||||
|
});
|
||||||
|
// console.log({ emojis, html });
|
||||||
|
menu.innerHTML = html;
|
||||||
|
provide(
|
||||||
|
Promise.resolve({
|
||||||
|
matched: emojis.length > 0,
|
||||||
|
fragment: menu,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const type = {
|
||||||
|
'@': 'accounts',
|
||||||
|
'#': 'hashtags',
|
||||||
|
}[key];
|
||||||
|
provide(
|
||||||
|
new Promise((resolve) => {
|
||||||
|
const searchResults = masto.v2.search({
|
||||||
|
type,
|
||||||
|
q: text,
|
||||||
|
limit: 5,
|
||||||
|
});
|
||||||
|
searchResults.then((value) => {
|
||||||
|
if (text !== textExpanderTextRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log({ value, type, v: value[type] });
|
||||||
|
const results = value[type];
|
||||||
|
console.log('RESULTS', value, results);
|
||||||
|
let html = '';
|
||||||
|
results.forEach((result) => {
|
||||||
|
const {
|
||||||
|
name,
|
||||||
|
avatarStatic,
|
||||||
|
displayName,
|
||||||
|
username,
|
||||||
|
acct,
|
||||||
|
emojis,
|
||||||
|
} = result;
|
||||||
|
const displayNameWithEmoji = emojifyText(displayName, emojis);
|
||||||
|
// const item = menuItem.cloneNode();
|
||||||
|
if (acct) {
|
||||||
|
html += `
|
||||||
|
<li role="option" data-value="${encodeHTML(acct)}">
|
||||||
|
<span class="avatar">
|
||||||
|
<img src="${encodeHTML(
|
||||||
|
avatarStatic,
|
||||||
|
)}" width="16" height="16" alt="" loading="lazy" />
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
<b>${displayNameWithEmoji || username}</b>
|
||||||
|
<br>@${encodeHTML(acct)}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
`;
|
||||||
|
} else {
|
||||||
|
html += `
|
||||||
|
<li role="option" data-value="${encodeHTML(name)}">
|
||||||
|
<span>#<b>${encodeHTML(name)}</b></span>
|
||||||
|
</li>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
menu.innerHTML = html;
|
||||||
|
});
|
||||||
|
console.log('MENU', results, menu);
|
||||||
|
resolve({
|
||||||
|
matched: results.length > 0,
|
||||||
|
fragment: menu,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
textExpanderRef.current.addEventListener(
|
||||||
|
'text-expander-change',
|
||||||
|
handleChange,
|
||||||
|
);
|
||||||
|
|
||||||
|
handleValue = (e) => {
|
||||||
|
const { key, item } = e.detail;
|
||||||
|
if (key === ':') {
|
||||||
|
e.detail.value = `:${item.dataset.value}:`;
|
||||||
|
} else {
|
||||||
|
e.detail.value = `${key}${item.dataset.value}`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
textExpanderRef.current.addEventListener(
|
||||||
|
'text-expander-value',
|
||||||
|
handleValue,
|
||||||
|
);
|
||||||
|
|
||||||
|
handleCommited = (e) => {
|
||||||
|
const { input } = e.detail;
|
||||||
|
setText(input.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
textExpanderRef.current.addEventListener(
|
||||||
|
'text-expander-committed',
|
||||||
|
handleCommited,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (textExpanderRef.current) {
|
||||||
|
textExpanderRef.current.removeEventListener(
|
||||||
|
'text-expander-change',
|
||||||
|
handleChange,
|
||||||
|
);
|
||||||
|
textExpanderRef.current.removeEventListener(
|
||||||
|
'text-expander-value',
|
||||||
|
handleValue,
|
||||||
|
);
|
||||||
|
textExpanderRef.current.removeEventListener(
|
||||||
|
'text-expander-committed',
|
||||||
|
handleCommited,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<text-expander ref={textExpanderRef} keys="@ # :">
|
||||||
|
<textarea
|
||||||
|
autoCapitalize="sentences"
|
||||||
|
autoComplete="on"
|
||||||
|
autoCorrect="on"
|
||||||
|
spellCheck="true"
|
||||||
|
dir="auto"
|
||||||
|
rows="6"
|
||||||
|
cols="50"
|
||||||
|
{...textareaProps}
|
||||||
|
ref={ref}
|
||||||
|
name="status"
|
||||||
|
value={text}
|
||||||
|
onInput={(e) => {
|
||||||
|
const { scrollHeight, offsetHeight, clientHeight, value } = e.target;
|
||||||
|
setText(value);
|
||||||
|
const offset = offsetHeight - clientHeight;
|
||||||
|
e.target.style.height = value ? scrollHeight + offset + 'px' : null;
|
||||||
|
props.onInput?.(e);
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
height: '4em',
|
||||||
|
maxHeight: `${maxCharacters / 50}em`,
|
||||||
|
'--text-weight': (1 + charCount / 140).toFixed(1) || 1,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</text-expander>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
function CharCountMeter({ maxCharacters = 500 }) {
|
||||||
|
const snapStates = useSnapshot(states);
|
||||||
|
const charCount = snapStates.composerCharacterCount;
|
||||||
|
const leftChars = maxCharacters - charCount;
|
||||||
|
if (charCount <= maxCharacters / 2) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<meter
|
||||||
|
class={`donut ${
|
||||||
|
leftChars <= -10
|
||||||
|
? 'explode'
|
||||||
|
: leftChars <= 0
|
||||||
|
? 'danger'
|
||||||
|
: leftChars <= 20
|
||||||
|
? 'warning'
|
||||||
|
: ''
|
||||||
|
}`}
|
||||||
|
value={charCount}
|
||||||
|
max={maxCharacters}
|
||||||
|
data-left={leftChars}
|
||||||
|
style={{
|
||||||
|
'--percentage': (charCount / maxCharacters) * 100,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function MediaAttachment({
|
function MediaAttachment({
|
||||||
attachment,
|
attachment,
|
||||||
disabled,
|
disabled,
|
||||||
|
@ -1220,16 +1293,6 @@ function encodeHTML(str) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// https://github.com/mastodon/mastodon/blob/c4a429ed47e85a6bbf0d470a41cc2f64cf120c19/app/javascript/mastodon/features/compose/util/counter.js
|
|
||||||
const urlRegexObj = new RegExp(urlRegex.source, urlRegex.flags);
|
|
||||||
const usernameRegex = /(^|[^\/\w])@(([a-z0-9_]+)@[a-z0-9\.\-]+[a-z0-9]+)/gi;
|
|
||||||
const urlPlaceholder = '$2xxxxxxxxxxxxxxxxxxxxxxx';
|
|
||||||
function countableText(inputText) {
|
|
||||||
return inputText
|
|
||||||
.replace(urlRegexObj, urlPlaceholder)
|
|
||||||
.replace(usernameRegex, '$1@$3');
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeNullUndefined(obj) {
|
function removeNullUndefined(obj) {
|
||||||
for (let key in obj) {
|
for (let key in obj) {
|
||||||
if (obj[key] === null || obj[key] === undefined) {
|
if (obj[key] === null || obj[key] === undefined) {
|
||||||
|
|
|
@ -18,4 +18,5 @@ export default proxy({
|
||||||
showCompose: false,
|
showCompose: false,
|
||||||
showSettings: false,
|
showSettings: false,
|
||||||
showAccount: false,
|
showAccount: false,
|
||||||
|
composeCharacterCount: 0,
|
||||||
});
|
});
|
||||||
|
|
Loading…
Add table
Reference in a new issue