From 4aa6fdb4dd272afe2e56bbadbe5b3f443ecf215c Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 17 Jan 2024 12:21:19 -0600 Subject: [PATCH 01/11] Add DOMPurify --- package.json | 2 ++ .../landing-timeline/components/site-banner.tsx | 3 ++- src/normalizers/status-edit.ts | 7 ++++--- src/reducers/statuses.ts | 5 +++-- src/schemas/account.ts | 7 ++++--- src/schemas/poll.ts | 3 ++- yarn.lock | 17 +++++++++++++++++ 7 files changed, 34 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index ba82523fb..7b2c0e028 100644 --- a/package.json +++ b/package.json @@ -72,6 +72,7 @@ "@tailwindcss/forms": "^0.5.7", "@tailwindcss/typography": "^0.5.10", "@tanstack/react-query": "^5.0.0", + "@types/dompurify": "^3.0.5", "@types/escape-html": "^1.0.1", "@types/http-link-header": "^1.0.3", "@types/leaflet": "^1.8.0", @@ -106,6 +107,7 @@ "cryptocurrency-icons": "^0.18.1", "cssnano": "^6.0.0", "detect-passive-events": "^2.0.0", + "dompurify": "^3.0.8", "dotenv": "^16.0.0", "emoji-datasource": "14.0.0", "emoji-mart": "^5.5.2", diff --git a/src/features/landing-timeline/components/site-banner.tsx b/src/features/landing-timeline/components/site-banner.tsx index 194faecc6..44776a43f 100644 --- a/src/features/landing-timeline/components/site-banner.tsx +++ b/src/features/landing-timeline/components/site-banner.tsx @@ -1,3 +1,4 @@ +import * as DOMPurify from 'dompurify'; import React from 'react'; import Markup from 'soapbox/components/markup'; @@ -9,7 +10,7 @@ import { LogoText } from './logo-text'; const SiteBanner: React.FC = () => { const instance = useInstance(); - const description = instance.description; + const description = DOMPurify.sanitize(instance.description); return ( diff --git a/src/normalizers/status-edit.ts b/src/normalizers/status-edit.ts index f569ecce3..40e5acd35 100644 --- a/src/normalizers/status-edit.ts +++ b/src/normalizers/status-edit.ts @@ -1,6 +1,7 @@ /** * Status edit normalizer - */ +*/ +import * as DOMPurify from 'dompurify'; import escapeTextContentForBrowser from 'escape-html'; import { Map as ImmutableMap, @@ -60,8 +61,8 @@ const normalizeStatusPoll = (statusEdit: ImmutableMap) => { const normalizeContent = (statusEdit: ImmutableMap) => { const emojiMap = makeEmojiMap(statusEdit.get('emojis')); - const contentHtml = stripCompatibilityFeatures(emojify(statusEdit.get('content'), emojiMap)); - const spoilerHtml = emojify(escapeTextContentForBrowser(statusEdit.get('spoiler_text')), emojiMap); + const contentHtml = DOMPurify.sanitize(stripCompatibilityFeatures(emojify(statusEdit.get('content'), emojiMap)), { ADD_ATTR: ['target'] }); + const spoilerHtml = DOMPurify.sanitize(emojify(escapeTextContentForBrowser(statusEdit.get('spoiler_text')), emojiMap), { ADD_ATTR: ['target'] }); return statusEdit .set('contentHtml', contentHtml) diff --git a/src/reducers/statuses.ts b/src/reducers/statuses.ts index 37d7ec2ad..4eff74506 100644 --- a/src/reducers/statuses.ts +++ b/src/reducers/statuses.ts @@ -1,3 +1,4 @@ +import * as DOMPurify from 'dompurify'; import escapeTextContentForBrowser from 'escape-html'; import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; @@ -117,8 +118,8 @@ export const calculateStatus = ( return status.merge({ search_index: domParser.parseFromString(searchContent, 'text/html').documentElement.textContent || '', - contentHtml: stripCompatibilityFeatures(emojify(status.content, emojiMap)), - spoilerHtml: emojify(escapeTextContentForBrowser(spoilerText), emojiMap), + contentHtml: DOMPurify.sanitize(stripCompatibilityFeatures(emojify(status.content, emojiMap)), { USE_PROFILES: { html: true } }), + spoilerHtml: DOMPurify.sanitize(emojify(escapeTextContentForBrowser(spoilerText), emojiMap), { USE_PROFILES: { html: true } }), hidden: expandSpoilers ? false : spoilerText.length > 0 || status.sensitive, }); } diff --git a/src/schemas/account.ts b/src/schemas/account.ts index fec6c18b2..555badab0 100644 --- a/src/schemas/account.ts +++ b/src/schemas/account.ts @@ -1,3 +1,4 @@ +import * as DOMPurify from 'dompurify'; import escapeTextContentForBrowser from 'escape-html'; import z from 'zod'; @@ -112,7 +113,7 @@ const transformAccount = ({ pleroma, other_setti const newFields = fields.map((field) => ({ ...field, - name_emojified: emojify(escapeTextContentForBrowser(field.name), customEmojiMap), + name_emojified: DOMPurify.sanitize(emojify(escapeTextContentForBrowser(field.name), customEmojiMap), { USE_PROFILES: { html: true } }), value_emojified: emojify(field.value, customEmojiMap), value_plain: unescapeHTML(field.value), })); @@ -130,7 +131,7 @@ const transformAccount = ({ pleroma, other_setti avatar_static: account.avatar_static || account.avatar, discoverable: account.discoverable || account.source?.pleroma?.discoverable || false, display_name: displayName, - display_name_html: emojify(escapeTextContentForBrowser(displayName), customEmojiMap), + display_name_html: DOMPurify.sanitize(emojify(escapeTextContentForBrowser(displayName), customEmojiMap), { USE_PROFILES: { html: true } }), domain, fields: newFields, fqn: account.fqn || (account.acct.includes('@') ? account.acct : `${account.acct}@${domain}`), @@ -138,7 +139,7 @@ const transformAccount = ({ pleroma, other_setti moderator: pleroma?.is_moderator || false, local: pleroma?.is_local !== undefined ? pleroma.is_local : account.acct.split('@')[1] === undefined, location: account.location || pleroma?.location || other_settings?.location || '', - note_emojified: emojify(account.note, customEmojiMap), + note_emojified: DOMPurify.sanitize(emojify(account.note, customEmojiMap), { USE_PROFILES: { html: true } }), pleroma: (() => { if (!pleroma) return undefined; const { relationship, ...rest } = pleroma; diff --git a/src/schemas/poll.ts b/src/schemas/poll.ts index 65ead6e14..a3012b178 100644 --- a/src/schemas/poll.ts +++ b/src/schemas/poll.ts @@ -1,3 +1,4 @@ +import * as DOMPurify from 'dompurify'; import escapeTextContentForBrowser from 'escape-html'; import { z } from 'zod'; @@ -30,7 +31,7 @@ const pollSchema = z.object({ const emojifiedOptions = poll.options.map((option) => ({ ...option, - title_emojified: emojify(escapeTextContentForBrowser(option.title), emojiMap), + title_emojified: DOMPurify.sanitize(emojify(escapeTextContentForBrowser(option.title), emojiMap), { ALLOWED_TAGS: [] }), })); // If the user has votes, they have certainly voted. diff --git a/yarn.lock b/yarn.lock index 26b5253bd..ec70d9234 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2375,6 +2375,13 @@ "@types/node" "*" "@types/responselike" "^1.0.0" +"@types/dompurify@^3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@types/dompurify/-/dompurify-3.0.5.tgz#02069a2fcb89a163bacf1a788f73cb415dd75cb7" + integrity sha512-1Wg0g3BtQF7sSb27fJQAKck1HECM6zV1EB66j8JH9i3LCjYabJa0FSdiSgsD5K/RbrsR0SiraKacLB+T8ZVYAg== + dependencies: + "@types/trusted-types" "*" + "@types/escape-html@^1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@types/escape-html/-/escape-html-1.0.1.tgz#b19b4646915f0ae2c306bf984dc0a59c5cfc97ba" @@ -2612,6 +2619,11 @@ resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.2.tgz#31f6eec1ed7ec23f4f05608d3a2d381df041f564" integrity sha512-7aqorHYgdNO4DM36stTiGO3DvKoex9TQRwsJU6vMaFGyqpBA1MNZkz+PG3gaNUPpTAOYhT1WR7M1JyA3fbS9Cw== +"@types/trusted-types@*": + version "2.0.7" + resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.7.tgz#baccb07a970b91707df3a3e8ba6896c57ead2d11" + integrity sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw== + "@types/trusted-types@^2.0.2": version "2.0.4" resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.4.tgz#2b38784cd16957d3782e8e2b31c03bc1d13b4d65" @@ -4096,6 +4108,11 @@ domhandler@^4.2.0, domhandler@^4.3.1: dependencies: domelementtype "^2.2.0" +dompurify@^3.0.8: + version "3.0.8" + resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.0.8.tgz#e0021ab1b09184bc8af7e35c7dd9063f43a8a437" + integrity sha512-b7uwreMYL2eZhrSCRC4ahLTeZcPZxSmYfmcQGXGkXiZSNW1X85v+SDM5KsWcpivIiUBH47Ji7NtyUdpLeF5JZQ== + domutils@^2.8.0: version "2.8.0" resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.8.0.tgz#4437def5db6e2d1f5d6ee859bd95ca7d02048135" From 5e801b899d562436be9c8ef988852c4a856dc57c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Wed, 7 Feb 2024 17:33:22 +0100 Subject: [PATCH 02/11] Use media query to detect touchscreens MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- src/components/dropdown-menu/dropdown-menu.tsx | 8 +++----- src/components/status-reaction-wrapper.tsx | 8 ++++---- .../compose/components/privacy-dropdown.tsx | 4 ++-- .../ui/components/modals/media-modal.tsx | 4 ++-- src/is-mobile.ts | 17 +---------------- 5 files changed, 12 insertions(+), 29 deletions(-) diff --git a/src/components/dropdown-menu/dropdown-menu.tsx b/src/components/dropdown-menu/dropdown-menu.tsx index 5af959cd7..ab40f164e 100644 --- a/src/components/dropdown-menu/dropdown-menu.tsx +++ b/src/components/dropdown-menu/dropdown-menu.tsx @@ -7,7 +7,7 @@ import { useHistory } from 'react-router-dom'; import { closeDropdownMenu as closeDropdownMenuRedux, openDropdownMenu } from 'soapbox/actions/dropdown-menu'; import { closeModal, openModal } from 'soapbox/actions/modals'; import { useAppDispatch } from 'soapbox/hooks'; -import { isUserTouching } from 'soapbox/is-mobile'; +import { userTouching } from 'soapbox/is-mobile'; import { IconButton, Portal } from '../ui'; @@ -53,8 +53,6 @@ const DropdownMenu = (props: IDropdownMenu) => { const arrowRef = useRef(null); - const isOnMobile = isUserTouching(); - const { x, y, strategy, refs, middlewareData, placement } = useFloating({ placement: initialPlacement, middleware: [ @@ -92,7 +90,7 @@ const DropdownMenu = (props: IDropdownMenu) => { * On mobile screens, let's replace the Popper dropdown with a Modal. */ const handleOpen = () => { - if (isOnMobile) { + if (userTouching.matches) { dispatch( openModal('ACTIONS', { status: filteredProps.status, @@ -113,7 +111,7 @@ const DropdownMenu = (props: IDropdownMenu) => { const handleClose = () => { (refs.reference.current as HTMLButtonElement)?.focus(); - if (isOnMobile) { + if (userTouching.matches) { dispatch(closeModal('ACTIONS')); } else { closeDropdownMenu(); diff --git a/src/components/status-reaction-wrapper.tsx b/src/components/status-reaction-wrapper.tsx index c1d1224de..4bac8c878 100644 --- a/src/components/status-reaction-wrapper.tsx +++ b/src/components/status-reaction-wrapper.tsx @@ -4,7 +4,7 @@ import { simpleEmojiReact } from 'soapbox/actions/emoji-reacts'; import { openModal } from 'soapbox/actions/modals'; import { EmojiSelector, Portal } from 'soapbox/components/ui'; import { useAppDispatch, useAppSelector, useOwnAccount, useSoapboxConfig } from 'soapbox/hooks'; -import { isUserTouching } from 'soapbox/is-mobile'; +import { userTouching } from 'soapbox/is-mobile'; import { getReactForStatus } from 'soapbox/utils/emoji-reacts'; interface IStatusReactionWrapper { @@ -39,7 +39,7 @@ const StatusReactionWrapper: React.FC = ({ statusId, chi clearTimeout(timeout.current); } - if (!isUserTouching()) { + if (!userTouching.matches) { setVisible(true); } }; @@ -51,7 +51,7 @@ const StatusReactionWrapper: React.FC = ({ statusId, chi // Unless the user is touching, delay closing the emoji selector briefly // so the user can move the mouse diagonally to make a selection. - if (isUserTouching()) { + if (userTouching.matches) { setVisible(false); } else { timeout.current = setTimeout(() => { @@ -73,7 +73,7 @@ const StatusReactionWrapper: React.FC = ({ statusId, chi const handleClick: React.EventHandler = e => { const meEmojiReact = getReactForStatus(status, soapboxConfig.allowedEmoji)?.name || '👍'; - if (isUserTouching()) { + if (userTouching.matches) { if (ownAccount) { if (visible) { handleReact(meEmojiReact); diff --git a/src/features/compose/components/privacy-dropdown.tsx b/src/features/compose/components/privacy-dropdown.tsx index e26c8c6e4..1d46c3c0a 100644 --- a/src/features/compose/components/privacy-dropdown.tsx +++ b/src/features/compose/components/privacy-dropdown.tsx @@ -11,7 +11,7 @@ import { closeModal, openModal } from 'soapbox/actions/modals'; import Icon from 'soapbox/components/icon'; import { IconButton } from 'soapbox/components/ui'; import { useAppDispatch, useCompose } from 'soapbox/hooks'; -import { isUserTouching } from 'soapbox/is-mobile'; +import { userTouching } from 'soapbox/is-mobile'; import Motion from '../../ui/util/optional-motion'; @@ -173,7 +173,7 @@ const PrivacyDropdown: React.FC = ({ const onModalClose = () => dispatch(closeModal('ACTIONS')); const handleToggle: React.MouseEventHandler = (e) => { - if (isUserTouching()) { + if (userTouching.matches) { if (open) { onModalClose(); } else { diff --git a/src/features/ui/components/modals/media-modal.tsx b/src/features/ui/components/modals/media-modal.tsx index 1f406cbbd..6f78a8ed5 100644 --- a/src/features/ui/components/modals/media-modal.tsx +++ b/src/features/ui/components/modals/media-modal.tsx @@ -15,7 +15,7 @@ import PlaceholderStatus from 'soapbox/features/placeholder/components/placehold import Thread from 'soapbox/features/status/components/thread'; import Video from 'soapbox/features/video'; import { useAppDispatch, useAppSelector } from 'soapbox/hooks'; -import { isUserTouching } from 'soapbox/is-mobile'; +import { userTouching } from 'soapbox/is-mobile'; import { makeGetStatus } from 'soapbox/selectors'; import ImageLoader from '../image-loader'; @@ -104,7 +104,7 @@ const MediaModal: React.FC = (props) => { const getIndex = () => index !== null ? index : props.index; const toggleNavigation = () => { - setNavigationHidden(value => !value && isUserTouching()); + setNavigationHidden(value => !value && userTouching.matches); }; const handleStatusClick: React.MouseEventHandler = e => { diff --git a/src/is-mobile.ts b/src/is-mobile.ts index bbe38a123..cada4d479 100644 --- a/src/is-mobile.ts +++ b/src/is-mobile.ts @@ -1,5 +1,3 @@ -import { supportsPassiveEvents } from 'detect-passive-events'; - /** Breakpoint at which the application is considered "mobile". */ const LAYOUT_BREAKPOINT = 630; @@ -11,20 +9,7 @@ export function isMobile(width: number) { /** Whether the device is iOS (best guess). */ const iOS: boolean = /iPad|iPhone|iPod/.test(navigator.userAgent) && !(window as any).MSStream; -let userTouching = false; -const listenerOptions = supportsPassiveEvents ? { passive: true } as EventListenerOptions : false; - -function touchListener(): void { - userTouching = true; - window.removeEventListener('touchstart', touchListener, listenerOptions); -} - -window.addEventListener('touchstart', touchListener, listenerOptions); - -/** Whether the user has touched the screen since the page loaded. */ -export function isUserTouching(): boolean { - return userTouching; -} +export const userTouching = window.matchMedia('(pointer: coarse)'); /** Whether the device is iOS (best guess). */ export function isIOS(): boolean { From d576b8ed47d4623920302f8840592ce7be4c43b1 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 8 Feb 2024 15:01:20 -0600 Subject: [PATCH 03/11] Switch to isomorphic-dompurify --- package.json | 2 +- .../components/site-banner.tsx | 2 +- src/normalizers/status-edit.ts | 2 +- src/reducers/statuses.ts | 2 +- src/schemas/account.ts | 2 +- src/schemas/poll.ts | 2 +- yarn.lock | 48 +++++++++++++++++++ 7 files changed, 54 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index c71e853dc..3a6a1b2a6 100644 --- a/package.json +++ b/package.json @@ -112,7 +112,6 @@ "cryptocurrency-icons": "^0.18.1", "cssnano": "^6.0.0", "detect-passive-events": "^2.0.0", - "dompurify": "^3.0.8", "dotenv": "^16.0.0", "emoji-datasource": "14.0.0", "emoji-mart": "^5.5.2", @@ -126,6 +125,7 @@ "intersection-observer": "^0.12.2", "intl-messageformat": "10.5.8", "intl-pluralrules": "^2.0.0", + "isomorphic-dompurify": "^2.3.0", "leaflet": "^1.8.0", "lexical": "^0.12.4", "line-awesome": "^1.3.0", diff --git a/src/features/landing-timeline/components/site-banner.tsx b/src/features/landing-timeline/components/site-banner.tsx index 44776a43f..ffbdafe35 100644 --- a/src/features/landing-timeline/components/site-banner.tsx +++ b/src/features/landing-timeline/components/site-banner.tsx @@ -1,4 +1,4 @@ -import * as DOMPurify from 'dompurify'; +import DOMPurify from 'isomorphic-dompurify'; import React from 'react'; import Markup from 'soapbox/components/markup'; diff --git a/src/normalizers/status-edit.ts b/src/normalizers/status-edit.ts index 40e5acd35..a72e473ac 100644 --- a/src/normalizers/status-edit.ts +++ b/src/normalizers/status-edit.ts @@ -1,7 +1,6 @@ /** * Status edit normalizer */ -import * as DOMPurify from 'dompurify'; import escapeTextContentForBrowser from 'escape-html'; import { Map as ImmutableMap, @@ -9,6 +8,7 @@ import { Record as ImmutableRecord, fromJS, } from 'immutable'; +import DOMPurify from 'isomorphic-dompurify'; import emojify from 'soapbox/features/emoji'; import { normalizeAttachment } from 'soapbox/normalizers/attachment'; diff --git a/src/reducers/statuses.ts b/src/reducers/statuses.ts index 9083e4e66..b857ba540 100644 --- a/src/reducers/statuses.ts +++ b/src/reducers/statuses.ts @@ -1,6 +1,6 @@ -import * as DOMPurify from 'dompurify'; import escapeTextContentForBrowser from 'escape-html'; import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; +import DOMPurify from 'isomorphic-dompurify'; import emojify from 'soapbox/features/emoji'; import { normalizeStatus } from 'soapbox/normalizers'; diff --git a/src/schemas/account.ts b/src/schemas/account.ts index 014820d34..b419b13cf 100644 --- a/src/schemas/account.ts +++ b/src/schemas/account.ts @@ -1,5 +1,5 @@ -import * as DOMPurify from 'dompurify'; import escapeTextContentForBrowser from 'escape-html'; +import DOMPurify from 'isomorphic-dompurify'; import z from 'zod'; import emojify from 'soapbox/features/emoji'; diff --git a/src/schemas/poll.ts b/src/schemas/poll.ts index a3012b178..1a60af410 100644 --- a/src/schemas/poll.ts +++ b/src/schemas/poll.ts @@ -1,5 +1,5 @@ -import * as DOMPurify from 'dompurify'; import escapeTextContentForBrowser from 'escape-html'; +import DOMPurify from 'isomorphic-dompurify'; import { z } from 'zod'; import emojify from 'soapbox/features/emoji'; diff --git a/yarn.lock b/yarn.lock index 7906e8f98..1235e8045 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3897,6 +3897,13 @@ cssstyle@^3.0.0: dependencies: rrweb-cssom "^0.6.0" +cssstyle@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/cssstyle/-/cssstyle-4.0.1.tgz#ef29c598a1e90125c870525490ea4f354db0660a" + integrity sha512-8ZYiJ3A/3OkDd093CBT/0UKDWry7ak4BdPTFP2+QEP7cmhouyq/Up709ASSj2cK02BbZiMgk7kYjZNS4QP5qrQ== + dependencies: + rrweb-cssom "^0.6.0" + csstype@^3.0.2: version "3.0.9" resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.9.tgz#6410af31b26bd0520933d02cbc64fce9ce3fbf0b" @@ -5716,6 +5723,15 @@ isexe@^2.0.0: resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= +isomorphic-dompurify@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/isomorphic-dompurify/-/isomorphic-dompurify-2.3.0.tgz#bc48fbdf52f84cf7e0a63a5e8ec89052e7dbc3c5" + integrity sha512-FCoKY4/mW/jnn/+VgE7wXGC2D/RXzVCAmGYuGWEuZXtyWnwmE2100caciIv+RbHk90q9LA0OW5IBn2f+ywHtww== + dependencies: + "@types/dompurify" "^3.0.5" + dompurify "^3.0.8" + jsdom "^24.0.0" + iterator.prototype@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/iterator.prototype/-/iterator.prototype-1.1.2.tgz#5e29c8924f01916cb9335f1ff80619dcff22b0c0" @@ -5809,6 +5825,33 @@ jsdom@^23.0.0: ws "^8.14.2" xml-name-validator "^5.0.0" +jsdom@^24.0.0: + version "24.0.0" + resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-24.0.0.tgz#e2dc04e4c79da368481659818ee2b0cd7c39007c" + integrity sha512-UDS2NayCvmXSXVP6mpTj+73JnNQadZlr9N68189xib2tx5Mls7swlTNao26IoHv46BZJFvXygyRtyXd1feAk1A== + dependencies: + cssstyle "^4.0.1" + data-urls "^5.0.0" + decimal.js "^10.4.3" + form-data "^4.0.0" + html-encoding-sniffer "^4.0.0" + http-proxy-agent "^7.0.0" + https-proxy-agent "^7.0.2" + is-potential-custom-element-name "^1.0.1" + nwsapi "^2.2.7" + parse5 "^7.1.2" + rrweb-cssom "^0.6.0" + saxes "^6.0.0" + symbol-tree "^3.2.4" + tough-cookie "^4.1.3" + w3c-xmlserializer "^5.0.0" + webidl-conversions "^7.0.0" + whatwg-encoding "^3.1.1" + whatwg-mimetype "^4.0.0" + whatwg-url "^14.0.0" + ws "^8.16.0" + xml-name-validator "^5.0.0" + jsesc@^2.5.1: version "2.5.2" resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" @@ -9500,6 +9543,11 @@ ws@^8.14.2: resolved "https://registry.yarnpkg.com/ws/-/ws-8.14.2.tgz#6c249a806eb2db7a20d26d51e7709eab7b2e6c7f" integrity sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g== +ws@^8.16.0: + version "8.16.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.16.0.tgz#d1cd774f36fbc07165066a60e40323eab6446fd4" + integrity sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ== + xcase@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/xcase/-/xcase-2.0.1.tgz#c7fa72caa0f440db78fd5673432038ac984450b9" From d7e0ded57a77bf04ae1e6beb2abe08f3479e6ef4 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 8 Feb 2024 15:02:26 -0600 Subject: [PATCH 04/11] Remove @types/dompurify --- package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/package.json b/package.json index 3a6a1b2a6..a3150d855 100644 --- a/package.json +++ b/package.json @@ -76,7 +76,6 @@ "@tailwindcss/forms": "^0.5.7", "@tailwindcss/typography": "^0.5.10", "@tanstack/react-query": "^5.0.0", - "@types/dompurify": "^3.0.5", "@types/escape-html": "^1.0.1", "@types/http-link-header": "^1.0.3", "@types/leaflet": "^1.8.0", From 694af98d21280149ad41027d3800fbefa0bfe663 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 8 Feb 2024 15:05:06 -0600 Subject: [PATCH 05/11] Remove space that was added for no reason --- src/normalizers/status-edit.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/normalizers/status-edit.ts b/src/normalizers/status-edit.ts index a72e473ac..26619c174 100644 --- a/src/normalizers/status-edit.ts +++ b/src/normalizers/status-edit.ts @@ -1,6 +1,6 @@ /** * Status edit normalizer -*/ + */ import escapeTextContentForBrowser from 'escape-html'; import { Map as ImmutableMap, From 8e3c6e83104c2309f4d1118777a43fd6dfaa471b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Sun, 11 Feb 2024 13:06:30 +0100 Subject: [PATCH 06/11] Update lexical, remove unused code MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- package.json | 14 +- .../editor/plugins/autosuggest-plugin.tsx | 2 +- .../utils/set-floating-elem-position.ts | 45 ---- src/features/compose/editor/utils/url.ts | 16 -- yarn.lock | 248 +++++++++--------- 5 files changed, 132 insertions(+), 193 deletions(-) delete mode 100644 src/features/compose/editor/utils/set-floating-elem-position.ts diff --git a/package.json b/package.json index a3150d855..a557c3ae7 100644 --- a/package.json +++ b/package.json @@ -55,12 +55,12 @@ "@fontsource/roboto-mono": "^5.0.0", "@fontsource/tajawal": "^5.0.8", "@gamestdio/websocket": "^0.3.2", - "@lexical/clipboard": "^0.12.4", - "@lexical/hashtag": "^0.12.4", - "@lexical/link": "^0.12.4", - "@lexical/react": "^0.12.4", - "@lexical/selection": "^0.12.4", - "@lexical/utils": "^0.12.4", + "@lexical/clipboard": "^0.13.1", + "@lexical/hashtag": "^0.13.1", + "@lexical/link": "^0.13.1", + "@lexical/react": "^0.13.1", + "@lexical/selection": "^0.13.1", + "@lexical/utils": "^0.13.1", "@popperjs/core": "^2.11.5", "@reach/combobox": "^0.18.0", "@reach/menu-button": "^0.18.0", @@ -126,7 +126,7 @@ "intl-pluralrules": "^2.0.0", "isomorphic-dompurify": "^2.3.0", "leaflet": "^1.8.0", - "lexical": "^0.12.4", + "lexical": "^0.13.1", "line-awesome": "^1.3.0", "localforage": "^1.10.0", "lodash": "^4.7.11", diff --git a/src/features/compose/editor/plugins/autosuggest-plugin.tsx b/src/features/compose/editor/plugins/autosuggest-plugin.tsx index ee5729059..5afc6c748 100644 --- a/src/features/compose/editor/plugins/autosuggest-plugin.tsx +++ b/src/features/compose/editor/plugins/autosuggest-plugin.tsx @@ -324,7 +324,7 @@ const AutosuggestPlugin = ({ dispatch(chooseEmoji(suggestion)); replaceMatch($createEmojiNode(suggestion)); } else if (suggestion[0] === '#') { - node.setTextContent(`${suggestion} `); + (node as TextNode).setTextContent(`${suggestion} `); node.select(); } else { const account = selectAccount(getState(), suggestion)!; diff --git a/src/features/compose/editor/utils/set-floating-elem-position.ts b/src/features/compose/editor/utils/set-floating-elem-position.ts deleted file mode 100644 index 371b383cc..000000000 --- a/src/features/compose/editor/utils/set-floating-elem-position.ts +++ /dev/null @@ -1,45 +0,0 @@ -/** - * This source code is derived from code from Meta Platforms, Inc. - * and affiliates, licensed under the MIT license located in the - * LICENSE file in the /src/features/compose/editor directory. - */ - -const VERTICAL_GAP = 10; -const HORIZONTAL_OFFSET = 5; - -export const setFloatingElemPosition = ( - targetRect: ClientRect | null, - floatingElem: HTMLElement, - anchorElem: HTMLElement, - verticalGap: number = VERTICAL_GAP, - horizontalOffset: number = HORIZONTAL_OFFSET, -): void => { - const scrollerElem = anchorElem.parentElement; - - if (targetRect === null || !scrollerElem) { - floatingElem.style.opacity = '0'; - floatingElem.style.transform = 'translate(-10000px, -10000px)'; - return; - } - - const floatingElemRect = floatingElem.getBoundingClientRect(); - const anchorElementRect = anchorElem.getBoundingClientRect(); - const editorScrollerRect = scrollerElem.getBoundingClientRect(); - - let top = targetRect.top - floatingElemRect.height - verticalGap; - let left = targetRect.left - horizontalOffset; - - if (top < editorScrollerRect.top) { - top += floatingElemRect.height + targetRect.height + verticalGap * 2; - } - - if (left + floatingElemRect.width > editorScrollerRect.right) { - left = editorScrollerRect.right - floatingElemRect.width - horizontalOffset; - } - - top -= anchorElementRect.top; - left -= anchorElementRect.left; - - floatingElem.style.opacity = '1'; - floatingElem.style.transform = `translate(${left}px, ${top}px)`; -}; diff --git a/src/features/compose/editor/utils/url.ts b/src/features/compose/editor/utils/url.ts index 412a77a2d..ca168f0a4 100644 --- a/src/features/compose/editor/utils/url.ts +++ b/src/features/compose/editor/utils/url.ts @@ -4,22 +4,6 @@ * LICENSE file in the /src/features/compose/editor directory. */ -export const sanitizeUrl = (url: string): string => { - /** A pattern that matches safe URLs. */ - const SAFE_URL_PATTERN = - /^(?:(?:https?|mailto|ftp|tel|file|sms):|[^&:/?#]*(?:[/?#]|$))/gi; - - /** A pattern that matches safe data URLs. */ - const DATA_URL_PATTERN = - /^data:(?:image\/(?:bmp|gif|jpeg|jpg|png|tiff|webp)|video\/(?:mpeg|mp4|ogg|webm)|audio\/(?:mp3|oga|ogg|opus));base64,[a-z0-9+/]+=*$/i; - - url = String(url).trim(); - - if (url.match(SAFE_URL_PATTERN) || url.match(DATA_URL_PATTERN)) return url; - - return 'https://'; -}; - // Source: https://stackoverflow.com/a/8234912/2013580 const urlRegExp = new RegExp( /((([A-Za-z]{3,9}:(?:\/\/)?)(?:[-;:&=+$,\w]+@)?[A-Za-z0-9.-]+|(?:www.|[-;:&=+$,\w]+@)[A-Za-z0-9.-]+)((?:\/[+~%/.\w-_]*)?\??(?:[-+=&;%@.\w_]*)#?(?:[\w]*))?)/, diff --git a/yarn.lock b/yarn.lock index 1235e8045..f38bd96c6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1672,160 +1672,160 @@ "@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/sourcemap-codec" "^1.4.14" -"@lexical/clipboard@0.12.4", "@lexical/clipboard@^0.12.4": - version "0.12.4" - resolved "https://registry.yarnpkg.com/@lexical/clipboard/-/clipboard-0.12.4.tgz#b9c3a38ab98a67c678ee80238036a166d3161491" - integrity sha512-kFR+UdhtLCMTQgZCyDmYzp2yjPFMNpUZ4TaRjuRBpCRFYwKMlgie4p1J4VJm6sT23kkAFZtVjOfp+gDEYnPHRQ== +"@lexical/clipboard@0.13.1", "@lexical/clipboard@^0.13.1": + version "0.13.1" + resolved "https://registry.yarnpkg.com/@lexical/clipboard/-/clipboard-0.13.1.tgz#ca132306129974ea2c9e51d6a8637f8fcffcdb3d" + integrity sha512-gMSbVeqb7S+XAi/EMMlwl+FCurLPugN2jAXcp5k5ZaUd7be8B+iupbYdoKkjt4qBhxmvmfe9k46GoC0QOPl/nw== dependencies: - "@lexical/html" "0.12.4" - "@lexical/list" "0.12.4" - "@lexical/selection" "0.12.4" - "@lexical/utils" "0.12.4" + "@lexical/html" "0.13.1" + "@lexical/list" "0.13.1" + "@lexical/selection" "0.13.1" + "@lexical/utils" "0.13.1" -"@lexical/code@0.12.4": - version "0.12.4" - resolved "https://registry.yarnpkg.com/@lexical/code/-/code-0.12.4.tgz#aa91cf1b070e012b359e9ba023ef880ed8c7fec0" - integrity sha512-pX7rJCjbjCl6VdOPl2hl/UkjP3iPPyCQgH2VQ+WlXapDd+0uZ54nPL1MKCCaFUZocHPmOmSRKKGUp6K2CNiqzg== +"@lexical/code@0.13.1": + version "0.13.1" + resolved "https://registry.yarnpkg.com/@lexical/code/-/code-0.13.1.tgz#e13688390582a4b63a639daff1f16bcb82aa854d" + integrity sha512-QK77r3QgEtJy96ahYXNgpve8EY64BQgBSnPDOuqVrLdl92nPzjqzlsko2OZldlrt7gjXcfl9nqfhZ/CAhStfOg== dependencies: - "@lexical/utils" "0.12.4" + "@lexical/utils" "0.13.1" prismjs "^1.27.0" -"@lexical/dragon@0.12.4": - version "0.12.4" - resolved "https://registry.yarnpkg.com/@lexical/dragon/-/dragon-0.12.4.tgz#dc9961abf31a7e5a40db1b81e07a290ccdca93a5" - integrity sha512-7DaXdQ/5GJ8HRpPYr2+SjaUi912tG9L6ukg9IglG1t51lWGxqLx2chW17tp50XDTtY05w9VnoMaxtgsuCN5Pmg== +"@lexical/dragon@0.13.1": + version "0.13.1" + resolved "https://registry.yarnpkg.com/@lexical/dragon/-/dragon-0.13.1.tgz#32ba02bff4d8f02a6317d874671ee0b0a2dcdc53" + integrity sha512-aNlqfif4//jW7gOxbBgdrbDovU6m3EwQrUw+Y/vqRkY+sWmloyAUeNwCPH1QP3Q5cvfolzOeN5igfBljsFr+1g== -"@lexical/hashtag@0.12.4", "@lexical/hashtag@^0.12.4": - version "0.12.4" - resolved "https://registry.yarnpkg.com/@lexical/hashtag/-/hashtag-0.12.4.tgz#95e2dced69dd0378c567c855e834492f367d7a86" - integrity sha512-iCxQRBZmgwAV6kypmxtWg7HVhBC7PKclmqLNaLDLoKBm+keEXpKnGB5iEtgK/tCMiwkzrg+wGcrw5qi+YjvM9Q== +"@lexical/hashtag@0.13.1", "@lexical/hashtag@^0.13.1": + version "0.13.1" + resolved "https://registry.yarnpkg.com/@lexical/hashtag/-/hashtag-0.13.1.tgz#eb273c199a0115ec0f0191c2449e97f512360f2e" + integrity sha512-Dl0dUG4ZXNjYYuAUR0GMGpLGsA+cps2/ln3xEmy28bZR0sKkjXugsu2QOIxZjYIPBewDrXzPcvK8md45cMYoSg== dependencies: - "@lexical/utils" "0.12.4" + "@lexical/utils" "0.13.1" -"@lexical/history@0.12.4": - version "0.12.4" - resolved "https://registry.yarnpkg.com/@lexical/history/-/history-0.12.4.tgz#bb97c6a079d57ea446f40d7de647e9ee5c7f63cd" - integrity sha512-XLbSSr9FueAxuKHo4LBi+lZNVAEReNNDCt4MM2Ol8UZhWPlpNskSB/sECYEEQ6/ItlzgtnKyKWjfDFBHRWvC2g== +"@lexical/history@0.13.1": + version "0.13.1" + resolved "https://registry.yarnpkg.com/@lexical/history/-/history-0.13.1.tgz#3bb54716dc69779d3b35894bd72637a7fc2ed284" + integrity sha512-cZXt30MalEEiRaflE9tHeGYnwT1xSDjXLsf9M409DSU9POJyZ1fsULJrG1tWv2uFQOhwal33rve9+MatUlITrg== dependencies: - "@lexical/utils" "0.12.4" + "@lexical/utils" "0.13.1" -"@lexical/html@0.12.4": - version "0.12.4" - resolved "https://registry.yarnpkg.com/@lexical/html/-/html-0.12.4.tgz#25dd678d3d2bb735fc23340867bfe87e66248495" - integrity sha512-RD/n9n1eCuTZtLaTEI3wuUDlJjCn6j+/0c9GvzqLKhNz9f+E5zMVExhzTT4cZQh5WXbzGFNlwC/cuOtaM3wODg== +"@lexical/html@0.13.1": + version "0.13.1" + resolved "https://registry.yarnpkg.com/@lexical/html/-/html-0.13.1.tgz#e56035d0c6528ffb932390e0d3d357c82f69253a" + integrity sha512-XkZrnCSHIUavtpMol6aG8YsJ5KqC9hMxEhAENf3HTGi3ocysCByyXOyt1EhEYpjJvgDG4wRqt25xGDbLjj1/sA== dependencies: - "@lexical/selection" "0.12.4" - "@lexical/utils" "0.12.4" + "@lexical/selection" "0.13.1" + "@lexical/utils" "0.13.1" -"@lexical/link@0.12.4", "@lexical/link@^0.12.4": - version "0.12.4" - resolved "https://registry.yarnpkg.com/@lexical/link/-/link-0.12.4.tgz#364628ae06396cd0182c978efaa9e66d77b34758" - integrity sha512-gmEs0GJGDhgwV1x0IrO7Br2GCALijZLIayGWoLAgYiXZee4WZpvjbngZuC6yghYBhrme6muPRMG2sLMwV2cWiQ== +"@lexical/link@0.13.1", "@lexical/link@^0.13.1": + version "0.13.1" + resolved "https://registry.yarnpkg.com/@lexical/link/-/link-0.13.1.tgz#f1c4c12c828c0251e5d7fb4fb336f2d62380fc57" + integrity sha512-7E3B2juL2UoMj2n+CiyFZ7tlpsdViAoIE7MpegXwfe/VQ66wFwk/VxGTa/69ng2EoF7E0kh+SldvGQDrWAWb1g== dependencies: - "@lexical/utils" "0.12.4" + "@lexical/utils" "0.13.1" -"@lexical/list@0.12.4": - version "0.12.4" - resolved "https://registry.yarnpkg.com/@lexical/list/-/list-0.12.4.tgz#f57fe71ff599e298569722e0364c26a5cf417082" - integrity sha512-qxwRIz+4Aj2u2fzyGPo86vX+1ebwCnamppr/c5ZWuqpRTWtYDWjq5LQKIwAvZBxCzPdtP5jzwyZ6VYWQXYW4Kg== +"@lexical/list@0.13.1": + version "0.13.1" + resolved "https://registry.yarnpkg.com/@lexical/list/-/list-0.13.1.tgz#461cb989157bdf4a43eaa8596fdb09df60d114ee" + integrity sha512-6U1pmNZcKLuOWiWRML8Raf9zSEuUCMlsOye82niyF6I0rpPgYo5UFghAAbGISDsyqzM1B2L4BgJ6XrCk/dJptg== dependencies: - "@lexical/utils" "0.12.4" + "@lexical/utils" "0.13.1" -"@lexical/mark@0.12.4": - version "0.12.4" - resolved "https://registry.yarnpkg.com/@lexical/mark/-/mark-0.12.4.tgz#dfe221143d9d2c006b680d88ab2cba281bfb7a45" - integrity sha512-NFFk/3AFFJARjsth8wd5HdeW8XhcaECoQ8wwnJ4fRZzgN0lu3ZSiq+CuVm0NRN5xA5KoUT6sfIQqGOzIPfvdsw== +"@lexical/mark@0.13.1": + version "0.13.1" + resolved "https://registry.yarnpkg.com/@lexical/mark/-/mark-0.13.1.tgz#084bb49a8bc1c5c5a4ed5c5d4a20c98ea85ec8b1" + integrity sha512-dW27PW8wWDOKFqXTBUuUfV+umU0KfwvXGkPUAxRJrvwUWk5RKaS48LhgbNlQ5BfT84Q8dSiQzvbaa6T40t9a3A== dependencies: - "@lexical/utils" "0.12.4" + "@lexical/utils" "0.13.1" -"@lexical/markdown@0.12.4": - version "0.12.4" - resolved "https://registry.yarnpkg.com/@lexical/markdown/-/markdown-0.12.4.tgz#ca492a9c76ce7d24e49a51603f770fdfe23d0b51" - integrity sha512-cOk0dkafyvQI4DMwwMfkP329bRVfyhXcVF3dcRiydl6ZIgqOrj/EMi+C0qxQkcqg0MO26Rky6LLJ4vQi6AgJDg== +"@lexical/markdown@0.13.1": + version "0.13.1" + resolved "https://registry.yarnpkg.com/@lexical/markdown/-/markdown-0.13.1.tgz#1fd2efcacff4ce733682a8161a3f3d78dba37503" + integrity sha512-6tbdme2h5Zy/M88loVQVH5G0Nt7VMR9UUkyiSaicyBRDOU2OHacaXEp+KSS/XuF+d7TA+v/SzyDq8HS77cO1wA== dependencies: - "@lexical/code" "0.12.4" - "@lexical/link" "0.12.4" - "@lexical/list" "0.12.4" - "@lexical/rich-text" "0.12.4" - "@lexical/text" "0.12.4" - "@lexical/utils" "0.12.4" + "@lexical/code" "0.13.1" + "@lexical/link" "0.13.1" + "@lexical/list" "0.13.1" + "@lexical/rich-text" "0.13.1" + "@lexical/text" "0.13.1" + "@lexical/utils" "0.13.1" -"@lexical/offset@0.12.4": - version "0.12.4" - resolved "https://registry.yarnpkg.com/@lexical/offset/-/offset-0.12.4.tgz#00c0020a98e32216bd6f119949d3a3bd64b4b139" - integrity sha512-6fjXCx+YD1TMl6GFL4wowhBgbIg+UX3j2OOXh3F7WEp3SDvzoJsJ6F7xRctrHQbluCITM3oDwOyHa1J0m5lrFg== +"@lexical/offset@0.13.1": + version "0.13.1" + resolved "https://registry.yarnpkg.com/@lexical/offset/-/offset-0.13.1.tgz#f37417822aef3dc81580d4abb96e43ba9d547225" + integrity sha512-j/RZcztJ7dyTrfA2+C3yXDzWDXV+XmMpD5BYeQCEApaHvlo20PHt1BISk7RcrnQW8PdzGvpKblRWf//c08LS9w== -"@lexical/overflow@0.12.4": - version "0.12.4" - resolved "https://registry.yarnpkg.com/@lexical/overflow/-/overflow-0.12.4.tgz#3e7725e356044a5c9a7a80e53edc23cddf026da9" - integrity sha512-mEWgVukoOgcyDruHvzk1amy9jgGDVXFYiPn20ykxgrVQz6XEpq+lfyic/BUnN4toNR8p6jc/Yxi2lF1ELCU0Kg== +"@lexical/overflow@0.13.1": + version "0.13.1" + resolved "https://registry.yarnpkg.com/@lexical/overflow/-/overflow-0.13.1.tgz#42c036dc3ad3eb929fda5aa0a00a725b74f72669" + integrity sha512-Uw34j+qG2UJRCIR+bykfFMduFk7Pc4r/kNt8N1rjxGuGXAsreTVch1iOhu7Ev6tJgkURsduKuaJCAi7iHnKl7g== -"@lexical/plain-text@0.12.4": - version "0.12.4" - resolved "https://registry.yarnpkg.com/@lexical/plain-text/-/plain-text-0.12.4.tgz#10ef4d56e1569e0d8ad1bc12569cffd736414957" - integrity sha512-osbqOyt19oFG0kTbV71jxxCdgnUqNYW6QXIIaS1SwcCN/N1CdFZ0sNpjPkHIFx9AdZ/Tmi4u9SNFUo16DjvThA== +"@lexical/plain-text@0.13.1": + version "0.13.1" + resolved "https://registry.yarnpkg.com/@lexical/plain-text/-/plain-text-0.13.1.tgz#e7e713029443c30facce27b34836bf604cf92c0f" + integrity sha512-4j5KAsMKUvJ8LhVDSS4zczbYXzdfmgYSAVhmqpSnJtud425Nk0TAfpUBLFoivxZB7KMoT1LGWQZvd47IvJPvtA== -"@lexical/react@^0.12.4": - version "0.12.4" - resolved "https://registry.yarnpkg.com/@lexical/react/-/react-0.12.4.tgz#4c53c32d8575dff685334b116e5a2bdf19a34da5" - integrity sha512-tz4ebqJ++YP/Y6FCjk5aU3bvgrps8+i9abqvaaNCSzSQavI0qHtdS7EGy4S9qyO6qKuthXcOGIQxGTweRTkDsA== +"@lexical/react@^0.13.1": + version "0.13.1" + resolved "https://registry.yarnpkg.com/@lexical/react/-/react-0.13.1.tgz#6c35bf43e24560d2ca3aa2c6ff607ef37de87bac" + integrity sha512-Sy6EL230KAb0RZsZf1dZrRrc3+rvCDQWltcd8C/cqBUYlxsLYCW9s4f3RB2werngD/PtLYbBB48SYXNkIALITA== dependencies: - "@lexical/clipboard" "0.12.4" - "@lexical/code" "0.12.4" - "@lexical/dragon" "0.12.4" - "@lexical/hashtag" "0.12.4" - "@lexical/history" "0.12.4" - "@lexical/link" "0.12.4" - "@lexical/list" "0.12.4" - "@lexical/mark" "0.12.4" - "@lexical/markdown" "0.12.4" - "@lexical/overflow" "0.12.4" - "@lexical/plain-text" "0.12.4" - "@lexical/rich-text" "0.12.4" - "@lexical/selection" "0.12.4" - "@lexical/table" "0.12.4" - "@lexical/text" "0.12.4" - "@lexical/utils" "0.12.4" - "@lexical/yjs" "0.12.4" + "@lexical/clipboard" "0.13.1" + "@lexical/code" "0.13.1" + "@lexical/dragon" "0.13.1" + "@lexical/hashtag" "0.13.1" + "@lexical/history" "0.13.1" + "@lexical/link" "0.13.1" + "@lexical/list" "0.13.1" + "@lexical/mark" "0.13.1" + "@lexical/markdown" "0.13.1" + "@lexical/overflow" "0.13.1" + "@lexical/plain-text" "0.13.1" + "@lexical/rich-text" "0.13.1" + "@lexical/selection" "0.13.1" + "@lexical/table" "0.13.1" + "@lexical/text" "0.13.1" + "@lexical/utils" "0.13.1" + "@lexical/yjs" "0.13.1" react-error-boundary "^3.1.4" -"@lexical/rich-text@0.12.4": - version "0.12.4" - resolved "https://registry.yarnpkg.com/@lexical/rich-text/-/rich-text-0.12.4.tgz#545a1d6bd88e930c572d17fe504a8796f6af0c9d" - integrity sha512-gWMDmdRRFPk00JfQv52650qcpjTN6oBrrYwBydYvEG8WTC8o1k8qEOZaOFja6GElPt0520dpyvcWHTlIL0jv3Q== +"@lexical/rich-text@0.13.1": + version "0.13.1" + resolved "https://registry.yarnpkg.com/@lexical/rich-text/-/rich-text-0.13.1.tgz#8251e81a3985a4d76bef027cf6c0dc90c661e4ec" + integrity sha512-HliB9Ync06mv9DBg/5j0lIsTJp+exLHlaLJe+n8Zq1QNTzZzu2LsIT/Crquk50In7K/cjtlaQ/d5RB0LkjMHYg== -"@lexical/selection@0.12.4", "@lexical/selection@^0.12.4": - version "0.12.4" - resolved "https://registry.yarnpkg.com/@lexical/selection/-/selection-0.12.4.tgz#756922edbf42f3cb0bd6f99239d77ba2615c859c" - integrity sha512-9lJt9PBJW7lWYiPDo/PGl2nZ6NrdYaDBidEoMNhyusPjeBEr35z4Hm0qWUhDrPDQPhK2i1oBw6nZa94bxuS9Lw== +"@lexical/selection@0.13.1", "@lexical/selection@^0.13.1": + version "0.13.1" + resolved "https://registry.yarnpkg.com/@lexical/selection/-/selection-0.13.1.tgz#466d7cd0ee1b04680bd949112f1f5cb6a6618efa" + integrity sha512-Kt9eSwjxPznj7yzIYipu9yYEgmRJhHiq3DNxHRxInYcZJWWNNHum2xKyxwwcN8QYBBzgfPegfM/geqQEJSV1lQ== -"@lexical/table@0.12.4": - version "0.12.4" - resolved "https://registry.yarnpkg.com/@lexical/table/-/table-0.12.4.tgz#b40426de069b7e962e95e38f2ff1bc10ca649388" - integrity sha512-Lyy6y1HOQqzU8O2cH5Zhzek46B0UU7NceM2fJKM7qiBSuxY/nE0BzkFq0xDk3x5W+vhXob6Z32sJSNFImtuqKw== +"@lexical/table@0.13.1": + version "0.13.1" + resolved "https://registry.yarnpkg.com/@lexical/table/-/table-0.13.1.tgz#814d3b8a2afb821aff151c92cce831809f9d67a1" + integrity sha512-VQzgkfkEmnvn6C64O/kvl0HI3bFoBh3WA/U67ALw+DS11Mb5CKjbt0Gzm/258/reIxNMpshjjicpWMv9Miwauw== dependencies: - "@lexical/utils" "0.12.4" + "@lexical/utils" "0.13.1" -"@lexical/text@0.12.4": - version "0.12.4" - resolved "https://registry.yarnpkg.com/@lexical/text/-/text-0.12.4.tgz#65ba9620492d673cd68c8380725d4e4fe845e603" - integrity sha512-r/7402eCf6C/7BqUNR7ZLZQQjsE62wjeuf0rFeW1ulOpwiti/dFn1o+EsCb0hvNeHPzfGgRC+FuDT9KSEKu7Ig== +"@lexical/text@0.13.1": + version "0.13.1" + resolved "https://registry.yarnpkg.com/@lexical/text/-/text-0.13.1.tgz#12104d42da7a707a19853679f3a88e8ed6ce8084" + integrity sha512-NYy3TZKt3qzReDwN2Rr5RxyFlg84JjXP2JQGMrXSSN7wYe73ysQIU6PqdVrz4iZkP+w34F3pl55dJ24ei3An9w== -"@lexical/utils@0.12.4", "@lexical/utils@^0.12.4": - version "0.12.4" - resolved "https://registry.yarnpkg.com/@lexical/utils/-/utils-0.12.4.tgz#83ed97d31201e1b911cfa38b940909c3cca41d77" - integrity sha512-ColV11ANBY6deT7CdGwP4lzv3pb5caFfFLcVKdGDMMJSUYFQ5l69aZvDP2qWWnNqzGLb+AJSunMd142wWc5LGg== +"@lexical/utils@0.13.1", "@lexical/utils@^0.13.1": + version "0.13.1" + resolved "https://registry.yarnpkg.com/@lexical/utils/-/utils-0.13.1.tgz#f2a72f71c859933781294830b38b25b5b33122a9" + integrity sha512-AtQQKzYymkbOaQxaBXjRBS8IPxF9zWQnqwHTUTrJqJ4hX71aIQd/thqZbfQETAFJfC8pNBZw5zpxN6yPHk23dQ== dependencies: - "@lexical/list" "0.12.4" - "@lexical/selection" "0.12.4" - "@lexical/table" "0.12.4" + "@lexical/list" "0.13.1" + "@lexical/selection" "0.13.1" + "@lexical/table" "0.13.1" -"@lexical/yjs@0.12.4": - version "0.12.4" - resolved "https://registry.yarnpkg.com/@lexical/yjs/-/yjs-0.12.4.tgz#ea986b66932558062bab2ccc1b46c34c0260ea3e" - integrity sha512-qtCiABugE1CiZ7K5iFfQnB1KqfWtLyiRK0nxAaSxuZzQTO4+Kh3WDh7ULppPa53Sf3pKpw8Sq2XB4AXP6csbkg== +"@lexical/yjs@0.13.1": + version "0.13.1" + resolved "https://registry.yarnpkg.com/@lexical/yjs/-/yjs-0.13.1.tgz#2a71ae3c4b3cc5c660bbe66d537eb0cbf3c7c1b6" + integrity sha512-4GbqQM+PwNTV59AZoNrfTe/0rLjs+cX6Y6yAdZSRPBwr5L3JzYeU1TTcFCVQTtsE7KF8ddVP8sD7w9pi8rOWLA== dependencies: - "@lexical/offset" "0.12.4" + "@lexical/offset" "0.13.1" "@mdn/browser-compat-data@^5.2.34", "@mdn/browser-compat-data@^5.3.13": version "5.3.16" @@ -6047,10 +6047,10 @@ levn@^0.4.1: prelude-ls "^1.2.1" type-check "~0.4.0" -lexical@^0.12.4: - version "0.12.4" - resolved "https://registry.yarnpkg.com/lexical/-/lexical-0.12.4.tgz#1f38d40eb1b5bdcf30a79864027bf7443de52fb5" - integrity sha512-giNrnp45H6P4IHFhkKaHEPTF+bKLBWdEIDL/FGjRZf+to7l7TORIBk/23Zdchzt/VGgKGWu950EOvGh53gkVMQ== +lexical@^0.13.1: + version "0.13.1" + resolved "https://registry.yarnpkg.com/lexical/-/lexical-0.13.1.tgz#0abffe9bc05a7a9da8a6128ea478bf08c11654db" + integrity sha512-jaqRYzVEfBKbX4FwYpd/g+MyOjRaraAel0iQsTrwvx3hyN0bswUZuzb6H6nGlFSjcdrc77wKpyKwoWj4aUd+Bw== li@^1.3.0: version "1.3.0" From 124421a541aff4d9d5590eadead8127ab2e19ef2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Sun, 11 Feb 2024 15:45:18 +0100 Subject: [PATCH 07/11] Remove more unused code MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- .../compose/editor/plugins/link-plugin.tsx | 11 +- .../editor/utils/get-dom-range-rect.ts | 28 --- .../compose/editor/utils/get-selected-node.ts | 26 --- .../compose/editor/utils/is-html-element.ts | 4 - src/features/compose/editor/utils/point.ts | 57 ------ src/features/compose/editor/utils/rect.ts | 163 ------------------ src/features/compose/editor/utils/url.ts | 16 -- 7 files changed, 10 insertions(+), 295 deletions(-) delete mode 100644 src/features/compose/editor/utils/get-dom-range-rect.ts delete mode 100644 src/features/compose/editor/utils/get-selected-node.ts delete mode 100644 src/features/compose/editor/utils/is-html-element.ts delete mode 100644 src/features/compose/editor/utils/point.ts delete mode 100644 src/features/compose/editor/utils/rect.ts delete mode 100644 src/features/compose/editor/utils/url.ts diff --git a/src/features/compose/editor/plugins/link-plugin.tsx b/src/features/compose/editor/plugins/link-plugin.tsx index 175f3184f..fac30f66c 100644 --- a/src/features/compose/editor/plugins/link-plugin.tsx +++ b/src/features/compose/editor/plugins/link-plugin.tsx @@ -7,7 +7,16 @@ import { LinkPlugin as LexicalLinkPlugin } from '@lexical/react/LexicalLinkPlugin'; import * as React from 'react'; -import { validateUrl } from '../utils/url'; +// Source: https://stackoverflow.com/a/8234912/2013580 +const urlRegExp = new RegExp( + /((([A-Za-z]{3,9}:(?:\/\/)?)(?:[-;:&=+$,\w]+@)?[A-Za-z0-9.-]+|(?:www.|[-;:&=+$,\w]+@)[A-Za-z0-9.-]+)((?:\/[+~%/.\w-_]*)?\??(?:[-+=&;%@.\w_]*)#?(?:[\w]*))?)/, +); + +export const validateUrl = (url: string): boolean => { + // TODO Fix UI for link insertion; it should never default to an invalid URL such as https://. + // Maybe show a dialog where they user can type the URL before inserting it. + return url === 'https://' || urlRegExp.test(url); +}; const LinkPlugin = (): JSX.Element => { return ; diff --git a/src/features/compose/editor/utils/get-dom-range-rect.ts b/src/features/compose/editor/utils/get-dom-range-rect.ts deleted file mode 100644 index fe6d10ad0..000000000 --- a/src/features/compose/editor/utils/get-dom-range-rect.ts +++ /dev/null @@ -1,28 +0,0 @@ -/** - * This source code is derived from code from Meta Platforms, Inc. - * and affiliates, licensed under the MIT license located in the - * LICENSE file in the /src/features/compose/editor directory. - */ - -/* eslint-disable eqeqeq */ - -export const getDOMRangeRect = ( - nativeSelection: Selection, - rootElement: HTMLElement, -): DOMRect => { - const domRange = nativeSelection.getRangeAt(0); - - let rect; - - if (nativeSelection.anchorNode === rootElement) { - let inner = rootElement; - while (inner.firstElementChild != null) { - inner = inner.firstElementChild as HTMLElement; - } - rect = inner.getBoundingClientRect(); - } else { - rect = domRange.getBoundingClientRect(); - } - - return rect; -}; diff --git a/src/features/compose/editor/utils/get-selected-node.ts b/src/features/compose/editor/utils/get-selected-node.ts deleted file mode 100644 index 2f093b983..000000000 --- a/src/features/compose/editor/utils/get-selected-node.ts +++ /dev/null @@ -1,26 +0,0 @@ -/** - * This source code is derived from code from Meta Platforms, Inc. - * and affiliates, licensed under the MIT license located in the - * LICENSE file in the /src/features/compose/editor directory. - */ - -import { $isAtNodeEnd } from '@lexical/selection'; -import { ElementNode, RangeSelection, TextNode } from 'lexical'; - -export const getSelectedNode = ( - selection: RangeSelection, -): TextNode | ElementNode => { - const anchor = selection.anchor; - const focus = selection.focus; - const anchorNode = selection.anchor.getNode(); - const focusNode = selection.focus.getNode(); - if (anchorNode === focusNode) { - return anchorNode; - } - const isBackward = selection.isBackward(); - if (isBackward) { - return $isAtNodeEnd(focus) ? anchorNode : focusNode; - } else { - return $isAtNodeEnd(anchor) ? focusNode : anchorNode; - } -}; diff --git a/src/features/compose/editor/utils/is-html-element.ts b/src/features/compose/editor/utils/is-html-element.ts deleted file mode 100644 index f7ff6f639..000000000 --- a/src/features/compose/editor/utils/is-html-element.ts +++ /dev/null @@ -1,4 +0,0 @@ -const isHTMLElement = (x: unknown): x is HTMLElement => x instanceof HTMLElement; - -export default isHTMLElement; -export { isHTMLElement }; diff --git a/src/features/compose/editor/utils/point.ts b/src/features/compose/editor/utils/point.ts deleted file mode 100644 index 38e825b18..000000000 --- a/src/features/compose/editor/utils/point.ts +++ /dev/null @@ -1,57 +0,0 @@ -/** - * This source code is derived from code from Meta Platforms, Inc. - * and affiliates, licensed under the MIT license located in the - * LICENSE file in the /src/features/compose/editor directory. - */ - -class Point { - - private readonly _x: number; - private readonly _y: number; - - constructor(x: number, y: number) { - this._x = x; - this._y = y; - } - - get x(): number { - return this._x; - } - - get y(): number { - return this._y; - } - - public equals({ x, y }: Point): boolean { - return this.x === x && this.y === y; - } - - public calcDeltaXTo({ x }: Point): number { - return this.x - x; - } - - public calcDeltaYTo({ y }: Point): number { - return this.y - y; - } - - public calcHorizontalDistanceTo(point: Point): number { - return Math.abs(this.calcDeltaXTo(point)); - } - - public calcVerticalDistance(point: Point): number { - return Math.abs(this.calcDeltaYTo(point)); - } - - public calcDistanceTo(point: Point): number { - return Math.sqrt( - Math.pow(this.calcDeltaXTo(point), 2) + - Math.pow(this.calcDeltaYTo(point), 2), - ); - } - -} - -const isPoint = (x: unknown): x is Point => x instanceof Point; - -export default Point; -export { Point, isPoint }; diff --git a/src/features/compose/editor/utils/rect.ts b/src/features/compose/editor/utils/rect.ts deleted file mode 100644 index 2cccc9a60..000000000 --- a/src/features/compose/editor/utils/rect.ts +++ /dev/null @@ -1,163 +0,0 @@ -/* eslint-disable no-dupe-class-members */ -/** - * This source code is derived from code from Meta Platforms, Inc. - * and affiliates, licensed under the MIT license located in the - * LICENSE file in the /src/features/compose/editor directory. - */ - -import { isPoint, Point } from './point'; - -type ContainsPointReturn = { - result: boolean; - reason: { - isOnTopSide: boolean; - isOnBottomSide: boolean; - isOnLeftSide: boolean; - isOnRightSide: boolean; - }; -}; - -class Rect { - - private readonly _left: number; - private readonly _top: number; - private readonly _right: number; - private readonly _bottom: number; - - constructor(left: number, top: number, right: number, bottom: number) { - const [physicTop, physicBottom] = - top <= bottom ? [top, bottom] : [bottom, top]; - - const [physicLeft, physicRight] = - left <= right ? [left, right] : [right, left]; - - this._top = physicTop; - this._right = physicRight; - this._left = physicLeft; - this._bottom = physicBottom; - } - - get top(): number { - return this._top; - } - - get right(): number { - return this._right; - } - - get bottom(): number { - return this._bottom; - } - - get left(): number { - return this._left; - } - - get width(): number { - return Math.abs(this._left - this._right); - } - - get height(): number { - return Math.abs(this._bottom - this._top); - } - - public equals({ top, left, bottom, right }: Rect): boolean { - return ( - top === this._top && - bottom === this._bottom && - left === this._left && - right === this._right - ); - } - - public contains({ x, y }: Point): ContainsPointReturn; - public contains({ top, left, bottom, right }: Rect): boolean; - public contains(target: Point | Rect): boolean | ContainsPointReturn { - if (isPoint(target)) { - const { x, y } = target; - - const isOnTopSide = y < this._top; - const isOnBottomSide = y > this._bottom; - const isOnLeftSide = x < this._left; - const isOnRightSide = x > this._right; - - const result = - !isOnTopSide && !isOnBottomSide && !isOnLeftSide && !isOnRightSide; - - return { - reason: { - isOnBottomSide, - isOnLeftSide, - isOnRightSide, - isOnTopSide, - }, - result, - }; - } else { - const { top, left, bottom, right } = target; - - return ( - top >= this._top && - top <= this._bottom && - bottom >= this._top && - bottom <= this._bottom && - left >= this._left && - left <= this._right && - right >= this._left && - right <= this._right - ); - } - } - - public intersectsWith(rect: Rect): boolean { - const { left: x1, top: y1, width: w1, height: h1 } = rect; - const { left: x2, top: y2, width: w2, height: h2 } = this; - const maxX = x1 + w1 >= x2 + w2 ? x1 + w1 : x2 + w2; - const maxY = y1 + h1 >= y2 + h2 ? y1 + h1 : y2 + h2; - const minX = x1 <= x2 ? x1 : x2; - const minY = y1 <= y2 ? y1 : y2; - return maxX - minX <= w1 + w2 && maxY - minY <= h1 + h2; - } - - public generateNewRect({ - left = this.left, - top = this.top, - right = this.right, - bottom = this.bottom, - }): Rect { - return new Rect(left, top, right, bottom); - } - - static fromLTRB( - left: number, - top: number, - right: number, - bottom: number, - ): Rect { - return new Rect(left, top, right, bottom); - } - - static fromLWTH( - left: number, - width: number, - top: number, - height: number, - ): Rect { - return new Rect(left, top, left + width, top + height); - } - - static fromPoints(startPoint: Point, endPoint: Point): Rect { - const { y: top, x: left } = startPoint; - const { y: bottom, x: right } = endPoint; - return Rect.fromLTRB(left, top, right, bottom); - } - - static fromDOM(dom: HTMLElement): Rect { - const { top, width, left, height } = dom.getBoundingClientRect(); - return Rect.fromLWTH(left, width, top, height); - } - -} - -export default Rect; -export { Rect }; diff --git a/src/features/compose/editor/utils/url.ts b/src/features/compose/editor/utils/url.ts deleted file mode 100644 index ca168f0a4..000000000 --- a/src/features/compose/editor/utils/url.ts +++ /dev/null @@ -1,16 +0,0 @@ -/** - * This source code is derived from code from Meta Platforms, Inc. - * and affiliates, licensed under the MIT license located in the - * LICENSE file in the /src/features/compose/editor directory. - */ - -// Source: https://stackoverflow.com/a/8234912/2013580 -const urlRegExp = new RegExp( - /((([A-Za-z]{3,9}:(?:\/\/)?)(?:[-;:&=+$,\w]+@)?[A-Za-z0-9.-]+|(?:www.|[-;:&=+$,\w]+@)[A-Za-z0-9.-]+)((?:\/[+~%/.\w-_]*)?\??(?:[-+=&;%@.\w_]*)#?(?:[\w]*))?)/, -); - -export const validateUrl = (url: string): boolean => { - // TODO Fix UI for link insertion; it should never default to an invalid URL such as https://. - // Maybe show a dialog where they user can type the URL before inserting it. - return url === 'https://' || urlRegExp.test(url); -}; From 0a580e8faaa33b2ddd3175c37c7db1341522d009 Mon Sep 17 00:00:00 2001 From: Soapbox Bot Date: Sun, 11 Feb 2024 15:05:55 +0000 Subject: [PATCH 08/11] Update docker Docker tag to v24.0.9 --- .gitlab-ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 30ce26ee6..ad50ecfdb 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -111,9 +111,9 @@ pages: docker: stage: deploy - image: docker:24.0.7 + image: docker:24.0.9 services: - - docker:24.0.7-dind + - docker:24.0.9-dind tags: - dind # https://medium.com/devops-with-valentine/how-to-build-a-docker-image-and-push-it-to-the-gitlab-container-registry-from-a-gitlab-ci-pipeline-acac0d1f26df From ee05e4d80241018b3d3eb341206af4447355bab4 Mon Sep 17 00:00:00 2001 From: Soapbox Bot Date: Sun, 11 Feb 2024 16:06:11 +0000 Subject: [PATCH 09/11] Update dependency husky to v9 --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index a557c3ae7..8c075ccd8 100644 --- a/package.json +++ b/package.json @@ -208,7 +208,7 @@ "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-tailwindcss": "^3.13.0", "fake-indexeddb": "^5.0.0", - "husky": "^8.0.0", + "husky": "^9.0.0", "jsdom": "^23.0.0", "lint-staged": ">=10", "react-intl-translations-manager": "^5.0.3", diff --git a/yarn.lock b/yarn.lock index f38bd96c6..338e14205 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5316,10 +5316,10 @@ human-signals@^5.0.0: resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-5.0.0.tgz#42665a284f9ae0dade3ba41ebc37eb4b852f3a28" integrity sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ== -husky@^8.0.0: - version "8.0.3" - resolved "https://registry.yarnpkg.com/husky/-/husky-8.0.3.tgz#4936d7212e46d1dea28fef29bb3a108872cd9184" - integrity sha512-+dQSyqPh4x1hlO1swXBiNb2HzTDN1I2IGLQx1GrBuiqFJfoMrnZWwVmatvSiO+Iz8fBUnf+lekwNo4c2LlXItg== +husky@^9.0.0: + version "9.0.10" + resolved "https://registry.yarnpkg.com/husky/-/husky-9.0.10.tgz#ddca8908deb5f244e9286865ebc80b54387672c2" + integrity sha512-TQGNknoiy6bURzIO77pPRu+XHi6zI7T93rX+QnJsoYFf3xdjKOur+IlfqzJGMHIK/wXrLg+GsvMs8Op7vI2jVA== iconv-lite@0.6.3: version "0.6.3" From cb14b34309d2f068e3b2c5aab9ccadbde55f4418 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sun, 11 Feb 2024 12:40:14 -0600 Subject: [PATCH 10/11] Add NSpec and SoapboxSigner --- package.json | 2 + src/actions/accounts.ts | 4 +- src/actions/nostr.ts | 4 +- src/api/hooks/nostr/useSignerStream.ts | 12 ++-- src/features/nostr/SoapboxSigner.ts | 44 +++++++++++++ src/features/nostr/sign.ts | 9 ++- src/types/nostr.ts | 12 ---- src/types/window.d.ts | 4 +- yarn.lock | 86 +++++++++++++++++++++++++- 9 files changed, 150 insertions(+), 27 deletions(-) create mode 100644 src/features/nostr/SoapboxSigner.ts delete mode 100644 src/types/nostr.ts diff --git a/package.json b/package.json index a3150d855..ef7024d71 100644 --- a/package.json +++ b/package.json @@ -61,6 +61,7 @@ "@lexical/react": "^0.12.4", "@lexical/selection": "^0.12.4", "@lexical/utils": "^0.12.4", + "@noble/hashes": "^1.3.3", "@popperjs/core": "^2.11.5", "@reach/combobox": "^0.18.0", "@reach/menu-button": "^0.18.0", @@ -133,6 +134,7 @@ "mini-css-extract-plugin": "^2.6.0", "nostr-machina": "^0.1.0", "nostr-tools": "^1.14.2", + "nspec": "^0.1.0", "path-browserify": "^1.0.1", "postcss": "^8.4.29", "process": "^0.11.10", diff --git a/src/actions/accounts.ts b/src/actions/accounts.ts index 4c7df890c..4b38bab80 100644 --- a/src/actions/accounts.ts +++ b/src/actions/accounts.ts @@ -2,7 +2,7 @@ import { nip19 } from 'nostr-tools'; import { importEntities } from 'soapbox/entity-store/actions'; import { Entities } from 'soapbox/entity-store/entities'; -import { getPublicKey } from 'soapbox/features/nostr/sign'; +import { signer } from 'soapbox/features/nostr/sign'; import { selectAccount } from 'soapbox/selectors'; import { isLoggedIn } from 'soapbox/utils/auth'; import { getFeatures, parseVersion, PLEROMA } from 'soapbox/utils/features'; @@ -134,7 +134,7 @@ const createAccount = (params: Record) => async (dispatch: AppDispatch, getState: () => RootState) => { const { instance } = getState(); const { nostrSignup } = getFeatures(instance); - const pubkey = nostrSignup ? await getPublicKey() : undefined; + const pubkey = nostrSignup ? await signer.getPublicKey() : undefined; dispatch({ type: ACCOUNT_CREATE_REQUEST, params }); return api(getState, 'app').post('/api/v1/accounts', params, { diff --git a/src/actions/nostr.ts b/src/actions/nostr.ts index 4a155c435..882717505 100644 --- a/src/actions/nostr.ts +++ b/src/actions/nostr.ts @@ -1,6 +1,6 @@ import { nip19 } from 'nostr-tools'; -import { getPublicKey } from 'soapbox/features/nostr/sign'; +import { signer } from 'soapbox/features/nostr/sign'; import { type AppDispatch } from 'soapbox/store'; import { verifyCredentials } from './auth'; @@ -8,7 +8,7 @@ import { verifyCredentials } from './auth'; /** Log in with a Nostr pubkey. */ function nostrLogIn() { return async (dispatch: AppDispatch) => { - const pubkey = await getPublicKey(); + const pubkey = await signer.getPublicKey(); const npub = nip19.npubEncode(pubkey); return dispatch(verifyCredentials(npub)); diff --git a/src/api/hooks/nostr/useSignerStream.ts b/src/api/hooks/nostr/useSignerStream.ts index c2a0b13a1..ff41aadac 100644 --- a/src/api/hooks/nostr/useSignerStream.ts +++ b/src/api/hooks/nostr/useSignerStream.ts @@ -2,7 +2,7 @@ import { NiceRelay } from 'nostr-machina'; import { type Event } from 'nostr-tools'; import { useEffect, useMemo } from 'react'; -import { nip04, signEvent } from 'soapbox/features/nostr/sign'; +import { signer } from 'soapbox/features/nostr/sign'; import { useInstance } from 'soapbox/hooks'; import { connectRequestSchema, nwcRequestSchema } from 'soapbox/schemas/nostr'; import { jsonSchema } from 'soapbox/schemas/utils'; @@ -21,7 +21,7 @@ function useSignerStream() { async function handleConnectEvent(event: Event) { if (!relay || !pubkey) return; - const decrypted = await nip04.decrypt(pubkey, event.content); + const decrypted = await signer.nip04!.decrypt(pubkey, event.content); const reqMsg = jsonSchema.pipe(connectRequestSchema).safeParse(decrypted); if (!reqMsg.success) { @@ -32,12 +32,12 @@ function useSignerStream() { const respMsg = { id: reqMsg.data.id, - result: await signEvent(reqMsg.data.params[0], reqMsg.data.params[1]), + result: await signer.signEvent(reqMsg.data.params[0]), }; - const respEvent = await signEvent({ + const respEvent = await signer.signEvent({ kind: 24133, - content: await nip04.encrypt(pubkey, JSON.stringify(respMsg)), + content: await signer.nip04!.encrypt(pubkey, JSON.stringify(respMsg)), tags: [['p', pubkey]], created_at: Math.floor(Date.now() / 1000), }); @@ -48,7 +48,7 @@ function useSignerStream() { async function handleWalletEvent(event: Event) { if (!relay || !pubkey) return; - const decrypted = await nip04.decrypt(pubkey, event.content); + const decrypted = await signer.nip04!.decrypt(pubkey, event.content); const reqMsg = jsonSchema.pipe(nwcRequestSchema).safeParse(decrypted); if (!reqMsg.success) { diff --git a/src/features/nostr/SoapboxSigner.ts b/src/features/nostr/SoapboxSigner.ts new file mode 100644 index 000000000..98e8db51e --- /dev/null +++ b/src/features/nostr/SoapboxSigner.ts @@ -0,0 +1,44 @@ +import { hexToBytes } from '@noble/hashes/utils'; +import { type NostrSigner, type NostrEvent, NSecSigner } from 'nspec'; + +/** Use key from `localStorage` if available, falling back to NIP-07. */ +export class SoapboxSigner implements NostrSigner { + + #signer: NostrSigner; + + constructor() { + const privateKey = localStorage.getItem('soapbox:nostr:privateKey'); + const signer = privateKey ? new NSecSigner(hexToBytes(privateKey)) : window.nostr; + + if (!signer) { + throw new Error('No Nostr signer available'); + } + + this.#signer = signer; + } + + async getPublicKey(): Promise { + return this.#signer.getPublicKey(); + } + + async signEvent(event: Omit): Promise { + return this.#signer.signEvent(event); + } + + nip04 = { + encrypt: (pubkey: string, plaintext: string): Promise => { + if (!this.#signer.nip04) { + throw new Error('NIP-04 not supported by signer'); + } + return this.#signer.nip04.encrypt(pubkey, plaintext); + }, + + decrypt: (pubkey: string, ciphertext: string): Promise => { + if (!this.#signer.nip04) { + throw new Error('NIP-04 not supported by signer'); + } + return this.#signer.nip04.decrypt(pubkey, ciphertext); + }, + }; + +} \ No newline at end of file diff --git a/src/features/nostr/sign.ts b/src/features/nostr/sign.ts index 8158d734d..392285901 100644 --- a/src/features/nostr/sign.ts +++ b/src/features/nostr/sign.ts @@ -6,6 +6,7 @@ import { finishEvent, nip04 as _nip04, } from 'nostr-tools'; +import { type NostrSigner } from 'nspec'; import { powWorker } from 'soapbox/workers'; @@ -60,4 +61,10 @@ const nip04 = { }, }; -export { getPublicKey, signEvent, nip04 }; \ No newline at end of file +const signer: NostrSigner = { + getPublicKey, + signEvent, + nip04, +}; + +export { signer }; \ No newline at end of file diff --git a/src/types/nostr.ts b/src/types/nostr.ts deleted file mode 100644 index 1e1547fa9..000000000 --- a/src/types/nostr.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type { Event, EventTemplate } from 'nostr-tools'; - -interface Nostr { - getPublicKey(): Promise; - signEvent(event: EventTemplate): Promise; - nip04?: { - encrypt: (pubkey: string, plaintext: string) => Promise; - decrypt: (pubkey: string, ciphertext: string) => Promise; - }; -} - -export default Nostr; \ No newline at end of file diff --git a/src/types/window.d.ts b/src/types/window.d.ts index b63397ce1..71fc2587e 100644 --- a/src/types/window.d.ts +++ b/src/types/window.d.ts @@ -1,7 +1,7 @@ -import type Nostr from './nostr'; +import type { NostrSigner } from 'nspec'; declare global { interface Window { - nostr?: Nostr; + nostr?: NostrSigner; } } \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 1235e8045..337362e61 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1832,7 +1832,7 @@ resolved "https://registry.yarnpkg.com/@mdn/browser-compat-data/-/browser-compat-data-5.3.16.tgz#c3b6585c256461fe5e2eac85182b11b36ea2678b" integrity sha512-b0kKg2weqKDLI+Ai5+tocgUEIidccdSfzUndbS2YnwIp5aVvd3M0D+DCcbrsSOSgMyrV9QKMqogtqMIjKwvDxw== -"@noble/ciphers@^0.2.0": +"@noble/ciphers@0.2.0", "@noble/ciphers@^0.2.0": version "0.2.0" resolved "https://registry.yarnpkg.com/@noble/ciphers/-/ciphers-0.2.0.tgz#a12cda60f3cf1ab5d7c77068c3711d2366649ed7" integrity sha512-6YBxJDAapHSdd3bLDv6x2wRPwq4QFMUaB3HvljNBUTThDd12eSm7/3F+2lnfzx2jvM+S6Nsy0jEt9QbPqSwqRw== @@ -1844,16 +1844,35 @@ dependencies: "@noble/hashes" "1.3.1" +"@noble/curves@1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.2.0.tgz#92d7e12e4e49b23105a2555c6984d41733d65c35" + integrity sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw== + dependencies: + "@noble/hashes" "1.3.2" + +"@noble/curves@~1.3.0": + version "1.3.0" + resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.3.0.tgz#01be46da4fd195822dab821e72f71bf4aeec635e" + integrity sha512-t01iSXPuN+Eqzb4eBX0S5oubSqXbK/xXa1Ne18Hj8f9pStxztHCE2gfboSp/dZRLSqfuLpRK2nDXDK+W9puocA== + dependencies: + "@noble/hashes" "1.3.3" + "@noble/hashes@1.3.1": version "1.3.1" resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.1.tgz#8831ef002114670c603c458ab8b11328406953a9" integrity sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA== -"@noble/hashes@~1.3.0", "@noble/hashes@~1.3.1": +"@noble/hashes@1.3.2", "@noble/hashes@~1.3.0", "@noble/hashes@~1.3.1": version "1.3.2" resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.2.tgz#6f26dbc8fbc7205873ce3cee2f690eba0d421b39" integrity sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ== +"@noble/hashes@1.3.3", "@noble/hashes@^1.3.3", "@noble/hashes@~1.3.2": + version "1.3.3" + resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.3.tgz#39908da56a4adc270147bb07968bf3b16cfe1699" + integrity sha512-V7/fPHgl+jsVPXqqeOzT8egNj2iBIVt+ECeMMG8TdcnTikP3oaBtUVqpT/gYCR68aEBJSF+XbYUxStjbFMqIIA== + "@nodelib/fs.scandir@2.1.5": version "2.1.5" resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" @@ -2108,6 +2127,11 @@ resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.1.1.tgz#ebb651ee52ff84f420097055f4bf46cfba403938" integrity sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA== +"@scure/base@^1.1.5", "@scure/base@~1.1.4": + version "1.1.5" + resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.1.5.tgz#1d85d17269fe97694b9c592552dd9e5e33552157" + integrity sha512-Brj9FiG2W1MRQSTB212YVPRrcbjkv48FoZi/u4l/zds/ieRrqsh7aUf6CLwkAq61oKXr/ZlTzlY66gLIj3TFTQ== + "@scure/bip32@1.3.1": version "1.3.1" resolved "https://registry.yarnpkg.com/@scure/bip32/-/bip32-1.3.1.tgz#7248aea723667f98160f593d621c47e208ccbb10" @@ -2117,6 +2141,15 @@ "@noble/hashes" "~1.3.1" "@scure/base" "~1.1.0" +"@scure/bip32@^1.3.3": + version "1.3.3" + resolved "https://registry.yarnpkg.com/@scure/bip32/-/bip32-1.3.3.tgz#a9624991dc8767087c57999a5d79488f48eae6c8" + integrity sha512-LJaN3HwRbfQK0X1xFSi0Q9amqOgzQnnDngIt+ZlsBC3Bm7/nE7K0kwshZHyaru79yIVRv/e1mQAjZyuZG6jOFQ== + dependencies: + "@noble/curves" "~1.3.0" + "@noble/hashes" "~1.3.2" + "@scure/base" "~1.1.4" + "@scure/bip39@1.2.1": version "1.2.1" resolved "https://registry.yarnpkg.com/@scure/bip39/-/bip39-1.2.1.tgz#5cee8978656b272a917b7871c981e0541ad6ac2a" @@ -2125,6 +2158,14 @@ "@noble/hashes" "~1.3.0" "@scure/base" "~1.1.0" +"@scure/bip39@^1.2.2": + version "1.2.2" + resolved "https://registry.yarnpkg.com/@scure/bip39/-/bip39-1.2.2.tgz#f3426813f4ced11a47489cbcf7294aa963966527" + integrity sha512-HYf9TUXG80beW+hGAt3TRM8wU6pQoYur9iNypTROm42dorCGmLnFe3eWjz3gOq6G62H2WRh0FCzAR1PI+29zIA== + dependencies: + "@noble/hashes" "~1.3.2" + "@scure/base" "~1.1.4" + "@sentry-internal/tracing@7.74.1": version "7.74.1" resolved "https://registry.yarnpkg.com/@sentry-internal/tracing/-/tracing-7.74.1.tgz#55ff387e61d2c9533a9a0d099d376332426c8e08" @@ -6274,6 +6315,11 @@ lowercase-keys@^2.0.0: resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-2.0.0.tgz#2603e78b7b4b0006cbca2fbcc8a3202558ac9479" integrity sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA== +lru-cache@^10.2.0: + version "10.2.0" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.2.0.tgz#0bd445ca57363465900f4d1f9bd8db343a4d95c3" + integrity sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q== + lru-cache@^4.1.2: version "4.1.5" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.5.tgz#8bbe50ea85bed59bc9e33dcab8235ee9bcf443cd" @@ -6570,6 +6616,25 @@ nostr-tools@^1.14.0, nostr-tools@^1.14.2: "@scure/bip32" "1.3.1" "@scure/bip39" "1.2.1" +nostr-tools@^2.1.4: + version "2.1.5" + resolved "https://registry.yarnpkg.com/nostr-tools/-/nostr-tools-2.1.5.tgz#d38ac1139343cf13654841b8727bab8dd70563eb" + integrity sha512-Gug/j54YGQ0ewB09dZW3mS9qfXWFlcOQMlyb1MmqQsuNO/95mfNOQSBi+jZ61O++Y+jG99SzAUPFLopUsKf0MA== + dependencies: + "@noble/ciphers" "0.2.0" + "@noble/curves" "1.2.0" + "@noble/hashes" "1.3.1" + "@scure/base" "1.1.1" + "@scure/bip32" "1.3.1" + "@scure/bip39" "1.2.1" + optionalDependencies: + nostr-wasm v0.1.0 + +nostr-wasm@v0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/nostr-wasm/-/nostr-wasm-0.1.0.tgz#17af486745feb2b7dd29503fdd81613a24058d94" + integrity sha512-78BTryCLcLYv96ONU8Ws3Q1JzjlAt+43pWQhIl86xZmWeegYCNLPml7yQ+gG3vR6V5h4XGj+TxO+SS5dsThQIA== + npm-run-path@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea" @@ -6584,6 +6649,18 @@ npm-run-path@^5.1.0: dependencies: path-key "^4.0.0" +nspec@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/nspec/-/nspec-0.1.0.tgz#abde817cf34cb042d7315a70cf515037e489401b" + integrity sha512-HPVyFFVR2x49K7HJzEjlvvBR7x5t79G6bh7/SQvfm25hXVFq9xvYBQ6i3nluwJkizcBxm+fvErM5yqJEnM/1tA== + dependencies: + "@scure/base" "^1.1.5" + "@scure/bip32" "^1.3.3" + "@scure/bip39" "^1.2.2" + lru-cache "^10.2.0" + nostr-tools "^2.1.4" + zod "^3.22.4" + nth-check@^2.0.1: version "2.1.1" resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.1.1.tgz#c9eab428effce36cd6b92c924bdb000ef1f1ed1d" @@ -9625,3 +9702,8 @@ zod@^3.21.0, zod@^3.21.4: version "3.22.3" resolved "https://registry.yarnpkg.com/zod/-/zod-3.22.3.tgz#2fbc96118b174290d94e8896371c95629e87a060" integrity sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug== + +zod@^3.22.4: + version "3.22.4" + resolved "https://registry.yarnpkg.com/zod/-/zod-3.22.4.tgz#f31c3a9386f61b1f228af56faa9255e845cf3fff" + integrity sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg== From 7f74ec80af8645590efa7a80c17bb8e320273724 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sun, 11 Feb 2024 12:45:33 -0600 Subject: [PATCH 11/11] Let signer be undefined if there's no way to sign --- src/actions/accounts.ts | 2 +- src/actions/nostr.ts | 4 ++ src/api/hooks/nostr/useSignerStream.ts | 14 +++--- src/features/nostr/sign.ts | 69 +++----------------------- 4 files changed, 18 insertions(+), 71 deletions(-) diff --git a/src/actions/accounts.ts b/src/actions/accounts.ts index 4b38bab80..2099cb63f 100644 --- a/src/actions/accounts.ts +++ b/src/actions/accounts.ts @@ -134,7 +134,7 @@ const createAccount = (params: Record) => async (dispatch: AppDispatch, getState: () => RootState) => { const { instance } = getState(); const { nostrSignup } = getFeatures(instance); - const pubkey = nostrSignup ? await signer.getPublicKey() : undefined; + const pubkey = (signer && nostrSignup) ? await signer.getPublicKey() : undefined; dispatch({ type: ACCOUNT_CREATE_REQUEST, params }); return api(getState, 'app').post('/api/v1/accounts', params, { diff --git a/src/actions/nostr.ts b/src/actions/nostr.ts index 882717505..21044140b 100644 --- a/src/actions/nostr.ts +++ b/src/actions/nostr.ts @@ -8,6 +8,10 @@ import { verifyCredentials } from './auth'; /** Log in with a Nostr pubkey. */ function nostrLogIn() { return async (dispatch: AppDispatch) => { + if (!signer) { + throw new Error('No Nostr signer available'); + } + const pubkey = await signer.getPublicKey(); const npub = nip19.npubEncode(pubkey); diff --git a/src/api/hooks/nostr/useSignerStream.ts b/src/api/hooks/nostr/useSignerStream.ts index ff41aadac..cc5d6403d 100644 --- a/src/api/hooks/nostr/useSignerStream.ts +++ b/src/api/hooks/nostr/useSignerStream.ts @@ -1,5 +1,5 @@ import { NiceRelay } from 'nostr-machina'; -import { type Event } from 'nostr-tools'; +import { type NostrEvent } from 'nspec'; import { useEffect, useMemo } from 'react'; import { signer } from 'soapbox/features/nostr/sign'; @@ -14,13 +14,13 @@ function useSignerStream() { const pubkey = instance.nostr?.pubkey; const relay = useMemo(() => { - if (relayUrl) { + if (relayUrl && signer) { return new NiceRelay(relayUrl); } - }, [relayUrl]); + }, [relayUrl, !!signer]); - async function handleConnectEvent(event: Event) { - if (!relay || !pubkey) return; + async function handleConnectEvent(event: NostrEvent) { + if (!relay || !pubkey || !signer) return; const decrypted = await signer.nip04!.decrypt(pubkey, event.content); const reqMsg = jsonSchema.pipe(connectRequestSchema).safeParse(decrypted); @@ -45,8 +45,8 @@ function useSignerStream() { relay.send(['EVENT', respEvent]); } - async function handleWalletEvent(event: Event) { - if (!relay || !pubkey) return; + async function handleWalletEvent(event: NostrEvent) { + if (!relay || !pubkey || !signer) return; const decrypted = await signer.nip04!.decrypt(pubkey, event.content); diff --git a/src/features/nostr/sign.ts b/src/features/nostr/sign.ts index 392285901..d33cd926a 100644 --- a/src/features/nostr/sign.ts +++ b/src/features/nostr/sign.ts @@ -1,70 +1,13 @@ -import { - type Event, - type EventTemplate, - generatePrivateKey, - getPublicKey as _getPublicKey, - finishEvent, - nip04 as _nip04, -} from 'nostr-tools'; import { type NostrSigner } from 'nspec'; -import { powWorker } from 'soapbox/workers'; +import { SoapboxSigner } from './SoapboxSigner'; -/** localStorage key for the Nostr private key (if not using NIP-07). */ -const LOCAL_KEY = 'soapbox:nostr:privateKey'; +let signer: NostrSigner | undefined; -/** Get the private key from the browser, or generate one. */ -const getPrivateKey = (): string => { - const local = localStorage.getItem(LOCAL_KEY); - - if (!local) { - const key = generatePrivateKey(); - localStorage.setItem(LOCAL_KEY, key); - return key; - } - - return local; -}; - -/** Get the user's public key from NIP-07, or generate one. */ -async function getPublicKey(): Promise { - return window.nostr ? window.nostr.getPublicKey() : _getPublicKey(getPrivateKey()); +try { + signer = new SoapboxSigner(); +} catch (_) { + // No signer available } -interface SignEventOpts { - pow?: number; -} - -/** Sign an event with NIP-07, or the locally generated key. */ -async function signEvent(template: EventTemplate, opts: SignEventOpts = {}): Promise> { - if (opts.pow) { - const event = await powWorker.mine({ ...template, pubkey: await getPublicKey() }, opts.pow) as Omit, 'sig'>; - return window.nostr ? window.nostr.signEvent(event) as Promise> : finishEvent(event, getPrivateKey()) ; - } else { - return window.nostr ? window.nostr.signEvent(template) as Promise> : finishEvent(template, getPrivateKey()) ; - } -} - -/** Crypto function with NIP-07, or the local key. */ -const nip04 = { - /** Encrypt with NIP-07, or the local key. */ - encrypt: async (pubkey: string, content: string) => { - return window.nostr?.nip04 - ? window.nostr.nip04.encrypt(pubkey, content) - : _nip04.encrypt(getPrivateKey(), pubkey, content); - }, - /** Decrypt with NIP-07, or the local key. */ - decrypt: async (pubkey: string, content: string) => { - return window.nostr?.nip04 - ? window.nostr.nip04.decrypt(pubkey, content) - : _nip04.decrypt(getPrivateKey(), pubkey, content); - }, -}; - -const signer: NostrSigner = { - getPublicKey, - signEvent, - nip04, -}; - export { signer }; \ No newline at end of file