From 825011fdbb609f5dfe58db59de892676fc7586de Mon Sep 17 00:00:00 2001 From: Mary Date: Wed, 17 Jan 2024 19:01:05 +0700 Subject: [PATCH 01/27] feat: new web textinput --- package.json | 1 + .../composer/text-input/TextInput.web.old.tsx | 243 +++++++++++++ .../com/composer/text-input/TextInput.web.tsx | 333 +++++++----------- .../composer/text-input/web/styles/style.css | 45 +++ yarn.lock | 26 ++ 5 files changed, 444 insertions(+), 204 deletions(-) create mode 100644 src/view/com/composer/text-input/TextInput.web.old.tsx create mode 100644 src/view/com/composer/text-input/web/styles/style.css diff --git a/package.json b/package.json index 517c328c10..379355a6e8 100644 --- a/package.json +++ b/package.json @@ -167,6 +167,7 @@ "react-native-web-webview": "^1.0.2", "react-native-webview": "13.6.4", "react-responsive": "^9.0.2", + "react-textarea-autosize": "^8.5.3", "rn-fetch-blob": "^0.12.0", "sentry-expo": "~7.0.1", "tippy.js": "^6.3.7", diff --git a/src/view/com/composer/text-input/TextInput.web.old.tsx b/src/view/com/composer/text-input/TextInput.web.old.tsx new file mode 100644 index 0000000000..ec3a042a3e --- /dev/null +++ b/src/view/com/composer/text-input/TextInput.web.old.tsx @@ -0,0 +1,243 @@ +import React from 'react' +import {StyleSheet, View} from 'react-native' +import {RichText, AppBskyRichtextFacet} from '@atproto/api' +import EventEmitter from 'eventemitter3' +import {useEditor, EditorContent, JSONContent} from '@tiptap/react' +import {Document} from '@tiptap/extension-document' +import History from '@tiptap/extension-history' +import Hardbreak from '@tiptap/extension-hard-break' +import {Mention} from '@tiptap/extension-mention' +import {Paragraph} from '@tiptap/extension-paragraph' +import {Placeholder} from '@tiptap/extension-placeholder' +import {Text} from '@tiptap/extension-text' +import isEqual from 'lodash.isequal' +import {createSuggestion} from './web/Autocomplete' +import {useColorSchemeStyle} from 'lib/hooks/useColorSchemeStyle' +import {isUriImage, blobToDataUri} from 'lib/media/util' +import {Emoji} from './web/EmojiPicker.web' +import {LinkDecorator} from './web/LinkDecorator' +import {generateJSON} from '@tiptap/html' +import {useActorAutocompleteFn} from '#/state/queries/actor-autocomplete' + +export interface TextInputRef { + focus: () => void + blur: () => void + getCursorPosition: () => DOMRect | undefined +} + +interface TextInputProps { + richtext: RichText + placeholder: string + suggestedLinks: Set + setRichText: (v: RichText | ((v: RichText) => RichText)) => void + onPhotoPasted: (uri: string) => void + onPressPublish: (richtext: RichText) => Promise + onSuggestedLinksChanged: (uris: Set) => void + onError: (err: string) => void +} + +export const textInputWebEmitter = new EventEmitter() + +export const TextInput = React.forwardRef(function TextInputImpl( + { + richtext, + placeholder, + suggestedLinks, + setRichText, + onPhotoPasted, + onPressPublish, + onSuggestedLinksChanged, + }: // onError, TODO + TextInputProps, + ref, +) { + const autocomplete = useActorAutocompleteFn() + + const modeClass = useColorSchemeStyle('ProseMirror-light', 'ProseMirror-dark') + const extensions = React.useMemo( + () => [ + Document, + LinkDecorator, + Mention.configure({ + HTMLAttributes: { + class: 'mention', + }, + suggestion: createSuggestion({autocomplete}), + }), + Paragraph, + Placeholder.configure({ + placeholder, + }), + Text, + History, + Hardbreak, + ], + [autocomplete, placeholder], + ) + + React.useEffect(() => { + textInputWebEmitter.addListener('publish', onPressPublish) + return () => { + textInputWebEmitter.removeListener('publish', onPressPublish) + } + }, [onPressPublish]) + React.useEffect(() => { + textInputWebEmitter.addListener('photo-pasted', onPhotoPasted) + return () => { + textInputWebEmitter.removeListener('photo-pasted', onPhotoPasted) + } + }, [onPhotoPasted]) + + const editor = useEditor( + { + extensions, + editorProps: { + attributes: { + class: modeClass, + }, + handlePaste: (_, event) => { + const items = event.clipboardData?.items + + if (items === undefined) { + return + } + + getImageFromUri(items, (uri: string) => { + textInputWebEmitter.emit('photo-pasted', uri) + }) + }, + handleKeyDown: (_, event) => { + if ((event.metaKey || event.ctrlKey) && event.code === 'Enter') { + textInputWebEmitter.emit('publish') + return true + } + }, + }, + content: generateJSON(richtext.text.toString(), extensions), + autofocus: 'end', + editable: true, + injectCSS: true, + onCreate({editor: editorProp}) { + // HACK + // the 'enter' animation sometimes causes autofocus to fail + // (see Composer.web.tsx in shell) + // so we wait 200ms (the anim is 150ms) and then focus manually + // -prf + setTimeout(() => { + editorProp.chain().focus('end').run() + }, 200) + }, + onUpdate({editor: editorProp}) { + const json = editorProp.getJSON() + + const newRt = new RichText({text: editorJsonToText(json).trimEnd()}) + newRt.detectFacetsWithoutResolution() + setRichText(newRt) + + const set: Set = new Set() + + if (newRt.facets) { + for (const facet of newRt.facets) { + for (const feature of facet.features) { + if (AppBskyRichtextFacet.isLink(feature)) { + set.add(feature.uri) + } + } + } + } + + if (!isEqual(set, suggestedLinks)) { + onSuggestedLinksChanged(set) + } + }, + }, + [modeClass], + ) + + const onEmojiInserted = React.useCallback( + (emoji: Emoji) => { + editor?.chain().focus().insertContent(emoji.native).run() + }, + [editor], + ) + React.useEffect(() => { + textInputWebEmitter.addListener('emoji-inserted', onEmojiInserted) + return () => { + textInputWebEmitter.removeListener('emoji-inserted', onEmojiInserted) + } + }, [onEmojiInserted]) + + React.useImperativeHandle(ref, () => ({ + focus: () => {}, // TODO + blur: () => {}, // TODO + getCursorPosition: () => { + const pos = editor?.state.selection.$anchor.pos + return pos ? editor?.view.coordsAtPos(pos) : undefined + }, + })) + + return ( + + + + ) +}) + +function editorJsonToText(json: JSONContent): string { + let text = '' + if (json.type === 'doc' || json.type === 'paragraph') { + if (json.content?.length) { + for (const node of json.content) { + text += editorJsonToText(node) + } + } + text += '\n' + } else if (json.type === 'hardBreak') { + text += '\n' + } else if (json.type === 'text') { + text += json.text || '' + } else if (json.type === 'mention') { + text += `@${json.attrs?.id || ''}` + } + return text +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + alignSelf: 'flex-start', + padding: 5, + marginLeft: 8, + marginBottom: 10, + }, +}) + +function getImageFromUri( + items: DataTransferItemList, + callback: (uri: string) => void, +) { + for (let index = 0; index < items.length; index++) { + const item = items[index] + const {kind, type} = item + + if (type === 'text/plain') { + item.getAsString(async itemString => { + if (isUriImage(itemString)) { + const response = await fetch(itemString) + const blob = await response.blob() + blobToDataUri(blob).then(callback, err => console.error(err)) + } + }) + } + + if (kind === 'file') { + const file = item.getAsFile() + + if (file instanceof Blob) { + blobToDataUri(new Blob([file], {type: item.type})).then(callback, err => + console.error(err), + ) + } + } + } +} diff --git a/src/view/com/composer/text-input/TextInput.web.tsx b/src/view/com/composer/text-input/TextInput.web.tsx index ec3a042a3e..55104727ff 100644 --- a/src/view/com/composer/text-input/TextInput.web.tsx +++ b/src/view/com/composer/text-input/TextInput.web.tsx @@ -1,25 +1,15 @@ import React from 'react' -import {StyleSheet, View} from 'react-native' -import {RichText, AppBskyRichtextFacet} from '@atproto/api' -import EventEmitter from 'eventemitter3' -import {useEditor, EditorContent, JSONContent} from '@tiptap/react' -import {Document} from '@tiptap/extension-document' -import History from '@tiptap/extension-history' -import Hardbreak from '@tiptap/extension-hard-break' -import {Mention} from '@tiptap/extension-mention' -import {Paragraph} from '@tiptap/extension-paragraph' -import {Placeholder} from '@tiptap/extension-placeholder' -import {Text} from '@tiptap/extension-text' + +import {EventEmitter} from 'eventemitter3' import isEqual from 'lodash.isequal' -import {createSuggestion} from './web/Autocomplete' -import {useColorSchemeStyle} from 'lib/hooks/useColorSchemeStyle' -import {isUriImage, blobToDataUri} from 'lib/media/util' -import {Emoji} from './web/EmojiPicker.web' -import {LinkDecorator} from './web/LinkDecorator' -import {generateJSON} from '@tiptap/html' -import {useActorAutocompleteFn} from '#/state/queries/actor-autocomplete' - -export interface TextInputRef { + +import TextareaAutosize from 'react-textarea-autosize' + +import {AppBskyRichtextFacet, RichText} from '@atproto/api' + +import './web/styles/style.css' + +interface TextInputRef { focus: () => void blur: () => void getCursorPosition: () => DOMRect | undefined @@ -38,206 +28,141 @@ interface TextInputProps { export const textInputWebEmitter = new EventEmitter() -export const TextInput = React.forwardRef(function TextInputImpl( - { - richtext, - placeholder, - suggestedLinks, - setRichText, - onPhotoPasted, - onPressPublish, - onSuggestedLinksChanged, - }: // onError, TODO - TextInputProps, - ref, -) { - const autocomplete = useActorAutocompleteFn() - - const modeClass = useColorSchemeStyle('ProseMirror-light', 'ProseMirror-dark') - const extensions = React.useMemo( - () => [ - Document, - LinkDecorator, - Mention.configure({ - HTMLAttributes: { - class: 'mention', - }, - suggestion: createSuggestion({autocomplete}), - }), - Paragraph, - Placeholder.configure({ - placeholder, - }), - Text, - History, - Hardbreak, - ], - [autocomplete, placeholder], - ) - - React.useEffect(() => { - textInputWebEmitter.addListener('publish', onPressPublish) - return () => { - textInputWebEmitter.removeListener('publish', onPressPublish) - } - }, [onPressPublish]) - React.useEffect(() => { - textInputWebEmitter.addListener('photo-pasted', onPhotoPasted) - return () => { - textInputWebEmitter.removeListener('photo-pasted', onPhotoPasted) - } - }, [onPhotoPasted]) - - const editor = useEditor( +export const TextInput = React.forwardRef( + function TextInputImpl( { - extensions, - editorProps: { - attributes: { - class: modeClass, - }, - handlePaste: (_, event) => { - const items = event.clipboardData?.items - - if (items === undefined) { - return - } - - getImageFromUri(items, (uri: string) => { - textInputWebEmitter.emit('photo-pasted', uri) - }) - }, - handleKeyDown: (_, event) => { - if ((event.metaKey || event.ctrlKey) && event.code === 'Enter') { - textInputWebEmitter.emit('publish') - return true - } - }, - }, - content: generateJSON(richtext.text.toString(), extensions), - autofocus: 'end', - editable: true, - injectCSS: true, - onCreate({editor: editorProp}) { - // HACK - // the 'enter' animation sometimes causes autofocus to fail - // (see Composer.web.tsx in shell) - // so we wait 200ms (the anim is 150ms) and then focus manually - // -prf - setTimeout(() => { - editorProp.chain().focus('end').run() - }, 200) + richtext, + placeholder, + suggestedLinks, + setRichText, + // onPhotoPasted, + // onPressPublish, + onSuggestedLinksChanged, + // onError, + }, + ref, + ) { + const overlayRef = React.useRef(null) + const inputRef = React.useRef(null) + + const [_cursor, setCursor] = React.useState() + + React.useImperativeHandle(ref, () => ({ + focus: () => inputRef.current?.focus(), + blur: () => inputRef.current?.blur(), + getCursorPosition: () => { + // TODO + return undefined }, - onUpdate({editor: editorProp}) { - const json = editorProp.getJSON() - - const newRt = new RichText({text: editorJsonToText(json).trimEnd()}) + })) + + const textOverlay = React.useMemo(() => { + return ( +
+ {Array.from(richtext.segments(), (segment, index) => { + const isLink = segment.facet + ? !AppBskyRichtextFacet.isTag(segment.facet.features[0]) + : false + + return ( + + {segment.text} + + ) + })} +
+ ) + }, [richtext]) + + const handleInputSelection = React.useCallback(() => { + const textInput = inputRef.current! + + const start = textInput.selectionStart + const end = textInput.selectionEnd + + setCursor(start === end ? start : undefined) + }, [inputRef]) + + const handleChange = React.useCallback( + (ev: React.ChangeEvent) => { + const newText = ev.target.value + const newRt = new RichText({text: newText}) newRt.detectFacetsWithoutResolution() + setRichText(newRt) - const set: Set = new Set() + // Gather suggested links + { + const set: Set = new Set() - if (newRt.facets) { - for (const facet of newRt.facets) { - for (const feature of facet.features) { - if (AppBskyRichtextFacet.isLink(feature)) { - set.add(feature.uri) + if (newRt.facets) { + for (const facet of newRt.facets) { + for (const feature of facet.features) { + if (AppBskyRichtextFacet.isLink(feature)) { + set.add(feature.uri) + } } } } + + if (!isEqual(set, suggestedLinks)) { + onSuggestedLinksChanged(set) + } } + }, + [setRichText, suggestedLinks, onSuggestedLinksChanged], + ) - if (!isEqual(set, suggestedLinks)) { - onSuggestedLinksChanged(set) + const handleKeyDown = React.useCallback( + (ev: React.KeyboardEvent) => { + const key = ev.key + + if (key === 'Backspace') { + setTimeout(handleInputSelection, 0) } }, - }, - [modeClass], - ) + [handleInputSelection], + ) - const onEmojiInserted = React.useCallback( - (emoji: Emoji) => { - editor?.chain().focus().insertContent(emoji.native).run() - }, - [editor], - ) - React.useEffect(() => { - textInputWebEmitter.addListener('emoji-inserted', onEmojiInserted) - return () => { - textInputWebEmitter.removeListener('emoji-inserted', onEmojiInserted) - } - }, [onEmojiInserted]) - - React.useImperativeHandle(ref, () => ({ - focus: () => {}, // TODO - blur: () => {}, // TODO - getCursorPosition: () => { - const pos = editor?.state.selection.$anchor.pos - return pos ? editor?.view.coordsAtPos(pos) : undefined - }, - })) - - return ( - - - - ) -}) - -function editorJsonToText(json: JSONContent): string { - let text = '' - if (json.type === 'doc' || json.type === 'paragraph') { - if (json.content?.length) { - for (const node of json.content) { - text += editorJsonToText(node) - } - } - text += '\n' - } else if (json.type === 'hardBreak') { - text += '\n' - } else if (json.type === 'text') { - text += json.text || '' - } else if (json.type === 'mention') { - text += `@${json.attrs?.id || ''}` - } - return text -} + React.useLayoutEffect(() => { + const textInput = inputRef.current! -const styles = StyleSheet.create({ - container: { - flex: 1, - alignSelf: 'flex-start', - padding: 5, - marginLeft: 8, - marginBottom: 10, - }, -}) - -function getImageFromUri( - items: DataTransferItemList, - callback: (uri: string) => void, -) { - for (let index = 0; index < items.length; index++) { - const item = items[index] - const {kind, type} = item - - if (type === 'text/plain') { - item.getAsString(async itemString => { - if (isUriImage(itemString)) { - const response = await fetch(itemString) - const blob = await response.blob() - blobToDataUri(blob).then(callback, err => console.error(err)) + const handleSelectionChange = () => { + if (document.activeElement !== textInput) { + return } - }) - } - if (kind === 'file') { - const file = item.getAsFile() + handleInputSelection() + } + + document.addEventListener('selectionchange', handleSelectionChange) - if (file instanceof Blob) { - blobToDataUri(new Blob([file], {type: item.type})).then(callback, err => - console.error(err), - ) + return () => { + document.removeEventListener('selectionchange', handleSelectionChange) } - } - } -} + }, [inputRef, handleInputSelection]) + + return ( +
+
+ {textOverlay} +
+ + +
+ ) + }, +) diff --git a/src/view/com/composer/text-input/web/styles/style.css b/src/view/com/composer/text-input/web/styles/style.css new file mode 100644 index 0000000000..735b4446de --- /dev/null +++ b/src/view/com/composer/text-input/web/styles/style.css @@ -0,0 +1,45 @@ +.rt-editor { + font-size: 18px; + line-height: 24px; + width: 100%; + position: relative; +} + +.rt-overlay { + position: absolute; + inset: 0; + z-index: 0; + white-space: pre-wrap; + overflow-wrap: break-word; +} +.rt-segment { +} +.rt-segment-text { +} +.rt-segment-link { + color: #0085ff; +} + +.rt-input { + caret-color: #fff; + color: transparent; + font: inherit; + line-height: inherit; + background: transparent; + width: 100%; + border: 0; + display: block; + position: relative; + z-index: 1; + resize: none; +} +.rt-input::placeholder { + color: #878788; + opacity: 1; +} + +.rt-overlay, +.rt-input { + padding: 5px; + margin: 0 0 10px 8px; +} diff --git a/yarn.lock b/yarn.lock index 760e0b331a..5520bcca6a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -18552,6 +18552,15 @@ react-test-renderer@18.2.0: react-shallow-renderer "^16.15.0" scheduler "^0.23.0" +react-textarea-autosize@^8.5.3: + version "8.5.3" + resolved "https://registry.yarnpkg.com/react-textarea-autosize/-/react-textarea-autosize-8.5.3.tgz#d1e9fe760178413891484847d3378706052dd409" + integrity sha512-XT1024o2pqCuZSuBt9FwHlaDeNtVrtCXu0Rnz88t1jUGheCLa3PhjE1GH8Ctm2axEtvdCl5SUHYschyQ0L5QHQ== + dependencies: + "@babel/runtime" "^7.20.13" + use-composed-ref "^1.3.0" + use-latest "^1.2.1" + react@18.2.0: version "18.2.0" resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5" @@ -20865,6 +20874,11 @@ use-callback-ref@^1.3.0: dependencies: tslib "^2.0.0" +use-composed-ref@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/use-composed-ref/-/use-composed-ref-1.3.0.tgz#3d8104db34b7b264030a9d916c5e94fbe280dbda" + integrity sha512-GLMG0Jc/jiKov/3Ulid1wbv3r54K9HlMW29IWcDFPEqFkSO2nS0MuefWgMJpeHQ9YJeXDL3ZUF+P3jdXlZX/cQ== + use-deep-compare@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/use-deep-compare/-/use-deep-compare-1.1.0.tgz#85580dde751f68400bf6ef7e043c7f986595cef8" @@ -20872,6 +20886,11 @@ use-deep-compare@^1.1.0: dependencies: dequal "1.0.0" +use-isomorphic-layout-effect@^1.1.1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz#497cefb13d863d687b08477d9e5a164ad8c1a6fb" + integrity sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA== + use-latest-callback@^0.1.5: version "0.1.6" resolved "https://registry.yarnpkg.com/use-latest-callback/-/use-latest-callback-0.1.6.tgz#3fa6e7babbb5f9bfa24b5094b22939e1e92ebcf6" @@ -20882,6 +20901,13 @@ use-latest-callback@^0.1.9: resolved "https://registry.yarnpkg.com/use-latest-callback/-/use-latest-callback-0.1.9.tgz#10191dc54257e65a8e52322127643a8940271e2a" integrity sha512-CL/29uS74AwreI/f2oz2hLTW7ZqVeV5+gxFeGudzQrgkCytrHw33G4KbnQOrRlAEzzAFXi7dDLMC9zhWcVpzmw== +use-latest@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/use-latest/-/use-latest-1.2.1.tgz#d13dfb4b08c28e3e33991546a2cee53e14038cf2" + integrity sha512-xA+AVm/Wlg3e2P/JiItTziwS7FK92LWrDB0p+hgXloIMuVCeJJ8v6f0eeHyPZaJrM+usM1FkFfbNCrJGs8A/zw== + dependencies: + use-isomorphic-layout-effect "^1.1.1" + use-sidecar@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/use-sidecar/-/use-sidecar-1.1.2.tgz#2f43126ba2d7d7e117aa5855e5d8f0276dfe73c2" From 2bce2a7739d9d209f14c4a7c2f54195dac7b0582 Mon Sep 17 00:00:00 2001 From: Mary Date: Wed, 17 Jan 2024 19:02:14 +0700 Subject: [PATCH 02/27] feat: partial autocomplete suggestions --- .../com/composer/text-input/TextInput.web.tsx | 91 ++++++++++++++++++- 1 file changed, 90 insertions(+), 1 deletion(-) diff --git a/src/view/com/composer/text-input/TextInput.web.tsx b/src/view/com/composer/text-input/TextInput.web.tsx index 55104727ff..44c0d355f5 100644 --- a/src/view/com/composer/text-input/TextInput.web.tsx +++ b/src/view/com/composer/text-input/TextInput.web.tsx @@ -28,6 +28,21 @@ interface TextInputProps { export const textInputWebEmitter = new EventEmitter() +const MENTION_AUTOCOMPLETE_RE = /(?<=^|\s)@([a-zA-Z0-9-.]+)$/ +const TRIM_MENTION_RE = /[.]+$/ + +const enum Suggestion { + MENTION, +} + +interface MatchedSuggestion { + type: Suggestion + range: Range | undefined + index: number + length: number + query: string +} + export const TextInput = React.forwardRef( function TextInputImpl( { @@ -45,7 +60,8 @@ export const TextInput = React.forwardRef( const overlayRef = React.useRef(null) const inputRef = React.useRef(null) - const [_cursor, setCursor] = React.useState() + const [cursor, setCursor] = React.useState() + const [_suggestion, setSuggestion] = React.useState() React.useImperativeHandle(ref, () => ({ focus: () => inputRef.current?.focus(), @@ -56,6 +72,56 @@ export const TextInput = React.forwardRef( }, })) + React.useEffect(() => { + if (cursor == null) { + setSuggestion(undefined) + return + } + + const text = richtext.text + const candidate = text.length === cursor ? text : text.slice(0, cursor) + + let match: RegExpExecArray | null + let type: Suggestion + + if ((match = MENTION_AUTOCOMPLETE_RE.exec(candidate))) { + type = Suggestion.MENTION + } else { + setSuggestion(undefined) + return + } + + const overlay = overlayRef.current! + + const start = match.index! + const length = match[0].length + + const matched = match[1].toLowerCase() + + const rangeStart = findNodePosition(overlay, start) + const rangeEnd = findNodePosition(overlay, start + length) + + let range: Range | undefined + if (rangeStart && rangeEnd) { + range = new Range() + range.setStart(rangeStart.node, rangeStart.position) + range.setEnd(rangeEnd.node, rangeEnd.position) + } + + const nextSuggestion: MatchedSuggestion = { + type: type, + range: range, + index: start, + length: length, + query: + type === Suggestion.MENTION + ? matched.replace(TRIM_MENTION_RE, '') + : matched, + } + + setSuggestion(nextSuggestion) + }, [richtext, cursor, overlayRef, setSuggestion]) + const textOverlay = React.useMemo(() => { return (
@@ -166,3 +232,26 @@ export const TextInput = React.forwardRef( ) }, ) + +const findNodePosition = ( + node: Node, + position: number, +): {node: Node; position: number} | undefined => { + if (node.nodeType === Node.TEXT_NODE) { + return {node, position} + } + + const children = node.childNodes + for (let idx = 0, len = children.length; idx < len; idx++) { + const child = children[idx] + const textContentLength = child.textContent!.length + + if (position <= textContentLength!) { + return findNodePosition(child, position) + } + + position -= textContentLength! + } + + return +} From 2d5a02390e01b81fa1cad2661abb8f2a0554b62f Mon Sep 17 00:00:00 2001 From: Mary Date: Thu, 18 Jan 2024 14:36:00 +0700 Subject: [PATCH 03/27] fix: prevent double-render on suggestion match --- src/view/com/composer/text-input/TextInput.web.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/view/com/composer/text-input/TextInput.web.tsx b/src/view/com/composer/text-input/TextInput.web.tsx index 44c0d355f5..939dc694f6 100644 --- a/src/view/com/composer/text-input/TextInput.web.tsx +++ b/src/view/com/composer/text-input/TextInput.web.tsx @@ -161,6 +161,7 @@ export const TextInput = React.forwardRef( newRt.detectFacetsWithoutResolution() setRichText(newRt) + handleInputSelection() // Gather suggested links { @@ -181,7 +182,12 @@ export const TextInput = React.forwardRef( } } }, - [setRichText, suggestedLinks, onSuggestedLinksChanged], + [ + setRichText, + suggestedLinks, + onSuggestedLinksChanged, + handleInputSelection, + ], ) const handleKeyDown = React.useCallback( From e77f48c1ce767b5e8f8a786c704fe9d1dd43b614 Mon Sep 17 00:00:00 2001 From: Mary Date: Thu, 18 Jan 2024 14:41:19 +0700 Subject: [PATCH 04/27] fix: light theme styling --- src/view/com/composer/text-input/TextInput.web.tsx | 6 +++++- src/view/com/composer/text-input/web/styles/style.css | 8 +++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/view/com/composer/text-input/TextInput.web.tsx b/src/view/com/composer/text-input/TextInput.web.tsx index 939dc694f6..1814e6ecab 100644 --- a/src/view/com/composer/text-input/TextInput.web.tsx +++ b/src/view/com/composer/text-input/TextInput.web.tsx @@ -9,6 +9,8 @@ import {AppBskyRichtextFacet, RichText} from '@atproto/api' import './web/styles/style.css' +import {useColorSchemeStyle} from 'lib/hooks/useColorSchemeStyle' + interface TextInputRef { focus: () => void blur: () => void @@ -60,6 +62,8 @@ export const TextInput = React.forwardRef( const overlayRef = React.useRef(null) const inputRef = React.useRef(null) + const modeClass = useColorSchemeStyle('rt-editor-light', 'rt-editor-dark') + const [cursor, setCursor] = React.useState() const [_suggestion, setSuggestion] = React.useState() @@ -220,7 +224,7 @@ export const TextInput = React.forwardRef( }, [inputRef, handleInputSelection]) return ( -
+
{textOverlay}
diff --git a/src/view/com/composer/text-input/web/styles/style.css b/src/view/com/composer/text-input/web/styles/style.css index 735b4446de..e443257f16 100644 --- a/src/view/com/composer/text-input/web/styles/style.css +++ b/src/view/com/composer/text-input/web/styles/style.css @@ -4,6 +4,12 @@ width: 100%; position: relative; } +.rt-editor-light { + --rt-caret-color: #000000; +} +.rt-editor-dark { + --rt-caret-color: #ffffff; +} .rt-overlay { position: absolute; @@ -21,7 +27,7 @@ } .rt-input { - caret-color: #fff; + caret-color: var(--rt-caret-color); color: transparent; font: inherit; line-height: inherit; From ce3102bb87a330e4dd2339c3f288e5efd8d6c246 Mon Sep 17 00:00:00 2001 From: Mary Date: Thu, 18 Jan 2024 14:48:46 +0700 Subject: [PATCH 05/27] fix: get emoji picker to work --- .../com/composer/text-input/TextInput.web.tsx | 33 ++++++++++++++----- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/src/view/com/composer/text-input/TextInput.web.tsx b/src/view/com/composer/text-input/TextInput.web.tsx index 1814e6ecab..10ff86a7f2 100644 --- a/src/view/com/composer/text-input/TextInput.web.tsx +++ b/src/view/com/composer/text-input/TextInput.web.tsx @@ -67,14 +67,31 @@ export const TextInput = React.forwardRef( const [cursor, setCursor] = React.useState() const [_suggestion, setSuggestion] = React.useState() - React.useImperativeHandle(ref, () => ({ - focus: () => inputRef.current?.focus(), - blur: () => inputRef.current?.blur(), - getCursorPosition: () => { - // TODO - return undefined - }, - })) + React.useImperativeHandle( + ref, + () => ({ + focus: () => inputRef.current?.focus(), + blur: () => inputRef.current?.blur(), + getCursorPosition: () => { + const input = inputRef.current! + const overlay = overlayRef.current! + + const rangeStart = findNodePosition(overlay, input.selectionStart) + const rangeEnd = findNodePosition(overlay, input.selectionEnd) + + if (!rangeStart || !rangeEnd) { + return undefined + } + + const range = new Range() + range.setStart(rangeStart.node, rangeStart.position) + range.setEnd(rangeEnd.node, rangeEnd.position) + + return range.getBoundingClientRect() + }, + }), + [inputRef, overlayRef], + ) React.useEffect(() => { if (cursor == null) { From 92e4836b00c1f39f16f2ab75cdbec5e5893a6edf Mon Sep 17 00:00:00 2001 From: Mary Date: Thu, 18 Jan 2024 14:56:33 +0700 Subject: [PATCH 06/27] fix: ensure text node is always rendered --- src/view/com/composer/text-input/TextInput.web.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/view/com/composer/text-input/TextInput.web.tsx b/src/view/com/composer/text-input/TextInput.web.tsx index 10ff86a7f2..1ac779a754 100644 --- a/src/view/com/composer/text-input/TextInput.web.tsx +++ b/src/view/com/composer/text-input/TextInput.web.tsx @@ -158,7 +158,7 @@ export const TextInput = React.forwardRef( `rt-segment ` + (!isLink ? `rt-segment-text` : `rt-segment-link`) }> - {segment.text} + {segment.text || '\u200b'} ) })} From 0fbc422e7097403431e2f5a24535f87b2977f702 Mon Sep 17 00:00:00 2001 From: Mary Date: Thu, 18 Jan 2024 14:57:02 +0700 Subject: [PATCH 07/27] feat: make emoji picker work --- src/view/com/composer/text-input/TextInput.web.tsx | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/view/com/composer/text-input/TextInput.web.tsx b/src/view/com/composer/text-input/TextInput.web.tsx index 1ac779a754..d435cb7d55 100644 --- a/src/view/com/composer/text-input/TextInput.web.tsx +++ b/src/view/com/composer/text-input/TextInput.web.tsx @@ -8,6 +8,7 @@ import TextareaAutosize from 'react-textarea-autosize' import {AppBskyRichtextFacet, RichText} from '@atproto/api' import './web/styles/style.css' +import {Emoji} from './web/EmojiPicker.web' import {useColorSchemeStyle} from 'lib/hooks/useColorSchemeStyle' @@ -233,10 +234,19 @@ export const TextInput = React.forwardRef( handleInputSelection() } + const handleEmojiInsert = (emoji: Emoji) => { + // execCommand('insertText') is the only way to insert text without + // destroying undo/redo history + textInput.focus() + document.execCommand('insertText', false, emoji.native) + } + document.addEventListener('selectionchange', handleSelectionChange) + textInputWebEmitter.addListener('emoji-inserted', handleEmojiInsert) return () => { document.removeEventListener('selectionchange', handleSelectionChange) + textInputWebEmitter.removeListener('emoji-inserted', handleEmojiInsert) } }, [inputRef, handleInputSelection]) From 930d53ffdad968d357e41d15e1e2cb6e41e61ad9 Mon Sep 17 00:00:00 2001 From: Mary Date: Thu, 18 Jan 2024 16:42:04 +0700 Subject: [PATCH 08/27] chore: explain why ZWS is inserted --- src/view/com/composer/text-input/TextInput.web.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/view/com/composer/text-input/TextInput.web.tsx b/src/view/com/composer/text-input/TextInput.web.tsx index d435cb7d55..a6e5ebb9a2 100644 --- a/src/view/com/composer/text-input/TextInput.web.tsx +++ b/src/view/com/composer/text-input/TextInput.web.tsx @@ -159,7 +159,12 @@ export const TextInput = React.forwardRef( `rt-segment ` + (!isLink ? `rt-segment-text` : `rt-segment-link`) }> - {segment.text || '\u200b'} + { + // We need React to commit a text node to DOM so we can select + // it for `getCursorPosition` above, without it, we can't open + // the emoji picker on an empty input. + segment.text || '\u200b' + } ) })} From 74100707b27feb3e4c8d4c6ec052e46fce470d2f Mon Sep 17 00:00:00 2001 From: Mary Date: Thu, 18 Jan 2024 16:56:33 +0700 Subject: [PATCH 09/27] feat: paste to add images --- .../com/composer/text-input/TextInput.web.tsx | 34 ++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/src/view/com/composer/text-input/TextInput.web.tsx b/src/view/com/composer/text-input/TextInput.web.tsx index a6e5ebb9a2..ead8efa8b9 100644 --- a/src/view/com/composer/text-input/TextInput.web.tsx +++ b/src/view/com/composer/text-input/TextInput.web.tsx @@ -11,6 +11,7 @@ import './web/styles/style.css' import {Emoji} from './web/EmojiPicker.web' import {useColorSchemeStyle} from 'lib/hooks/useColorSchemeStyle' +import {blobToDataUri, isUriImage} from 'lib/media/util' interface TextInputRef { focus: () => void @@ -53,7 +54,7 @@ export const TextInput = React.forwardRef( placeholder, suggestedLinks, setRichText, - // onPhotoPasted, + onPhotoPasted, // onPressPublish, onSuggestedLinksChanged, // onError, @@ -228,6 +229,36 @@ export const TextInput = React.forwardRef( [handleInputSelection], ) + const handlePaste = React.useCallback( + (ev: React.ClipboardEvent) => { + const items = ev.clipboardData?.items ?? [] + + for (let idx = 0, len = items.length; idx < len; idx++) { + const item = items[idx] + + if (item.kind === 'file' && item.type.startsWith('image/')) { + const file = item.getAsFile() + + if (file) { + blobToDataUri(file).then(onPhotoPasted, console.error) + } + } + + if (item.type === 'text/plain') { + item.getAsString(async str => { + if (isUriImage(str)) { + const response = await fetch(str) + const blob = await response.blob() + + blobToDataUri(blob).then(onPhotoPasted, console.error) + } + }) + } + } + }, + [onPhotoPasted], + ) + React.useLayoutEffect(() => { const textInput = inputRef.current! @@ -266,6 +297,7 @@ export const TextInput = React.forwardRef( // value={richtext.text} onChange={handleChange} onKeyDown={handleKeyDown} + onPaste={handlePaste} className="rt-input" placeholder={placeholder} minRows={6} From 0e456bf868c8c7886b23bbcaac66b94ff7fa8287 Mon Sep 17 00:00:00 2001 From: Mary Date: Thu, 18 Jan 2024 16:59:11 +0700 Subject: [PATCH 10/27] fix: onPressPublish doesn't accept parameters --- src/view/com/composer/text-input/TextInput.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/view/com/composer/text-input/TextInput.tsx b/src/view/com/composer/text-input/TextInput.tsx index 57bfd0a882..0d199d6193 100644 --- a/src/view/com/composer/text-input/TextInput.tsx +++ b/src/view/com/composer/text-input/TextInput.tsx @@ -41,7 +41,7 @@ interface TextInputProps extends ComponentProps { suggestedLinks: Set setRichText: (v: RichText | ((v: RichText) => RichText)) => void onPhotoPasted: (uri: string) => void - onPressPublish: (richtext: RichText) => Promise + onPressPublish: () => Promise onSuggestedLinksChanged: (uris: Set) => void onError: (err: string) => void } From b954b7a1e08ca6aed9010bcc45cede9ac8eb819f Mon Sep 17 00:00:00 2001 From: Mary Date: Thu, 18 Jan 2024 16:59:31 +0700 Subject: [PATCH 11/27] feat: Ctrl+Enter to submit --- src/view/com/composer/text-input/TextInput.web.tsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/view/com/composer/text-input/TextInput.web.tsx b/src/view/com/composer/text-input/TextInput.web.tsx index ead8efa8b9..aca2d8a7bc 100644 --- a/src/view/com/composer/text-input/TextInput.web.tsx +++ b/src/view/com/composer/text-input/TextInput.web.tsx @@ -25,7 +25,7 @@ interface TextInputProps { suggestedLinks: Set setRichText: (v: RichText | ((v: RichText) => RichText)) => void onPhotoPasted: (uri: string) => void - onPressPublish: (richtext: RichText) => Promise + onPressPublish: () => Promise onSuggestedLinksChanged: (uris: Set) => void onError: (err: string) => void } @@ -55,7 +55,7 @@ export const TextInput = React.forwardRef( suggestedLinks, setRichText, onPhotoPasted, - // onPressPublish, + onPressPublish, onSuggestedLinksChanged, // onError, }, @@ -224,9 +224,12 @@ export const TextInput = React.forwardRef( if (key === 'Backspace') { setTimeout(handleInputSelection, 0) + } else if (key === 'Enter' && (ev.ctrlKey || ev.metaKey)) { + ev.preventDefault() + onPressPublish() } }, - [handleInputSelection], + [handleInputSelection, onPressPublish], ) const handlePaste = React.useCallback( From 40a5014f28125c3c1e3fbbbb4c8ca67efae128d6 Mon Sep 17 00:00:00 2001 From: Mary Date: Thu, 18 Jan 2024 17:50:00 +0700 Subject: [PATCH 12/27] chore: remove old files --- .../composer/text-input/TextInput.web.old.tsx | 243 ------------------ 1 file changed, 243 deletions(-) delete mode 100644 src/view/com/composer/text-input/TextInput.web.old.tsx diff --git a/src/view/com/composer/text-input/TextInput.web.old.tsx b/src/view/com/composer/text-input/TextInput.web.old.tsx deleted file mode 100644 index ec3a042a3e..0000000000 --- a/src/view/com/composer/text-input/TextInput.web.old.tsx +++ /dev/null @@ -1,243 +0,0 @@ -import React from 'react' -import {StyleSheet, View} from 'react-native' -import {RichText, AppBskyRichtextFacet} from '@atproto/api' -import EventEmitter from 'eventemitter3' -import {useEditor, EditorContent, JSONContent} from '@tiptap/react' -import {Document} from '@tiptap/extension-document' -import History from '@tiptap/extension-history' -import Hardbreak from '@tiptap/extension-hard-break' -import {Mention} from '@tiptap/extension-mention' -import {Paragraph} from '@tiptap/extension-paragraph' -import {Placeholder} from '@tiptap/extension-placeholder' -import {Text} from '@tiptap/extension-text' -import isEqual from 'lodash.isequal' -import {createSuggestion} from './web/Autocomplete' -import {useColorSchemeStyle} from 'lib/hooks/useColorSchemeStyle' -import {isUriImage, blobToDataUri} from 'lib/media/util' -import {Emoji} from './web/EmojiPicker.web' -import {LinkDecorator} from './web/LinkDecorator' -import {generateJSON} from '@tiptap/html' -import {useActorAutocompleteFn} from '#/state/queries/actor-autocomplete' - -export interface TextInputRef { - focus: () => void - blur: () => void - getCursorPosition: () => DOMRect | undefined -} - -interface TextInputProps { - richtext: RichText - placeholder: string - suggestedLinks: Set - setRichText: (v: RichText | ((v: RichText) => RichText)) => void - onPhotoPasted: (uri: string) => void - onPressPublish: (richtext: RichText) => Promise - onSuggestedLinksChanged: (uris: Set) => void - onError: (err: string) => void -} - -export const textInputWebEmitter = new EventEmitter() - -export const TextInput = React.forwardRef(function TextInputImpl( - { - richtext, - placeholder, - suggestedLinks, - setRichText, - onPhotoPasted, - onPressPublish, - onSuggestedLinksChanged, - }: // onError, TODO - TextInputProps, - ref, -) { - const autocomplete = useActorAutocompleteFn() - - const modeClass = useColorSchemeStyle('ProseMirror-light', 'ProseMirror-dark') - const extensions = React.useMemo( - () => [ - Document, - LinkDecorator, - Mention.configure({ - HTMLAttributes: { - class: 'mention', - }, - suggestion: createSuggestion({autocomplete}), - }), - Paragraph, - Placeholder.configure({ - placeholder, - }), - Text, - History, - Hardbreak, - ], - [autocomplete, placeholder], - ) - - React.useEffect(() => { - textInputWebEmitter.addListener('publish', onPressPublish) - return () => { - textInputWebEmitter.removeListener('publish', onPressPublish) - } - }, [onPressPublish]) - React.useEffect(() => { - textInputWebEmitter.addListener('photo-pasted', onPhotoPasted) - return () => { - textInputWebEmitter.removeListener('photo-pasted', onPhotoPasted) - } - }, [onPhotoPasted]) - - const editor = useEditor( - { - extensions, - editorProps: { - attributes: { - class: modeClass, - }, - handlePaste: (_, event) => { - const items = event.clipboardData?.items - - if (items === undefined) { - return - } - - getImageFromUri(items, (uri: string) => { - textInputWebEmitter.emit('photo-pasted', uri) - }) - }, - handleKeyDown: (_, event) => { - if ((event.metaKey || event.ctrlKey) && event.code === 'Enter') { - textInputWebEmitter.emit('publish') - return true - } - }, - }, - content: generateJSON(richtext.text.toString(), extensions), - autofocus: 'end', - editable: true, - injectCSS: true, - onCreate({editor: editorProp}) { - // HACK - // the 'enter' animation sometimes causes autofocus to fail - // (see Composer.web.tsx in shell) - // so we wait 200ms (the anim is 150ms) and then focus manually - // -prf - setTimeout(() => { - editorProp.chain().focus('end').run() - }, 200) - }, - onUpdate({editor: editorProp}) { - const json = editorProp.getJSON() - - const newRt = new RichText({text: editorJsonToText(json).trimEnd()}) - newRt.detectFacetsWithoutResolution() - setRichText(newRt) - - const set: Set = new Set() - - if (newRt.facets) { - for (const facet of newRt.facets) { - for (const feature of facet.features) { - if (AppBskyRichtextFacet.isLink(feature)) { - set.add(feature.uri) - } - } - } - } - - if (!isEqual(set, suggestedLinks)) { - onSuggestedLinksChanged(set) - } - }, - }, - [modeClass], - ) - - const onEmojiInserted = React.useCallback( - (emoji: Emoji) => { - editor?.chain().focus().insertContent(emoji.native).run() - }, - [editor], - ) - React.useEffect(() => { - textInputWebEmitter.addListener('emoji-inserted', onEmojiInserted) - return () => { - textInputWebEmitter.removeListener('emoji-inserted', onEmojiInserted) - } - }, [onEmojiInserted]) - - React.useImperativeHandle(ref, () => ({ - focus: () => {}, // TODO - blur: () => {}, // TODO - getCursorPosition: () => { - const pos = editor?.state.selection.$anchor.pos - return pos ? editor?.view.coordsAtPos(pos) : undefined - }, - })) - - return ( - - - - ) -}) - -function editorJsonToText(json: JSONContent): string { - let text = '' - if (json.type === 'doc' || json.type === 'paragraph') { - if (json.content?.length) { - for (const node of json.content) { - text += editorJsonToText(node) - } - } - text += '\n' - } else if (json.type === 'hardBreak') { - text += '\n' - } else if (json.type === 'text') { - text += json.text || '' - } else if (json.type === 'mention') { - text += `@${json.attrs?.id || ''}` - } - return text -} - -const styles = StyleSheet.create({ - container: { - flex: 1, - alignSelf: 'flex-start', - padding: 5, - marginLeft: 8, - marginBottom: 10, - }, -}) - -function getImageFromUri( - items: DataTransferItemList, - callback: (uri: string) => void, -) { - for (let index = 0; index < items.length; index++) { - const item = items[index] - const {kind, type} = item - - if (type === 'text/plain') { - item.getAsString(async itemString => { - if (isUriImage(itemString)) { - const response = await fetch(itemString) - const blob = await response.blob() - blobToDataUri(blob).then(callback, err => console.error(err)) - } - }) - } - - if (kind === 'file') { - const file = item.getAsFile() - - if (file instanceof Blob) { - blobToDataUri(new Blob([file], {type: item.type})).then(callback, err => - console.error(err), - ) - } - } - } -} From 00a363233b09c86248d0bebac07cd65d0523b941 Mon Sep 17 00:00:00 2001 From: Mary Date: Thu, 18 Jan 2024 17:50:38 +0700 Subject: [PATCH 13/27] feat: autocomplete suggestions --- package.json | 2 + .../com/composer/text-input/TextInput.web.tsx | 59 ++-- .../composer/text-input/web/Autocomplete.tsx | 322 ++++++++---------- .../composer/text-input/web/styles/style.css | 4 + yarn.lock | 34 ++ 5 files changed, 222 insertions(+), 199 deletions(-) diff --git a/package.json b/package.json index 379355a6e8..9b80bae281 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "@emoji-mart/react": "^1.1.1", "@expo/html-elements": "^0.4.2", "@expo/webpack-config": "^19.0.0", + "@floating-ui/react-dom": "^2.0.6", "@fortawesome/fontawesome-svg-core": "^6.1.1", "@fortawesome/free-regular-svg-icons": "^6.1.1", "@fortawesome/free-solid-svg-icons": "^6.1.1", @@ -205,6 +206,7 @@ "@types/lodash.shuffle": "^4.2.7", "@types/psl": "^1.1.1", "@types/react-avatar-editor": "^13.0.0", + "@types/react-dom": "^18.2.18", "@types/react-responsive": "^8.0.5", "@types/react-test-renderer": "^17.0.1", "@typescript-eslint/eslint-plugin": "^5.48.2", diff --git a/src/view/com/composer/text-input/TextInput.web.tsx b/src/view/com/composer/text-input/TextInput.web.tsx index aca2d8a7bc..c9f343fe6c 100644 --- a/src/view/com/composer/text-input/TextInput.web.tsx +++ b/src/view/com/composer/text-input/TextInput.web.tsx @@ -9,6 +9,11 @@ import {AppBskyRichtextFacet, RichText} from '@atproto/api' import './web/styles/style.css' import {Emoji} from './web/EmojiPicker.web' +import { + Autocomplete, + AutocompleteRef, + MatchedSuggestion, +} from './web/Autocomplete' import {useColorSchemeStyle} from 'lib/hooks/useColorSchemeStyle' import {blobToDataUri, isUriImage} from 'lib/media/util' @@ -35,18 +40,6 @@ export const textInputWebEmitter = new EventEmitter() const MENTION_AUTOCOMPLETE_RE = /(?<=^|\s)@([a-zA-Z0-9-.]+)$/ const TRIM_MENTION_RE = /[.]+$/ -const enum Suggestion { - MENTION, -} - -interface MatchedSuggestion { - type: Suggestion - range: Range | undefined - index: number - length: number - query: string -} - export const TextInput = React.forwardRef( function TextInputImpl( { @@ -63,11 +56,12 @@ export const TextInput = React.forwardRef( ) { const overlayRef = React.useRef(null) const inputRef = React.useRef(null) + const autocompleteRef = React.useRef(null) const modeClass = useColorSchemeStyle('rt-editor-light', 'rt-editor-dark') const [cursor, setCursor] = React.useState() - const [_suggestion, setSuggestion] = React.useState() + const [suggestion, setSuggestion] = React.useState() React.useImperativeHandle( ref, @@ -105,10 +99,10 @@ export const TextInput = React.forwardRef( const candidate = text.length === cursor ? text : text.slice(0, cursor) let match: RegExpExecArray | null - let type: Suggestion + let type: MatchedSuggestion['type'] if ((match = MENTION_AUTOCOMPLETE_RE.exec(candidate))) { - type = Suggestion.MENTION + type = 'mention' } else { setSuggestion(undefined) return @@ -137,9 +131,7 @@ export const TextInput = React.forwardRef( index: start, length: length, query: - type === Suggestion.MENTION - ? matched.replace(TRIM_MENTION_RE, '') - : matched, + type === 'mention' ? matched.replace(TRIM_MENTION_RE, '') : matched, } setSuggestion(nextSuggestion) @@ -227,9 +219,11 @@ export const TextInput = React.forwardRef( } else if (key === 'Enter' && (ev.ctrlKey || ev.metaKey)) { ev.preventDefault() onPressPublish() + } else { + autocompleteRef.current?.handleKeyDown(ev) } }, - [handleInputSelection, onPressPublish], + [autocompleteRef, handleInputSelection, onPressPublish], ) const handlePaste = React.useCallback( @@ -262,6 +256,27 @@ export const TextInput = React.forwardRef( [onPhotoPasted], ) + const acceptSuggestion = React.useCallback( + (match: MatchedSuggestion, res: string) => { + let text: string + + if (match.type === 'mention') { + text = `@${res} ` + } else { + return + } + + const input = inputRef.current! + + input.focus() + input.selectionStart = match.index + input.selectionEnd = match.index + match.length + + document.execCommand('insertText', false, text) + }, + [inputRef], + ) + React.useLayoutEffect(() => { const textInput = inputRef.current! @@ -305,6 +320,12 @@ export const TextInput = React.forwardRef( placeholder={placeholder} minRows={6} /> + +
) }, diff --git a/src/view/com/composer/text-input/web/Autocomplete.tsx b/src/view/com/composer/text-input/web/Autocomplete.tsx index 76058fed34..43c4545fad 100644 --- a/src/view/com/composer/text-input/web/Autocomplete.tsx +++ b/src/view/com/composer/text-input/web/Autocomplete.tsx @@ -1,201 +1,163 @@ -import React, { - forwardRef, - useEffect, - useImperativeHandle, - useState, -} from 'react' +import React from 'react' +import ReactDOM from 'react-dom' import {Pressable, StyleSheet, View} from 'react-native' -import {ReactRenderer} from '@tiptap/react' -import tippy, {Instance as TippyInstance} from 'tippy.js' + import { - SuggestionOptions, - SuggestionProps, - SuggestionKeyDownProps, -} from '@tiptap/suggestion' -import {ActorAutocompleteFn} from '#/state/queries/actor-autocomplete' + autoUpdate, + flip, + offset, + shift, + useFloating, +} from '@floating-ui/react-dom' +import {Trans} from '@lingui/macro' + +import {useGrapheme} from '../hooks/useGrapheme' + import {usePalette} from 'lib/hooks/usePalette' -import {Text} from 'view/com/util/text/Text' import {UserAvatar} from 'view/com/util/UserAvatar' -import {useGrapheme} from '../hooks/useGrapheme' -import {Trans} from '@lingui/macro' +import {Text} from 'view/com/util/text/Text' +import {useActorAutocompleteQuery} from 'state/queries/actor-autocomplete' + +export interface MatchedSuggestion { + type: 'mention' + range: Range | undefined + index: number + length: number + query: string +} -interface MentionListRef { - onKeyDown: (props: SuggestionKeyDownProps) => boolean +interface AutocompleteProps { + match: MatchedSuggestion | undefined + onSelect: (match: MatchedSuggestion, handle: string) => void } -export function createSuggestion({ - autocomplete, -}: { - autocomplete: ActorAutocompleteFn -}): Omit { - return { - async items({query}) { - const suggestions = await autocomplete({query}) - return suggestions.slice(0, 8) - }, - - render: () => { - let component: ReactRenderer | undefined - let popup: TippyInstance[] | undefined - - return { - onStart: props => { - component = new ReactRenderer(MentionList, { - props, - editor: props.editor, - }) +export interface AutocompleteRef { + handleKeyDown: (ev: React.KeyboardEvent) => void +} - if (!props.clientRect) { - return - } - - // @ts-ignore getReferenceClientRect doesnt like that clientRect can return null -prf - popup = tippy('body', { - getReferenceClientRect: props.clientRect, - appendTo: () => document.body, - content: component.element, - showOnCreate: true, - interactive: true, - trigger: 'manual', - placement: 'bottom-start', - }) - }, +export const Autocomplete = React.forwardRef< + AutocompleteRef, + AutocompleteProps +>(function AutocompleteImpl({match, onSelect}, ref) { + const pal = usePalette('default') + const {getGraphemeString} = useGrapheme() + + const {refs, floatingStyles} = useFloating({ + elements: {reference: match?.range}, + placement: 'bottom-start', + middleware: [ + shift({padding: 12}), + flip({padding: 12}), + offset({mainAxis: 4}), + ], + whileElementsMounted: autoUpdate, + }) + + const seenMatch = React.useRef() + const {data: items, isFetching} = useActorAutocompleteQuery( + match ? match.query : '', + ) + + const [hidden, setHidden] = React.useState(false) + const [cursor, setCursor] = React.useState(0) + + React.useImperativeHandle( + ref, + () => ({ + handleKeyDown: ev => { + if (hidden || !match || !items || items.length < 1) { + return + } - onUpdate(props) { - component?.updateProps(props) + const key = ev.key - if (!props.clientRect) { - return - } + if (key === 'ArrowUp') { + ev.preventDefault() + setCursor(cursor <= 0 ? items.length - 1 : cursor - 1) + } else if (key === 'ArrowDown') { + ev.preventDefault() + setCursor((cursor >= items.length - 1 ? -1 : cursor) + 1) + } else if (key === 'Enter') { + const item = items[cursor] - popup?.[0]?.setProps({ - // @ts-ignore getReferenceClientRect doesnt like that clientRect can return null -prf - getReferenceClientRect: props.clientRect, - }) - }, + ev.preventDefault() + onSelect(match, item.handle) + } else if (key === 'Escape') { + ev.preventDefault() + ev.stopPropagation() - onKeyDown(props) { - if (props.event.key === 'Escape') { - popup?.[0]?.hide() - - return true - } + setHidden(true) + } + }, + }), + [hidden, match, items, cursor, setHidden, onSelect], + ) - return component?.ref?.onKeyDown(props) || false - }, + if (seenMatch.current !== match) { + seenMatch.current = match + setHidden(false) + setCursor(0) - onExit() { - popup?.[0]?.destroy() - component?.destroy() - }, - } - }, + return null } -} - -const MentionList = forwardRef( - function MentionListImpl(props: SuggestionProps, ref) { - const [selectedIndex, setSelectedIndex] = useState(0) - const pal = usePalette('default') - const {getGraphemeString} = useGrapheme() - - const selectItem = (index: number) => { - const item = props.items[index] - - if (item) { - props.command({id: item.handle}) - } - } - - const upHandler = () => { - setSelectedIndex( - (selectedIndex + props.items.length - 1) % props.items.length, - ) - } - - const downHandler = () => { - setSelectedIndex((selectedIndex + 1) % props.items.length) - } - - const enterHandler = () => { - selectItem(selectedIndex) - } - - useEffect(() => setSelectedIndex(0), [props.items]) - - useImperativeHandle(ref, () => ({ - onKeyDown: ({event}) => { - if (event.key === 'ArrowUp') { - upHandler() - return true - } - - if (event.key === 'ArrowDown') { - downHandler() - return true - } - if (event.key === 'Enter' || event.key === 'Tab') { - enterHandler() - return true - } + if (hidden || !match) { + return null + } - return false - }, - })) - - const {items} = props - - return ( -
- - {items.length > 0 ? ( - items.map((item, index) => { - const {name: displayName} = getGraphemeString( - item.displayName ?? item.handle, - 30, // Heuristic value; can be modified - ) - const isSelected = selectedIndex === index - - return ( - { - selectItem(index) - }} - accessibilityRole="button"> - - - - {displayName} - - - - @{item.handle} + return ReactDOM.createPortal( +
+ + {items && items.length > 0 ? ( + items.slice(0, 8).map((item, index) => { + const {name: displayName} = getGraphemeString( + item.displayName ?? item.handle, + 30, // Heuristic value; can be modified + ) + const isSelected = cursor === index + + return ( + { + onSelect(match!, item.handle) + }} + accessibilityRole="button"> + + + + {displayName} - - ) - }) - ) : ( - - No result - - )} - -
- ) - }, -) +
+ + @{item.handle} + + + ) + }) + ) : ( + + {isFetching ? Loading... : No result} + + )} + +
, + document.body, + ) +}) const styles = StyleSheet.create({ container: { diff --git a/src/view/com/composer/text-input/web/styles/style.css b/src/view/com/composer/text-input/web/styles/style.css index e443257f16..f641a03513 100644 --- a/src/view/com/composer/text-input/web/styles/style.css +++ b/src/view/com/composer/text-input/web/styles/style.css @@ -49,3 +49,7 @@ padding: 5px; margin: 0 0 10px 8px; } + +.rt-autocomplete { + z-index: 3; +} diff --git a/yarn.lock b/yarn.lock index 5520bcca6a..9b79c02bd5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3370,6 +3370,13 @@ dependencies: "@floating-ui/utils" "^0.1.1" +"@floating-ui/core@^1.5.3": + version "1.5.3" + resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.5.3.tgz#b6aa0827708d70971c8679a16cf680a515b8a52a" + integrity sha512-O0WKDOo0yhJuugCx6trZQj5jVJ9yR0ystG2JaNAemYUWce+pmM6WUEFIibnWyEJKdrDxhm75NoSRME35FNaM/Q== + dependencies: + "@floating-ui/utils" "^0.2.0" + "@floating-ui/dom@^1.3.0": version "1.5.1" resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-1.5.1.tgz#88b70defd002fe851f17b4a25efb2d3c04d7a8d7" @@ -3378,6 +3385,14 @@ "@floating-ui/core" "^1.4.1" "@floating-ui/utils" "^0.1.1" +"@floating-ui/dom@^1.5.4": + version "1.5.4" + resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-1.5.4.tgz#28df1e1cb373884224a463235c218dcbd81a16bb" + integrity sha512-jByEsHIY+eEdCjnTVu+E3ephzTOzkQ8hgUfGwos+bg7NlH33Zc5uO+QHz1mrQUOgIKKDD1RtS201P9NvAfq3XQ== + dependencies: + "@floating-ui/core" "^1.5.3" + "@floating-ui/utils" "^0.2.0" + "@floating-ui/react-dom@^2.0.0": version "2.0.1" resolved "https://registry.yarnpkg.com/@floating-ui/react-dom/-/react-dom-2.0.1.tgz#7972a4fc488a8c746cded3cfe603b6057c308a91" @@ -3385,11 +3400,23 @@ dependencies: "@floating-ui/dom" "^1.3.0" +"@floating-ui/react-dom@^2.0.6": + version "2.0.6" + resolved "https://registry.yarnpkg.com/@floating-ui/react-dom/-/react-dom-2.0.6.tgz#5ffcf40b6550817a973b54cdd443374f51ca7a5c" + integrity sha512-IB8aCRFxr8nFkdYZgH+Otd9EVQPJoynxeFRGTB8voPoZMRWo8XjYuCRgpI1btvuKY69XMiLnW+ym7zoBHM90Rw== + dependencies: + "@floating-ui/dom" "^1.5.4" + "@floating-ui/utils@^0.1.1": version "0.1.1" resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.1.1.tgz#1a5b1959a528e374e8037c4396c3e825d6cf4a83" integrity sha512-m0G6wlnhm/AX0H12IOWtK8gASEMffnX08RtKkCgTdHb9JpHKGloI7icFfLg9ZmQeavcvR0PKmzxClyuFPSjKWw== +"@floating-ui/utils@^0.2.0": + version "0.2.1" + resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.2.1.tgz#16308cea045f0fc777b6ff20a9f25474dd8293d2" + integrity sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q== + "@fortawesome/fontawesome-common-types@6.4.2": version "6.4.2" resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.4.2.tgz#1766039cad33f8ad87f9467b98e0d18fbc8f01c5" @@ -7500,6 +7527,13 @@ dependencies: "@types/react" "*" +"@types/react-dom@^18.2.18": + version "18.2.18" + resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.2.18.tgz#16946e6cd43971256d874bc3d0a72074bb8571dd" + integrity sha512-TJxDm6OfAX2KJWJdMEVTwWke5Sc/E/RlnPGvGfS0W7+6ocy2xhDVQVh/KvC2Uf7kACs+gDytdusDSdWfWkaNzw== + dependencies: + "@types/react" "*" + "@types/react-responsive@^8.0.5": version "8.0.5" resolved "https://registry.yarnpkg.com/@types/react-responsive/-/react-responsive-8.0.5.tgz#77769862d2a0711434feb972be08e3e6c334440a" From 23368a9fea5024cf11c102229d81780f3cf00cd4 Mon Sep 17 00:00:00 2001 From: Mary Date: Thu, 18 Jan 2024 17:52:34 +0700 Subject: [PATCH 14/27] chore: remove link decorator as well --- .../composer/text-input/web/LinkDecorator.ts | 105 ------------------ 1 file changed, 105 deletions(-) delete mode 100644 src/view/com/composer/text-input/web/LinkDecorator.ts diff --git a/src/view/com/composer/text-input/web/LinkDecorator.ts b/src/view/com/composer/text-input/web/LinkDecorator.ts deleted file mode 100644 index 19945de086..0000000000 --- a/src/view/com/composer/text-input/web/LinkDecorator.ts +++ /dev/null @@ -1,105 +0,0 @@ -/** - * TipTap is a stateful rich-text editor, which is extremely useful - * when you _want_ it to be stateful formatting such as bold and italics. - * - * However we also use "stateless" behaviors, specifically for URLs - * where the text itself drives the formatting. - * - * This plugin uses a regex to detect URIs and then applies - * link decorations (a with the "autolink") class. That avoids - * adding any stateful formatting to TipTap's document model. - * - * We then run the URI detection again when constructing the - * RichText object from TipTap's output and merge their features into - * the facet-set. - */ - -import {Mark} from '@tiptap/core' -import {Plugin, PluginKey} from '@tiptap/pm/state' -import {Node as ProsemirrorNode} from '@tiptap/pm/model' -import {Decoration, DecorationSet} from '@tiptap/pm/view' -import {isValidDomain} from 'lib/strings/url-helpers' - -export const LinkDecorator = Mark.create({ - name: 'link-decorator', - priority: 1000, - keepOnSplit: false, - inclusive() { - return true - }, - addProseMirrorPlugins() { - return [linkDecorator()] - }, -}) - -function getDecorations(doc: ProsemirrorNode) { - const decorations: Decoration[] = [] - - doc.descendants((node, pos) => { - if (node.isText && node.text) { - const textContent = node.textContent - - // links - iterateUris(textContent, (from, to) => { - decorations.push( - Decoration.inline(pos + from, pos + to, { - class: 'autolink', - }), - ) - }) - } - }) - - return DecorationSet.create(doc, decorations) -} - -function linkDecorator() { - const linkDecoratorPlugin: Plugin = new Plugin({ - key: new PluginKey('link-decorator'), - - state: { - init: (_, {doc}) => getDecorations(doc), - apply: (transaction, decorationSet) => { - if (transaction.docChanged) { - return getDecorations(transaction.doc) - } - return decorationSet.map(transaction.mapping, transaction.doc) - }, - }, - - props: { - decorations(state) { - return linkDecoratorPlugin.getState(state) - }, - }, - }) - return linkDecoratorPlugin -} - -function iterateUris(str: string, cb: (from: number, to: number) => void) { - let match - const re = - /(^|\s|\()((https?:\/\/[\S]+)|((?[a-z][a-z0-9]*(\.[a-z0-9]+)+)[\S]*))/gim - while ((match = re.exec(str))) { - let uri = match[2] - if (!uri.startsWith('http')) { - const domain = match.groups?.domain - if (!domain || !isValidDomain(domain)) { - continue - } - uri = `https://${uri}` - } - let from = str.indexOf(match[2], match.index) - let to = from + match[2].length + 1 - // strip ending puncuation - if (/[.,;!?]$/.test(uri)) { - uri = uri.slice(0, -1) - to-- - } - if (/[)]$/.test(uri) && !uri.includes('(')) { - uri = uri.slice(0, -1) - to-- - } - cb(from, to) - } -} From 878668257364745bf919a67f761aa6a6c4a6020b Mon Sep 17 00:00:00 2001 From: Mary Date: Thu, 18 Jan 2024 17:54:34 +0700 Subject: [PATCH 15/27] chore: remove just about the rest --- bskyweb/templates/base.html | 38 -- package.json | 16 +- .../composer/text-input/web/styles/style.css | 3 + web/index.html | 38 -- yarn.lock | 412 +----------------- 5 files changed, 7 insertions(+), 500 deletions(-) diff --git a/bskyweb/templates/base.html b/bskyweb/templates/base.html index 57ad064f8c..bf411181f0 100644 --- a/bskyweb/templates/base.html +++ b/bskyweb/templates/base.html @@ -98,44 +98,6 @@ scrollbar-gutter: stable both-edges; } - /* ProseMirror */ - .ProseMirror { - font: 18px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; - min-height: 140px; - } - .ProseMirror-dark { - color: white; - } - .ProseMirror p { - margin: 0; - } - .ProseMirror p.is-editor-empty:first-child::before { - color: #8d8e96; - content: attr(data-placeholder); - float: left; - height: 0; - pointer-events: none; - } - .ProseMirror .mention { - color: #0085ff; - } - .ProseMirror a, - .ProseMirror .autolink { - color: #0085ff; - } - /* OLLIE: TODO -- this is not accessible */ - /* Remove focus state on inputs */ - .ProseMirror-focused { - outline: 0; - } - textarea:focus, - input:focus { - outline: 0; - } - .tippy-content .items { - width: fit-content; - } - /* Tooltips */ [data-tooltip] { position: relative; diff --git a/package.json b/package.json index 9b80bae281..5f0d47fdd4 100644 --- a/package.json +++ b/package.json @@ -72,18 +72,6 @@ "@segment/sovran-react-native": "^0.4.5", "@sentry/react-native": "5.5.0", "@tanstack/react-query": "^5.8.1", - "@tiptap/core": "^2.0.0-beta.220", - "@tiptap/extension-document": "^2.0.0-beta.220", - "@tiptap/extension-hard-break": "^2.0.3", - "@tiptap/extension-history": "^2.0.3", - "@tiptap/extension-mention": "^2.0.0-beta.220", - "@tiptap/extension-paragraph": "^2.0.0-beta.220", - "@tiptap/extension-placeholder": "^2.0.0-beta.220", - "@tiptap/extension-text": "^2.0.0-beta.220", - "@tiptap/html": "^2.1.11", - "@tiptap/pm": "^2.0.0-beta.220", - "@tiptap/react": "^2.0.0-beta.220", - "@tiptap/suggestion": "^2.0.0-beta.220", "@types/node": "^18.16.2", "@zxing/text-encoding": "^0.9.0", "array.prototype.findlast": "^1.2.3", @@ -171,7 +159,6 @@ "react-textarea-autosize": "^8.5.3", "rn-fetch-blob": "^0.12.0", "sentry-expo": "~7.0.1", - "tippy.js": "^6.3.7", "tlds": "^1.234.0", "use-deep-compare": "^1.1.0", "zeego": "^1.6.2", @@ -245,8 +232,7 @@ "webpack-dev-server": "^4.11.1" }, "resolutions": { - "@types/react": "^18", - "**/zeed-dom": "0.10.9" + "@types/react": "^18" }, "jest": { "preset": "jest-expo/ios", diff --git a/src/view/com/composer/text-input/web/styles/style.css b/src/view/com/composer/text-input/web/styles/style.css index f641a03513..f1d3bdc9be 100644 --- a/src/view/com/composer/text-input/web/styles/style.css +++ b/src/view/com/composer/text-input/web/styles/style.css @@ -39,6 +39,9 @@ z-index: 1; resize: none; } +.rt-input:focus { + outline: none; +} .rt-input::placeholder { color: #878788; opacity: 1; diff --git a/web/index.html b/web/index.html index 5a56e5f765..323f546475 100644 --- a/web/index.html +++ b/web/index.html @@ -102,44 +102,6 @@ scrollbar-gutter: stable both-edges; } - /* ProseMirror */ - .ProseMirror { - font: 18px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; - min-height: 140px; - } - .ProseMirror-dark { - color: white; - } - .ProseMirror p { - margin: 0; - } - .ProseMirror p.is-editor-empty:first-child::before { - color: #8d8e96; - content: attr(data-placeholder); - float: left; - height: 0; - pointer-events: none; - } - .ProseMirror .mention { - color: #0085ff; - } - .ProseMirror a, - .ProseMirror .autolink { - color: #0085ff; - } - /* OLLIE: TODO -- this is not accessible */ - /* Remove focus state on inputs */ - .ProseMirror-focused { - outline: 0; - } - textarea:focus, - input:focus { - outline: 0; - } - .tippy-content .items { - width: fit-content; - } - /* Tooltips */ [data-tooltip] { position: relative; diff --git a/yarn.lock b/yarn.lock index 9b79c02bd5..d0bd7e248b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4311,11 +4311,6 @@ schema-utils "^3.0.0" source-map "^0.7.3" -"@popperjs/core@^2.9.0": - version "2.11.8" - resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.8.tgz#6b79032e760a0899cd4204710beede972a3a185f" - integrity sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A== - "@radix-ui/primitive@1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@radix-ui/primitive/-/primitive-1.0.1.tgz#e46f9958b35d10e9f6dc71c497305c22e3e55dbd" @@ -5089,37 +5084,6 @@ dependencies: nanoid "^3.1.23" -"@remirror/core-constants@^2.0.2": - version "2.0.2" - resolved "https://registry.yarnpkg.com/@remirror/core-constants/-/core-constants-2.0.2.tgz#f05eccdc69e3a65e7d524b52548f567904a11a1a" - integrity sha512-dyHY+sMF0ihPus3O27ODd4+agdHMEmuRdyiZJ2CCWjPV5UFmn17ZbElvk6WOGVE4rdCJKZQCrPV2BcikOMLUGQ== - -"@remirror/core-helpers@^3.0.0": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@remirror/core-helpers/-/core-helpers-3.0.0.tgz#3a35c2346bc23ebc3cee585b7840b5567755c5f1" - integrity sha512-tusEgQJIqg4qKj6HSBUFcyRnWnziw3neh4T9wOmsPGHFC3w9kl5KSrDb9UAgE8uX6y32FnS7vJ955mWOl3n50A== - dependencies: - "@remirror/core-constants" "^2.0.2" - "@remirror/types" "^1.0.1" - "@types/object.omit" "^3.0.0" - "@types/object.pick" "^1.3.2" - "@types/throttle-debounce" "^2.1.0" - case-anything "^2.1.13" - dash-get "^1.0.2" - deepmerge "^4.3.1" - fast-deep-equal "^3.1.3" - make-error "^1.3.6" - object.omit "^3.0.0" - object.pick "^1.3.0" - throttle-debounce "^3.0.1" - -"@remirror/types@^1.0.1": - version "1.0.1" - resolved "https://registry.yarnpkg.com/@remirror/types/-/types-1.0.1.tgz#768502497a0fbbc23338a1586b893f729310cf70" - integrity sha512-VlZQxwGnt1jtQ18D6JqdIF+uFZo525WEqrfp9BOc3COPpK4+AWCgdnAWL+ho6imWcoINlGjR/+3b6y5C1vBVEA== - dependencies: - type-fest "^2.19.0" - "@remix-run/node@^1.19.3": version "1.19.3" resolved "https://registry.yarnpkg.com/@remix-run/node/-/node-1.19.3.tgz#d27e2f742fc45379525cb3fca466a883ca06d6c9" @@ -7023,104 +6987,6 @@ dependencies: pretty-format "^29.0.0" -"@tiptap/core@^2.0.0-beta.220": - version "2.1.6" - resolved "https://registry.yarnpkg.com/@tiptap/core/-/core-2.1.6.tgz#681350ebbdc429850059f43d4aa31181b78293f0" - integrity sha512-gm8n1oiBhSP6CDhalmmWwLD7yzIUqJJ246/t8rY3o+HJ/I+p0rqCx0mPvMiwcIBmYX8tUCVz7mb9aSFUu/umOQ== - -"@tiptap/extension-bubble-menu@^2.1.6": - version "2.1.6" - resolved "https://registry.yarnpkg.com/@tiptap/extension-bubble-menu/-/extension-bubble-menu-2.1.6.tgz#23b2664963a7347f864a7255be83592927268399" - integrity sha512-13YDJB19xbDL/SZaPs8NvUAA+w5MIWugP8ByKQeIlL8vlcbiJjqoT77YP6v300DtFyVrnLo/iMJh9RMB4NOnwg== - dependencies: - tippy.js "^6.3.7" - -"@tiptap/extension-document@^2.0.0-beta.220": - version "2.1.6" - resolved "https://registry.yarnpkg.com/@tiptap/extension-document/-/extension-document-2.1.6.tgz#91b1239d4eaa3c12cb382fc9805eedd3d32b1b7c" - integrity sha512-econFqLeQR8pe0xv7kjw6ZPRhcNXGrNi9854celX0lhqTqtBxvU6nWHzUDzoq/lmnXYgpFTPv42AwUEspvpwdw== - -"@tiptap/extension-floating-menu@^2.1.6": - version "2.1.6" - resolved "https://registry.yarnpkg.com/@tiptap/extension-floating-menu/-/extension-floating-menu-2.1.6.tgz#df5ddcafc70981028d8869bc0269f3bb850fdde4" - integrity sha512-Xy4esdjsZlgNxMbBC6+wLoiTfqaqFjuFquqcYEPqzgBizYa15Ww6wIx5+h2K+hzyJkSPI7ZX/rPjKXML8lNteQ== - dependencies: - tippy.js "^6.3.7" - -"@tiptap/extension-hard-break@^2.0.3": - version "2.1.6" - resolved "https://registry.yarnpkg.com/@tiptap/extension-hard-break/-/extension-hard-break-2.1.6.tgz#7ddd962b3d936267b6ea84c34f6b9369447be933" - integrity sha512-znFYceEFbrgxhHZF+/wNQlAn3MWG9/VRqQAFxPGne0csewibKZRwZbeSYZQ16x1vSAlAQsKhIaAst/na/2H8LA== - -"@tiptap/extension-history@^2.0.3": - version "2.1.6" - resolved "https://registry.yarnpkg.com/@tiptap/extension-history/-/extension-history-2.1.6.tgz#799d7412c270e29b1fcaa08cd8343724b88eb6a6" - integrity sha512-ltHz9cW3bWi7Z3m960F5eLPAqZDBNOpUP31t9YdKqhyxA16eygryj1USVeus9DX5OBoW79I8EecFAuRo3Rymlw== - -"@tiptap/extension-mention@^2.0.0-beta.220": - version "2.1.6" - resolved "https://registry.yarnpkg.com/@tiptap/extension-mention/-/extension-mention-2.1.6.tgz#70ee72f917c11c6f2542e9cffc8afb38c3719749" - integrity sha512-GgoiCRhcpAv6wH7vHPFxa3f+vhiicGMqwJo+ZKT0VdyegHfHfMVRIN57sTV8R9/ZXCAjL1smqwLhF+PlWheN2A== - -"@tiptap/extension-paragraph@^2.0.0-beta.220": - version "2.1.6" - resolved "https://registry.yarnpkg.com/@tiptap/extension-paragraph/-/extension-paragraph-2.1.6.tgz#c0b76f9242e823ccbeb1ba6af9df6a09ee971044" - integrity sha512-k0QSIaJPVgTn9+X2580JFCjV2RCH1Fo+gPodABDnjunfoUVSjuq0rlILEtTuha3evlS6kDKiz7lk7pIoCo36Cw== - -"@tiptap/extension-placeholder@^2.0.0-beta.220": - version "2.1.6" - resolved "https://registry.yarnpkg.com/@tiptap/extension-placeholder/-/extension-placeholder-2.1.6.tgz#86524a01c5b18de8932f4f5a34102603f1dacdfd" - integrity sha512-M6C80FnbDPiZWVGFIVVOUMbqNUMhXRzlJr7uwUWP98OJfj3Du4pk8mF5Lo5MsWH3C/XW3YRbqlGPpdas3onSkQ== - -"@tiptap/extension-text@^2.0.0-beta.220": - version "2.1.6" - resolved "https://registry.yarnpkg.com/@tiptap/extension-text/-/extension-text-2.1.6.tgz#23f36114ee164e3da2fd326145ac7b7f8bd34c56" - integrity sha512-CqV0N6ngoXZFeJGlQ86FSZJ/0k7+BN3S6aSUcb5DRAKsSEv/Ga1LvSG24sHy+dwjTuj3EtRPJSVZTFcSB17ZSA== - -"@tiptap/html@^2.1.11": - version "2.1.11" - resolved "https://registry.yarnpkg.com/@tiptap/html/-/html-2.1.11.tgz#998421b526f200d01c549f37eb8fae2a0d1f0ed6" - integrity sha512-VKmBb1c3YN9hZfBzkV+QERf3ZWBUHHxjv2/BOr/Dw6mbb6+0iA1nxO9vQYPUb+xAmlm0n8vWwc7YQ8rxBwTKWQ== - dependencies: - zeed-dom "^0.9.19" - -"@tiptap/pm@^2.0.0-beta.220": - version "2.1.6" - resolved "https://registry.yarnpkg.com/@tiptap/pm/-/pm-2.1.6.tgz#4c196a7147fedd71316ef3413bb0e98d5c97726d" - integrity sha512-JkFlZp2z6Se2Ttnabi4lkP2yLNMH/eebO7ScYL1kXvwNLgELC/I3fwQVmnYA0E8pqJ5KQXOSl14NaB1mVPJqlg== - dependencies: - prosemirror-changeset "^2.2.0" - prosemirror-collab "^1.3.0" - prosemirror-commands "^1.3.1" - prosemirror-dropcursor "^1.5.0" - prosemirror-gapcursor "^1.3.1" - prosemirror-history "^1.3.0" - prosemirror-inputrules "^1.2.0" - prosemirror-keymap "^1.2.0" - prosemirror-markdown "^1.10.1" - prosemirror-menu "^1.2.1" - prosemirror-model "^1.18.1" - prosemirror-schema-basic "^1.2.0" - prosemirror-schema-list "^1.2.2" - prosemirror-state "^1.4.1" - prosemirror-tables "^1.3.0" - prosemirror-trailing-node "^2.0.2" - prosemirror-transform "^1.7.0" - prosemirror-view "^1.28.2" - -"@tiptap/react@^2.0.0-beta.220": - version "2.1.6" - resolved "https://registry.yarnpkg.com/@tiptap/react/-/react-2.1.6.tgz#b4004fd564a48afa1cbe75f27d5e43fabc90276c" - integrity sha512-HEsoFlcE61gQz9TllEtBa+5d909MA/ersbxGYOUWIY2HhH5lvNIUvyJ3pdzMkK/4cSniMsDDqobFexsGyTAsrw== - dependencies: - "@tiptap/extension-bubble-menu" "^2.1.6" - "@tiptap/extension-floating-menu" "^2.1.6" - -"@tiptap/suggestion@^2.0.0-beta.220": - version "2.1.6" - resolved "https://registry.yarnpkg.com/@tiptap/suggestion/-/suggestion-2.1.6.tgz#ae3c64f869454b802a8e94b358c33cd829eab8e0" - integrity sha512-8nMVARHbJ4Q9eeB7gmvqNommx6/RuFkrJEmmqxSrgyiqYEqb/if5ZTa1LGRWRNZYuzmeVN/r3eUu33jn+o5kJg== - "@tokenizer/token@^0.3.0": version "0.3.0" resolved "https://registry.yarnpkg.com/@tokenizer/token/-/token-0.3.0.tgz#fe98a93fe789247e998c75e74e9c7c63217aa276" @@ -7475,16 +7341,6 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-18.17.6.tgz#0296e9a30b22d2a8fcaa48d3c45afe51474ca55b" integrity sha512-fGmT/P7z7ecA6bv/ia5DlaWCH4YeZvAQMNpUhrJjtAhOhZfoxS1VLUgU2pdk63efSjQaOJWdXMuAJsws+8I6dg== -"@types/object.omit@^3.0.0": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@types/object.omit/-/object.omit-3.0.0.tgz#0d31e1208eac8fe2ad5c9499a1016a8273bbfafc" - integrity sha512-I27IoPpH250TUzc9FzXd0P1BV/BMJuzqD3jOz98ehf9dQqGkxlq+hO1bIqZGWqCg5bVOy0g4AUVJtnxe0klDmw== - -"@types/object.pick@^1.3.2": - version "1.3.2" - resolved "https://registry.yarnpkg.com/@types/object.pick/-/object.pick-1.3.2.tgz#9eb28118240ad8f658b9c9c6caf35359fdb37150" - integrity sha512-sn7L+qQ6RLPdXRoiaE7bZ/Ek+o4uICma/lBFPyJEKDTPTBP1W8u0c4baj3EiS4DiqLs+Hk+KUGvMVJtAw3ePJg== - "@types/parse-json@^4.0.0": version "4.0.0" resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0" @@ -7615,11 +7471,6 @@ resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.1.tgz#20f18294f797f2209b5f65c8e3b5c8e8261d127c" integrity sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw== -"@types/throttle-debounce@^2.1.0": - version "2.1.0" - resolved "https://registry.yarnpkg.com/@types/throttle-debounce/-/throttle-debounce-2.1.0.tgz#1c3df624bfc4b62f992d3012b84c56d41eab3776" - integrity sha512-5eQEtSCoESnh2FsiLTxE121IiE60hnMqcb435fShf4bpLRjEu1Eoekht23y6zXS9Ts3l+Szu3TARnTsA0GkOkQ== - "@types/tough-cookie@*": version "4.0.2" resolved "https://registry.yarnpkg.com/@types/tough-cookie/-/tough-cookie-4.0.2.tgz#6286b4c7228d58ab7866d19716f3696e03a09397" @@ -9218,11 +9069,6 @@ caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001517, caniuse-lite@^1.0.30001520: resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001522.tgz#44b87a406c901269adcdb834713e23582dd71856" integrity sha512-TKiyTVZxJGhsTszLuzb+6vUZSjVOAhClszBr2Ta2k9IwtNBT/4dzmL6aywt0HCgEZlmwJzXJd8yNiob6HgwTRg== -case-anything@^2.1.13: - version "2.1.13" - resolved "https://registry.yarnpkg.com/case-anything/-/case-anything-2.1.13.tgz#0cdc16278cb29a7fcdeb072400da3f342ba329e9" - integrity sha512-zlOQ80VrQ2Ue+ymH5OuM/DlDq64mEm+B9UTdHULv5osUMD6HalNTblf2b1u/m6QecjsnOkBpqVZ+XPwIVsy7Ng== - case-sensitive-paths-webpack-plugin@^2.4.0: version "2.4.0" resolved "https://registry.yarnpkg.com/case-sensitive-paths-webpack-plugin/-/case-sensitive-paths-webpack-plugin-2.4.0.tgz#db64066c6422eed2e08cc14b986ca43796dbc6d4" @@ -9863,11 +9709,6 @@ create-require@^1.1.0: resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== -crelt@^1.0.0: - version "1.0.6" - resolved "https://registry.yarnpkg.com/crelt/-/crelt-1.0.6.tgz#7cc898ea74e190fb6ef9dae57f8f81cf7302df72" - integrity sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g== - cross-fetch@^3.1.5: version "3.1.8" resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.8.tgz#0327eba65fd68a7d119f8fb2bf9334a1a7956f82" @@ -10146,11 +9987,6 @@ damerau-levenshtein@^1.0.8: resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz#b43d286ccbd36bc5b2f7ed41caf2d0aba1f8a6e7" integrity sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA== -dash-get@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/dash-get/-/dash-get-1.0.2.tgz#4c9e9ad5ef04c4bf9d3c9a451f6f7997298dcc7c" - integrity sha512-4FbVrHDwfOASx7uQVxeiCTo7ggSdYZbqs8lH+WU6ViypPlDbe9y6IP5VVUDQBv9DcnyaiPT5XT0UWHgJ64zLeQ== - data-uri-to-buffer@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-3.0.1.tgz#594b8973938c5bc2c33046535785341abc4f3636" @@ -10254,7 +10090,7 @@ deep-is@^0.1.3: resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== -deepmerge@^4.2.2, deepmerge@^4.3.0, deepmerge@^4.3.1: +deepmerge@^4.2.2, deepmerge@^4.3.0: version "4.3.1" resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.1.tgz#44b5f2147cd3b00d4b56137685966f26fd25dd4a" integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A== @@ -10785,11 +10621,6 @@ entities@^4.2.0, entities@^4.4.0: resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48" integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw== -entities@~3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/entities/-/entities-3.0.1.tgz#2b887ca62585e96db3903482d336c1006c3001d4" - integrity sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q== - env-editor@^0.4.1: version "0.4.2" resolved "https://registry.yarnpkg.com/env-editor/-/env-editor-0.4.2.tgz#4e76568d0bd8f5c2b6d314a9412c8fe9aa3ae861" @@ -13294,13 +13125,6 @@ is-docker@^2.0.0, is-docker@^2.1.1: resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.2.1.tgz#33eeabe23cfe86f14bde4408a02c0cfb853acdaa" integrity sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ== -is-extendable@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-1.0.1.tgz#a7470f9e426733d81bd81e1155264e3a3507cab4" - integrity sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA== - dependencies: - is-plain-object "^2.0.4" - is-extglob@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-1.0.0.tgz#ac468177c4943405a092fc8f29760c6ffc6206c0" @@ -15165,13 +14989,6 @@ lines-and-columns@^1.1.6: resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== -linkify-it@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-4.0.1.tgz#01f1d5e508190d06669982ba31a7d9f56a5751ec" - integrity sha512-C7bfi1UZmoj8+PQx22XyeXCuBlokoyWQL5pWSP+EI6nzRylyThouddufc2c1NDIcP9k5agmN9fLpA7VNJfIiqw== - dependencies: - uc.micro "^1.0.1" - lint-staged@^13.2.3: version "13.3.0" resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-13.3.0.tgz#7965d72a8d6a6c932f85e9c13ccf3596782d28a5" @@ -15450,7 +15267,7 @@ make-dir@^4.0.0: dependencies: semver "^7.5.3" -make-error@^1.1.1, make-error@^1.3.6: +make-error@^1.1.1: version "1.3.6" resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== @@ -15462,17 +15279,6 @@ makeerror@1.0.12: dependencies: tmpl "1.0.5" -markdown-it@^13.0.1: - version "13.0.1" - resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-13.0.1.tgz#c6ecc431cacf1a5da531423fc6a42807814af430" - integrity sha512-lTlxriVoy2criHP0JKRhO2VDG9c2ypWCsT237eDiLqi09rmbKoUetyGHq2uOIRoRS//kfoJckS0eUzzkDR+k2Q== - dependencies: - argparse "^2.0.1" - entities "~3.0.1" - linkify-it "^4.0.1" - mdurl "^1.0.1" - uc.micro "^1.0.5" - marky@^1.2.2: version "1.2.5" resolved "https://registry.yarnpkg.com/marky/-/marky-1.2.5.tgz#55796b688cbd72390d2d399eaaf1832c9413e3c0" @@ -15525,11 +15331,6 @@ mdn-data@2.0.4: resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.4.tgz#699b3c38ac6f1d728091a64650b65d388502fd5b" integrity sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA== -mdurl@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e" - integrity sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g== - media-typer@0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" @@ -16407,20 +16208,6 @@ object.hasown@^1.1.2: define-properties "^1.1.4" es-abstract "^1.20.4" -object.omit@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/object.omit/-/object.omit-3.0.0.tgz#0e3edc2fce2ba54df5577ff529f6d97bd8a522af" - integrity sha512-EO+BCv6LJfu+gBIF3ggLicFebFLN5zqzz/WWJlMFfkMyGth+oBkhxzDl0wx2W4GkLzuQs/FsSkXZb2IMWQqmBQ== - dependencies: - is-extendable "^1.0.0" - -object.pick@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/object.pick/-/object.pick-1.3.0.tgz#87a10ac4c1694bd2e1cbf53591a66141fb5dd747" - integrity sha512-tqa/UMy/CCoYmj+H5qc07qvSL9dqcs/WZENZ1JbtWBlATP+iVOe778gE6MSijnyCnORzDuX6hU+LA4SZ09YjFQ== - dependencies: - isobject "^3.0.1" - object.values@^1.1.0, object.values@^1.1.6: version "1.1.6" resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.6.tgz#4abbaa71eba47d63589d402856f908243eea9b1d" @@ -16555,11 +16342,6 @@ ora@^5.1.0, ora@^5.4.1: strip-ansi "^6.0.0" wcwidth "^1.0.1" -orderedmap@^2.0.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/orderedmap/-/orderedmap-2.1.1.tgz#61481269c44031c449915497bf5a4ad273c512d2" - integrity sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g== - os-homedir@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" @@ -17822,160 +17604,6 @@ proper-lockfile@^3.0.2: retry "^0.12.0" signal-exit "^3.0.2" -prosemirror-changeset@^2.2.0: - version "2.2.1" - resolved "https://registry.yarnpkg.com/prosemirror-changeset/-/prosemirror-changeset-2.2.1.tgz#dae94b63aec618fac7bb9061648e6e2a79988383" - integrity sha512-J7msc6wbxB4ekDFj+n9gTW/jav/p53kdlivvuppHsrZXCaQdVgRghoZbSS3kwrRyAstRVQ4/+u5k7YfLgkkQvQ== - dependencies: - prosemirror-transform "^1.0.0" - -prosemirror-collab@^1.3.0: - version "1.3.1" - resolved "https://registry.yarnpkg.com/prosemirror-collab/-/prosemirror-collab-1.3.1.tgz#0e8c91e76e009b53457eb3b3051fb68dad029a33" - integrity sha512-4SnynYR9TTYaQVXd/ieUvsVV4PDMBzrq2xPUWutHivDuOshZXqQ5rGbZM84HEaXKbLdItse7weMGOUdDVcLKEQ== - dependencies: - prosemirror-state "^1.0.0" - -prosemirror-commands@^1.0.0, prosemirror-commands@^1.3.1: - version "1.5.2" - resolved "https://registry.yarnpkg.com/prosemirror-commands/-/prosemirror-commands-1.5.2.tgz#e94aeea52286f658cd984270de9b4c3fff580852" - integrity sha512-hgLcPaakxH8tu6YvVAaILV2tXYsW3rAdDR8WNkeKGcgeMVQg3/TMhPdVoh7iAmfgVjZGtcOSjKiQaoeKjzd2mQ== - dependencies: - prosemirror-model "^1.0.0" - prosemirror-state "^1.0.0" - prosemirror-transform "^1.0.0" - -prosemirror-dropcursor@^1.5.0: - version "1.8.1" - resolved "https://registry.yarnpkg.com/prosemirror-dropcursor/-/prosemirror-dropcursor-1.8.1.tgz#49b9fb2f583e0d0f4021ff87db825faa2be2832d" - integrity sha512-M30WJdJZLyXHi3N8vxN6Zh5O8ZBbQCz0gURTfPmTIBNQ5pxrdU7A58QkNqfa98YEjSAL1HUyyU34f6Pm5xBSGw== - dependencies: - prosemirror-state "^1.0.0" - prosemirror-transform "^1.1.0" - prosemirror-view "^1.1.0" - -prosemirror-gapcursor@^1.3.1: - version "1.3.2" - resolved "https://registry.yarnpkg.com/prosemirror-gapcursor/-/prosemirror-gapcursor-1.3.2.tgz#5fa336b83789c6199a7341c9493587e249215cb4" - integrity sha512-wtjswVBd2vaQRrnYZaBCbyDqr232Ed4p2QPtRIUK5FuqHYKGWkEwl08oQM4Tw7DOR0FsasARV5uJFvMZWxdNxQ== - dependencies: - prosemirror-keymap "^1.0.0" - prosemirror-model "^1.0.0" - prosemirror-state "^1.0.0" - prosemirror-view "^1.0.0" - -prosemirror-history@^1.0.0, prosemirror-history@^1.3.0: - version "1.3.2" - resolved "https://registry.yarnpkg.com/prosemirror-history/-/prosemirror-history-1.3.2.tgz#ce6ad7ab9db83e761aee716f3040d74738311b15" - integrity sha512-/zm0XoU/N/+u7i5zepjmZAEnpvjDtzoPWW6VmKptcAnPadN/SStsBjMImdCEbb3seiNTpveziPTIrXQbHLtU1g== - dependencies: - prosemirror-state "^1.2.2" - prosemirror-transform "^1.0.0" - prosemirror-view "^1.31.0" - rope-sequence "^1.3.0" - -prosemirror-inputrules@^1.2.0: - version "1.2.1" - resolved "https://registry.yarnpkg.com/prosemirror-inputrules/-/prosemirror-inputrules-1.2.1.tgz#8faf3d78c16150aedac71d326a3e3947417ce557" - integrity sha512-3LrWJX1+ULRh5SZvbIQlwZafOXqp1XuV21MGBu/i5xsztd+9VD15x6OtN6mdqSFI7/8Y77gYUbQ6vwwJ4mr6QQ== - dependencies: - prosemirror-state "^1.0.0" - prosemirror-transform "^1.0.0" - -prosemirror-keymap@^1.0.0, prosemirror-keymap@^1.1.2, prosemirror-keymap@^1.2.0: - version "1.2.2" - resolved "https://registry.yarnpkg.com/prosemirror-keymap/-/prosemirror-keymap-1.2.2.tgz#14a54763a29c7b2704f561088ccf3384d14eb77e" - integrity sha512-EAlXoksqC6Vbocqc0GtzCruZEzYgrn+iiGnNjsJsH4mrnIGex4qbLdWWNza3AW5W36ZRrlBID0eM6bdKH4OStQ== - dependencies: - prosemirror-state "^1.0.0" - w3c-keyname "^2.2.0" - -prosemirror-markdown@^1.10.1: - version "1.11.2" - resolved "https://registry.yarnpkg.com/prosemirror-markdown/-/prosemirror-markdown-1.11.2.tgz#f6e529e669d11fa3eec859e93c0d2c91788d6c80" - integrity sha512-Eu5g4WPiCdqDTGhdSsG9N6ZjACQRYrsAkrF9KYfdMaCmjIApH75aVncsWYOJvEk2i1B3i8jZppv3J/tnuHGiUQ== - dependencies: - markdown-it "^13.0.1" - prosemirror-model "^1.0.0" - -prosemirror-menu@^1.2.1: - version "1.2.4" - resolved "https://registry.yarnpkg.com/prosemirror-menu/-/prosemirror-menu-1.2.4.tgz#3cfdc7c06d10f9fbd1bce29082c498bd11a0a79a" - integrity sha512-S/bXlc0ODQup6aiBbWVsX/eM+xJgCTAfMq/nLqaO5ID/am4wS0tTCIkzwytmao7ypEtjj39i7YbJjAgO20mIqA== - dependencies: - crelt "^1.0.0" - prosemirror-commands "^1.0.0" - prosemirror-history "^1.0.0" - prosemirror-state "^1.0.0" - -prosemirror-model@^1.0.0, prosemirror-model@^1.16.0, prosemirror-model@^1.18.1, prosemirror-model@^1.19.0, prosemirror-model@^1.8.1: - version "1.19.3" - resolved "https://registry.yarnpkg.com/prosemirror-model/-/prosemirror-model-1.19.3.tgz#f0d55285487fefd962d0ac695f716f4ec6705006" - integrity sha512-tgSnwN7BS7/UM0sSARcW+IQryx2vODKX4MI7xpqY2X+iaepJdKBPc7I4aACIsDV/LTaTjt12Z56MhDr9LsyuZQ== - dependencies: - orderedmap "^2.0.0" - -prosemirror-schema-basic@^1.2.0: - version "1.2.2" - resolved "https://registry.yarnpkg.com/prosemirror-schema-basic/-/prosemirror-schema-basic-1.2.2.tgz#6695f5175e4628aab179bf62e5568628b9cfe6c7" - integrity sha512-/dT4JFEGyO7QnNTe9UaKUhjDXbTNkiWTq/N4VpKaF79bBjSExVV2NXmJpcM7z/gD7mbqNjxbmWW5nf1iNSSGnw== - dependencies: - prosemirror-model "^1.19.0" - -prosemirror-schema-list@^1.2.2: - version "1.3.0" - resolved "https://registry.yarnpkg.com/prosemirror-schema-list/-/prosemirror-schema-list-1.3.0.tgz#05374702cf35a3ba5e7ec31079e355a488d52519" - integrity sha512-Hz/7gM4skaaYfRPNgr421CU4GSwotmEwBVvJh5ltGiffUJwm7C8GfN/Bc6DR1EKEp5pDKhODmdXXyi9uIsZl5A== - dependencies: - prosemirror-model "^1.0.0" - prosemirror-state "^1.0.0" - prosemirror-transform "^1.7.3" - -prosemirror-state@^1.0.0, prosemirror-state@^1.2.2, prosemirror-state@^1.3.1, prosemirror-state@^1.4.1: - version "1.4.3" - resolved "https://registry.yarnpkg.com/prosemirror-state/-/prosemirror-state-1.4.3.tgz#94aecf3ffd54ec37e87aa7179d13508da181a080" - integrity sha512-goFKORVbvPuAQaXhpbemJFRKJ2aixr+AZMGiquiqKxaucC6hlpHNZHWgz5R7dS4roHiwq9vDctE//CZ++o0W1Q== - dependencies: - prosemirror-model "^1.0.0" - prosemirror-transform "^1.0.0" - prosemirror-view "^1.27.0" - -prosemirror-tables@^1.3.0: - version "1.3.4" - resolved "https://registry.yarnpkg.com/prosemirror-tables/-/prosemirror-tables-1.3.4.tgz#0b7cc16d49f90c5b834c9f29291c545478ce9ab0" - integrity sha512-z6uLSQ1BLC3rgbGwZmpfb+xkdvD7W/UOsURDfognZFYaTtc0gsk7u/t71Yijp2eLflVpffMk6X0u0+u+MMDvIw== - dependencies: - prosemirror-keymap "^1.1.2" - prosemirror-model "^1.8.1" - prosemirror-state "^1.3.1" - prosemirror-transform "^1.2.1" - prosemirror-view "^1.13.3" - -prosemirror-trailing-node@^2.0.2: - version "2.0.7" - resolved "https://registry.yarnpkg.com/prosemirror-trailing-node/-/prosemirror-trailing-node-2.0.7.tgz#ba782a7929f18bcae650b1c7082a2d10443eab19" - integrity sha512-8zcZORYj/8WEwsGo6yVCRXFMOfBo0Ub3hCUvmoWIZYfMP26WqENU0mpEP27w7mt8buZWuGrydBewr0tOArPb1Q== - dependencies: - "@remirror/core-constants" "^2.0.2" - "@remirror/core-helpers" "^3.0.0" - escape-string-regexp "^4.0.0" - -prosemirror-transform@^1.0.0, prosemirror-transform@^1.1.0, prosemirror-transform@^1.2.1, prosemirror-transform@^1.7.0, prosemirror-transform@^1.7.3: - version "1.7.4" - resolved "https://registry.yarnpkg.com/prosemirror-transform/-/prosemirror-transform-1.7.4.tgz#ea878c90563f3586064dd5ccf6cabb50b2753fd9" - integrity sha512-GO38mvqJ2yeI0BbL5E1CdHcly032Dlfn9nHqlnCHqlNf9e9jZwJixxp6VRtOeDZ1uTDpDIziezMKbA41LpAx3A== - dependencies: - prosemirror-model "^1.0.0" - -prosemirror-view@^1.0.0, prosemirror-view@^1.1.0, prosemirror-view@^1.13.3, prosemirror-view@^1.27.0, prosemirror-view@^1.28.2, prosemirror-view@^1.31.0: - version "1.31.7" - resolved "https://registry.yarnpkg.com/prosemirror-view/-/prosemirror-view-1.31.7.tgz#dccb2879314e1e1a24d48044c15374754e50ef00" - integrity sha512-Pr7w93yOYmxQwzGIRSaNLZ/1uM6YjnenASzN2H6fO6kGekuzRbgZ/4bHbBTd1u4sIQmL33/TcGmzxxidyPwCjg== - dependencies: - prosemirror-model "^1.16.0" - prosemirror-state "^1.0.0" - prosemirror-transform "^1.1.0" - proxy-addr@~2.0.7: version "2.0.7" resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" @@ -19052,11 +18680,6 @@ rollup@^2.43.1: optionalDependencies: fsevents "~2.3.2" -rope-sequence@^1.3.0: - version "1.3.4" - resolved "https://registry.yarnpkg.com/rope-sequence/-/rope-sequence-1.3.4.tgz#df85711aaecd32f1e756f76e43a415171235d425" - integrity sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ== - rtl-detect@^1.0.2: version "1.0.4" resolved "https://registry.yarnpkg.com/rtl-detect/-/rtl-detect-1.0.4.tgz#40ae0ea7302a150b96bc75af7d749607392ecac6" @@ -20387,11 +20010,6 @@ throat@^6.0.1: resolved "https://registry.yarnpkg.com/throat/-/throat-6.0.2.tgz#51a3fbb5e11ae72e2cf74861ed5c8020f89f29fe" integrity sha512-WKexMoJj3vEuK0yFEapj8y64V0A6xcuPuK9Gt1d0R+dzCSJc0lHqQytAbSB4cDAK0dWh4T0E2ETkoLE2WZ41OQ== -throttle-debounce@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/throttle-debounce/-/throttle-debounce-3.0.1.tgz#32f94d84dfa894f786c9a1f290e7a645b6a19abb" - integrity sha512-dTEWWNu6JmeVXY0ZYoPuH5cRIwc0MeGbJwah9KUNYSJwommQpCzTySTpEe8Gs1J23aeWEuAobe4Ag7EHVt/LOg== - through2@^2.0.1: version "2.0.5" resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.5.tgz#01c1e39eb31d07cb7d03a96a70823260b23132cd" @@ -20415,13 +20033,6 @@ tiny-hashes@^1.0.1: resolved "https://registry.yarnpkg.com/tiny-hashes/-/tiny-hashes-1.0.1.tgz#ddbe9060312ddb4efe0a174bb3a27e1331c425a1" integrity sha512-knIN5zj4fl7kW4EBU5sLP20DWUvi/rVouvJezV0UAym2DkQaqm365Nyc8F3QEiOvunNDMxR8UhcXd1d5g+Wg1g== -tippy.js@^6.3.7: - version "6.3.7" - resolved "https://registry.yarnpkg.com/tippy.js/-/tippy.js-6.3.7.tgz#8ccfb651d642010ed9a32ff29b0e9e19c5b8c61c" - integrity sha512-E1d3oP2emgJ9dRQZdf3Kkn0qJgI6ZLpyS5z6ZkY1DF3kaQaBsGZsndEpHwx+eC+tYM41HaSNvNtLx8tU57FzTQ== - dependencies: - "@popperjs/core" "^2.9.0" - tlds@^1.234.0: version "1.242.0" resolved "https://registry.yarnpkg.com/tlds/-/tlds-1.242.0.tgz#da136a9c95b0efa1a4cd57dca8ef240c08ada4b7" @@ -20629,7 +20240,7 @@ type-fest@^1.0.2: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-1.4.0.tgz#e9fb813fe3bf1744ec359d55d1affefa76f14be1" integrity sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA== -type-fest@^2.19.0, type-fest@^2.3.3: +type-fest@^2.3.3: version "2.19.0" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-2.19.0.tgz#88068015bb33036a598b952e55e9311a60fd3a9b" integrity sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA== @@ -20715,11 +20326,6 @@ ua-parser-js@^1.0.35: resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-1.0.35.tgz#c4ef44343bc3db0a3cbefdf21822f1b1fc1ab011" integrity sha512-fKnGuqmTBnIE+/KXSzCn4db8RTigUzw1AN0DmdU6hJovUTbYJKyqj+8Mt1c4VfRDnOVJnENmfYkIPZ946UrSAA== -uc.micro@^1.0.1, uc.micro@^1.0.5: - version "1.0.6" - resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.6.tgz#9c411a802a409a91fc6cf74081baba34b24499ac" - integrity sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA== - uglify-js@^3.1.4: version "3.17.4" resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.17.4.tgz#61678cf5fa3f5b7eb789bb345df29afb8257c22c" @@ -21073,11 +20679,6 @@ w3c-hr-time@^1.0.2: dependencies: browser-process-hrtime "^1.0.0" -w3c-keyname@^2.2.0: - version "2.2.8" - resolved "https://registry.yarnpkg.com/w3c-keyname/-/w3c-keyname-2.2.8.tgz#7b17c8c6883d4e8b86ac8aba79d39e880f8869c5" - integrity sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ== - w3c-xmlserializer@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/w3c-xmlserializer/-/w3c-xmlserializer-2.0.0.tgz#3e7104a05b75146cc60f564380b7f683acf1020a" @@ -21912,13 +21513,6 @@ yocto-queue@^1.0.0: resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-1.0.0.tgz#7f816433fb2cbc511ec8bf7d263c3b58a1a3c251" integrity sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g== -zeed-dom@0.10.9, zeed-dom@^0.9.19: - version "0.10.9" - resolved "https://registry.yarnpkg.com/zeed-dom/-/zeed-dom-0.10.9.tgz#b3eb5d9b7cf1be17e1fb3a708379df5edce195be" - integrity sha512-qQQ7Wu7IJ3Vo/LjeKWj97A2Hi17di4ZdmgNZj6AWbDbpt3hvO4EMfjYVA2/2unLYT+XpmMq5fqaLqCeU7Im83A== - dependencies: - css-what "^6.1.0" - zeego@^1.6.2: version "1.7.0" resolved "https://registry.yarnpkg.com/zeego/-/zeego-1.7.0.tgz#8034adb842199c4ccf21bcb19877800bff18606b" From 00bf782fee7ca0e4e1eb552377ada54a01b88a17 Mon Sep 17 00:00:00 2001 From: Mary Date: Thu, 18 Jan 2024 18:12:36 +0700 Subject: [PATCH 16/27] fix: don't put margin on overlay/input --- src/view/com/composer/text-input/web/styles/style.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/view/com/composer/text-input/web/styles/style.css b/src/view/com/composer/text-input/web/styles/style.css index f1d3bdc9be..ccb658c62a 100644 --- a/src/view/com/composer/text-input/web/styles/style.css +++ b/src/view/com/composer/text-input/web/styles/style.css @@ -2,6 +2,7 @@ font-size: 18px; line-height: 24px; width: 100%; + margin: 0 0 10px 8px; position: relative; } .rt-editor-light { @@ -50,7 +51,6 @@ .rt-overlay, .rt-input { padding: 5px; - margin: 0 0 10px 8px; } .rt-autocomplete { From 86616d4e70190f108e9732ec9473b2a20812c02e Mon Sep 17 00:00:00 2001 From: Mary Date: Thu, 18 Jan 2024 18:13:52 +0700 Subject: [PATCH 17/27] chore: move styling to index.html --- bskyweb/templates/base.html | 60 ++++++++++++++++++ .../com/composer/text-input/TextInput.web.tsx | 1 - .../composer/text-input/web/styles/style.css | 58 ------------------ web/index.html | 61 +++++++++++++++++++ 4 files changed, 121 insertions(+), 59 deletions(-) delete mode 100644 src/view/com/composer/text-input/web/styles/style.css diff --git a/bskyweb/templates/base.html b/bskyweb/templates/base.html index bf411181f0..b32ef98949 100644 --- a/bskyweb/templates/base.html +++ b/bskyweb/templates/base.html @@ -98,6 +98,66 @@ scrollbar-gutter: stable both-edges; } + /* RichText composer */ + .rt-editor { + font-size: 18px; + line-height: 24px; + width: 100%; + margin: 0 0 10px 8px; + position: relative; + } + .rt-editor-light { + --rt-caret-color: #000000; + } + .rt-editor-dark { + --rt-caret-color: #ffffff; + } + + .rt-overlay { + position: absolute; + inset: 0; + z-index: 0; + white-space: pre-wrap; + overflow-wrap: break-word; + } + .rt-segment { + } + .rt-segment-text { + } + .rt-segment-link { + color: #0085ff; + } + + .rt-input { + caret-color: var(--rt-caret-color); + color: transparent; + font: inherit; + line-height: inherit; + background: transparent; + width: 100%; + border: 0; + display: block; + position: relative; + z-index: 1; + resize: none; + } + .rt-input:focus { + outline: none; + } + .rt-input::placeholder { + color: #878788; + opacity: 1; + } + + .rt-overlay, + .rt-input { + padding: 5px; + } + + .rt-autocomplete { + z-index: 3; + } + /* Tooltips */ [data-tooltip] { position: relative; diff --git a/src/view/com/composer/text-input/TextInput.web.tsx b/src/view/com/composer/text-input/TextInput.web.tsx index c9f343fe6c..c1649387d0 100644 --- a/src/view/com/composer/text-input/TextInput.web.tsx +++ b/src/view/com/composer/text-input/TextInput.web.tsx @@ -7,7 +7,6 @@ import TextareaAutosize from 'react-textarea-autosize' import {AppBskyRichtextFacet, RichText} from '@atproto/api' -import './web/styles/style.css' import {Emoji} from './web/EmojiPicker.web' import { Autocomplete, diff --git a/src/view/com/composer/text-input/web/styles/style.css b/src/view/com/composer/text-input/web/styles/style.css deleted file mode 100644 index ccb658c62a..0000000000 --- a/src/view/com/composer/text-input/web/styles/style.css +++ /dev/null @@ -1,58 +0,0 @@ -.rt-editor { - font-size: 18px; - line-height: 24px; - width: 100%; - margin: 0 0 10px 8px; - position: relative; -} -.rt-editor-light { - --rt-caret-color: #000000; -} -.rt-editor-dark { - --rt-caret-color: #ffffff; -} - -.rt-overlay { - position: absolute; - inset: 0; - z-index: 0; - white-space: pre-wrap; - overflow-wrap: break-word; -} -.rt-segment { -} -.rt-segment-text { -} -.rt-segment-link { - color: #0085ff; -} - -.rt-input { - caret-color: var(--rt-caret-color); - color: transparent; - font: inherit; - line-height: inherit; - background: transparent; - width: 100%; - border: 0; - display: block; - position: relative; - z-index: 1; - resize: none; -} -.rt-input:focus { - outline: none; -} -.rt-input::placeholder { - color: #878788; - opacity: 1; -} - -.rt-overlay, -.rt-input { - padding: 5px; -} - -.rt-autocomplete { - z-index: 3; -} diff --git a/web/index.html b/web/index.html index 323f546475..8f30b12e09 100644 --- a/web/index.html +++ b/web/index.html @@ -102,6 +102,67 @@ scrollbar-gutter: stable both-edges; } + /* RichText composer */ + .rt-editor { + font-size: 18px; + line-height: 24px; + width: 100%; + margin: 0 0 10px 8px; + position: relative; + } + .rt-editor-light { + --rt-caret-color: #000000; + } + .rt-editor-dark { + --rt-caret-color: #ffffff; + } + + .rt-overlay { + position: absolute; + inset: 0; + z-index: 0; + white-space: pre-wrap; + overflow-wrap: break-word; + } + .rt-segment { + } + .rt-segment-text { + } + .rt-segment-link { + color: #0085ff; + } + + .rt-input { + caret-color: var(--rt-caret-color); + color: transparent; + font: inherit; + line-height: inherit; + background: transparent; + width: 100%; + border: 0; + display: block; + position: relative; + z-index: 1; + resize: none; + } + .rt-input:focus { + outline: none; + } + .rt-input::placeholder { + color: #878788; + opacity: 1; + } + + .rt-overlay, + .rt-input { + padding: 5px; + } + + .rt-autocomplete { + z-index: 3; + } + + /* Tooltips */ [data-tooltip] { position: relative; From 03e1c63a7af4116ab63c375e11893f976d722cd7 Mon Sep 17 00:00:00 2001 From: Mary Date: Thu, 18 Jan 2024 18:17:44 +0700 Subject: [PATCH 18/27] fix: size differences --- bskyweb/templates/base.html | 2 ++ web/index.html | 2 ++ 2 files changed, 4 insertions(+) diff --git a/bskyweb/templates/base.html b/bskyweb/templates/base.html index b32ef98949..19476108da 100644 --- a/bskyweb/templates/base.html +++ b/bskyweb/templates/base.html @@ -136,7 +136,9 @@ background: transparent; width: 100%; border: 0; + margin: 0; display: block; + box-sizing: border-box; position: relative; z-index: 1; resize: none; diff --git a/web/index.html b/web/index.html index 8f30b12e09..1db1423445 100644 --- a/web/index.html +++ b/web/index.html @@ -140,7 +140,9 @@ background: transparent; width: 100%; border: 0; + margin: 0; display: block; + box-sizing: border-box; position: relative; z-index: 1; resize: none; From 19a36c42cef7177b84c85d68886bb84fefe6cacd Mon Sep 17 00:00:00 2001 From: Mary Date: Fri, 19 Jan 2024 08:48:57 +0700 Subject: [PATCH 19/27] refactor: use html strings --- .../com/composer/text-input/TextInput.web.tsx | 57 +++++++++++-------- 1 file changed, 34 insertions(+), 23 deletions(-) diff --git a/src/view/com/composer/text-input/TextInput.web.tsx b/src/view/com/composer/text-input/TextInput.web.tsx index c1649387d0..0f00a5e86a 100644 --- a/src/view/com/composer/text-input/TextInput.web.tsx +++ b/src/view/com/composer/text-input/TextInput.web.tsx @@ -137,30 +137,25 @@ export const TextInput = React.forwardRef( }, [richtext, cursor, overlayRef, setSuggestion]) const textOverlay = React.useMemo(() => { + let html = '' + + for (const segment of richtext.segments()) { + const isLink = segment.facet + ? !AppBskyRichtextFacet.isTag(segment.facet.features[0]) + : false + + const klass = + `rt-segment ` + (!isLink ? `rt-segment-text` : `rt-segment-link`) + const text = escape(segment.text, false) || '​' + + html += `${text}` + } + return ( -
- {Array.from(richtext.segments(), (segment, index) => { - const isLink = segment.facet - ? !AppBskyRichtextFacet.isTag(segment.facet.features[0]) - : false - - return ( - - { - // We need React to commit a text node to DOM so we can select - // it for `getCursorPosition` above, without it, we can't open - // the emoji picker on an empty input. - segment.text || '\u200b' - } - - ) - })} -
+
) }, [richtext]) @@ -352,3 +347,19 @@ const findNodePosition = ( return } + +const escape = (str: string, attr: boolean) => { + let escaped = '' + let last = 0 + + for (let idx = 0, len = str.length; idx < len; idx++) { + const char = str.charCodeAt(idx) + + if (char === 38 || (attr ? char === 34 : char === 60)) { + escaped += str.substring(last, idx) + ('&#' + char + ';') + last = idx + 1 + } + } + + return escaped + str.substring(last) +} From ad9323dcf6ef32baeabd9943e4fd88cadc709154 Mon Sep 17 00:00:00 2001 From: Mary Date: Tue, 30 Jan 2024 09:08:18 +0700 Subject: [PATCH 20/27] refactor: remove react-dom usage for portal --- package.json | 5 +- .../composer/text-input/web/Autocomplete.tsx | 108 +++++++++--------- yarn.lock | 7 -- 3 files changed, 59 insertions(+), 61 deletions(-) diff --git a/package.json b/package.json index a03e3858ee..6061df60d0 100644 --- a/package.json +++ b/package.json @@ -150,6 +150,7 @@ "react-native-safe-area-context": "4.8.2", "react-native-screens": "~3.29.0", "react-native-svg": "14.1.0", + "react-native-ui-text-view": "link:./modules/react-native-ui-text-view", "react-native-url-polyfill": "^1.3.0", "react-native-uuid": "^2.0.1", "react-native-version-number": "^0.3.6", @@ -164,8 +165,7 @@ "tlds": "^1.234.0", "use-deep-compare": "^1.1.0", "zeego": "^1.6.2", - "zod": "^3.20.2", - "react-native-ui-text-view": "link:./modules/react-native-ui-text-view" + "zod": "^3.20.2" }, "devDependencies": { "@atproto/dev-env": "^0.2.28", @@ -196,7 +196,6 @@ "@types/lodash.shuffle": "^4.2.7", "@types/psl": "^1.1.1", "@types/react-avatar-editor": "^13.0.0", - "@types/react-dom": "^18.2.18", "@types/react-responsive": "^8.0.5", "@types/react-test-renderer": "^17.0.1", "@typescript-eslint/eslint-plugin": "^5.48.2", diff --git a/src/view/com/composer/text-input/web/Autocomplete.tsx b/src/view/com/composer/text-input/web/Autocomplete.tsx index 43c4545fad..0a01a0f8f4 100644 --- a/src/view/com/composer/text-input/web/Autocomplete.tsx +++ b/src/view/com/composer/text-input/web/Autocomplete.tsx @@ -1,5 +1,4 @@ import React from 'react' -import ReactDOM from 'react-dom' import {Pressable, StyleSheet, View} from 'react-native' import { @@ -13,6 +12,8 @@ import {Trans} from '@lingui/macro' import {useGrapheme} from '../hooks/useGrapheme' +import {Portal} from '#/components/Portal' + import {usePalette} from 'lib/hooks/usePalette' import {UserAvatar} from 'view/com/util/UserAvatar' import {Text} from 'view/com/util/text/Text' @@ -105,57 +106,62 @@ export const Autocomplete = React.forwardRef< return null } - return ReactDOM.createPortal( -
- - {items && items.length > 0 ? ( - items.slice(0, 8).map((item, index) => { - const {name: displayName} = getGraphemeString( - item.displayName ?? item.handle, - 30, // Heuristic value; can be modified - ) - const isSelected = cursor === index - - return ( - { - onSelect(match!, item.handle) - }} - accessibilityRole="button"> - - - - {displayName} + return ( + +
+ + {items && items.length > 0 ? ( + items.slice(0, 8).map((item, index) => { + const {name: displayName} = getGraphemeString( + item.displayName ?? item.handle, + 30, // Heuristic value; can be modified + ) + const isSelected = cursor === index + + return ( + { + onSelect(match!, item.handle) + }} + accessibilityRole="button"> + + + + {displayName} + + + + @{item.handle} - - - @{item.handle} - - - ) - }) - ) : ( - - {isFetching ? Loading... : No result} - - )} - -
, - document.body, +
+ ) + }) + ) : ( + + {isFetching ? ( + Loading... + ) : ( + No result + )} + + )} +
+
+ ) }) diff --git a/yarn.lock b/yarn.lock index 47f308bff8..f229f09b11 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7484,13 +7484,6 @@ dependencies: "@types/react" "*" -"@types/react-dom@^18.2.18": - version "18.2.18" - resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.2.18.tgz#16946e6cd43971256d874bc3d0a72074bb8571dd" - integrity sha512-TJxDm6OfAX2KJWJdMEVTwWke5Sc/E/RlnPGvGfS0W7+6ocy2xhDVQVh/KvC2Uf7kACs+gDytdusDSdWfWkaNzw== - dependencies: - "@types/react" "*" - "@types/react-responsive@^8.0.5": version "8.0.5" resolved "https://registry.yarnpkg.com/@types/react-responsive/-/react-responsive-8.0.5.tgz#77769862d2a0711434feb972be08e3e6c334440a" From b4fd957959d68b4bc314ee1d986c847b89351f54 Mon Sep 17 00:00:00 2001 From: Mary Date: Tue, 30 Jan 2024 09:11:01 +0700 Subject: [PATCH 21/27] refactor: use getImageFromUri again --- .../com/composer/text-input/TextInput.web.tsx | 36 +++++++------------ 1 file changed, 13 insertions(+), 23 deletions(-) diff --git a/src/view/com/composer/text-input/TextInput.web.tsx b/src/view/com/composer/text-input/TextInput.web.tsx index a868e5e180..8ef2847930 100644 --- a/src/view/com/composer/text-input/TextInput.web.tsx +++ b/src/view/com/composer/text-input/TextInput.web.tsx @@ -99,6 +99,13 @@ export const TextInput = React.forwardRef( [inputRef, overlayRef], ) + React.useEffect(() => { + textInputWebEmitter.addListener('photo-pasted', onPhotoPasted) + return () => { + textInputWebEmitter.removeListener('photo-pasted', onPhotoPasted) + } + }, [onPhotoPasted]) + React.useEffect(() => { const handleDrop = (event: DragEvent) => { const transfer = event.dataTransfer @@ -273,32 +280,15 @@ export const TextInput = React.forwardRef( const handlePaste = React.useCallback( (ev: React.ClipboardEvent) => { - const items = ev.clipboardData?.items ?? [] + const transfer = ev.clipboardData - for (let idx = 0, len = items.length; idx < len; idx++) { - const item = items[idx] - - if (item.kind === 'file' && item.type.startsWith('image/')) { - const file = item.getAsFile() - - if (file) { - blobToDataUri(file).then(onPhotoPasted, console.error) - } - } - - if (item.type === 'text/plain') { - item.getAsString(async str => { - if (isUriImage(str)) { - const response = await fetch(str) - const blob = await response.blob() - - blobToDataUri(blob).then(onPhotoPasted, console.error) - } - }) - } + if (transfer) { + getImageFromUri(transfer.items, (uri: string) => { + textInputWebEmitter.emit('photo-pasted', uri) + }) } }, - [onPhotoPasted], + [], ) const acceptSuggestion = React.useCallback( From 15a7c83b6eefed9da9e1eeaafb97ee9be413a041 Mon Sep 17 00:00:00 2001 From: Mary Date: Tue, 30 Jan 2024 09:11:55 +0700 Subject: [PATCH 22/27] chore: descriptive comments on effects --- src/view/com/composer/text-input/TextInput.web.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/view/com/composer/text-input/TextInput.web.tsx b/src/view/com/composer/text-input/TextInput.web.tsx index 8ef2847930..05ecfca6a2 100644 --- a/src/view/com/composer/text-input/TextInput.web.tsx +++ b/src/view/com/composer/text-input/TextInput.web.tsx @@ -99,6 +99,7 @@ export const TextInput = React.forwardRef( [inputRef, overlayRef], ) + // Sets up an event listener for photo-pasted React.useEffect(() => { textInputWebEmitter.addListener('photo-pasted', onPhotoPasted) return () => { @@ -106,6 +107,7 @@ export const TextInput = React.forwardRef( } }, [onPhotoPasted]) + // Image drag-and-drop functionality React.useEffect(() => { const handleDrop = (event: DragEvent) => { const transfer = event.dataTransfer @@ -146,6 +148,7 @@ export const TextInput = React.forwardRef( } }, [setIsDropping]) + // Mention autocompletion functionality React.useEffect(() => { if (cursor == null) { setSuggestion(undefined) From 60389873f32814fe072e00b3d4f6f555f62d5fd1 Mon Sep 17 00:00:00 2001 From: Mary Date: Tue, 30 Jan 2024 09:20:36 +0700 Subject: [PATCH 23/27] fix: properly set font-family --- bskyweb/templates/base.html | 2 +- web/index.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bskyweb/templates/base.html b/bskyweb/templates/base.html index 690cdb24a4..05c8d4f006 100644 --- a/bskyweb/templates/base.html +++ b/bskyweb/templates/base.html @@ -122,7 +122,7 @@ /* RichText composer */ .rt-editor { - font-size: 18px; + font: 18px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; line-height: 24px; width: 100%; margin: 0 0 10px 8px; diff --git a/web/index.html b/web/index.html index bd6241bea6..33b03ba1fc 100644 --- a/web/index.html +++ b/web/index.html @@ -126,7 +126,7 @@ /* RichText composer */ .rt-editor { - font-size: 18px; + font: 18px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; line-height: 24px; width: 100%; margin: 0 0 10px 8px; From e2a9ae3d880dff6bb8ee3786494c6d1c3eb1b467 Mon Sep 17 00:00:00 2001 From: Mary Date: Tue, 30 Jan 2024 09:20:45 +0700 Subject: [PATCH 24/27] fix: properly set selection color --- bskyweb/templates/base.html | 4 ++++ web/index.html | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/bskyweb/templates/base.html b/bskyweb/templates/base.html index 05c8d4f006..4406719d5c 100644 --- a/bskyweb/templates/base.html +++ b/bskyweb/templates/base.html @@ -168,6 +168,10 @@ .rt-input:focus { outline: none; } + .rt-input::selection { + background: #0085ff; + color: #ffffff; + } .rt-input::placeholder { color: #878788; opacity: 1; diff --git a/web/index.html b/web/index.html index 33b03ba1fc..3fe5ce336b 100644 --- a/web/index.html +++ b/web/index.html @@ -172,6 +172,10 @@ .rt-input:focus { outline: none; } + .rt-input::selection { + background: #0085ff; + color: #ffffff; + } .rt-input::placeholder { color: #878788; opacity: 1; From f1a55c73704f52c064dda7e0a1a6a0c81d40a21d Mon Sep 17 00:00:00 2001 From: Mary Date: Tue, 30 Jan 2024 09:22:26 +0700 Subject: [PATCH 25/27] fix: set autofocus on composer --- src/view/com/composer/text-input/TextInput.web.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/view/com/composer/text-input/TextInput.web.tsx b/src/view/com/composer/text-input/TextInput.web.tsx index 05ecfca6a2..63a5c36d06 100644 --- a/src/view/com/composer/text-input/TextInput.web.tsx +++ b/src/view/com/composer/text-input/TextInput.web.tsx @@ -351,6 +351,7 @@ export const TextInput = React.forwardRef( Date: Tue, 30 Jan 2024 09:28:38 +0700 Subject: [PATCH 26/27] fix: oops, readd react-dom types --- package.json | 1 + yarn.lock | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/package.json b/package.json index 6061df60d0..157710ff44 100644 --- a/package.json +++ b/package.json @@ -196,6 +196,7 @@ "@types/lodash.shuffle": "^4.2.7", "@types/psl": "^1.1.1", "@types/react-avatar-editor": "^13.0.0", + "@types/react-dom": "^18.2.18", "@types/react-responsive": "^8.0.5", "@types/react-test-renderer": "^17.0.1", "@typescript-eslint/eslint-plugin": "^5.48.2", diff --git a/yarn.lock b/yarn.lock index f229f09b11..47f308bff8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7484,6 +7484,13 @@ dependencies: "@types/react" "*" +"@types/react-dom@^18.2.18": + version "18.2.18" + resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.2.18.tgz#16946e6cd43971256d874bc3d0a72074bb8571dd" + integrity sha512-TJxDm6OfAX2KJWJdMEVTwWke5Sc/E/RlnPGvGfS0W7+6ocy2xhDVQVh/KvC2Uf7kACs+gDytdusDSdWfWkaNzw== + dependencies: + "@types/react" "*" + "@types/react-responsive@^8.0.5": version "8.0.5" resolved "https://registry.yarnpkg.com/@types/react-responsive/-/react-responsive-8.0.5.tgz#77769862d2a0711434feb972be08e3e6c334440a" From 719d695cedb898e4d2aaaeb1b586fdb9c1a8dd89 Mon Sep 17 00:00:00 2001 From: Mary Date: Tue, 20 Feb 2024 11:31:35 +0700 Subject: [PATCH 27/27] fix: use inert on the overlay --- src/view/com/composer/text-input/TextInput.web.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/view/com/composer/text-input/TextInput.web.tsx b/src/view/com/composer/text-input/TextInput.web.tsx index 633e42336b..7db5048dcd 100644 --- a/src/view/com/composer/text-input/TextInput.web.tsx +++ b/src/view/com/composer/text-input/TextInput.web.tsx @@ -344,7 +344,14 @@ export const TextInput = React.forwardRef( return (
-
+
{textOverlay}