From 2756f28d72f6259dc02685ac10e35522e33b7e14 Mon Sep 17 00:00:00 2001 From: Nevo David Date: Fri, 1 Aug 2025 11:14:11 +0700 Subject: [PATCH] feat: mention --- apps/frontend/src/app/global.scss | 41 +++ .../src/components/new-launch/editor.tsx | 109 ++++---- .../new-launch/mention.component.tsx | 241 ++++++++++++++++++ .../src/utils/strip.html.validation.ts | 2 +- .../src/integrations/social.abstract.ts | 5 + .../integrations/social/bluesky.provider.ts | 117 ++++++--- .../integrations/social/linkedin.provider.ts | 24 ++ .../social/social.integrations.interface.ts | 4 +- .../src/integrations/social/x.provider.ts | 36 ++- package.json | 5 +- pnpm-lock.yaml | 42 ++- 11 files changed, 536 insertions(+), 90 deletions(-) create mode 100644 apps/frontend/src/components/new-launch/mention.component.tsx diff --git a/apps/frontend/src/app/global.scss b/apps/frontend/src/app/global.scss index 4a047bb9..2227ab94 100644 --- a/apps/frontend/src/app/global.scss +++ b/apps/frontend/src/app/global.scss @@ -626,3 +626,44 @@ html[dir='rtl'] [dir='ltr'] { .mantine-Overlay-root { background: rgba(65, 64, 66, 0.3) !important; } + +.dropdown-menu { + @apply shadow-menu; + background: var(--new-bgColorInner); + border: 1px solid var(--new-bgLineColor); + border-radius: 18px; + display: flex; + flex-direction: column; + overflow: auto; + position: relative; + + button { + align-items: center; + background-color: transparent; + display: flex; + text-align: left; + width: 100%; + padding: 10px; + + &:hover, + &:hover.is-selected { + background-color: var(--new-bgLineColor); + } + } +} + +.tiptap { + :first-child { + margin-top: 0; + } + + .mention { + background-color: var(--purple-light); + border-radius: 0.4rem; + box-decoration-break: clone; + color: #ae8afc; + &::after { + content: '\200B'; + } + } +} diff --git a/apps/frontend/src/components/new-launch/editor.tsx b/apps/frontend/src/components/new-launch/editor.tsx index e0a02494..f71a0ae9 100644 --- a/apps/frontend/src/components/new-launch/editor.tsx +++ b/apps/frontend/src/components/new-launch/editor.tsx @@ -50,6 +50,12 @@ import { BulletList, ListItem } from '@tiptap/extension-list'; import { Bullets } from '@gitroom/frontend/components/new-launch/bullets.component'; import Heading from '@tiptap/extension-heading'; import { HeadingComponent } from '@gitroom/frontend/components/new-launch/heading.component'; +import Mention from '@tiptap/extension-mention'; +import { suggestion } from '@gitroom/frontend/components/new-launch/mention.component'; +import { useCustomProviderFunction } from '@gitroom/frontend/components/launches/helpers/use.custom.provider.function'; +import { useIntegration } from '@gitroom/frontend/components/launches/helpers/use.integration'; +import { useFetch } from '@gitroom/helpers/utils/custom.fetch'; +import { useDebouncedCallback } from 'use-debounce'; const InterceptBoldShortcut = Extension.create({ name: 'preventBoldWithUnderline', @@ -79,51 +85,6 @@ const InterceptUnderlineShortcut = Extension.create({ }, }); -const Span = Node.create({ - name: 'mention', - - inline: true, - group: 'inline', - selectable: false, - atom: true, - - addAttributes() { - return { - linkedinId: { - default: null, - }, - label: { - default: '', - }, - }; - }, - - parseHTML() { - return [ - { - tag: 'span[data-linkedin-id]', - }, - ]; - }, - - renderHTML({ HTMLAttributes }) { - return [ - 'span', - mergeAttributes( - // Exclude linkedinId from HTMLAttributes to avoid duplication - Object.fromEntries( - Object.entries(HTMLAttributes).filter(([key]) => key !== 'linkedinId') - ), - { - 'data-linkedin-id': HTMLAttributes.linkedinId, - class: 'mention', - } - ), - `@${HTMLAttributes.label}`, - ]; - }, -}); - export const EditorWrapper: FC<{ totalPosts: number; value: string; @@ -717,6 +678,43 @@ export const OnlyEditor = forwardRef< paste?: (event: ClipboardEvent | File[]) => void; } >(({ editorType, value, onChange, paste }, ref) => { + const fetch = useFetch(); + const { internal } = useLaunchStore( + useShallow((state) => ({ + internal: state.internal.find((p) => p.integration.id === state.current), + })) + ); + + const loadList = useCallback( + async (query: string) => { + if (query.length < 2) { + return []; + } + + if (!internal?.integration.id) { + return []; + } + + try { + const load = await fetch('/integrations/function', { + method: 'POST', + body: JSON.stringify({ + name: 'mention', + id: internal.integration.id, + data: { query }, + }), + }); + + const result = await load.json(); + return result; + } catch (error) { + console.error('Error loading mentions:', error); + return []; + } + }, + [internal, fetch] + ); + const editor = useEditor({ extensions: [ Document, @@ -726,9 +724,28 @@ export const OnlyEditor = forwardRef< Bold, InterceptBoldShortcut, InterceptUnderlineShortcut, - Span, BulletList, ListItem, + ...(internal?.integration?.id + ? [ + Mention.configure({ + HTMLAttributes: { + class: 'mention', + }, + renderHTML({ options, node }) { + return [ + 'span', + mergeAttributes(options.HTMLAttributes, { + 'data-mention-id': node.attrs.id || '', + 'data-mention-label': node.attrs.label || '', + }), + `@${node.attrs.label}`, + ]; + }, + suggestion: suggestion(loadList), + }), + ] + : []), Heading.configure({ levels: [1, 2, 3], }), diff --git a/apps/frontend/src/components/new-launch/mention.component.tsx b/apps/frontend/src/components/new-launch/mention.component.tsx new file mode 100644 index 00000000..90a14a9b --- /dev/null +++ b/apps/frontend/src/components/new-launch/mention.component.tsx @@ -0,0 +1,241 @@ +import React, { FC, useEffect, useImperativeHandle, useState } from 'react'; +import { computePosition, flip, shift } from '@floating-ui/dom'; +import { posToDOMRect, ReactRenderer } from '@tiptap/react'; +import { timer } from '@gitroom/helpers/utils/timer'; + +// Debounce utility for TipTap +const debounce = ( + func: (...args: any[]) => Promise, + wait: number +) => { + let timeout: NodeJS.Timeout; + return (...args: any[]): Promise => { + clearTimeout(timeout); + return new Promise((resolve) => { + timeout = setTimeout(async () => { + try { + const result = await func(...args); + resolve(result); + } catch (error) { + console.error('Debounced function error:', error); + resolve([] as T); + } + }, wait); + }); + }; +}; + +const MentionList: FC = (props: any) => { + const [selectedIndex, setSelectedIndex] = useState(0); + + const selectItem = (index: number) => { + const item = props.items[index]; + + if (item) { + props.command(item); + } + }; + + const upHandler = () => { + setSelectedIndex( + (selectedIndex + props.items.length - 1) % props.items.length + ); + }; + + const downHandler = () => { + setSelectedIndex((selectedIndex + 1) % props.items.length); + }; + + const enterHandler = () => { + selectItem(selectedIndex); + }; + + useEffect(() => setSelectedIndex(0), [props.items]); + + useImperativeHandle(props.ref, () => ({ + onKeyDown: ({ event }: { event: any }) => { + if (event.key === 'ArrowUp') { + upHandler(); + return true; + } + + if (event.key === 'ArrowDown') { + downHandler(); + return true; + } + + if (event.key === 'Enter') { + enterHandler(); + return true; + } + + return false; + }, + })); + + return ( +
+ {props?.items?.none ? ( +
+ We don't have autocomplete for this social media +
+ ) : props?.loading ? ( +
+ Loading... +
+ ) : props?.items ? ( + props.items.map((item: any, index: any) => ( + + )) + ) : ( +
Loading...
+ )} +
+ ); +}; + +const updatePosition = (editor: any, element: any) => { + const virtualElement = { + getBoundingClientRect: () => + posToDOMRect( + editor.view, + editor.state.selection.from, + editor.state.selection.to + ), + }; + + computePosition(virtualElement, element, { + placement: 'bottom-start', + strategy: 'absolute', + middleware: [shift(), flip()], + }).then(({ x, y, strategy }) => { + element.style.width = 'max-content'; + element.style.position = strategy; + element.style.left = `${x}px`; + element.style.top = `${y}px`; + element.style.zIndex = '1000'; + }); +}; + +export const suggestion = ( + loadList: ( + query: string + ) => Promise<{ image: string; label: string; id: string }[]> +) => { + // Create debounced version of loadList once + const debouncedLoadList = debounce(loadList, 500); + let component: any; + + return { + items: async ({ query }: { query: string }) => { + if (!query || query.length < 2) { + return []; + } + + try { + component.updateProps({ loading: true }); + const result = await debouncedLoadList(query); + console.log(result); + return result; + } catch (error) { + console.error('Error in suggestion items:', error); + return []; + } + }, + + render: () => { + let currentQuery = ''; + let isLoadingQuery = false; + + return { + onBeforeStart: (props: any) => { + component = new ReactRenderer(MentionList, { + props: { + ...props, + loading: true, + }, + editor: props.editor, + }); + component.updateProps({ ...props, loading: true }); + updatePosition(props.editor, component.element); + }, + onStart: (props: any) => { + currentQuery = props.query || ''; + isLoadingQuery = currentQuery.length >= 2; + + if (!props.clientRect) { + return; + } + + component.element.style.position = 'absolute'; + component.element.style.zIndex = '1000'; + + const container = + document.querySelector('.mantine-Paper-root') || document.body; + container.appendChild(component.element); + + updatePosition(props.editor, component.element); + component.updateProps({ ...props, loading: true }); + }, + + onUpdate(props: any) { + const newQuery = props.query || ''; + const queryChanged = newQuery !== currentQuery; + currentQuery = newQuery; + + // If query changed and is valid, we're loading until results come in + if (queryChanged && newQuery.length >= 2) { + isLoadingQuery = true; + } + + // If we have results, we're no longer loading + if (props.items && props.items.length > 0) { + isLoadingQuery = false; + } + + // Show loading if we have a valid query but no results yet + const shouldShowLoading = + isLoadingQuery && + newQuery.length >= 2 && + (!props.items || props.items.length === 0); + + component.updateProps({ ...props, loading: false }); + + if (!props.clientRect) { + return; + } + + updatePosition(props.editor, component.element); + }, + + onKeyDown(props: any) { + if (props.event.key === 'Escape') { + component.destroy(); + + return true; + } + + return component.ref?.onKeyDown(props); + }, + + onExit() { + component.element.remove(); + component.destroy(); + }, + }; + }, + }; +}; diff --git a/libraries/helpers/src/utils/strip.html.validation.ts b/libraries/helpers/src/utils/strip.html.validation.ts index 3745c7b3..4b76ab75 100644 --- a/libraries/helpers/src/utils/strip.html.validation.ts +++ b/libraries/helpers/src/utils/strip.html.validation.ts @@ -194,7 +194,7 @@ export const stripHtmlValidation = ( export const convertLinkedinMention = (value: string) => { return value.replace( - /(.+?)<\/span>/gi, + /(.*?)<\/span>/gi, (match, id, name) => { return `@[${name.replace('@', '')}](${id})`; } diff --git a/libraries/nestjs-libraries/src/integrations/social.abstract.ts b/libraries/nestjs-libraries/src/integrations/social.abstract.ts index dc45d39b..a9311d94 100644 --- a/libraries/nestjs-libraries/src/integrations/social.abstract.ts +++ b/libraries/nestjs-libraries/src/integrations/social.abstract.ts @@ -1,5 +1,6 @@ import { timer } from '@gitroom/helpers/utils/timer'; import { concurrencyService } from '@gitroom/helpers/utils/concurrency.service'; +import { Integration } from '@prisma/client'; export class RefreshToken { constructor( @@ -31,6 +32,10 @@ export abstract class SocialAbstract { return undefined; } + public async mention(token: string, d: { query: string }, id: string, integration: Integration): Promise<{ id: string; label: string; image: string }[] | {none: true}> { + return {none: true}; + } + async runInConcurrent(func: (...args: any[]) => Promise) { const value = await concurrencyService(this.identifier.split('-')[0], async () => { try { diff --git a/libraries/nestjs-libraries/src/integrations/social/bluesky.provider.ts b/libraries/nestjs-libraries/src/integrations/social/bluesky.provider.ts index 026d4970..18d526f0 100644 --- a/libraries/nestjs-libraries/src/integrations/social/bluesky.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/bluesky.provider.ts @@ -9,13 +9,13 @@ import { RefreshToken, SocialAbstract, } from '@gitroom/nestjs-libraries/integrations/social.abstract'; -import { - BskyAgent, - RichText, +import { + BskyAgent, + RichText, AppBskyEmbedVideo, AppBskyVideoDefs, AtpAgent, - BlobRef + BlobRef, } from '@atproto/api'; import dayjs from 'dayjs'; import { Integration } from '@prisma/client'; @@ -59,16 +59,19 @@ async function reduceImageBySize(url: string, maxSizeKB = 976) { } } -async function uploadVideo(agent: AtpAgent, videoPath: string): Promise { - const { data: serviceAuth } = await agent.com.atproto.server.getServiceAuth( - { - aud: `did:web:${agent.dispatchUrl.host}`, - lxm: "com.atproto.repo.uploadBlob", - exp: Date.now() / 1000 + 60 * 30, // 30 minutes - }, - ); +async function uploadVideo( + agent: AtpAgent, + videoPath: string +): Promise { + const { data: serviceAuth } = await agent.com.atproto.server.getServiceAuth({ + aud: `did:web:${agent.dispatchUrl.host}`, + lxm: 'com.atproto.repo.uploadBlob', + exp: Date.now() / 1000 + 60 * 30, // 30 minutes + }); - async function downloadVideo(url: string): Promise<{ video: Buffer, size: number }> { + async function downloadVideo( + url: string + ): Promise<{ video: Buffer; size: number }> { const response = await fetch(url); if (!response.ok) { throw new Error(`Failed to fetch video: ${response.statusText}`); @@ -81,35 +84,37 @@ async function uploadVideo(agent: AtpAgent, videoPath: string): Promise setTimeout(resolve, 1000)); } - - console.log("posting video..."); + + console.log('posting video...'); return { - $type: "app.bsky.embed.video", + $type: 'app.bsky.embed.video', video: blob, } satisfies AppBskyEmbedVideo.Main; } @@ -243,8 +248,10 @@ export class BlueskyProvider extends SocialAbstract implements SocialProvider { const cidUrl = [] as { cid: string; url: string; rev: string }[]; for (const post of postDetails) { // Separate images and videos - const imageMedia = post.media?.filter((p) => p.path.indexOf('mp4') === -1) || []; - const videoMedia = post.media?.filter((p) => p.path.indexOf('mp4') !== -1) || []; + const imageMedia = + post.media?.filter((p) => p.path.indexOf('mp4') === -1) || []; + const videoMedia = + post.media?.filter((p) => p.path.indexOf('mp4') !== -1) || []; // Upload images const images = await Promise.all( @@ -313,7 +320,11 @@ export class BlueskyProvider extends SocialAbstract implements SocialProvider { if (postDetails?.[0]?.settings?.active_thread_finisher) { const rt = new RichText({ - text: stripHtmlValidation('normal', postDetails?.[0]?.settings?.thread_finisher, true), + text: stripHtmlValidation( + 'normal', + postDetails?.[0]?.settings?.thread_finisher, + true + ), }); await rt.detectFacets(agent); @@ -487,4 +498,34 @@ export class BlueskyProvider extends SocialAbstract implements SocialProvider { return true; } + + override async mention( + token: string, + d: { query: string }, + id: string, + integration: Integration + ) { + const agent = new BskyAgent({ + service: 'https://bsky.social', + }); + + const body = JSON.parse( + AuthService.fixedDecryption(integration.customInstanceDetails!) + ); + + await agent.login({ + identifier: body.identifier, + password: body.password, + }); + + const list = await agent.searchActors({ + q: d.query + }); + + return list.data.actors.map(p => ({ + label: p.displayName, + id: p.handle, + image: p.avatar + })) + } } diff --git a/libraries/nestjs-libraries/src/integrations/social/linkedin.provider.ts b/libraries/nestjs-libraries/src/integrations/social/linkedin.provider.ts index 0f479501..cb484979 100644 --- a/libraries/nestjs-libraries/src/integrations/social/linkedin.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/linkedin.provider.ts @@ -715,4 +715,28 @@ export class LinkedinProvider extends SocialAbstract implements SocialProvider { }, }); } + + async mention(token: string, data: { query: string }) { + const { elements } = await ( + await fetch( + `https://api.linkedin.com/v2/organizations?q=vanityName&vanityName=${encodeURIComponent( + data.query + )}&projection=(elements*(id,localizedName,logoV2(original~:playableStreams)))`, + { + headers: { + 'X-Restli-Protocol-Version': '2.0.0', + 'Content-Type': 'application/json', + 'LinkedIn-Version': '202504', + Authorization: `Bearer ${token}`, + }, + } + ) + ).json(); + + return elements.map((p: any) => ({ + id: String(p.id), + label: p.localizedName, + image: p.logoV2?.['original~']?.elements?.[0]?.identifiers?.[0]?.identifier || '', + })); + } } diff --git a/libraries/nestjs-libraries/src/integrations/social/social.integrations.interface.ts b/libraries/nestjs-libraries/src/integrations/social/social.integrations.interface.ts index 1c9115e9..cd14254f 100644 --- a/libraries/nestjs-libraries/src/integrations/social/social.integrations.interface.ts +++ b/libraries/nestjs-libraries/src/integrations/social/social.integrations.interface.ts @@ -133,6 +133,6 @@ export interface SocialProvider url: string ) => Promise<{ client_id: string; client_secret: string }>; mention?: ( - query: string - ) => Promise<{ id: string; name: string; picture: string }[]>; + token: string, data: { query: string }, id: string, integration: Integration + ) => Promise<{ id: string; label: string; image: string }[] | {none: true}>; } diff --git a/libraries/nestjs-libraries/src/integrations/social/x.provider.ts b/libraries/nestjs-libraries/src/integrations/social/x.provider.ts index 574c648e..36c18669 100644 --- a/libraries/nestjs-libraries/src/integrations/social/x.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/x.provider.ts @@ -315,7 +315,10 @@ export class XProvider extends SocialAbstract implements SocialProvider { const media_ids = (uploadAll[post.id] || []).filter((f) => f); // @ts-ignore - const { data }: { data: { id: string } } = await this.runInConcurrent( async () => client.v2.tweet({ + const { data }: { data: { id: string } } = await this.runInConcurrent( + async () => + // @ts-ignore + client.v2.tweet({ ...(!postDetails?.[0]?.settings?.who_can_reply_post || postDetails?.[0]?.settings?.who_can_reply_post === 'everyone' ? {} @@ -492,4 +495,35 @@ export class XProvider extends SocialAbstract implements SocialProvider { } return []; } + + override async mention(token: string, d: { query: string }) { + const [accessTokenSplit, accessSecretSplit] = token.split(':'); + const client = new TwitterApi({ + appKey: process.env.X_API_KEY!, + appSecret: process.env.X_API_SECRET!, + accessToken: accessTokenSplit, + accessSecret: accessSecretSplit, + }); + + try { + const data = await client.v2.userByUsername(d.query, { + 'user.fields': ['username', 'name', 'profile_image_url'], + }); + + if (!data?.data?.username) { + return []; + } + + return [ + { + id: data.data.username, + image: data.data.profile_image_url, + label: data.data.name, + }, + ]; + } catch (err) { + console.log(err); + } + return []; + } } diff --git a/package.json b/package.json index 4e9eb4f6..6d046ffe 100644 --- a/package.json +++ b/package.json @@ -86,12 +86,14 @@ "@tiptap/extension-heading": "^3.0.7", "@tiptap/extension-history": "^3.0.7", "@tiptap/extension-list": "^3.0.7", + "@tiptap/extension-mention": "^3.0.7", "@tiptap/extension-paragraph": "^3.0.6", "@tiptap/extension-text": "^3.0.6", "@tiptap/extension-underline": "^3.0.6", "@tiptap/pm": "^3.0.6", "@tiptap/react": "^3.0.6", "@tiptap/starter-kit": "^3.0.6", + "@tiptap/suggestion": "^3.0.7", "@types/bcrypt": "^5.0.2", "@types/concat-stream": "^2.0.3", "@types/facebook-nodejs-business-sdk": "^20.0.2", @@ -207,11 +209,12 @@ "tailwind-scrollbar": "^3.1.0", "tailwindcss": "3.4.17", "tailwindcss-rtl": "^0.9.0", + "tippy.js": "^6.3.7", "tldts": "^6.1.47", "transloadit": "^3.0.2", "tslib": "^2.3.0", "tweetnacl": "^1.0.3", - "twitter-api-v2": "^1.23.2", + "twitter-api-v2": "^1.24.0", "twitter-text": "^3.1.0", "use-debounce": "^10.0.0", "utf-8-validate": "^5.0.10", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a4fa529f..cc4ed2d8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -138,6 +138,9 @@ importers: '@tiptap/extension-list': specifier: ^3.0.7 version: 3.0.7(@tiptap/core@3.0.6(@tiptap/pm@3.0.6))(@tiptap/pm@3.0.6) + '@tiptap/extension-mention': + specifier: ^3.0.7 + version: 3.0.7(@tiptap/core@3.0.6(@tiptap/pm@3.0.6))(@tiptap/pm@3.0.6)(@tiptap/suggestion@3.0.7(@tiptap/core@3.0.6(@tiptap/pm@3.0.6))(@tiptap/pm@3.0.6)) '@tiptap/extension-paragraph': specifier: ^3.0.6 version: 3.0.6(@tiptap/core@3.0.6(@tiptap/pm@3.0.6)) @@ -156,6 +159,9 @@ importers: '@tiptap/starter-kit': specifier: ^3.0.6 version: 3.0.6 + '@tiptap/suggestion': + specifier: ^3.0.7 + version: 3.0.7(@tiptap/core@3.0.6(@tiptap/pm@3.0.6))(@tiptap/pm@3.0.6) '@types/bcrypt': specifier: ^5.0.2 version: 5.0.2 @@ -501,6 +507,9 @@ importers: tailwindcss-rtl: specifier: ^0.9.0 version: 0.9.0 + tippy.js: + specifier: ^6.3.7 + version: 6.3.7 tldts: specifier: ^6.1.47 version: 6.1.86 @@ -514,7 +523,7 @@ importers: specifier: ^1.0.3 version: 1.0.3 twitter-api-v2: - specifier: ^1.23.2 + specifier: ^1.24.0 version: 1.24.0 twitter-text: specifier: ^3.1.0 @@ -5921,6 +5930,13 @@ packages: '@tiptap/core': ^3.0.7 '@tiptap/pm': ^3.0.7 + '@tiptap/extension-mention@3.0.7': + resolution: {integrity: sha512-PHEx6NdmarjvPPvTd8D9AqK1JIaVYTsnQLxJUERakOLzujgUCToZ7FpMQDhPj97YLvF0t3jeyjZOPmFuj5kw4w==} + peerDependencies: + '@tiptap/core': ^3.0.7 + '@tiptap/pm': ^3.0.7 + '@tiptap/suggestion': ^3.0.7 + '@tiptap/extension-ordered-list@3.0.6': resolution: {integrity: sha512-9SbeGO6kGKoX8GwhaSgpFNCGxlzfGu5otK5DE+Unn5F8/gIYGBJkXTZE1tj8XzPmH6lWhmKJQPudANnW6yuKqg==} peerDependencies: @@ -5966,6 +5982,12 @@ packages: '@tiptap/starter-kit@3.0.6': resolution: {integrity: sha512-7xqcx5hwa+o0J6vpqJRSQNxKHOO6/vSwwicmaHxZ4zdGtlUjJrdreeYaaUpCf0wvpBT1DAQlRnancuD6DJkkPg==} + '@tiptap/suggestion@3.0.7': + resolution: {integrity: sha512-HSMvzAejdvcnVaRZOhXJWAvQqaQs3UYDZaA0ZnzgiJ/sNSbtTyn9XVbX6MfVNYrbtBua4iKaXuJwp6CP0KdHQg==} + peerDependencies: + '@tiptap/core': ^3.0.7 + '@tiptap/pm': ^3.0.7 + '@tokenizer/inflate@0.2.7': resolution: {integrity: sha512-MADQgmZT1eKjp06jpI2yozxaU9uVs4GzzgSL+uEq7bVcJ9V1ZXQkeGNql1fsSI0gMy1vhvNTNbUqrx+pZfJVmg==} engines: {node: '>=18'} @@ -14277,6 +14299,9 @@ packages: resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==} engines: {node: '>=14.0.0'} + tippy.js@6.3.7: + resolution: {integrity: sha512-E1d3oP2emgJ9dRQZdf3Kkn0qJgI6ZLpyS5z6ZkY1DF3kaQaBsGZsndEpHwx+eC+tYM41HaSNvNtLx8tU57FzTQ==} + tlds@1.259.0: resolution: {integrity: sha512-AldGGlDP0PNgwppe2quAvuBl18UcjuNtOnDuUkqhd6ipPqrYYBt3aTxK1QTsBVknk97lS2JcafWMghjGWFtunw==} hasBin: true @@ -22336,6 +22361,12 @@ snapshots: '@tiptap/core': 3.0.6(@tiptap/pm@3.0.6) '@tiptap/pm': 3.0.6 + '@tiptap/extension-mention@3.0.7(@tiptap/core@3.0.6(@tiptap/pm@3.0.6))(@tiptap/pm@3.0.6)(@tiptap/suggestion@3.0.7(@tiptap/core@3.0.6(@tiptap/pm@3.0.6))(@tiptap/pm@3.0.6))': + dependencies: + '@tiptap/core': 3.0.6(@tiptap/pm@3.0.6) + '@tiptap/pm': 3.0.6 + '@tiptap/suggestion': 3.0.7(@tiptap/core@3.0.6(@tiptap/pm@3.0.6))(@tiptap/pm@3.0.6) + '@tiptap/extension-ordered-list@3.0.6(@tiptap/extension-list@3.0.7(@tiptap/core@3.0.6(@tiptap/pm@3.0.6))(@tiptap/pm@3.0.6))': dependencies: '@tiptap/extension-list': 3.0.7(@tiptap/core@3.0.6(@tiptap/pm@3.0.6))(@tiptap/pm@3.0.6) @@ -22424,6 +22455,11 @@ snapshots: '@tiptap/extensions': 3.0.6(@tiptap/core@3.0.6(@tiptap/pm@3.0.6))(@tiptap/pm@3.0.6) '@tiptap/pm': 3.0.6 + '@tiptap/suggestion@3.0.7(@tiptap/core@3.0.6(@tiptap/pm@3.0.6))(@tiptap/pm@3.0.6)': + dependencies: + '@tiptap/core': 3.0.6(@tiptap/pm@3.0.6) + '@tiptap/pm': 3.0.6 + '@tokenizer/inflate@0.2.7': dependencies: debug: 4.4.1(supports-color@5.5.0) @@ -33414,6 +33450,10 @@ snapshots: tinyspy@3.0.2: {} + tippy.js@6.3.7: + dependencies: + '@popperjs/core': 2.11.8 + tlds@1.259.0: {} tldts-core@6.1.86: {}