From 330be974d0c8451c3bcfd3f7056fb8454940187e Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Sun, 8 Sep 2024 14:00:53 -0500 Subject: [PATCH 01/13] Upgrade all tiptap deps to latest --- package.json | 24 +-- yarn.lock | 415 ++++++++++++++++++++++----------------------------- 2 files changed, 187 insertions(+), 252 deletions(-) diff --git a/package.json b/package.json index 720af2e367..37f5fe272e 100644 --- a/package.json +++ b/package.json @@ -88,18 +88,18 @@ "@tanstack/query-async-storage-persister": "^5.25.0", "@tanstack/react-query": "^5.8.1", "@tanstack/react-query-persist-client": "^5.25.0", - "@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", + "@tiptap/core": "^2.6.6", + "@tiptap/extension-document": "^2.6.6", + "@tiptap/extension-hard-break": "^2.6.6", + "@tiptap/extension-history": "^2.6.6", + "@tiptap/extension-mention": "^2.6.6", + "@tiptap/extension-paragraph": "^2.6.6", + "@tiptap/extension-placeholder": "^2.6.6", + "@tiptap/extension-text": "^2.6.6", + "@tiptap/html": "^2.6.6", + "@tiptap/pm": "^2.6.6", + "@tiptap/react": "^2.6.6", + "@tiptap/suggestion": "^2.6.6", "@types/invariant": "^2.2.37", "@types/lodash.throttle": "^4.1.9", "@types/node": "^18.16.2", diff --git a/yarn.lock b/yarn.lock index 8d72b9d84b..e38b5a7ccd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5942,32 +5942,6 @@ 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" - "@rnx-kit/chromium-edge-launcher@^1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@rnx-kit/chromium-edge-launcher/-/chromium-edge-launcher-1.0.0.tgz#c0df8ea00a902c7a417cd9655aab06de398b939c" @@ -6871,103 +6845,105 @@ 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/core@^2.6.6": + version "2.6.6" + resolved "https://registry.yarnpkg.com/@tiptap/core/-/core-2.6.6.tgz#34edc749dece70e99231b669582b92afae1c3ec8" + integrity sha512-VO5qTsjt6rwworkuo0s5AqYMfDA0ZwiTiH6FHKFSu2G/6sS7HKcc/LjPq+5Legzps4QYdBDl3W28wGsGuS1GdQ== -"@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== +"@tiptap/extension-bubble-menu@^2.6.6": + version "2.6.6" + resolved "https://registry.yarnpkg.com/@tiptap/extension-bubble-menu/-/extension-bubble-menu-2.6.6.tgz#6e2b745fd10262258ce51562963694bfc8178ef1" + integrity sha512-IkfmlZq67aaegym5sBddBc/xXWCArxn5WJEl1oxKEayjQhybKSaqI7tk0lOx/x7fa5Ml1WlGpCFh+KKXbQTG0g== 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-document@^2.6.6": + version "2.6.6" + resolved "https://registry.yarnpkg.com/@tiptap/extension-document/-/extension-document-2.6.6.tgz#d766c645409b3799d0f9fa6ac8a318e34052db15" + integrity sha512-6qlH5VWzLHHRVeeciRC6C4ZHpMsAGPNG16EF53z0GeMSaaFD/zU3B239QlmqXmLsAl8bpf8Bn93N0t2ABUvScw== -"@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== +"@tiptap/extension-floating-menu@^2.6.6": + version "2.6.6" + resolved "https://registry.yarnpkg.com/@tiptap/extension-floating-menu/-/extension-floating-menu-2.6.6.tgz#37a16b966518b24fce021e2591669b470467c419" + integrity sha512-lPkESOfAUxgmXRiNqUU23WSyja5FUfSWjsW4hqe+BKNjsUt1OuFMEtYJtNc+MCGhhtPfFvM3Jg6g9jd6g5XsLQ== 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-hard-break@^2.6.6": + version "2.6.6" + resolved "https://registry.yarnpkg.com/@tiptap/extension-hard-break/-/extension-hard-break-2.6.6.tgz#cc31c641e1dbd37ff8fab3c6f66ac2f700d863f1" + integrity sha512-bsUuyYBrMDEiudx1dOQSr9MzKv13m0xHWrOK+DYxuIDYJb5g+c9un5cK7Js+et/HEYYSPOoH/iTW6h+4I5YeUg== -"@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-history@^2.6.6": + version "2.6.6" + resolved "https://registry.yarnpkg.com/@tiptap/extension-history/-/extension-history-2.6.6.tgz#8067d7f50ef529c84179eeb946e6f81af8853b90" + integrity sha512-tPTzAmPGqMX5Bd5H8lzRpmsaMvB9DvI5Dy2za/VQuFtxgXmDiFVgHRkRXIuluSkPTuANu84XBOQ0cBijqY8x4w== -"@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-mention@^2.6.6": + version "2.6.6" + resolved "https://registry.yarnpkg.com/@tiptap/extension-mention/-/extension-mention-2.6.6.tgz#c46878bed263eb0ddb8f57b33530dd4ae7000920" + integrity sha512-fghNe4ZQRiZ7i3+sSrZx87zPZjaCwVtxn56/5UinoBUP/ZpCGwGtI+ErKhCBVyLW1fKyd0MmlihK/IGIeCBw1A== -"@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-paragraph@^2.6.6": + version "2.6.6" + resolved "https://registry.yarnpkg.com/@tiptap/extension-paragraph/-/extension-paragraph-2.6.6.tgz#fb975b58e5055bb52e3df0fb9f996ed2407322e5" + integrity sha512-fD/onCr16UQWx+/xEmuFC2MccZZ7J5u4YaENh8LMnAnBXf78iwU7CAcmuc9rfAEO3qiLoYGXgLKiHlh2ZfD4wA== -"@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-placeholder@^2.6.6": + version "2.6.6" + resolved "https://registry.yarnpkg.com/@tiptap/extension-placeholder/-/extension-placeholder-2.6.6.tgz#a4e7a0150f3ab1c685cca75b2ee8f6fb73fbda1a" + integrity sha512-J0ZMvF93NsRrt+R7IQ3GhxNq32vq+88g25oV/YFJiwvC48HMu1tQB6kG1I3LJpu5b8lN+LnfANNqDOEhiBfjaA== -"@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/extension-text@^2.6.6": + version "2.6.6" + resolved "https://registry.yarnpkg.com/@tiptap/extension-text/-/extension-text-2.6.6.tgz#8851aa57e4fcd2194968dc64f5f5599ad858dbfa" + integrity sha512-e84uILnRzNzcwK1DVQNpXVmBG1Cq3BJipTOIDl1LHifOok7MBjhI/X+/NR0bd3N2t6gmDTWi63+4GuJ5EeDmsg== -"@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== +"@tiptap/html@^2.6.6": + version "2.6.6" + resolved "https://registry.yarnpkg.com/@tiptap/html/-/html-2.6.6.tgz#ac0c57d9ca7ac5429c3c60f19cf6828a1cd3aa29" + integrity sha512-OjS+rmu3jNTGbt0BR9pKVaK2w2y8dhnWOqqu4Fn7CKMJGD0HkDM+pYV/ks5ZU2TgTkPT6edosOantnrkvJJcmQ== dependencies: - zeed-dom "^0.9.19" + zeed-dom "^0.10.9" -"@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== +"@tiptap/pm@^2.6.6": + version "2.6.6" + resolved "https://registry.yarnpkg.com/@tiptap/pm/-/pm-2.6.6.tgz#8ce1956f7fb0e222cc9399bffc81349c6b23bbc0" + integrity sha512-56FGLPn3fwwUlIbLs+BO21bYfyqP9fKyZQbQyY0zWwA/AG2kOwoXaRn7FOVbjP6CylyWpFJnpRRmgn694QKHEg== 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" + prosemirror-changeset "^2.2.1" + prosemirror-collab "^1.3.1" + prosemirror-commands "^1.5.2" + prosemirror-dropcursor "^1.8.1" + prosemirror-gapcursor "^1.3.2" + prosemirror-history "^1.4.1" + prosemirror-inputrules "^1.4.0" + prosemirror-keymap "^1.2.2" + prosemirror-markdown "^1.13.0" + prosemirror-menu "^1.2.4" + prosemirror-model "^1.22.2" + prosemirror-schema-basic "^1.2.3" + prosemirror-schema-list "^1.4.1" + prosemirror-state "^1.4.3" + prosemirror-tables "^1.4.0" + prosemirror-trailing-node "^2.0.9" + prosemirror-transform "^1.9.0" + prosemirror-view "^1.33.9" -"@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== +"@tiptap/react@^2.6.6": + version "2.6.6" + resolved "https://registry.yarnpkg.com/@tiptap/react/-/react-2.6.6.tgz#661be83100c4a4206d7c933e6e0b66c91e7e9819" + integrity sha512-AUmdb/J1O/vCO2b8LL68ctcZr9a3931BwX4fUUZ1kCrCA5lTj2xz0rjeAtpxEdzLnR+Z7q96vB7vf7bPYOUAew== dependencies: - "@tiptap/extension-bubble-menu" "^2.1.6" - "@tiptap/extension-floating-menu" "^2.1.6" + "@tiptap/extension-bubble-menu" "^2.6.6" + "@tiptap/extension-floating-menu" "^2.6.6" + "@types/use-sync-external-store" "^0.0.6" + use-sync-external-store "^1.2.2" -"@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== +"@tiptap/suggestion@^2.6.6": + version "2.6.6" + resolved "https://registry.yarnpkg.com/@tiptap/suggestion/-/suggestion-2.6.6.tgz#acb07c410db862b58d9118199ef5ff53d3608fd6" + integrity sha512-jogG0QgGit9UtTznVnhQfNImZfQM89NR0is20yRQzC0HmD8B8f3jmGrotG63Why2oKbeoe3CpM5/5eDE/paqCA== "@tokenizer/token@^0.3.0": version "0.3.0" @@ -7362,16 +7338,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" @@ -7495,11 +7461,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" @@ -7510,6 +7471,11 @@ resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.3.tgz#a136f83b0758698df454e328759dbd3d44555311" integrity sha512-NfQ4gyz38SL8sDNrSixxU2Os1a5xcdFxipAFxYEuLUlvU2uDwS4NUpsImcf1//SlWItCVMMLiylsxbmNMToV/g== +"@types/use-sync-external-store@^0.0.6": + version "0.0.6" + resolved "https://registry.yarnpkg.com/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz#60be8d21baab8c305132eb9cb912ed497852aadc" + integrity sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg== + "@types/ws@^8.5.5": version "8.5.5" resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.5.tgz#af587964aa06682702ee6dcbc7be41a80e4b28eb" @@ -9068,11 +9034,6 @@ caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001517, caniuse-lite@^1.0.30001520, can resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001655.tgz" integrity sha512-jRGVy3iSGO5Uutn2owlb5gR6qsGngTw9ZTb4ali9f3glshcNmJ2noam4Mo9zia5P9Dk3jNNydy7vQjuE5dQmfg== -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" @@ -9985,11 +9946,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-urls@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-2.0.0.tgz#156485a72963a970f5d5821aaf642bef2bf2db9b" @@ -10088,7 +10044,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== @@ -10571,11 +10527,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" @@ -13182,13 +13133,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" @@ -15047,12 +14991,12 @@ 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== +linkify-it@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-5.0.0.tgz#9ef238bfa6dc70bd8e7f9572b52d369af569b421" + integrity sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ== dependencies: - uc.micro "^1.0.1" + uc.micro "^2.0.0" lint-staged@^13.2.3: version "13.3.0" @@ -15334,7 +15278,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== @@ -15346,16 +15290,17 @@ 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== +markdown-it@^14.0.0: + version "14.1.0" + resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-14.1.0.tgz#3c3c5992883c633db4714ccb4d7b5935d98b7d45" + integrity sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg== dependencies: argparse "^2.0.1" - entities "~3.0.1" - linkify-it "^4.0.1" - mdurl "^1.0.1" - uc.micro "^1.0.5" + entities "^4.4.0" + linkify-it "^5.0.0" + mdurl "^2.0.0" + punycode.js "^2.3.1" + uc.micro "^2.1.0" marky@^1.2.2: version "1.2.5" @@ -15419,10 +15364,10 @@ 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== +mdurl@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-2.0.0.tgz#80676ec0433025dd3e17ee983d0fe8de5a2237e0" + integrity sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w== media-typer@0.3.0: version "0.3.0" @@ -16267,20 +16212,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" @@ -17709,30 +17640,30 @@ prop-types@^15.6.1, prop-types@^15.7.2, prop-types@^15.8.1: object-assign "^4.1.1" react-is "^16.13.1" -prosemirror-changeset@^2.2.0: +prosemirror-changeset@^2.2.1: 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: +prosemirror-collab@^1.3.1: 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== +prosemirror-commands@^1.0.0, prosemirror-commands@^1.5.2: + version "1.6.0" + resolved "https://registry.yarnpkg.com/prosemirror-commands/-/prosemirror-commands-1.6.0.tgz#b79f034ed371576e7bf83ddd4ede689c8ccbd9ab" + integrity sha512-xn1U/g36OqXn2tn5nGmvnnimAj/g1pUx2ypJJIe8WkVX83WyJVC5LTARaxZa2AtQRwntu9Jc5zXs9gL9svp/mg== dependencies: prosemirror-model "^1.0.0" prosemirror-state "^1.0.0" prosemirror-transform "^1.0.0" -prosemirror-dropcursor@^1.5.0: +prosemirror-dropcursor@^1.8.1: version "1.8.1" resolved "https://registry.yarnpkg.com/prosemirror-dropcursor/-/prosemirror-dropcursor-1.8.1.tgz#49b9fb2f583e0d0f4021ff87db825faa2be2832d" integrity sha512-M30WJdJZLyXHi3N8vxN6Zh5O8ZBbQCz0gURTfPmTIBNQ5pxrdU7A58QkNqfa98YEjSAL1HUyyU34f6Pm5xBSGw== @@ -17741,7 +17672,7 @@ prosemirror-dropcursor@^1.5.0: prosemirror-transform "^1.1.0" prosemirror-view "^1.1.0" -prosemirror-gapcursor@^1.3.1: +prosemirror-gapcursor@^1.3.2: version "1.3.2" resolved "https://registry.yarnpkg.com/prosemirror-gapcursor/-/prosemirror-gapcursor-1.3.2.tgz#5fa336b83789c6199a7341c9493587e249215cb4" integrity sha512-wtjswVBd2vaQRrnYZaBCbyDqr232Ed4p2QPtRIUK5FuqHYKGWkEwl08oQM4Tw7DOR0FsasARV5uJFvMZWxdNxQ== @@ -17751,25 +17682,25 @@ prosemirror-gapcursor@^1.3.1: 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== +prosemirror-history@^1.0.0, prosemirror-history@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/prosemirror-history/-/prosemirror-history-1.4.1.tgz#cc370a46fb629e83a33946a0e12612e934ab8b98" + integrity sha512-2JZD8z2JviJrboD9cPuX/Sv/1ChFng+xh2tChQ2X4bB2HeK+rra/bmJ3xGntCcjhOqIzSDG6Id7e8RJ9QPXLEQ== 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== +prosemirror-inputrules@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/prosemirror-inputrules/-/prosemirror-inputrules-1.4.0.tgz#ef1519bb2cb0d1e0cec74bad1a97f1c1555068bb" + integrity sha512-6ygpPRuTJ2lcOXs9JkefieMst63wVJBgHZGl5QOytN7oSZs3Co/BYbc3Yx9zm9H37Bxw8kVzCnDsihsVsL4yEg== 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: +prosemirror-keymap@^1.0.0, prosemirror-keymap@^1.1.2, prosemirror-keymap@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/prosemirror-keymap/-/prosemirror-keymap-1.2.2.tgz#14a54763a29c7b2704f561088ccf3384d14eb77e" integrity sha512-EAlXoksqC6Vbocqc0GtzCruZEzYgrn+iiGnNjsJsH4mrnIGex4qbLdWWNza3AW5W36ZRrlBID0eM6bdKH4OStQ== @@ -17777,15 +17708,15 @@ prosemirror-keymap@^1.0.0, prosemirror-keymap@^1.1.2, prosemirror-keymap@^1.2.0: 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== +prosemirror-markdown@^1.13.0: + version "1.13.0" + resolved "https://registry.yarnpkg.com/prosemirror-markdown/-/prosemirror-markdown-1.13.0.tgz#67ebfa40af48a22d1e4ed6cad2e29851eb61e649" + integrity sha512-UziddX3ZYSYibgx8042hfGKmukq5Aljp2qoBiJRejD/8MH70siQNz5RB1TrdTPheqLMy4aCe4GYNF10/3lQS5g== dependencies: - markdown-it "^13.0.1" - prosemirror-model "^1.0.0" + markdown-it "^14.0.0" + prosemirror-model "^1.20.0" -prosemirror-menu@^1.2.1: +prosemirror-menu@^1.2.4: 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== @@ -17795,30 +17726,30 @@ prosemirror-menu@^1.2.1: 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== +prosemirror-model@^1.0.0, prosemirror-model@^1.19.0, prosemirror-model@^1.20.0, prosemirror-model@^1.21.0, prosemirror-model@^1.22.2, prosemirror-model@^1.8.1: + version "1.22.3" + resolved "https://registry.yarnpkg.com/prosemirror-model/-/prosemirror-model-1.22.3.tgz#52fdf5897f348b0f07f64bea89156d90afdf645a" + integrity sha512-V4XCysitErI+i0rKFILGt/xClnFJaohe/wrrlT2NSZ+zk8ggQfDH4x2wNK7Gm0Hp4CIoWizvXFP7L9KMaCuI0Q== 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== +prosemirror-schema-basic@^1.2.3: + version "1.2.3" + resolved "https://registry.yarnpkg.com/prosemirror-schema-basic/-/prosemirror-schema-basic-1.2.3.tgz#649c349bb21c61a56febf9deb71ac68fca4cedf2" + integrity sha512-h+H0OQwZVqMon1PNn0AG9cTfx513zgIG2DY00eJ00Yvgb3UD+GQ/VlWW5rcaxacpCGT1Yx8nuhwXk4+QbXUfJA== 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== +prosemirror-schema-list@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/prosemirror-schema-list/-/prosemirror-schema-list-1.4.1.tgz#78b8d25531db48ca9688836dbde50e13ac19a4a1" + integrity sha512-jbDyaP/6AFfDfu70VzySsD75Om2t3sXTOdl5+31Wlxlg62td1haUpty/ybajSfJ1pkGadlOfwQq9kgW5IMo1Rg== 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: +prosemirror-state@^1.0.0, prosemirror-state@^1.2.2, prosemirror-state@^1.3.1, prosemirror-state@^1.4.3: version "1.4.3" resolved "https://registry.yarnpkg.com/prosemirror-state/-/prosemirror-state-1.4.3.tgz#94aecf3ffd54ec37e87aa7179d13508da181a080" integrity sha512-goFKORVbvPuAQaXhpbemJFRKJ2aixr+AZMGiquiqKxaucC6hlpHNZHWgz5R7dS4roHiwq9vDctE//CZ++o0W1Q== @@ -17827,10 +17758,10 @@ prosemirror-state@^1.0.0, prosemirror-state@^1.2.2, prosemirror-state@^1.3.1, pr 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== +prosemirror-tables@^1.4.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/prosemirror-tables/-/prosemirror-tables-1.5.0.tgz#3ba1ea3d53852505cc0d2037ce386973bb639a7d" + integrity sha512-VMx4zlYWm7aBlZ5xtfJHpqa3Xgu3b7srV54fXYnXgsAcIGRqKSrhiK3f89omzzgaAgAtDOV4ImXnLKhVfheVNQ== dependencies: prosemirror-keymap "^1.1.2" prosemirror-model "^1.8.1" @@ -17838,28 +17769,27 @@ prosemirror-tables@^1.3.0: 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== +prosemirror-trailing-node@^2.0.9: + version "2.0.9" + resolved "https://registry.yarnpkg.com/prosemirror-trailing-node/-/prosemirror-trailing-node-2.0.9.tgz#a087e6d1372e888cd3e57c977507b6b85dc658e4" + integrity sha512-YvyIn3/UaLFlFKrlJB6cObvUhmwFNZVhy1Q8OpW/avoTbD/Y7H5EcjK4AZFKhmuS6/N6WkGgt7gWtBWDnmFvHg== 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== +prosemirror-transform@^1.0.0, prosemirror-transform@^1.1.0, prosemirror-transform@^1.2.1, prosemirror-transform@^1.7.3, prosemirror-transform@^1.9.0: + version "1.10.0" + resolved "https://registry.yarnpkg.com/prosemirror-transform/-/prosemirror-transform-1.10.0.tgz#2211ddcb13f12e4e530de97c077f82ab46177b0c" + integrity sha512-9UOgFSgN6Gj2ekQH5CTDJ8Rp/fnKR2IkYfGdzzp5zQMFsS4zDllLVx/+jGcX86YlACpG7UR5fwAXiWzxqWtBTg== dependencies: - prosemirror-model "^1.0.0" + prosemirror-model "^1.21.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== +prosemirror-view@^1.0.0, prosemirror-view@^1.1.0, prosemirror-view@^1.13.3, prosemirror-view@^1.27.0, prosemirror-view@^1.31.0, prosemirror-view@^1.33.9: + version "1.34.2" + resolved "https://registry.yarnpkg.com/prosemirror-view/-/prosemirror-view-1.34.2.tgz#239a1766e46e364666e2b850855361929c4236df" + integrity sha512-tPX/V2Xd70vrAGQ/V9CppJtPKnQyQMypJGlLylvdI94k6JaG+4P6fVmXPR1zc1eVTW0gq3c6zsfqwJKCRLaG9Q== dependencies: - prosemirror-model "^1.16.0" + prosemirror-model "^1.20.0" prosemirror-state "^1.0.0" prosemirror-transform "^1.1.0" @@ -17896,6 +17826,11 @@ pump@^3.0.0: end-of-stream "^1.1.0" once "^1.3.1" +punycode.js@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/punycode.js/-/punycode.js-2.3.1.tgz#6b53e56ad75588234e79f4affa90972c7dd8cdb7" + integrity sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA== + punycode@^2.1.0, punycode@^2.1.1: version "2.3.0" resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.0.tgz#f67fa67c94da8f4d0cfff981aee4118064199b8f" @@ -20330,11 +20265,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" @@ -20580,7 +20510,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== @@ -20671,10 +20601,10 @@ 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== +uc.micro@^2.0.0, uc.micro@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-2.1.0.tgz#f8d3f7d0ec4c3dea35a7e3c8efa4cb8b45c9e7ee" + integrity sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A== uglify-js@^3.1.4: version "3.17.4" @@ -20906,6 +20836,11 @@ use-sidecar@^1.1.2: detect-node-es "^1.1.0" tslib "^2.0.0" +use-sync-external-store@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.2.tgz#c3b6390f3a30eba13200d2302dcdf1e7b57b2ef9" + integrity sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw== + util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" @@ -21845,7 +21780,7 @@ 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: +zeed-dom@0.10.9, zeed-dom@^0.10.9: version "0.10.9" resolved "https://registry.yarnpkg.com/zeed-dom/-/zeed-dom-0.10.9.tgz#b3eb5d9b7cf1be17e1fb3a708379df5edce195be" integrity sha512-qQQ7Wu7IJ3Vo/LjeKWj97A2Hi17di4ZdmgNZj6AWbDbpt3hvO4EMfjYVA2/2unLYT+XpmMq5fqaLqCeU7Im83A== From 0ab51794e26f9bfdbbfb0d88e6c405ba48cdad07 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Sun, 8 Sep 2024 13:41:02 -0500 Subject: [PATCH 02/13] Dump in files --- .../composer/text-input/web/Tags/index.tsx | 2 + .../composer/text-input/web/Tags/plugin.tsx | 197 +++++++++++++++ .../com/composer/text-input/web/Tags/utils.ts | 74 ++++++ .../com/composer/text-input/web/Tags/view.tsx | 227 ++++++++++++++++++ 4 files changed, 500 insertions(+) create mode 100644 src/view/com/composer/text-input/web/Tags/index.tsx create mode 100644 src/view/com/composer/text-input/web/Tags/plugin.tsx create mode 100644 src/view/com/composer/text-input/web/Tags/utils.ts create mode 100644 src/view/com/composer/text-input/web/Tags/view.tsx diff --git a/src/view/com/composer/text-input/web/Tags/index.tsx b/src/view/com/composer/text-input/web/Tags/index.tsx new file mode 100644 index 0000000000..8df8f65d0b --- /dev/null +++ b/src/view/com/composer/text-input/web/Tags/index.tsx @@ -0,0 +1,2 @@ +export {Tags} from './plugin' +export {createTagsAutocomplete} from './view' diff --git a/src/view/com/composer/text-input/web/Tags/plugin.tsx b/src/view/com/composer/text-input/web/Tags/plugin.tsx new file mode 100644 index 0000000000..851ed3e1fe --- /dev/null +++ b/src/view/com/composer/text-input/web/Tags/plugin.tsx @@ -0,0 +1,197 @@ +/** + * This is basically a fork of the Mention plugin from Tiptap. + * + * @see https://github.com/ueberdosis/tiptap/blob/025dfff1d9e4796edf3a451f7f53d06a07b95d69/packages/extension-mention/src/mention.ts + */ + +import {mergeAttributes, Node} from '@tiptap/core' +import {Node as ProseMirrorNode} from '@tiptap/pm/model' +import {PluginKey} from '@tiptap/pm/state' +import Suggestion, {SuggestionOptions} from '@tiptap/suggestion' + +import {findSuggestionMatch} from './utils' + +export type TagOptions = { + HTMLAttributes: Record + renderLabel: (props: {options: TagOptions; node: ProseMirrorNode}) => string + suggestion: Omit +} + +export const TagsPluginKey = new PluginKey('tags') + +export const Tags = Node.create({ + name: 'tag', + + addOptions() { + return { + HTMLAttributes: {}, + renderLabel({node}) { + return `#${node.attrs.id}` + }, + suggestion: { + char: '#', + allowSpaces: true, + pluginKey: TagsPluginKey, + command: ({editor, range, props}) => { + const {tag, punctuation} = props + + // increase range.to by one when the next node is of type "text" + // and starts with a space character + const nodeAfter = editor.view.state.selection.$to.nodeAfter + const overrideSpace = nodeAfter?.text?.startsWith(' ') + + if (overrideSpace) { + range.to += 1 + } + + editor + .chain() + .focus() + .insertContentAt(range, [ + { + type: this.name, + attrs: {id: tag}, + }, + { + type: 'text', + text: `${punctuation || ''} `, + }, + ]) + .run() + + window.getSelection()?.collapseToEnd() + }, + /** + * This method and `findSuggestionMatch` below both have to return a + * truthy value, otherwise the suggestiond plugin will call `onExit` + * and we lose the ability to add a tag + */ + allow: ({state, range}) => { + const $from = state.doc.resolve(range.from) + const type = state.schema.nodes[this.name] + const allow = !!$from.parent.type.contentMatch.matchType(type) + return allow + }, + findSuggestionMatch({$position}) { + const text = $position.nodeBefore?.isText && $position.nodeBefore.text + const cursorPosition = $position.pos + + if (!text) { + return null + } + + return findSuggestionMatch({text, cursorPosition}) + }, + }, + } + }, + + group: 'inline', + + inline: true, + + atom: true, + + selectable: true, + + addAttributes() { + return { + id: { + default: null, + parseHTML: element => element.getAttribute('data-id'), + renderHTML: attributes => { + if (!attributes.id) { + return {} + } + + return { + 'data-id': attributes.id, + } + }, + }, + + label: { + default: null, + parseHTML: element => element.getAttribute('data-label'), + renderHTML: attributes => { + if (!attributes.label) { + return {} + } + + return { + 'data-label': attributes.label, + } + }, + }, + } + }, + + parseHTML() { + return [ + { + tag: `span[data-type="${this.name}"]`, + }, + ] + }, + + renderHTML({node, HTMLAttributes}) { + return [ + 'span', + mergeAttributes( + {'data-type': this.name}, + this.options.HTMLAttributes, + HTMLAttributes, + ), + this.options.renderLabel({ + options: this.options, + node, + }), + ] + }, + + renderText({node}) { + return this.options.renderLabel({ + options: this.options, + node, + }) + }, + + addKeyboardShortcuts() { + return { + Backspace: () => + this.editor.commands.command(({tr, state}) => { + let isTag = false + const {selection} = state + const {empty, anchor} = selection + + if (!empty) { + return false + } + + state.doc.nodesBetween(anchor - 1, anchor, (node, pos) => { + if (node.type.name === this.name) { + isTag = true + tr.insertText( + this.options.suggestion.char || '', + pos, + pos + node.nodeSize, + ) + + return false + } + }) + + return isTag + }), + } + }, + + addProseMirrorPlugins() { + return [ + Suggestion({ + editor: this.editor, + ...this.options.suggestion, + }), + ] + }, +}) diff --git a/src/view/com/composer/text-input/web/Tags/utils.ts b/src/view/com/composer/text-input/web/Tags/utils.ts new file mode 100644 index 0000000000..6894d2ba04 --- /dev/null +++ b/src/view/com/composer/text-input/web/Tags/utils.ts @@ -0,0 +1,74 @@ +import { + HASHTAG_WITH_TRAILING_PUNCTUATION_REGEX, + TRAILING_PUNCTUATION_REGEX, + LEADING_HASH_REGEX, +} from '@atproto/api' + +/** + * This method eventually receives the `query` property from the result of + * `findSuggestionMatch` below. + */ +export function parsePunctuationFromTag(value: string) { + const reg = TRAILING_PUNCTUATION_REGEX + const tag = value.replace(reg, '') + const punctuation = value.match(reg)?.[0] || '' + + return {tag, punctuation} +} + +/** + * A result must be returned from this method in order for the suggestion + * plugin to remain active and allow for the user to select a suggestion. + * + * That's why we use the loose regex form that includes trialing punctuation. + * We strip that our later. + */ +export function findSuggestionMatch({ + text, + cursorPosition, +}: { + text: string + cursorPosition: number +}) { + const match = Array.from( + text.matchAll(HASHTAG_WITH_TRAILING_PUNCTUATION_REGEX), + ).pop() + + if (!match || match.input === undefined || match.index === undefined) { + return null + } + + const startIndex = cursorPosition - text.length + let [matchedString, tagWithTrailingPunctuation] = match + + const sanitized = tagWithTrailingPunctuation + .replace(TRAILING_PUNCTUATION_REGEX, '') + .replace(LEADING_HASH_REGEX, '') + + // one of our hashtag spec rules + if (sanitized.length > 64) return null + + const from = + startIndex + match.index + matchedString.indexOf(tagWithTrailingPunctuation) + const to = from + tagWithTrailingPunctuation.length + + if (from < cursorPosition && to >= cursorPosition) { + return { + range: { + from, + to, + }, + /** + * This is passed to the `items({ query })` method configured in + * `createTagsAutocomplete`. + * + * We parse out the punctuation later. + */ + query: tagWithTrailingPunctuation.replace(LEADING_HASH_REGEX, ''), + // raw text string + text: matchedString, + } + } + + return null +} diff --git a/src/view/com/composer/text-input/web/Tags/view.tsx b/src/view/com/composer/text-input/web/Tags/view.tsx new file mode 100644 index 0000000000..dce26e0596 --- /dev/null +++ b/src/view/com/composer/text-input/web/Tags/view.tsx @@ -0,0 +1,227 @@ +import React, {forwardRef, useImperativeHandle, useState} from 'react' +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 {TagsAutocompleteModel} from 'state/models/ui/tags-autocomplete' +import {usePalette} from 'lib/hooks/usePalette' +import {Text} from 'view/com/util/text/Text' + +import {parsePunctuationFromTag} from './utils' + +type AutocompleteResult = string +type ListProps = SuggestionProps & { + autocompleteModel: TagsAutocompleteModel +} +type AutocompleteRef = { + onKeyDown: (props: SuggestionKeyDownProps) => boolean +} + +export function createTagsAutocomplete({ + autocompleteModel, +}: { + autocompleteModel: TagsAutocompleteModel +}): Omit { + return { + /** + * This `query` param comes from the result of `findSuggestionMatch` + */ + async items({query}) { + autocompleteModel.setActive(true) + await autocompleteModel.search(query) + return autocompleteModel.suggestions.slice(0, 8) + }, + render() { + let component: ReactRenderer | undefined + let popup: TippyInstance[] | undefined + + return { + onStart: props => { + component = new ReactRenderer(Autocomplete, { + props: { + ...props, + autocompleteModel, + }, + editor: props.editor, + }) + + 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', + }) + }, + + onUpdate(props) { + component?.updateProps(props) + + if (!props.clientRect) return + + popup?.[0]?.setProps({ + // @ts-ignore getReferenceClientRect doesnt like that clientRect can return null -prf + getReferenceClientRect: props.clientRect, + }) + }, + + onKeyDown(props) { + if (props.event.key === 'Escape') { + popup?.[0]?.hide() + + return true + } + + return component?.ref?.onKeyDown(props) || false + }, + onExit() { + popup?.[0]?.destroy() + component?.destroy() + }, + } + }, + } +} + +const Autocomplete = forwardRef( + function AutocompleteImpl(props, ref) { + const {items, command, autocompleteModel} = props + const pal = usePalette('default') + const [selectedIndex, setSelectedIndex] = useState(0) + + const commit = React.useCallback( + (query: string) => { + const {tag, punctuation} = parsePunctuationFromTag(query) + /* + * This values here are passed directly to the `command` method + * configured in the `Tags` plugin. + * + * The type error is ignored because we parse the tag and punctuation + * separately above. We could do this in `command` definition, but we + * only want to `commitRecentTag` with the sanitized tag. + */ + // @ts-ignore + command({tag, punctuation}) + }, + [command], + ) + + const selectItem = React.useCallback( + (index: number) => { + const item = items[index] + if (item) commit(item) + }, + [items, commit], + ) + + useImperativeHandle(ref, () => ({ + onKeyDown: ({event}) => { + if (event.key === 'ArrowUp') { + setSelectedIndex( + (selectedIndex + props.items.length - 1) % props.items.length, + ) + return true + } + + if (event.key === 'ArrowDown') { + setSelectedIndex((selectedIndex + 1) % props.items.length) + return true + } + + if (event.key === 'Enter') { + if (!props.items.length) { + // no items, use whatever the user typed + commit(autocompleteModel.query) + } else { + selectItem(selectedIndex) + } + return true + } + + if (event.key === ' ') { + commit(autocompleteModel.query) + return true + } + + return false + }, + })) + + // hide entirely if no suggestions + if (!items.length) return null + + return ( +
+ + {items.map((tag, index) => { + const isSelected = selectedIndex === index + const isFirst = index === 0 + const isLast = index === items.length - 1 + + return ( + [ + styles.resultContainer, + { + backgroundColor: state.hovered + ? pal.viewLight.backgroundColor + : undefined, + }, + isSelected ? pal.viewLight : undefined, + isFirst + ? styles.firstResult + : isLast + ? styles.lastResult + : undefined, + ]} + onPress={() => selectItem(index)} + accessibilityRole="button"> + + #{tag} + + + ) + })} + +
+ ) + }, +) + +const styles = StyleSheet.create({ + container: { + width: 500, + borderRadius: 6, + borderWidth: 1, + borderStyle: 'solid', + padding: 4, + }, + resultContainer: { + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + flexDirection: 'row', + paddingHorizontal: 12, + paddingVertical: 8, + gap: 4, + }, + firstResult: { + borderTopLeftRadius: 2, + borderTopRightRadius: 2, + }, + lastResult: { + borderBottomLeftRadius: 2, + borderBottomRightRadius: 2, + }, +}) From bea6627d5907b86ae3aff799608f5d673f5201ba Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Mon, 9 Sep 2024 09:55:22 -0500 Subject: [PATCH 03/13] WIP, can't add spaces after tag --- .../com/composer/text-input/TextInput.web.tsx | 10 +- .../composer/text-input/web/Tags/plugin.tsx | 227 ++++++++++++------ .../com/composer/text-input/web/Tags/utils.ts | 58 ++--- .../com/composer/text-input/web/Tags/view.tsx | 22 +- 4 files changed, 197 insertions(+), 120 deletions(-) diff --git a/src/view/com/composer/text-input/TextInput.web.tsx b/src/view/com/composer/text-input/TextInput.web.tsx index acec61516a..73fdbde941 100644 --- a/src/view/com/composer/text-input/TextInput.web.tsx +++ b/src/view/com/composer/text-input/TextInput.web.tsx @@ -30,6 +30,7 @@ import {createSuggestion} from './web/Autocomplete' import {Emoji} from './web/EmojiPicker.web' import {LinkDecorator} from './web/LinkDecorator' import {TagDecorator} from './web/TagDecorator' +import {Tags, createTagsAutocomplete} from './web/Tags' export interface TextInputRef { focus: () => void @@ -71,7 +72,14 @@ export const TextInput = React.forwardRef(function TextInputImpl( () => [ Document, LinkDecorator, - TagDecorator, + // TagDecorator, + Tags.configure({ + HTMLAttributes: { + class: 'inline-tag', + }, + suggestion: createTagsAutocomplete({ + }), + }), Mention.configure({ HTMLAttributes: { class: 'mention', diff --git a/src/view/com/composer/text-input/web/Tags/plugin.tsx b/src/view/com/composer/text-input/web/Tags/plugin.tsx index 851ed3e1fe..d588c5ffc2 100644 --- a/src/view/com/composer/text-input/web/Tags/plugin.tsx +++ b/src/view/com/composer/text-input/web/Tags/plugin.tsx @@ -1,40 +1,108 @@ /** * This is basically a fork of the Mention plugin from Tiptap. * - * @see https://github.com/ueberdosis/tiptap/blob/025dfff1d9e4796edf3a451f7f53d06a07b95d69/packages/extension-mention/src/mention.ts + * @see https://github.com/ueberdosis/tiptap/blob/ec6121da1c3f808987d32de7a8c56b52520bfea8/packages/extension-mention/src/mention.ts */ -import {mergeAttributes, Node} from '@tiptap/core' -import {Node as ProseMirrorNode} from '@tiptap/pm/model' -import {PluginKey} from '@tiptap/pm/state' -import Suggestion, {SuggestionOptions} from '@tiptap/suggestion' +import { mergeAttributes, Node } from '@tiptap/core' +import { DOMOutputSpec, Node as ProseMirrorNode } from '@tiptap/pm/model' +import { PluginKey } from '@tiptap/pm/state' +import Suggestion, { SuggestionOptions } from '@tiptap/suggestion' import {findSuggestionMatch} from './utils' -export type TagOptions = { - HTMLAttributes: Record - renderLabel: (props: {options: TagOptions; node: ProseMirrorNode}) => string - suggestion: Omit +// See `addAttributes` below +export interface MentionNodeAttrs { + /** + * The identifier for the selected item that was mentioned, stored as a `data-id` + * attribute. + */ + id: string | null; + /** + * The label to be rendered by the editor as the displayed text for this mentioned + * item, if provided. Stored as a `data-label` attribute. See `renderLabel`. + */ + label?: string | null; } +export type MentionOptions = MentionNodeAttrs> = { + /** + * The HTML attributes for a mention node. + * @default {} + * @example { class: 'foo' } + */ + HTMLAttributes: Record + + /** + * A function to render the label of a mention. + * @deprecated use renderText and renderHTML instead + * @param props The render props + * @returns The label + * @example ({ options, node }) => `${options.suggestion.char}${node.attrs.label ?? node.attrs.id}` + */ + renderLabel?: (props: { options: MentionOptions; node: ProseMirrorNode }) => string + + /** + * A function to render the text of a mention. + * @param props The render props + * @returns The text + * @example ({ options, node }) => `${options.suggestion.char}${node.attrs.label ?? node.attrs.id}` + */ + renderText: (props: { options: MentionOptions; node: ProseMirrorNode }) => string + + /** + * A function to render the HTML of a mention. + * @param props The render props + * @returns The HTML as a ProseMirror DOM Output Spec + * @example ({ options, node }) => ['span', { 'data-type': 'mention' }, `${options.suggestion.char}${node.attrs.label ?? node.attrs.id}`] + */ + renderHTML: (props: { options: MentionOptions; node: ProseMirrorNode }) => DOMOutputSpec + + /** + * Whether to delete the trigger character with backspace. + * @default false + */ + deleteTriggerWithBackspace: boolean + + /** + * The suggestion options. + * @default {} + * @example { char: '@', pluginKey: MentionPluginKey, command: ({ editor, range, props }) => { ... } } + */ + suggestion: Omit, 'editor'> +} + +/** + * The plugin key for the mention plugin. + * @default 'mention' + */ export const TagsPluginKey = new PluginKey('tags') -export const Tags = Node.create({ - name: 'tag', +/** + * This extension allows you to insert mentions into the editor. + * @see https://www.tiptap.dev/api/extensions/mention + */ +export const Tags = Node.create({ + name: 'tags', addOptions() { return { HTMLAttributes: {}, - renderLabel({node}) { - return `#${node.attrs.id}` + renderText({ options, node }) { + return `${options.suggestion.char}${node.attrs.label ?? node.attrs.id}` + }, + deleteTriggerWithBackspace: false, + renderHTML({ options, node }) { + return [ + 'span', + mergeAttributes(this.HTMLAttributes, options.HTMLAttributes), + `${options.suggestion.char}${node.attrs.label ?? node.attrs.id}`, + ] }, suggestion: { char: '#', - allowSpaces: true, pluginKey: TagsPluginKey, - command: ({editor, range, props}) => { - const {tag, punctuation} = props - + command: ({ editor, range, props }) => { // increase range.to by one when the next node is of type "text" // and starts with a space character const nodeAfter = editor.view.state.selection.$to.nodeAfter @@ -50,38 +118,30 @@ export const Tags = Node.create({ .insertContentAt(range, [ { type: this.name, - attrs: {id: tag}, + attrs: props, }, { type: 'text', - text: `${punctuation || ''} `, + text: ' ', }, ]) .run() window.getSelection()?.collapseToEnd() }, - /** - * This method and `findSuggestionMatch` below both have to return a - * truthy value, otherwise the suggestiond plugin will call `onExit` - * and we lose the ability to add a tag - */ - allow: ({state, range}) => { + allow: ({ state, range }) => { const $from = state.doc.resolve(range.from) const type = state.schema.nodes[this.name] const allow = !!$from.parent.type.contentMatch.matchType(type) + console.log({ + allow, + $from, + state, + }) + return allow }, - findSuggestionMatch({$position}) { - const text = $position.nodeBefore?.isText && $position.nodeBefore.text - const cursorPosition = $position.pos - - if (!text) { - return null - } - - return findSuggestionMatch({text, cursorPosition}) - }, + findSuggestionMatch: findSuggestionMatch }, } }, @@ -90,9 +150,9 @@ export const Tags = Node.create({ inline: true, - atom: true, + selectable: false, - selectable: true, + atom: true, addAttributes() { return { @@ -134,23 +194,45 @@ export const Tags = Node.create({ ] }, - renderHTML({node, HTMLAttributes}) { - return [ - 'span', - mergeAttributes( - {'data-type': this.name}, - this.options.HTMLAttributes, - HTMLAttributes, - ), - this.options.renderLabel({ - options: this.options, - node, - }), - ] + renderHTML({ node, HTMLAttributes }) { + if (this.options.renderLabel !== undefined) { + console.warn('renderLabel is deprecated use renderText and renderHTML instead') + return [ + 'span', + mergeAttributes({ 'data-type': this.name }, this.options.HTMLAttributes, HTMLAttributes), + this.options.renderLabel({ + options: this.options, + node, + }), + ] + } + const mergedOptions = { ...this.options } + + mergedOptions.HTMLAttributes = mergeAttributes({ 'data-type': this.name }, this.options.HTMLAttributes, HTMLAttributes) + const html = this.options.renderHTML({ + options: mergedOptions, + node, + }) + + if (typeof html === 'string') { + return [ + 'span', + mergeAttributes({ 'data-type': this.name }, this.options.HTMLAttributes, HTMLAttributes), + html, + ] + } + return html }, - renderText({node}) { - return this.options.renderLabel({ + renderText({ node }) { + if (this.options.renderLabel !== undefined) { + console.warn('renderLabel is deprecated use renderText and renderHTML instead') + return this.options.renderLabel({ + options: this.options, + node, + }) + } + return this.options.renderText({ options: this.options, node, }) @@ -158,31 +240,30 @@ export const Tags = Node.create({ addKeyboardShortcuts() { return { - Backspace: () => - this.editor.commands.command(({tr, state}) => { - let isTag = false - const {selection} = state - const {empty, anchor} = selection + Backspace: () => this.editor.commands.command(({ tr, state }) => { + let isMention = false + const { selection } = state + const { empty, anchor } = selection + + if (!empty) { + return false + } + + state.doc.nodesBetween(anchor - 1, anchor, (node, pos) => { + if (node.type.name === this.name) { + isMention = true + tr.insertText( + this.options.deleteTriggerWithBackspace ? '' : this.options.suggestion.char || '', + pos, + pos + node.nodeSize, + ) - if (!empty) { return false } + }) - state.doc.nodesBetween(anchor - 1, anchor, (node, pos) => { - if (node.type.name === this.name) { - isTag = true - tr.insertText( - this.options.suggestion.char || '', - pos, - pos + node.nodeSize, - ) - - return false - } - }) - - return isTag - }), + return isMention + }), } }, diff --git a/src/view/com/composer/text-input/web/Tags/utils.ts b/src/view/com/composer/text-input/web/Tags/utils.ts index 6894d2ba04..ae474ca4cc 100644 --- a/src/view/com/composer/text-input/web/Tags/utils.ts +++ b/src/view/com/composer/text-input/web/Tags/utils.ts @@ -1,8 +1,5 @@ -import { - HASHTAG_WITH_TRAILING_PUNCTUATION_REGEX, - TRAILING_PUNCTUATION_REGEX, - LEADING_HASH_REGEX, -} from '@atproto/api' +import {TAG_REGEX, TRAILING_PUNCTUATION_REGEX} from '@atproto/api' +import {findSuggestionMatch as defaultFindSuggestionMatch} from '@tiptap/suggestion' /** * This method eventually receives the `query` property from the result of @@ -22,51 +19,42 @@ export function parsePunctuationFromTag(value: string) { * * That's why we use the loose regex form that includes trialing punctuation. * We strip that our later. + * + * @see https://github.com/ueberdosis/tiptap/blob/cf2067906f506486c6613f872be8b1fd318526c9/packages/suggestion/src/findSuggestionMatch.ts */ export function findSuggestionMatch({ - text, - cursorPosition, -}: { - text: string - cursorPosition: number -}) { - const match = Array.from( - text.matchAll(HASHTAG_WITH_TRAILING_PUNCTUATION_REGEX), - ).pop() + $position, +}: Parameters[0]) { + const text = $position.nodeBefore?.isText && $position.nodeBefore.text + + if (!text) { + return null + } + + const textFrom = $position.pos - text.length + const match = Array.from(text.matchAll(TAG_REGEX)).pop() if (!match || match.input === undefined || match.index === undefined) { return null } - const startIndex = cursorPosition - text.length - let [matchedString, tagWithTrailingPunctuation] = match + const [fullMatch, , tag] = match - const sanitized = tagWithTrailingPunctuation - .replace(TRAILING_PUNCTUATION_REGEX, '') - .replace(LEADING_HASH_REGEX, '') + if (!tag || tag.length === 0 || tag.length > 64) return null - // one of our hashtag spec rules - if (sanitized.length > 64) return null + // The absolute position of the match in the document + const from = textFrom + fullMatch.indexOf(tag) + const to = from + tag.length + 1 - const from = - startIndex + match.index + matchedString.indexOf(tagWithTrailingPunctuation) - const to = from + tagWithTrailingPunctuation.length - - if (from < cursorPosition && to >= cursorPosition) { + // If the $position is located within the matched substring, return that range + if (from < $position.pos && to >= $position.pos) { return { range: { from, to, }, - /** - * This is passed to the `items({ query })` method configured in - * `createTagsAutocomplete`. - * - * We parse out the punctuation later. - */ - query: tagWithTrailingPunctuation.replace(LEADING_HASH_REGEX, ''), - // raw text string - text: matchedString, + query: tag.replace(TRAILING_PUNCTUATION_REGEX, ''), + text: fullMatch, } } diff --git a/src/view/com/composer/text-input/web/Tags/view.tsx b/src/view/com/composer/text-input/web/Tags/view.tsx index dce26e0596..76d0b8a2b8 100644 --- a/src/view/com/composer/text-input/web/Tags/view.tsx +++ b/src/view/com/composer/text-input/web/Tags/view.tsx @@ -8,7 +8,7 @@ import { SuggestionKeyDownProps, } from '@tiptap/suggestion' -import {TagsAutocompleteModel} from 'state/models/ui/tags-autocomplete' +// import {TagsAutocompleteModel} from 'state/models/ui/tags-autocomplete' import {usePalette} from 'lib/hooks/usePalette' import {Text} from 'view/com/util/text/Text' @@ -16,25 +16,25 @@ import {parsePunctuationFromTag} from './utils' type AutocompleteResult = string type ListProps = SuggestionProps & { - autocompleteModel: TagsAutocompleteModel + // autocompleteModel: TagsAutocompleteModel } type AutocompleteRef = { onKeyDown: (props: SuggestionKeyDownProps) => boolean } export function createTagsAutocomplete({ - autocompleteModel, + // autocompleteModel, }: { - autocompleteModel: TagsAutocompleteModel + // autocompleteModel: TagsAutocompleteModel }): Omit { return { /** * This `query` param comes from the result of `findSuggestionMatch` */ async items({query}) { - autocompleteModel.setActive(true) - await autocompleteModel.search(query) - return autocompleteModel.suggestions.slice(0, 8) + // autocompleteModel.setActive(true) + // await autocompleteModel.search(query) + return ['tag']//autocompleteModel.suggestions.slice(0, 8) }, render() { let component: ReactRenderer | undefined @@ -45,7 +45,7 @@ export function createTagsAutocomplete({ component = new ReactRenderer(Autocomplete, { props: { ...props, - autocompleteModel, + //autocompleteModel, }, editor: props.editor, }) @@ -95,7 +95,7 @@ export function createTagsAutocomplete({ const Autocomplete = forwardRef( function AutocompleteImpl(props, ref) { - const {items, command, autocompleteModel} = props + const {items, command} = props const pal = usePalette('default') const [selectedIndex, setSelectedIndex] = useState(0) @@ -141,7 +141,7 @@ const Autocomplete = forwardRef( if (event.key === 'Enter') { if (!props.items.length) { // no items, use whatever the user typed - commit(autocompleteModel.query) + // commit(autocompleteModel.query) } else { selectItem(selectedIndex) } @@ -149,7 +149,7 @@ const Autocomplete = forwardRef( } if (event.key === ' ') { - commit(autocompleteModel.query) + // commit(autocompleteModel.query) return true } From 7ae1321608daf9306560a55e99c06110d27f5fcf Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Mon, 9 Sep 2024 21:24:32 -0500 Subject: [PATCH 04/13] For sure matches Mention --- .../composer/text-input/web/Tags/plugin.tsx | 123 +++++++++++------- .../com/composer/text-input/web/Tags/utils.ts | 9 +- 2 files changed, 80 insertions(+), 52 deletions(-) diff --git a/src/view/com/composer/text-input/web/Tags/plugin.tsx b/src/view/com/composer/text-input/web/Tags/plugin.tsx index d588c5ffc2..81d247b1b9 100644 --- a/src/view/com/composer/text-input/web/Tags/plugin.tsx +++ b/src/view/com/composer/text-input/web/Tags/plugin.tsx @@ -4,10 +4,10 @@ * @see https://github.com/ueberdosis/tiptap/blob/ec6121da1c3f808987d32de7a8c56b52520bfea8/packages/extension-mention/src/mention.ts */ -import { mergeAttributes, Node } from '@tiptap/core' -import { DOMOutputSpec, Node as ProseMirrorNode } from '@tiptap/pm/model' -import { PluginKey } from '@tiptap/pm/state' -import Suggestion, { SuggestionOptions } from '@tiptap/suggestion' +import {mergeAttributes, Node} from '@tiptap/core' +import {DOMOutputSpec, Node as ProseMirrorNode} from '@tiptap/pm/model' +import {PluginKey} from '@tiptap/pm/state' +import Suggestion, {SuggestionOptions} from '@tiptap/suggestion' import {findSuggestionMatch} from './utils' @@ -17,15 +17,18 @@ export interface MentionNodeAttrs { * The identifier for the selected item that was mentioned, stored as a `data-id` * attribute. */ - id: string | null; + id: string | null /** * The label to be rendered by the editor as the displayed text for this mentioned * item, if provided. Stored as a `data-label` attribute. See `renderLabel`. */ - label?: string | null; + label?: string | null } -export type MentionOptions = MentionNodeAttrs> = { +export type MentionOptions< + SuggestionItem = any, + Attrs extends Record = MentionNodeAttrs, +> = { /** * The HTML attributes for a mention node. * @default {} @@ -40,7 +43,10 @@ export type MentionOptions `${options.suggestion.char}${node.attrs.label ?? node.attrs.id}` */ - renderLabel?: (props: { options: MentionOptions; node: ProseMirrorNode }) => string + renderLabel?: (props: { + options: MentionOptions + node: ProseMirrorNode + }) => string /** * A function to render the text of a mention. @@ -48,7 +54,10 @@ export type MentionOptions `${options.suggestion.char}${node.attrs.label ?? node.attrs.id}` */ - renderText: (props: { options: MentionOptions; node: ProseMirrorNode }) => string + renderText: (props: { + options: MentionOptions + node: ProseMirrorNode + }) => string /** * A function to render the HTML of a mention. @@ -56,7 +65,10 @@ export type MentionOptions ['span', { 'data-type': 'mention' }, `${options.suggestion.char}${node.attrs.label ?? node.attrs.id}`] */ - renderHTML: (props: { options: MentionOptions; node: ProseMirrorNode }) => DOMOutputSpec + renderHTML: (props: { + options: MentionOptions + node: ProseMirrorNode + }) => DOMOutputSpec /** * Whether to delete the trigger character with backspace. @@ -88,11 +100,11 @@ export const Tags = Node.create({ addOptions() { return { HTMLAttributes: {}, - renderText({ options, node }) { + renderText({options, node}) { return `${options.suggestion.char}${node.attrs.label ?? node.attrs.id}` }, deleteTriggerWithBackspace: false, - renderHTML({ options, node }) { + renderHTML({options, node}) { return [ 'span', mergeAttributes(this.HTMLAttributes, options.HTMLAttributes), @@ -102,7 +114,7 @@ export const Tags = Node.create({ suggestion: { char: '#', pluginKey: TagsPluginKey, - command: ({ editor, range, props }) => { + command: ({editor, range, props}) => { // increase range.to by one when the next node is of type "text" // and starts with a space character const nodeAfter = editor.view.state.selection.$to.nodeAfter @@ -129,19 +141,13 @@ export const Tags = Node.create({ window.getSelection()?.collapseToEnd() }, - allow: ({ state, range }) => { + allow: ({state, range}) => { const $from = state.doc.resolve(range.from) const type = state.schema.nodes[this.name] const allow = !!$from.parent.type.contentMatch.matchType(type) - console.log({ - allow, - $from, - state, - }) - return allow }, - findSuggestionMatch: findSuggestionMatch + findSuggestionMatch: findSuggestionMatch, }, } }, @@ -194,21 +200,31 @@ export const Tags = Node.create({ ] }, - renderHTML({ node, HTMLAttributes }) { + renderHTML({node, HTMLAttributes}) { if (this.options.renderLabel !== undefined) { - console.warn('renderLabel is deprecated use renderText and renderHTML instead') + console.warn( + 'renderLabel is deprecated use renderText and renderHTML instead', + ) return [ 'span', - mergeAttributes({ 'data-type': this.name }, this.options.HTMLAttributes, HTMLAttributes), + mergeAttributes( + {'data-type': this.name}, + this.options.HTMLAttributes, + HTMLAttributes, + ), this.options.renderLabel({ options: this.options, node, }), ] } - const mergedOptions = { ...this.options } + const mergedOptions = {...this.options} - mergedOptions.HTMLAttributes = mergeAttributes({ 'data-type': this.name }, this.options.HTMLAttributes, HTMLAttributes) + mergedOptions.HTMLAttributes = mergeAttributes( + {'data-type': this.name}, + this.options.HTMLAttributes, + HTMLAttributes, + ) const html = this.options.renderHTML({ options: mergedOptions, node, @@ -217,16 +233,22 @@ export const Tags = Node.create({ if (typeof html === 'string') { return [ 'span', - mergeAttributes({ 'data-type': this.name }, this.options.HTMLAttributes, HTMLAttributes), + mergeAttributes( + {'data-type': this.name}, + this.options.HTMLAttributes, + HTMLAttributes, + ), html, ] } return html }, - renderText({ node }) { + renderText({node}) { if (this.options.renderLabel !== undefined) { - console.warn('renderLabel is deprecated use renderText and renderHTML instead') + console.warn( + 'renderLabel is deprecated use renderText and renderHTML instead', + ) return this.options.renderLabel({ options: this.options, node, @@ -240,30 +262,33 @@ export const Tags = Node.create({ addKeyboardShortcuts() { return { - Backspace: () => this.editor.commands.command(({ tr, state }) => { - let isMention = false - const { selection } = state - const { empty, anchor } = selection - - if (!empty) { - return false - } - - state.doc.nodesBetween(anchor - 1, anchor, (node, pos) => { - if (node.type.name === this.name) { - isMention = true - tr.insertText( - this.options.deleteTriggerWithBackspace ? '' : this.options.suggestion.char || '', - pos, - pos + node.nodeSize, - ) + Backspace: () => + this.editor.commands.command(({tr, state}) => { + let isMention = false + const {selection} = state + const {empty, anchor} = selection + if (!empty) { return false } - }) - return isMention - }), + state.doc.nodesBetween(anchor - 1, anchor, (node, pos) => { + if (node.type.name === this.name) { + isMention = true + tr.insertText( + this.options.deleteTriggerWithBackspace + ? '' + : this.options.suggestion.char || '', + pos, + pos + node.nodeSize, + ) + + return false + } + }) + + return isMention + }), } }, diff --git a/src/view/com/composer/text-input/web/Tags/utils.ts b/src/view/com/composer/text-input/web/Tags/utils.ts index ae474ca4cc..ef6c1c3699 100644 --- a/src/view/com/composer/text-input/web/Tags/utils.ts +++ b/src/view/com/composer/text-input/web/Tags/utils.ts @@ -42,9 +42,12 @@ export function findSuggestionMatch({ if (!tag || tag.length === 0 || tag.length > 64) return null + const leadingSpaceOffset = fullMatch.startsWith(' ') ? 1 : 0 + const hashtagOffset = 1 + // The absolute position of the match in the document - const from = textFrom + fullMatch.indexOf(tag) - const to = from + tag.length + 1 + const from = textFrom + match.index + leadingSpaceOffset + const to = from + tag.length + hashtagOffset // If the $position is located within the matched substring, return that range if (from < $position.pos && to >= $position.pos) { @@ -54,7 +57,7 @@ export function findSuggestionMatch({ to, }, query: tag.replace(TRAILING_PUNCTUATION_REGEX, ''), - text: fullMatch, + text: fullMatch.replace(/^\s{1}/, ''), } } From 927584e914018dea6ecf7ad13bd404dd7b042a23 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Mon, 9 Sep 2024 22:09:37 -0500 Subject: [PATCH 05/13] Ok working --- .../composer/text-input/web/Tags/plugin.tsx | 11 +++++++--- .../com/composer/text-input/web/Tags/utils.ts | 13 +++++++++++- .../com/composer/text-input/web/Tags/view.tsx | 20 +++++++------------ 3 files changed, 27 insertions(+), 17 deletions(-) diff --git a/src/view/com/composer/text-input/web/Tags/plugin.tsx b/src/view/com/composer/text-input/web/Tags/plugin.tsx index 81d247b1b9..96cd442e0c 100644 --- a/src/view/com/composer/text-input/web/Tags/plugin.tsx +++ b/src/view/com/composer/text-input/web/Tags/plugin.tsx @@ -94,7 +94,9 @@ export const TagsPluginKey = new PluginKey('tags') * This extension allows you to insert mentions into the editor. * @see https://www.tiptap.dev/api/extensions/mention */ -export const Tags = Node.create({ +export const Tags = Node.create< + MentionOptions +>({ name: 'tags', addOptions() { @@ -115,6 +117,8 @@ export const Tags = Node.create({ char: '#', pluginKey: TagsPluginKey, command: ({editor, range, props}) => { + const {tag, punctuation} = props + // increase range.to by one when the next node is of type "text" // and starts with a space character const nodeAfter = editor.view.state.selection.$to.nodeAfter @@ -130,11 +134,11 @@ export const Tags = Node.create({ .insertContentAt(range, [ { type: this.name, - attrs: props, + attrs: {id: tag}, }, { type: 'text', - text: ' ', + text: `${punctuation || ''} `, }, ]) .run() @@ -145,6 +149,7 @@ export const Tags = Node.create({ const $from = state.doc.resolve(range.from) const type = state.schema.nodes[this.name] const allow = !!$from.parent.type.contentMatch.matchType(type) + return allow }, findSuggestionMatch: findSuggestionMatch, diff --git a/src/view/com/composer/text-input/web/Tags/utils.ts b/src/view/com/composer/text-input/web/Tags/utils.ts index ef6c1c3699..d65869928a 100644 --- a/src/view/com/composer/text-input/web/Tags/utils.ts +++ b/src/view/com/composer/text-input/web/Tags/utils.ts @@ -49,6 +49,13 @@ export function findSuggestionMatch({ const from = textFrom + match.index + leadingSpaceOffset const to = from + tag.length + hashtagOffset + console.log({ + pos: $position.pos, + from, + to, + text, + }) + // If the $position is located within the matched substring, return that range if (from < $position.pos && to >= $position.pos) { return { @@ -56,7 +63,11 @@ export function findSuggestionMatch({ from, to, }, - query: tag.replace(TRAILING_PUNCTUATION_REGEX, ''), + /** + * TODO + * We parse out the punctuation later. + */ + query: tag, text: fullMatch.replace(/^\s{1}/, ''), } } diff --git a/src/view/com/composer/text-input/web/Tags/view.tsx b/src/view/com/composer/text-input/web/Tags/view.tsx index 76d0b8a2b8..6a1dd2d0f0 100644 --- a/src/view/com/composer/text-input/web/Tags/view.tsx +++ b/src/view/com/composer/text-input/web/Tags/view.tsx @@ -1,17 +1,16 @@ import React, {forwardRef, useImperativeHandle, useState} from 'react' import {Pressable, StyleSheet, View} from 'react-native' import {ReactRenderer} from '@tiptap/react' -import tippy, {Instance as TippyInstance} from 'tippy.js' import { + SuggestionKeyDownProps, SuggestionOptions, SuggestionProps, - SuggestionKeyDownProps, } from '@tiptap/suggestion' +import tippy, {Instance as TippyInstance} from 'tippy.js' // import {TagsAutocompleteModel} from 'state/models/ui/tags-autocomplete' import {usePalette} from 'lib/hooks/usePalette' import {Text} from 'view/com/util/text/Text' - import {parsePunctuationFromTag} from './utils' type AutocompleteResult = string @@ -22,9 +21,8 @@ type AutocompleteRef = { onKeyDown: (props: SuggestionKeyDownProps) => boolean } -export function createTagsAutocomplete({ - // autocompleteModel, -}: { +export function createTagsAutocomplete({}: // autocompleteModel, +{ // autocompleteModel: TagsAutocompleteModel }): Omit { return { @@ -32,9 +30,7 @@ export function createTagsAutocomplete({ * This `query` param comes from the result of `findSuggestionMatch` */ async items({query}) { - // autocompleteModel.setActive(true) - // await autocompleteModel.search(query) - return ['tag']//autocompleteModel.suggestions.slice(0, 8) + return [query, 'javascript'] }, render() { let component: ReactRenderer | undefined @@ -45,7 +41,6 @@ export function createTagsAutocomplete({ component = new ReactRenderer(Autocomplete, { props: { ...props, - //autocompleteModel, }, editor: props.editor, }) @@ -110,7 +105,6 @@ const Autocomplete = forwardRef( * separately above. We could do this in `command` definition, but we * only want to `commitRecentTag` with the sanitized tag. */ - // @ts-ignore command({tag, punctuation}) }, [command], @@ -141,7 +135,7 @@ const Autocomplete = forwardRef( if (event.key === 'Enter') { if (!props.items.length) { // no items, use whatever the user typed - // commit(autocompleteModel.query) + commit(props.query) } else { selectItem(selectedIndex) } @@ -149,7 +143,7 @@ const Autocomplete = forwardRef( } if (event.key === ' ') { - // commit(autocompleteModel.query) + commit(props.query) return true } From 95ee2a9e2c21afe8f2b9e55be6ea38aec56369d3 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Tue, 8 Oct 2024 10:46:47 -0500 Subject: [PATCH 06/13] Fix styles --- src/style.css | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/src/style.css b/src/style.css index ef22a44571..779422442c 100644 --- a/src/style.css +++ b/src/style.css @@ -15,6 +15,11 @@ --text: black; --background: white; --backgroundLight: hsl(211, 20%, 95%); + + --tagFg: black; + --tagBg: #c1ccd7; + --mentionFg: white; + --mentionBg: #14a571; } @media (prefers-color-scheme: dark) { :root { @@ -22,6 +27,11 @@ --text: white; --background: black; --backgroundLight: hsl(211, 20%, 20%); + + --tagFg: black; + --tagBg: #c1ccd7; + --mentionFg: white; + --mentionBg: #14a571; } } @@ -30,6 +40,11 @@ html.theme--light { --background: white; --backgroundLight: hsl(211, 20%, 95%); background-color: white; + + --tagFg: black; + --tagBg: #c1ccd7; + --mentionFg: white; + --mentionBg: #14a571; } html.theme--dark { color-scheme: dark; @@ -37,6 +52,11 @@ html.theme--dark { --text: white; --background: black; --backgroundLight: hsl(211, 20%, 20%); + + --tagFg: white; + --tagBg: #637f9c; + --mentionFg: white; + --mentionBg: #14a571; } html.theme--dim { color-scheme: dark; @@ -44,6 +64,9 @@ html.theme--dim { --text: white; --background: hsl(211, 20%, 4%); --backgroundLight: hsl(211, 20%, 10%); + + --tagFg: white; + --tagBg: #637f9c; } /* Buttons and inputs have a font set by UA, so we'll have to reset that */ @@ -120,6 +143,26 @@ a[role='link'][data-no-underline='1']:hover { .ProseMirror-focused { outline: 0; } +/* Hashtags in ProseMirror */ +.ProseMirror .inline-tag, +.ProseMirror .mention { + position: relative; +} +/* Neatoâ„¢ */ +.ProseMirror .inline-tag { + color: var(--tagFg); + background-color: var(--tagBg); +} +.ProseMirror .mention { + color: var(--mentionFg); + background-color: var(--mentionBg); +} +.ProseMirror .inline-tag, +.ProseMirror .mention { + border-radius: 4px; + padding: 0 3px 2px; +} + textarea:focus, input:focus { outline: 0; From f43d2ebefadc406e44247ffea85601cfb2be1bdd Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Tue, 8 Oct 2024 13:24:04 -0500 Subject: [PATCH 07/13] WIP model --- package.json | 1 + src/storage/index.ts | 9 +++++- src/storage/schema.ts | 9 ++++++ .../com/composer/text-input/TextInput.web.tsx | 17 +++++----- .../text-input/tagsAutocompleteState.ts | 31 +++++++++++++++++++ .../com/composer/text-input/web/Tags/view.tsx | 28 ++++++++++------- yarn.lock | 5 +++ 7 files changed, 81 insertions(+), 19 deletions(-) create mode 100644 src/view/com/composer/text-input/tagsAutocompleteState.ts diff --git a/package.json b/package.json index 37f5fe272e..607bbcc130 100644 --- a/package.json +++ b/package.json @@ -143,6 +143,7 @@ "expo-updates": "~0.25.14", "expo-web-browser": "~13.0.3", "fast-text-encoding": "^1.0.6", + "fuse.js": "^7.0.0", "history": "^5.3.0", "hls.js": "^1.5.11", "js-sha256": "^0.9.0", diff --git a/src/storage/index.ts b/src/storage/index.ts index 7ef226d3aa..bc98341d4c 100644 --- a/src/storage/index.ts +++ b/src/storage/index.ts @@ -1,7 +1,7 @@ import {MMKV} from 'react-native-mmkv' import {IS_DEV} from '#/env' -import {Device} from '#/storage/schema' +import {Account} from '#/storage/schema' export * from '#/storage/schema' @@ -74,6 +74,13 @@ export class Storage { */ export const device = new Storage<[], Device>({id: 'bsky_device'}) +/** + * Account data that's specific to the account + * + * `account.set([did, key], true)` + */ +export const account = new Storage<[string], Account>({id: 'bsky_account'}) + if (IS_DEV && typeof window !== 'undefined') { // @ts-ignore window.bsky_storage = { diff --git a/src/storage/schema.ts b/src/storage/schema.ts index cf410c77de..69a379ae17 100644 --- a/src/storage/schema.ts +++ b/src/storage/schema.ts @@ -1,3 +1,5 @@ +import {Result} from '#/view/com/composer/text-input/tagsAutocompleteState' + /** * Device data that's specific to the device and does not vary based account */ @@ -9,3 +11,10 @@ export type Device = { countryCode: string | undefined } } + +/** + * Account data that's specific to the current account + */ +export type Account = { + recentTags: Result[] | undefined +} diff --git a/src/view/com/composer/text-input/TextInput.web.tsx b/src/view/com/composer/text-input/TextInput.web.tsx index 73fdbde941..118d5d8c10 100644 --- a/src/view/com/composer/text-input/TextInput.web.tsx +++ b/src/view/com/composer/text-input/TextInput.web.tsx @@ -13,15 +13,17 @@ import {Text as TiptapText} from '@tiptap/extension-text' import {generateJSON} from '@tiptap/html' import {EditorContent, JSONContent, useEditor} from '@tiptap/react' +import {useColorSchemeStyle} from '#/lib/hooks/useColorSchemeStyle' import {usePalette} from '#/lib/hooks/usePalette' +import {blobToDataUri, isUriImage} from '#/lib/media/util' import {useActorAutocompleteFn} from '#/state/queries/actor-autocomplete' -import {useColorSchemeStyle} from 'lib/hooks/useColorSchemeStyle' -import {blobToDataUri, isUriImage} from 'lib/media/util' -import {textInputWebEmitter} from '#/view/com/composer/text-input/textInputWebEmitter' +import {useSession} from '#/state/session' +import {tagAutocompleteModel} from '#/view/com/composer/text-input/tagsAutocompleteState' import { LinkFacetMatch, suggestLinkCardUri, -} from 'view/com/composer/text-input/text-input-util' +} from '#/view/com/composer/text-input/text-input-util' +import {textInputWebEmitter} from '#/view/com/composer/text-input/textInputWebEmitter' import {atoms as a, useAlf} from '#/alf' import {Portal} from '#/components/Portal' import {normalizeTextStyles} from '#/components/Typography' @@ -29,8 +31,7 @@ import {Text} from '../../util/text/Text' import {createSuggestion} from './web/Autocomplete' import {Emoji} from './web/EmojiPicker.web' import {LinkDecorator} from './web/LinkDecorator' -import {TagDecorator} from './web/TagDecorator' -import {Tags, createTagsAutocomplete} from './web/Tags' +import {createTagsAutocomplete,Tags} from './web/Tags' export interface TextInputRef { focus: () => void @@ -65,6 +66,7 @@ export const TextInput = React.forwardRef(function TextInputImpl( const autocomplete = useActorAutocompleteFn() const pal = usePalette('default') const modeClass = useColorSchemeStyle('ProseMirror-light', 'ProseMirror-dark') + const {currentAccount} = useSession() const [isDropping, setIsDropping] = React.useState(false) @@ -78,6 +80,7 @@ export const TextInput = React.forwardRef(function TextInputImpl( class: 'inline-tag', }, suggestion: createTagsAutocomplete({ + model: tagAutocompleteModel({currentDid: currentAccount?.did!}), }), }), Mention.configure({ @@ -94,7 +97,7 @@ export const TextInput = React.forwardRef(function TextInputImpl( History, Hardbreak, ], - [autocomplete, placeholder], + [autocomplete, placeholder, currentAccount], ) React.useEffect(() => { diff --git a/src/view/com/composer/text-input/tagsAutocompleteState.ts b/src/view/com/composer/text-input/tagsAutocompleteState.ts new file mode 100644 index 0000000000..e7f78238e3 --- /dev/null +++ b/src/view/com/composer/text-input/tagsAutocompleteState.ts @@ -0,0 +1,31 @@ +import {account} from '#/storage' + +export type Result = { + value: string +} + +export type Model = { + search(query: string): Promise + save(tag: string): void +} + +export function tagAutocompleteModel({ + currentDid, +}: { + currentDid: string +}): Model { + let recentTags = account.get([currentDid, 'recentTags']) || [] + + return { + async search(query: string) { + if (!query) return [{value: query}] + return [{value: query}] + }, + save(tag: string) { + recentTags = [ + {value: tag}, + ...recentTags.filter(t => t.value !== tag), + ].slice(0, 40) + }, + } +} diff --git a/src/view/com/composer/text-input/web/Tags/view.tsx b/src/view/com/composer/text-input/web/Tags/view.tsx index 6a1dd2d0f0..960ff9c909 100644 --- a/src/view/com/composer/text-input/web/Tags/view.tsx +++ b/src/view/com/composer/text-input/web/Tags/view.tsx @@ -9,28 +9,32 @@ import { import tippy, {Instance as TippyInstance} from 'tippy.js' // import {TagsAutocompleteModel} from 'state/models/ui/tags-autocomplete' -import {usePalette} from 'lib/hooks/usePalette' -import {Text} from 'view/com/util/text/Text' +import {usePalette} from '#/lib/hooks/usePalette' +import { + Model, + Result, +} from '#/view/com/composer/text-input/tagsAutocompleteState' +import {Text} from '#/view/com/util/text/Text' import {parsePunctuationFromTag} from './utils' -type AutocompleteResult = string -type ListProps = SuggestionProps & { - // autocompleteModel: TagsAutocompleteModel +type ListProps = SuggestionProps & { + model: Model } type AutocompleteRef = { onKeyDown: (props: SuggestionKeyDownProps) => boolean } -export function createTagsAutocomplete({}: // autocompleteModel, -{ - // autocompleteModel: TagsAutocompleteModel +export function createTagsAutocomplete({ + model, +}: { + model: Model }): Omit { return { /** * This `query` param comes from the result of `findSuggestionMatch` */ async items({query}) { - return [query, 'javascript'] + return await model.search(query) }, render() { let component: ReactRenderer | undefined @@ -41,6 +45,7 @@ export function createTagsAutocomplete({}: // autocompleteModel, component = new ReactRenderer(Autocomplete, { props: { ...props, + model, }, editor: props.editor, }) @@ -113,7 +118,7 @@ const Autocomplete = forwardRef( const selectItem = React.useCallback( (index: number) => { const item = items[index] - if (item) commit(item) + if (item) commit(item.value) }, [items, commit], ) @@ -157,7 +162,8 @@ const Autocomplete = forwardRef( return (
- {items.map((tag, index) => { + {items.map(({value}, index) => { + const {tag} = parsePunctuationFromTag(value) const isSelected = selectedIndex === index const isFirst = index === 0 const isLast = index === items.length - 1 diff --git a/yarn.lock b/yarn.lock index e38b5a7ccd..e448571571 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12153,6 +12153,11 @@ functions-have-names@^1.2.2, functions-have-names@^1.2.3: resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834" integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ== +fuse.js@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/fuse.js/-/fuse.js-7.0.0.tgz#6573c9fcd4c8268e403b4fc7d7131ffcf99a9eb2" + integrity sha512-14F4hBIxqKvD4Zz/XjDc3y94mNZN6pRv3U13Udo0lNLCWRBUsrMv2xwcF/y/Z5sV6+FQW+/ow68cHpm4sunt8Q== + gensync@^1.0.0-beta.2: version "1.0.0-beta.2" resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" From 91d50e70dc5f3568d5980e50914eb1e0c8d9570d Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Tue, 22 Oct 2024 12:36:19 -0500 Subject: [PATCH 08/13] Hook up autocomplete model --- src/storage/index.ts | 2 +- src/view/com/composer/text-input/TextInput.web.tsx | 4 +++- src/view/com/composer/text-input/tagsAutocompleteState.ts | 6 +++++- src/view/com/composer/text-input/web/Tags/plugin.tsx | 4 ++-- src/view/com/composer/text-input/web/Tags/utils.ts | 7 ------- src/view/com/composer/text-input/web/Tags/view.tsx | 5 +++-- 6 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/storage/index.ts b/src/storage/index.ts index bc98341d4c..e3aaa9ffb8 100644 --- a/src/storage/index.ts +++ b/src/storage/index.ts @@ -1,7 +1,7 @@ import {MMKV} from 'react-native-mmkv' import {IS_DEV} from '#/env' -import {Account} from '#/storage/schema' +import {Account,Device} from '#/storage/schema' export * from '#/storage/schema' diff --git a/src/view/com/composer/text-input/TextInput.web.tsx b/src/view/com/composer/text-input/TextInput.web.tsx index 118d5d8c10..c55a2a72f7 100644 --- a/src/view/com/composer/text-input/TextInput.web.tsx +++ b/src/view/com/composer/text-input/TextInput.web.tsx @@ -31,7 +31,7 @@ import {Text} from '../../util/text/Text' import {createSuggestion} from './web/Autocomplete' import {Emoji} from './web/EmojiPicker.web' import {LinkDecorator} from './web/LinkDecorator' -import {createTagsAutocomplete,Tags} from './web/Tags' +import {createTagsAutocomplete, Tags} from './web/Tags' export interface TextInputRef { focus: () => void @@ -338,6 +338,8 @@ function editorJsonToText( text += json.text || '' } else if (json.type === 'mention') { text += `@${json.attrs?.id || ''}` + } else if (json.type === 'tag') { + text += `#${json.attrs?.id || ''}` } return text } diff --git a/src/view/com/composer/text-input/tagsAutocompleteState.ts b/src/view/com/composer/text-input/tagsAutocompleteState.ts index e7f78238e3..bbf65695fd 100644 --- a/src/view/com/composer/text-input/tagsAutocompleteState.ts +++ b/src/view/com/composer/text-input/tagsAutocompleteState.ts @@ -19,13 +19,17 @@ export function tagAutocompleteModel({ return { async search(query: string) { if (!query) return [{value: query}] - return [{value: query}] + return [ + {value: query}, + ...recentTags.filter(t => t.value.includes(query)), + ] }, save(tag: string) { recentTags = [ {value: tag}, ...recentTags.filter(t => t.value !== tag), ].slice(0, 40) + account.set([currentDid, 'recentTags'], recentTags) }, } } diff --git a/src/view/com/composer/text-input/web/Tags/plugin.tsx b/src/view/com/composer/text-input/web/Tags/plugin.tsx index 96cd442e0c..e11071f026 100644 --- a/src/view/com/composer/text-input/web/Tags/plugin.tsx +++ b/src/view/com/composer/text-input/web/Tags/plugin.tsx @@ -88,7 +88,7 @@ export type MentionOptions< * The plugin key for the mention plugin. * @default 'mention' */ -export const TagsPluginKey = new PluginKey('tags') +export const TagsPluginKey = new PluginKey('tag') /** * This extension allows you to insert mentions into the editor. @@ -97,7 +97,7 @@ export const TagsPluginKey = new PluginKey('tags') export const Tags = Node.create< MentionOptions >({ - name: 'tags', + name: 'tag', addOptions() { return { diff --git a/src/view/com/composer/text-input/web/Tags/utils.ts b/src/view/com/composer/text-input/web/Tags/utils.ts index d65869928a..87d695ae03 100644 --- a/src/view/com/composer/text-input/web/Tags/utils.ts +++ b/src/view/com/composer/text-input/web/Tags/utils.ts @@ -49,13 +49,6 @@ export function findSuggestionMatch({ const from = textFrom + match.index + leadingSpaceOffset const to = from + tag.length + hashtagOffset - console.log({ - pos: $position.pos, - from, - to, - text, - }) - // If the $position is located within the matched substring, return that range if (from < $position.pos && to >= $position.pos) { return { diff --git a/src/view/com/composer/text-input/web/Tags/view.tsx b/src/view/com/composer/text-input/web/Tags/view.tsx index 960ff9c909..26cedbeb98 100644 --- a/src/view/com/composer/text-input/web/Tags/view.tsx +++ b/src/view/com/composer/text-input/web/Tags/view.tsx @@ -95,7 +95,7 @@ export function createTagsAutocomplete({ const Autocomplete = forwardRef( function AutocompleteImpl(props, ref) { - const {items, command} = props + const {items, command, model} = props const pal = usePalette('default') const [selectedIndex, setSelectedIndex] = useState(0) @@ -111,8 +111,9 @@ const Autocomplete = forwardRef( * only want to `commitRecentTag` with the sanitized tag. */ command({tag, punctuation}) + model.save(tag) }, - [command], + [command, model], ) const selectItem = React.useCallback( From 43409aaba63658ce19402525c3f081f16d520139 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Tue, 22 Oct 2024 16:03:17 -0500 Subject: [PATCH 09/13] Improve autocomplete --- .../com/composer/text-input/TextInput.web.tsx | 9 +-- .../text-input/tagsAutocompleteState.ts | 80 ++++++++++++++----- .../com/composer/text-input/web/Tags/view.tsx | 57 +++++-------- 3 files changed, 82 insertions(+), 64 deletions(-) diff --git a/src/view/com/composer/text-input/TextInput.web.tsx b/src/view/com/composer/text-input/TextInput.web.tsx index c55a2a72f7..f2eae42f35 100644 --- a/src/view/com/composer/text-input/TextInput.web.tsx +++ b/src/view/com/composer/text-input/TextInput.web.tsx @@ -17,8 +17,6 @@ import {useColorSchemeStyle} from '#/lib/hooks/useColorSchemeStyle' import {usePalette} from '#/lib/hooks/usePalette' import {blobToDataUri, isUriImage} from '#/lib/media/util' import {useActorAutocompleteFn} from '#/state/queries/actor-autocomplete' -import {useSession} from '#/state/session' -import {tagAutocompleteModel} from '#/view/com/composer/text-input/tagsAutocompleteState' import { LinkFacetMatch, suggestLinkCardUri, @@ -66,7 +64,6 @@ export const TextInput = React.forwardRef(function TextInputImpl( const autocomplete = useActorAutocompleteFn() const pal = usePalette('default') const modeClass = useColorSchemeStyle('ProseMirror-light', 'ProseMirror-dark') - const {currentAccount} = useSession() const [isDropping, setIsDropping] = React.useState(false) @@ -79,9 +76,7 @@ export const TextInput = React.forwardRef(function TextInputImpl( HTMLAttributes: { class: 'inline-tag', }, - suggestion: createTagsAutocomplete({ - model: tagAutocompleteModel({currentDid: currentAccount?.did!}), - }), + suggestion: createTagsAutocomplete(), }), Mention.configure({ HTMLAttributes: { @@ -97,7 +92,7 @@ export const TextInput = React.forwardRef(function TextInputImpl( History, Hardbreak, ], - [autocomplete, placeholder, currentAccount], + [autocomplete, placeholder], ) React.useEffect(() => { diff --git a/src/view/com/composer/text-input/tagsAutocompleteState.ts b/src/view/com/composer/text-input/tagsAutocompleteState.ts index bbf65695fd..5ce70da21f 100644 --- a/src/view/com/composer/text-input/tagsAutocompleteState.ts +++ b/src/view/com/composer/text-input/tagsAutocompleteState.ts @@ -1,3 +1,7 @@ +import React from 'react' +import Fuse from 'fuse.js' + +import {useSession} from '#/state/session' import {account} from '#/storage' export type Result = { @@ -5,31 +9,65 @@ export type Result = { } export type Model = { - search(query: string): Promise + readonly suggestions: Result[] + setQuery(query: string): void save(tag: string): void } -export function tagAutocompleteModel({ - currentDid, -}: { - currentDid: string -}): Model { - let recentTags = account.get([currentDid, 'recentTags']) || [] +export function useTagAutocomplete() { + const {currentAccount} = useSession() + const [query, setQuery] = React.useState('') + const [searchSuggestions, setSearchSuggestions] = React.useState([]) + + const search = React.useCallback( + async (_query: string) => { + // TODO actually search + // TODO debounce/abort controller + setSearchSuggestions([]) + }, + [setSearchSuggestions], + ) + + const onSetQuery = React.useCallback( + (query: string) => { + setQuery(query) + search(query) + }, + [setQuery, search], + ) + + const saveRecentTag = React.useCallback( + (tag: string) => { + if (!currentAccount) { + throw new Error('No current account') + } + const recentTags = account.get([currentAccount.did, 'recentTags']) || [] + account.set( + [currentAccount.did, 'recentTags'], + [{value: tag}, ...recentTags.filter(t => t.value !== tag)].slice(0, 40), + ) + }, + [currentAccount], + ) + + const suggestions: Result[] = React.useMemo(() => { + if (!currentAccount) { + throw new Error('No current account') + } + const recentTags = account.get([currentAccount.did, 'recentTags']) || [] + const items = [ + ...recentTags.map(t => t.value), + ...searchSuggestions.map(s => s.value), + ] + const fuse = new Fuse(items) + // search amongst mixed set of tags + const results = fuse.search(query).map(r => r.item) + return results.map(value => ({value})) + }, [currentAccount, query, searchSuggestions]) return { - async search(query: string) { - if (!query) return [{value: query}] - return [ - {value: query}, - ...recentTags.filter(t => t.value.includes(query)), - ] - }, - save(tag: string) { - recentTags = [ - {value: tag}, - ...recentTags.filter(t => t.value !== tag), - ].slice(0, 40) - account.set([currentDid, 'recentTags'], recentTags) - }, + suggestions, + setQuery: onSetQuery, + saveRecentTag, } } diff --git a/src/view/com/composer/text-input/web/Tags/view.tsx b/src/view/com/composer/text-input/web/Tags/view.tsx index 26cedbeb98..b627e9de6d 100644 --- a/src/view/com/composer/text-input/web/Tags/view.tsx +++ b/src/view/com/composer/text-input/web/Tags/view.tsx @@ -8,34 +8,17 @@ import { } from '@tiptap/suggestion' import tippy, {Instance as TippyInstance} from 'tippy.js' -// import {TagsAutocompleteModel} from 'state/models/ui/tags-autocomplete' import {usePalette} from '#/lib/hooks/usePalette' -import { - Model, - Result, -} from '#/view/com/composer/text-input/tagsAutocompleteState' +import {useTagAutocomplete} from '#/view/com/composer/text-input/tagsAutocompleteState' import {Text} from '#/view/com/util/text/Text' import {parsePunctuationFromTag} from './utils' -type ListProps = SuggestionProps & { - model: Model -} type AutocompleteRef = { onKeyDown: (props: SuggestionKeyDownProps) => boolean } -export function createTagsAutocomplete({ - model, -}: { - model: Model -}): Omit { +export function createTagsAutocomplete(): Omit { return { - /** - * This `query` param comes from the result of `findSuggestionMatch` - */ - async items({query}) { - return await model.search(query) - }, render() { let component: ReactRenderer | undefined let popup: TippyInstance[] | undefined @@ -43,10 +26,7 @@ export function createTagsAutocomplete({ return { onStart: props => { component = new ReactRenderer(Autocomplete, { - props: { - ...props, - model, - }, + props, editor: props.editor, }) @@ -93,12 +73,17 @@ export function createTagsAutocomplete({ } } -const Autocomplete = forwardRef( +const Autocomplete = forwardRef( function AutocompleteImpl(props, ref) { - const {items, command, model} = props + const {command, query} = props + const {suggestions, setQuery, saveRecentTag} = useTagAutocomplete() const pal = usePalette('default') const [selectedIndex, setSelectedIndex] = useState(0) + React.useEffect(() => { + setQuery(query) + }, [query, setQuery]) + const commit = React.useCallback( (query: string) => { const {tag, punctuation} = parsePunctuationFromTag(query) @@ -111,36 +96,36 @@ const Autocomplete = forwardRef( * only want to `commitRecentTag` with the sanitized tag. */ command({tag, punctuation}) - model.save(tag) + saveRecentTag(tag) }, - [command, model], + [command, saveRecentTag], ) const selectItem = React.useCallback( (index: number) => { - const item = items[index] + const item = suggestions[index] if (item) commit(item.value) }, - [items, commit], + [suggestions, commit], ) useImperativeHandle(ref, () => ({ onKeyDown: ({event}) => { if (event.key === 'ArrowUp') { setSelectedIndex( - (selectedIndex + props.items.length - 1) % props.items.length, + (selectedIndex + suggestions.length - 1) % suggestions.length, ) return true } if (event.key === 'ArrowDown') { - setSelectedIndex((selectedIndex + 1) % props.items.length) + setSelectedIndex((selectedIndex + 1) % suggestions.length) return true } if (event.key === 'Enter') { - if (!props.items.length) { - // no items, use whatever the user typed + if (!suggestions.length) { + // no suggestions, use whatever the user typed commit(props.query) } else { selectItem(selectedIndex) @@ -158,16 +143,16 @@ const Autocomplete = forwardRef( })) // hide entirely if no suggestions - if (!items.length) return null + if (!suggestions.length) return null return (
- {items.map(({value}, index) => { + {suggestions.map(({value}, index) => { const {tag} = parsePunctuationFromTag(value) const isSelected = selectedIndex === index const isFirst = index === 0 - const isLast = index === items.length - 1 + const isLast = index === suggestions.length - 1 return ( Date: Tue, 22 Oct 2024 14:15:58 -0500 Subject: [PATCH 10/13] Basic outline tags on web --- package.json | 1 + src/components/Composer/OutlineTags/index.tsx | 7 + .../Composer/OutlineTags/index.web.tsx | 374 ++++++++++++++++++ src/view/com/composer/Composer.tsx | 3 + yarn.lock | 12 + 5 files changed, 397 insertions(+) create mode 100644 src/components/Composer/OutlineTags/index.tsx create mode 100644 src/components/Composer/OutlineTags/index.web.tsx diff --git a/package.json b/package.json index 607bbcc130..0143c1d78c 100644 --- a/package.json +++ b/package.json @@ -162,6 +162,7 @@ "nanoid": "^5.0.5", "normalize-url": "^8.0.0", "patch-package": "^6.5.1", + "pind": "^0.5.0", "postinstall-postinstall": "^2.1.0", "psl": "^1.9.0", "react": "18.2.0", diff --git a/src/components/Composer/OutlineTags/index.tsx b/src/components/Composer/OutlineTags/index.tsx new file mode 100644 index 0000000000..a4d18c61f5 --- /dev/null +++ b/src/components/Composer/OutlineTags/index.tsx @@ -0,0 +1,7 @@ +export function OutlineTags(_props: { + max?: number + initialTags?: string[] + onChangeTags: (tags: string[]) => void +}) { + return null +} diff --git a/src/components/Composer/OutlineTags/index.web.tsx b/src/components/Composer/OutlineTags/index.web.tsx new file mode 100644 index 0000000000..2181e3c255 --- /dev/null +++ b/src/components/Composer/OutlineTags/index.web.tsx @@ -0,0 +1,374 @@ +import React from 'react' +import { + NativeSyntheticEvent, + Platform, + Pressable, + StyleSheet, + TextInput, + TextInputKeyPressEventData, + View, +} from 'react-native' +import {TextInputFocusEventData} from 'react-native' +import {Pin} from 'pind' + +import {isWeb} from '#/platform/detection' +import {useSession} from '#/state/session' +import {tagAutocompleteModel} from '#/view/com/composer/text-input/tagsAutocompleteState' +import {atoms as a, useTheme} from '#/alf' +import {Button, ButtonIcon, ButtonProps,ButtonText} from '#/components/Button' +import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times' +import {Text} from '#/components/Typography' + +/** + * Basically `sanitizeHashtag` from `@atproto/api`, but ignores trailing + * punctuation in case the user intends to use `_` or `-`. + */ +export function sanitizeHashtagOnChange(hashtag: string) { + return hashtag.replace(/^\d+/g, '').slice(0, 64) +} + +function TagButton({ + children, + onPress, +}: { + children: string + onPress: ButtonProps['onPress'] +}) { + return ( + + ) +} + +export function OutlineTags({ + max = 8, + initialTags = [], + onChangeTags, +}: { + max?: number + initialTags?: string[] + onChangeTags: (tags: string[]) => void +}) { + const t = useTheme() + const {currentAccount} = useSession() + const dropdown = React.useRef(null) + const input = React.useRef(null) + const inputWidth = input.current + ? input.current.getBoundingClientRect().width + : 200 + const model = React.useMemo( + () => tagAutocompleteModel({currentDid: currentAccount?.did!}), + [currentAccount], + ) + const containerRef = React.useRef(null) + + const [value, setValue] = React.useState('') + const [tags, setTags] = React.useState(initialTags) + const [dropdownItems, setDropdownItems] = React.useState< + {value: string; label: string}[] + >([]) + const [selectedItemIndex, setSelectedItemIndex] = React.useState(0) + + const dropdownIsActive = Boolean(value.length) + + const closeDropdownAndReset = React.useCallback(() => { + setValue('') + setSelectedItemIndex(0) + setDropdownItems([]) + }, [setSelectedItemIndex, setDropdownItems]) + + const addTags = React.useCallback( + (_tags: string[]) => { + const _t = _tags.slice(0, max) + setTags(_t) + onChangeTags(_t) + }, + [onChangeTags, setTags, max], + ) + + const removeTag = React.useCallback( + (tag: string) => { + addTags(tags.filter(t => t !== tag)) + }, + [tags, addTags], + ) + + const addTagAndReset = React.useCallback( + (value: string) => { + const tag = sanitizeHashtagOnChange(value).replace(/^#{1}/, '') + + // enforce max hashtag length + if (tag.length > 0 && tag.length <= 64) { + addTags(Array.from(new Set([...tags, tag])).slice(0, max)) + } + + model.save(tag) + setValue('') + input.current?.focus() + closeDropdownAndReset() + }, + [max, tags, closeDropdownAndReset, setValue, addTags, model], + ) + + const onSubmitEditing = React.useCallback(() => { + const item = dropdownItems[selectedItemIndex] + addTagAndReset(item?.value || value) + }, [value, dropdownItems, selectedItemIndex, addTagAndReset]) + + const onKeyPress = React.useCallback( + (e: NativeSyntheticEvent) => { + const {key} = e.nativeEvent + + if (key === 'Backspace' && value === '') { + addTags(tags.slice(0, -1)) + closeDropdownAndReset() + } else if (key === ' ') { + e.preventDefault() // prevents an additional space on web + addTagAndReset(value) + } + + if (dropdownIsActive) { + if (key === 'Escape') { + closeDropdownAndReset() + } else if (key === 'ArrowUp') { + e.preventDefault() + setSelectedItemIndex( + (selectedItemIndex + dropdownItems.length - 1) % + dropdownItems.length, + ) + } else if (key === 'ArrowDown') { + e.preventDefault() + setSelectedItemIndex((selectedItemIndex + 1) % dropdownItems.length) + } else if ( + isWeb && + key === 'Tab' && + // @ts-ignore web only + !e.nativeEvent.shiftKey + ) { + e.preventDefault() + onSubmitEditing() + } + } + }, + [ + value, + tags, + dropdownIsActive, + selectedItemIndex, + dropdownItems.length, + closeDropdownAndReset, + setSelectedItemIndex, + addTags, + addTagAndReset, + onSubmitEditing, + ], + ) + + const search = React.useCallback( + async (value: string) => { + await model.search(value) + setDropdownItems( + model.suggestions.map(s => ({ + value: s.value, + label: s.value, + })), + ) + }, + [model, setDropdownItems], + ) + + const onChangeText = React.useCallback( + async (value: string) => { + const tag = sanitizeHashtagOnChange(value) + + setValue(tag) + + if (tag.length) { + search(tag) + } else { + closeDropdownAndReset() + } + }, + [setValue, closeDropdownAndReset, search], + ) + + const onBlur = React.useCallback( + (e: NativeSyntheticEvent) => { + // @ts-ignore + const target = e.nativeEvent.relatedTarget as HTMLElement | undefined + + if ( + !tags.length && + (!target || !target.id.includes('tag_autocomplete_option')) + ) { + setValue('') + } + }, + [tags, setValue], + ) + + React.useEffect(() => { + // outside click + function onClick(e: MouseEvent) { + const drop = dropdown.current + const control = input.current + + if ( + !drop || + !control || + e.target === drop || + e.target === control || + drop.contains(e.target as Node) || + control.contains(e.target as Node) + ) + return + + closeDropdownAndReset() + } + + document.addEventListener('click', onClick) + + return () => { + document.removeEventListener('click', onClick) + } + }, [closeDropdownAndReset]) + + return ( + + + {tags.map((tag, i) => ( + removeTag(tag)}> + {tag} + + ))} + + {tags.length >= max ? null : ( + + )} + + + + + {dropdownItems.map((item, index) => { + const isFirst = index === 0 + const isLast = index === dropdownItems.length - 1 + return ( + addTagAndReset(item.value)} + style={state => [ + t.atoms.border_contrast_low, + styles.dropdownItem, + { + backgroundColor: state.hovered + ? t.atoms.bg_contrast_25.backgroundColor + : undefined, + }, + selectedItemIndex === index + ? t.atoms.bg_contrast_25 + : undefined, + isFirst + ? styles.firstResult + : isLast + ? styles.lastResult + : undefined, + ]}> + {item.label} + + ) + })} + + + + ) +} + +const styles = StyleSheet.create({ + outer: { + flexDirection: 'row', + flexWrap: 'wrap', + alignItems: 'center', + gap: 8, + }, + input: { + flexGrow: 1, + minWidth: 100, + fontSize: 15, + lineHeight: Platform.select({ + web: 20, + native: 18, + }), + paddingTop: 5, + paddingBottom: 5, + }, + dropdown: { + width: '100%', + borderRadius: 6, + borderWidth: 1, + borderStyle: 'solid', + padding: 4, + }, + dropdownItem: { + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + flexDirection: 'row', + paddingHorizontal: 12, + paddingVertical: 8, + gap: 4, + }, + firstResult: { + borderTopLeftRadius: 2, + borderTopRightRadius: 2, + }, + lastResult: { + borderBottomLeftRadius: 2, + borderBottomRightRadius: 2, + }, +}) diff --git a/src/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx index 126addd1c8..79e9acfbe0 100644 --- a/src/view/com/composer/Composer.tsx +++ b/src/view/com/composer/Composer.tsx @@ -105,6 +105,7 @@ import * as Toast from '#/view/com/util/Toast' import {UserAvatar} from '#/view/com/util/UserAvatar' import {atoms as a, native, useTheme} from '#/alf' import {Button, ButtonIcon, ButtonText} from '#/components/Button' +import {OutlineTags} from '#/components/Composer/OutlineTags' import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo' import {EmojiArc_Stroke2_Corner0_Rounded as EmojiSmile} from '#/components/icons/Emoji' import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times' @@ -744,6 +745,8 @@ export const ComposePost = ({ + {}} /> + Date: Tue, 22 Oct 2024 14:22:49 -0500 Subject: [PATCH 11/13] Create post with outline tags --- src/lib/api/index.ts | 1 + src/view/com/composer/Composer.tsx | 17 +++++++++++++++-- src/view/com/composer/state/composer.ts | 11 +++++++++++ 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/src/lib/api/index.ts b/src/lib/api/index.ts index db5c9c2478..c36b1d49ba 100644 --- a/src/lib/api/index.ts +++ b/src/lib/api/index.ts @@ -111,6 +111,7 @@ export async function post( embed, langs, labels, + tags: draft.tags, }) } catch (e: any) { logger.error(`Failed to create post`, { diff --git a/src/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx index 79e9acfbe0..fa61623cc6 100644 --- a/src/view/com/composer/Composer.tsx +++ b/src/view/com/composer/Composer.tsx @@ -165,7 +165,13 @@ export const ComposePost = ({ const [draft, dispatch] = useReducer( composerReducer, - {initImageUris, initQuoteUri: initQuote?.uri, initText, initMention}, + { + initImageUris, + initQuoteUri: initQuote?.uri, + initText, + initMention, + initOutlineTags: [], + }, createComposerState, ) const richtext = draft.richtext @@ -509,6 +515,13 @@ export const ComposePost = ({ dispatch({type: 'embed_update_gif', alt: altText}) }, []) + const onChangeOutlineTags = useCallback( + (tags: string[]) => { + dispatch({type: 'tags_update', tags}) + }, + [dispatch], + ) + const { scrollHandler, onScrollViewContentSizeChange, @@ -745,7 +758,7 @@ export const ComposePost = ({ - {}} /> + diff --git a/src/view/com/composer/state/composer.ts b/src/view/com/composer/state/composer.ts index 049488f3a5..b32a4c70b4 100644 --- a/src/view/com/composer/state/composer.ts +++ b/src/view/com/composer/state/composer.ts @@ -53,6 +53,7 @@ export type ComposerDraft = { postgate: AppBskyFeedPostgate.Record threadgate: ThreadgateAllowUISetting[] embed: EmbedDraft + tags: string[] } export type ComposerAction = @@ -76,6 +77,7 @@ export type ComposerAction = | {type: 'embed_add_gif'; gif: Gif} | {type: 'embed_update_gif'; alt: string} | {type: 'embed_remove_gif'} + | {type: 'tags_update'; tags: string[]} export const MAX_IMAGES = 4 @@ -324,6 +326,12 @@ export function composerReducer( }, } } + case 'tags_update': { + return { + ...state, + tags: action.tags, + } + } default: return state } @@ -334,11 +342,13 @@ export function createComposerState({ initMention, initImageUris, initQuoteUri, + initOutlineTags, }: { initText: string | undefined initMention: string | undefined initImageUris: ComposerOpts['imageUris'] initQuoteUri: string | undefined + initOutlineTags: string[] }): ComposerDraft { let media: ImagesMedia | undefined if (initImageUris?.length) { @@ -379,5 +389,6 @@ export function createComposerState({ media, link: undefined, }, + tags: initOutlineTags, } } From 505c2f2f9d75a75a10b4e17159105dbad756f18d Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Tue, 22 Oct 2024 14:41:32 -0500 Subject: [PATCH 12/13] Basic presentation --- src/storage/index.ts | 3 ++- src/view/com/post-thread/PostThreadItem.tsx | 23 +++++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/src/storage/index.ts b/src/storage/index.ts index e3aaa9ffb8..426ed56e70 100644 --- a/src/storage/index.ts +++ b/src/storage/index.ts @@ -1,7 +1,7 @@ import {MMKV} from 'react-native-mmkv' import {IS_DEV} from '#/env' -import {Account,Device} from '#/storage/schema' +import {Account, Device} from '#/storage/schema' export * from '#/storage/schema' @@ -85,5 +85,6 @@ if (IS_DEV && typeof window !== 'undefined') { // @ts-ignore window.bsky_storage = { device, + account, } } diff --git a/src/view/com/post-thread/PostThreadItem.tsx b/src/view/com/post-thread/PostThreadItem.tsx index 99950495f9..91452ff63c 100644 --- a/src/view/com/post-thread/PostThreadItem.tsx +++ b/src/view/com/post-thread/PostThreadItem.tsx @@ -29,6 +29,7 @@ import {useComposerControls} from '#/state/shell/composer' import {useMergedThreadgateHiddenReplies} from '#/state/threadgate-hidden-replies' import {PostThreadFollowBtn} from '#/view/com/post-thread/PostThreadFollowBtn' import {atoms as a, useTheme} from '#/alf' +import {Button, ButtonText} from '#/components/Button' import {AppModerationCause} from '#/components/Pills' import {RichText} from '#/components/RichText' import {Text as NewText} from '#/components/Typography' @@ -367,6 +368,28 @@ let PostThreadItemLoaded = ({ /> )} + + {AppBskyFeedPost.isRecord(post.record) && post.record.tags ? ( + + {post.record.tags.map((tag, i) => ( + + ))} + + ) : null} Date: Tue, 22 Oct 2024 16:09:36 -0500 Subject: [PATCH 13/13] Revise autocomplete usage --- .../Composer/OutlineTags/index.web.tsx | 82 +++++++------------ .../text-input/tagsAutocompleteState.ts | 1 + 2 files changed, 30 insertions(+), 53 deletions(-) diff --git a/src/components/Composer/OutlineTags/index.web.tsx b/src/components/Composer/OutlineTags/index.web.tsx index 2181e3c255..29f8f58c45 100644 --- a/src/components/Composer/OutlineTags/index.web.tsx +++ b/src/components/Composer/OutlineTags/index.web.tsx @@ -12,10 +12,9 @@ import {TextInputFocusEventData} from 'react-native' import {Pin} from 'pind' import {isWeb} from '#/platform/detection' -import {useSession} from '#/state/session' -import {tagAutocompleteModel} from '#/view/com/composer/text-input/tagsAutocompleteState' +import {useTagAutocomplete} from '#/view/com/composer/text-input/tagsAutocompleteState' import {atoms as a, useTheme} from '#/alf' -import {Button, ButtonIcon, ButtonProps,ButtonText} from '#/components/Button' +import {Button, ButtonIcon, ButtonProps, ButtonText} from '#/components/Button' import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times' import {Text} from '#/components/Typography' @@ -57,32 +56,23 @@ export function OutlineTags({ onChangeTags: (tags: string[]) => void }) { const t = useTheme() - const {currentAccount} = useSession() const dropdown = React.useRef(null) const input = React.useRef(null) const inputWidth = input.current ? input.current.getBoundingClientRect().width : 200 - const model = React.useMemo( - () => tagAutocompleteModel({currentDid: currentAccount?.did!}), - [currentAccount], - ) + const {query, suggestions, setQuery, saveRecentTag} = useTagAutocomplete() const containerRef = React.useRef(null) - const [value, setValue] = React.useState('') const [tags, setTags] = React.useState(initialTags) - const [dropdownItems, setDropdownItems] = React.useState< - {value: string; label: string}[] - >([]) const [selectedItemIndex, setSelectedItemIndex] = React.useState(0) - const dropdownIsActive = Boolean(value.length) + const dropdownIsActive = Boolean(query.length) const closeDropdownAndReset = React.useCallback(() => { - setValue('') + setQuery('') setSelectedItemIndex(0) - setDropdownItems([]) - }, [setSelectedItemIndex, setDropdownItems]) + }, [setQuery, setSelectedItemIndex]) const addTags = React.useCallback( (_tags: string[]) => { @@ -109,29 +99,29 @@ export function OutlineTags({ addTags(Array.from(new Set([...tags, tag])).slice(0, max)) } - model.save(tag) - setValue('') + saveRecentTag(tag) + setQuery('') input.current?.focus() closeDropdownAndReset() }, - [max, tags, closeDropdownAndReset, setValue, addTags, model], + [max, tags, closeDropdownAndReset, setQuery, addTags, saveRecentTag], ) const onSubmitEditing = React.useCallback(() => { - const item = dropdownItems[selectedItemIndex] - addTagAndReset(item?.value || value) - }, [value, dropdownItems, selectedItemIndex, addTagAndReset]) + const item = suggestions[selectedItemIndex] + addTagAndReset(item?.value || query) + }, [query, suggestions, selectedItemIndex, addTagAndReset]) const onKeyPress = React.useCallback( (e: NativeSyntheticEvent) => { const {key} = e.nativeEvent - if (key === 'Backspace' && value === '') { + if (key === 'Backspace' && query === '') { addTags(tags.slice(0, -1)) closeDropdownAndReset() } else if (key === ' ') { e.preventDefault() // prevents an additional space on web - addTagAndReset(value) + addTagAndReset(query) } if (dropdownIsActive) { @@ -140,12 +130,11 @@ export function OutlineTags({ } else if (key === 'ArrowUp') { e.preventDefault() setSelectedItemIndex( - (selectedItemIndex + dropdownItems.length - 1) % - dropdownItems.length, + (selectedItemIndex + suggestions.length - 1) % suggestions.length, ) } else if (key === 'ArrowDown') { e.preventDefault() - setSelectedItemIndex((selectedItemIndex + 1) % dropdownItems.length) + setSelectedItemIndex((selectedItemIndex + 1) % suggestions.length) } else if ( isWeb && key === 'Tab' && @@ -158,11 +147,11 @@ export function OutlineTags({ } }, [ - value, + query, tags, dropdownIsActive, selectedItemIndex, - dropdownItems.length, + suggestions.length, closeDropdownAndReset, setSelectedItemIndex, addTags, @@ -171,32 +160,19 @@ export function OutlineTags({ ], ) - const search = React.useCallback( - async (value: string) => { - await model.search(value) - setDropdownItems( - model.suggestions.map(s => ({ - value: s.value, - label: s.value, - })), - ) - }, - [model, setDropdownItems], - ) - const onChangeText = React.useCallback( async (value: string) => { const tag = sanitizeHashtagOnChange(value) - setValue(tag) + setQuery(tag) if (tag.length) { - search(tag) + setQuery(tag) } else { - closeDropdownAndReset() + setSelectedItemIndex(0) } }, - [setValue, closeDropdownAndReset, search], + [setSelectedItemIndex, setQuery], ) const onBlur = React.useCallback( @@ -208,10 +184,10 @@ export function OutlineTags({ !tags.length && (!target || !target.id.includes('tag_autocomplete_option')) ) { - setValue('') + setQuery('') } }, - [tags, setValue], + [tags, setQuery], ) React.useEffect(() => { @@ -257,7 +233,7 @@ export function OutlineTags({ aria-controls="tags-autocomplete-dropdown" aria-haspopup="listbox" aria-expanded={dropdownIsActive} - value={value} + value={query} onBlur={onBlur} onKeyPress={onKeyPress} onSubmitEditing={onSubmitEditing} @@ -283,7 +259,7 @@ export function OutlineTags({ - {dropdownItems.map((item, index) => { + {suggestions.map((item, index) => { const isFirst = index === 0 - const isLast = index === dropdownItems.length - 1 + const isLast = index === suggestions.length - 1 return ( - {item.label} + {item.value} ) })} diff --git a/src/view/com/composer/text-input/tagsAutocompleteState.ts b/src/view/com/composer/text-input/tagsAutocompleteState.ts index 5ce70da21f..2aeabecb02 100644 --- a/src/view/com/composer/text-input/tagsAutocompleteState.ts +++ b/src/view/com/composer/text-input/tagsAutocompleteState.ts @@ -66,6 +66,7 @@ export function useTagAutocomplete() { }, [currentAccount, query, searchSuggestions]) return { + query, suggestions, setQuery: onSetQuery, saveRecentTag,