From 1e2d45f8867065d50fffa57aa2b55cf6a403a12a Mon Sep 17 00:00:00 2001 From: Nevo David Date: Thu, 31 Jul 2025 20:30:26 +0700 Subject: [PATCH 1/7] feat: mention --- .../src/integrations/social/social.integrations.interface.ts | 3 +++ 1 file changed, 3 insertions(+) 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 f666752b..1c9115e9 100644 --- a/libraries/nestjs-libraries/src/integrations/social/social.integrations.interface.ts +++ b/libraries/nestjs-libraries/src/integrations/social/social.integrations.interface.ts @@ -132,4 +132,7 @@ export interface SocialProvider externalUrl?: ( url: string ) => Promise<{ client_id: string; client_secret: string }>; + mention?: ( + query: string + ) => Promise<{ id: string; name: string; picture: string }[]>; } From 2756f28d72f6259dc02685ac10e35522e33b7e14 Mon Sep 17 00:00:00 2001 From: Nevo David Date: Fri, 1 Aug 2025 11:14:11 +0700 Subject: [PATCH 2/7] 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: {} From 449e2acab1d41606896f3645c74fbcaeb31bbe19 Mon Sep 17 00:00:00 2001 From: Nevo David Date: Fri, 1 Aug 2025 12:42:21 +0700 Subject: [PATCH 3/7] feat: mentions --- .../src/api/routes/integrations.controller.ts | 53 ++++++++++++++++++- .../src/components/new-launch/editor.tsx | 22 +------- .../new-launch/mention.component.tsx | 47 +++++++++------- .../src/utils/strip.html.validation.ts | 30 ++++++----- .../integrations/integration.repository.ts | 49 ++++++++++++++++- .../integrations/integration.service.ts | 17 +++++- .../database/prisma/posts/posts.repository.ts | 2 +- .../database/prisma/posts/posts.service.ts | 19 +++++-- .../src/database/prisma/schema.prisma | 12 +++++ .../integrations/social/bluesky.provider.ts | 12 +++-- .../integrations/social/linkedin.provider.ts | 6 ++- .../social/social.integrations.interface.ts | 1 + .../src/integrations/social/x.provider.ts | 4 ++ 13 files changed, 210 insertions(+), 64 deletions(-) diff --git a/apps/backend/src/api/routes/integrations.controller.ts b/apps/backend/src/api/routes/integrations.controller.ts index cf9a6609..d658fa6e 100644 --- a/apps/backend/src/api/routes/integrations.controller.ts +++ b/apps/backend/src/api/routes/integrations.controller.ts @@ -37,6 +37,7 @@ import { AuthorizationActions, Sections, } from '@gitroom/backend/services/auth/permissions/permission.exception.class'; +import { uniqBy } from 'lodash'; @ApiTags('Integrations') @Controller('/integrations') @@ -246,11 +247,59 @@ export class IntegrationsController { ) { return this._integrationService.setTimes(org.id, id, body); } + + @Post('/mentions') + async mentions( + @GetOrgFromRequest() org: Organization, + @Body() body: IntegrationFunctionDto + ) { + const getIntegration = await this._integrationService.getIntegrationById( + org.id, + body.id + ); + if (!getIntegration) { + throw new Error('Invalid integration'); + } + + const list = await this._integrationService.getMentions( + getIntegration.providerIdentifier, + body?.data?.query + ); + + let newList = []; + try { + newList = await this.functionIntegration(org, body); + } catch (err) {} + + if (newList.length) { + await this._integrationService.insertMentions( + getIntegration.providerIdentifier, + newList.map((p: any) => ({ + name: p.label, + username: p.id, + image: p.image, + })) + ); + } + + return uniqBy( + [ + ...list.map((p) => ({ + id: p.username, + image: p.image, + label: p.name, + })), + ...newList, + ], + (p) => p.id + ); + } + @Post('/function') async functionIntegration( @GetOrgFromRequest() org: Organization, @Body() body: IntegrationFunctionDto - ) { + ): Promise { const getIntegration = await this._integrationService.getIntegrationById( org.id, body.id @@ -266,8 +315,10 @@ export class IntegrationsController { throw new Error('Invalid provider'); } + // @ts-ignore if (integrationProvider[body.name]) { try { + // @ts-ignore const load = await integrationProvider[body.name]( getIntegration.token, body.data, diff --git a/apps/frontend/src/components/new-launch/editor.tsx b/apps/frontend/src/components/new-launch/editor.tsx index f71a0ae9..017d7b4c 100644 --- a/apps/frontend/src/components/new-launch/editor.tsx +++ b/apps/frontend/src/components/new-launch/editor.tsx @@ -511,23 +511,6 @@ export const Editor: FC<{ [props.value, id] ); - const addLinkedinTag = useCallback((text: string) => { - const id = text.split('(')[1].split(')')[0]; - const name = text.split('[')[1].split(']')[0]; - - editorRef?.current?.editor - .chain() - .focus() - .insertContent({ - type: 'mention', - attrs: { - linkedinId: id, - label: name, - }, - }) - .run(); - }, []); - return (
@@ -559,9 +542,6 @@ export const Editor: FC<{ > {'\uD83D\uDE00'}
- {identifier === 'linkedin' || identifier === 'linkedin-page' ? ( - - ) : null}
{ }, })); + if (props?.stop) { + return null; + } + return (
{props?.items?.none ? ( @@ -84,22 +88,26 @@ const MentionList: FC = (props: any) => { Loading...
) : props?.items ? ( - props.items.map((item: any, index: any) => ( - - )) + props.items.length === 0 ? ( +
No results found
+ ) : ( + props.items.map((item: any, index: any) => ( + + )) + ) ) : (
Loading...
)} @@ -142,11 +150,12 @@ export const suggestion = ( return { items: async ({ query }: { query: string }) => { if (!query || query.length < 2) { + component.updateProps({ loading: true, stop: true }); return []; } try { - component.updateProps({ loading: true }); + component.updateProps({ loading: true, stop: false }); const result = await debouncedLoadList(query); console.log(result); return result; @@ -169,7 +178,7 @@ export const suggestion = ( }, editor: props.editor, }); - component.updateProps({ ...props, loading: true }); + component.updateProps({ ...props, loading: true, stop: false }); updatePosition(props.editor, component.element); }, onStart: (props: any) => { @@ -212,7 +221,7 @@ export const suggestion = ( newQuery.length >= 2 && (!props.items || props.items.length === 0); - component.updateProps({ ...props, loading: false }); + component.updateProps({ ...props, loading: false, stop: false }); if (!props.clientRect) { return; diff --git a/libraries/helpers/src/utils/strip.html.validation.ts b/libraries/helpers/src/utils/strip.html.validation.ts index 4b76ab75..5ca9d725 100644 --- a/libraries/helpers/src/utils/strip.html.validation.ts +++ b/libraries/helpers/src/utils/strip.html.validation.ts @@ -135,7 +135,8 @@ export const stripHtmlValidation = ( type: 'none' | 'normal' | 'markdown' | 'html', value: string, replaceBold = false, - none = false + none = false, + convertMentionFunction?: (idOrHandle: string, name: string) => string, ): string => { if (type === 'html') { return striptags(value, [ @@ -171,18 +172,16 @@ export const stripHtmlValidation = ( } if (replaceBold) { - const processedHtml = convertLinkedinMention( + const processedHtml = convertMention( convertToAscii( html - .replace(/
    /, "\n
      ") - .replace(/<\/ul>\n/, "
    ") - .replace( - /([.\s\S]*?)<\/li.*?>/gm, - (match, p1) => { + .replace(/
      /, '\n
        ') + .replace(/<\/ul>\n/, '
      ') + .replace(/([.\s\S]*?)<\/li.*?>/gm, (match, p1) => { return `
    • - ${p1.replace(/\n/gm, '')}\n

    • `; - } - ) - ) + }) + ), + convertMentionFunction ); return striptags(processedHtml, ['h1', 'h2', 'h3']); @@ -192,11 +191,18 @@ export const stripHtmlValidation = ( return striptags(html, ['ul', 'li', 'h1', 'h2', 'h3']); }; -export const convertLinkedinMention = (value: string) => { +export const convertMention = ( + value: string, + process?: (idOrHandle: string, name: string) => string +) => { + if (!process) { + return value; + } + return value.replace( /(.*?)<\/span>/gi, (match, id, name) => { - return `@[${name.replace('@', '')}](${id})`; + return `` + process(id, name) + ``; } ); }; diff --git a/libraries/nestjs-libraries/src/database/prisma/integrations/integration.repository.ts b/libraries/nestjs-libraries/src/database/prisma/integrations/integration.repository.ts index c6541cdc..b432f246 100644 --- a/libraries/nestjs-libraries/src/database/prisma/integrations/integration.repository.ts +++ b/libraries/nestjs-libraries/src/database/prisma/integrations/integration.repository.ts @@ -15,9 +15,56 @@ export class IntegrationRepository { private _posts: PrismaRepository<'post'>, private _plugs: PrismaRepository<'plugs'>, private _exisingPlugData: PrismaRepository<'exisingPlugData'>, - private _customers: PrismaRepository<'customer'> + private _customers: PrismaRepository<'customer'>, + private _mentions: PrismaRepository<'mentions'> ) {} + getMentions(platform: string, q: string) { + return this._mentions.model.mentions.findMany({ + where: { + platform, + OR: [ + { + name: { + contains: q, + mode: 'insensitive', + }, + }, + { + username: { + contains: q, + mode: 'insensitive', + }, + }, + ], + }, + orderBy: { + name: 'asc', + }, + take: 100, + select: { + name: true, + username: true, + image: true, + }, + }); + } + + insertMentions( + platform: string, + mentions: { name: string; username: string; image: string }[] + ) { + return this._mentions.model.mentions.createMany({ + data: mentions.map((mention) => ({ + platform, + name: mention.name, + username: mention.username, + image: mention.image, + })), + skipDuplicates: true, + }); + } + updateProviderSettings(org: string, id: string, settings: string) { return this._integration.model.integration.update({ where: { diff --git a/libraries/nestjs-libraries/src/database/prisma/integrations/integration.service.ts b/libraries/nestjs-libraries/src/database/prisma/integrations/integration.service.ts index 2d7db625..d7519354 100644 --- a/libraries/nestjs-libraries/src/database/prisma/integrations/integration.service.ts +++ b/libraries/nestjs-libraries/src/database/prisma/integrations/integration.service.ts @@ -46,6 +46,17 @@ export class IntegrationService { return true; } + getMentions(platform: string, q: string) { + return this._integrationRepository.getMentions(platform, q); + } + + insertMentions( + platform: string, + mentions: { name: string; username: string; image: string }[] + ) { + return this._integrationRepository.insertMentions(platform, mentions); + } + async setTimes( orgId: string, integrationId: string, @@ -163,7 +174,11 @@ export class IntegrationService { await this.informAboutRefreshError(orgId, integration); } - async informAboutRefreshError(orgId: string, integration: Integration, err = '') { + async informAboutRefreshError( + orgId: string, + integration: Integration, + err = '' + ) { await this._notificationService.inAppNotification( orgId, `Could not refresh your ${integration.providerIdentifier} channel ${err}`, diff --git a/libraries/nestjs-libraries/src/database/prisma/posts/posts.repository.ts b/libraries/nestjs-libraries/src/database/prisma/posts/posts.repository.ts index 8d4b2287..f6db7693 100644 --- a/libraries/nestjs-libraries/src/database/prisma/posts/posts.repository.ts +++ b/libraries/nestjs-libraries/src/database/prisma/posts/posts.repository.ts @@ -394,7 +394,7 @@ export class PostsRepository { where: { orgId: orgId, name: { - in: tags.map((tag) => tag.label).filter(f => f), + in: tags.map((tag) => tag.label).filter((f) => f), }, }, }); diff --git a/libraries/nestjs-libraries/src/database/prisma/posts/posts.service.ts b/libraries/nestjs-libraries/src/database/prisma/posts/posts.service.ts index 7bb42cb8..620d54c5 100644 --- a/libraries/nestjs-libraries/src/database/prisma/posts/posts.service.ts +++ b/libraries/nestjs-libraries/src/database/prisma/posts/posts.service.ts @@ -378,7 +378,9 @@ export class PostsService { return post; } - const ids = (extract || []).map((e) => e.replace('(post:', '').replace(')', '')); + const ids = (extract || []).map((e) => + e.replace('(post:', '').replace(')', '') + ); const urls = await this._postRepository.getPostUrls(orgId, ids); const newPlainText = ids.reduce((acc, value) => { const findUrl = urls?.find?.((u) => u.id === value)?.releaseURL || ''; @@ -467,7 +469,13 @@ export class PostsService { await Promise.all( (newPosts || []).map(async (p) => ({ id: p.id, - message: stripHtmlValidation(getIntegration.editor, p.content, true), + message: stripHtmlValidation( + getIntegration.editor, + p.content, + true, + false, + getIntegration.mentionFormat + ), settings: JSON.parse(p.settings || '{}'), media: await this.updateMedia( p.id, @@ -535,7 +543,12 @@ export class PostsService { throw err; } - throw new BadBody(integration.providerIdentifier, JSON.stringify(err), {} as any, ''); + throw new BadBody( + integration.providerIdentifier, + JSON.stringify(err), + {} as any, + '' + ); } } diff --git a/libraries/nestjs-libraries/src/database/prisma/schema.prisma b/libraries/nestjs-libraries/src/database/prisma/schema.prisma index b63c40a3..6027caa2 100644 --- a/libraries/nestjs-libraries/src/database/prisma/schema.prisma +++ b/libraries/nestjs-libraries/src/database/prisma/schema.prisma @@ -658,6 +658,18 @@ model Errors { @@index([createdAt]) } +model Mentions { + name String + username String + platform String + image String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@id([name, username, platform, image]) + @@index([createdAt]) +} + enum OrderStatus { PENDING ACCEPTED diff --git a/libraries/nestjs-libraries/src/integrations/social/bluesky.provider.ts b/libraries/nestjs-libraries/src/integrations/social/bluesky.provider.ts index 18d526f0..589103c4 100644 --- a/libraries/nestjs-libraries/src/integrations/social/bluesky.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/bluesky.provider.ts @@ -519,13 +519,17 @@ export class BlueskyProvider extends SocialAbstract implements SocialProvider { }); const list = await agent.searchActors({ - q: d.query + q: d.query, }); - return list.data.actors.map(p => ({ + return list.data.actors.map((p) => ({ label: p.displayName, id: p.handle, - image: p.avatar - })) + image: p.avatar, + })); + } + + mentionFormat(idOrHandle: string, name: string) { + return `@${idOrHandle}`; } } diff --git a/libraries/nestjs-libraries/src/integrations/social/linkedin.provider.ts b/libraries/nestjs-libraries/src/integrations/social/linkedin.provider.ts index cb484979..98c2aba4 100644 --- a/libraries/nestjs-libraries/src/integrations/social/linkedin.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/linkedin.provider.ts @@ -716,7 +716,7 @@ export class LinkedinProvider extends SocialAbstract implements SocialProvider { }); } - async mention(token: string, data: { query: string }) { + override async mention(token: string, data: { query: string }) { const { elements } = await ( await fetch( `https://api.linkedin.com/v2/organizations?q=vanityName&vanityName=${encodeURIComponent( @@ -739,4 +739,8 @@ export class LinkedinProvider extends SocialAbstract implements SocialProvider { image: p.logoV2?.['original~']?.elements?.[0]?.identifiers?.[0]?.identifier || '', })); } + + mentionFormat(idOrHandle: string, name: string) { + return `@[${name.replace('@', '')}](urn:li:organization:${idOrHandle})`; + } } 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 cd14254f..20add654 100644 --- a/libraries/nestjs-libraries/src/integrations/social/social.integrations.interface.ts +++ b/libraries/nestjs-libraries/src/integrations/social/social.integrations.interface.ts @@ -135,4 +135,5 @@ export interface SocialProvider mention?: ( token: string, data: { query: string }, id: string, integration: Integration ) => Promise<{ id: string; label: string; image: string }[] | {none: true}>; + mentionFormat?(idOrHandle: string, name: string): string; } diff --git a/libraries/nestjs-libraries/src/integrations/social/x.provider.ts b/libraries/nestjs-libraries/src/integrations/social/x.provider.ts index 36c18669..87fe9972 100644 --- a/libraries/nestjs-libraries/src/integrations/social/x.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/x.provider.ts @@ -526,4 +526,8 @@ export class XProvider extends SocialAbstract implements SocialProvider { } return []; } + + mentionFormat(idOrHandle: string, name: string) { + return `@${idOrHandle}`; + } } From 58abf6eb00f861314476e654ab1f8bc5d4a3d748 Mon Sep 17 00:00:00 2001 From: Nevo David Date: Fri, 1 Aug 2025 13:45:52 +0700 Subject: [PATCH 4/7] feat: mentions --- .../src/api/routes/integrations.controller.ts | 4 +-- .../integrations/integration.repository.ts | 3 ++ .../integrations/social/threads.provider.ts | 30 +++++++++++++++++-- 3 files changed, 33 insertions(+), 4 deletions(-) diff --git a/apps/backend/src/api/routes/integrations.controller.ts b/apps/backend/src/api/routes/integrations.controller.ts index d658fa6e..5393f385 100644 --- a/apps/backend/src/api/routes/integrations.controller.ts +++ b/apps/backend/src/api/routes/integrations.controller.ts @@ -278,7 +278,7 @@ export class IntegrationsController { name: p.label, username: p.id, image: p.image, - })) + })).filter((f: any) => f.name) ); } @@ -292,7 +292,7 @@ export class IntegrationsController { ...newList, ], (p) => p.id - ); + ).filter(f => f.label && f.image && f.id); } @Post('/function') diff --git a/libraries/nestjs-libraries/src/database/prisma/integrations/integration.repository.ts b/libraries/nestjs-libraries/src/database/prisma/integrations/integration.repository.ts index b432f246..ae576223 100644 --- a/libraries/nestjs-libraries/src/database/prisma/integrations/integration.repository.ts +++ b/libraries/nestjs-libraries/src/database/prisma/integrations/integration.repository.ts @@ -54,6 +54,9 @@ export class IntegrationRepository { platform: string, mentions: { name: string; username: string; image: string }[] ) { + if (mentions.length === 0) { + return []; + } return this._mentions.model.mentions.createMany({ data: mentions.map((mention) => ({ platform, diff --git a/libraries/nestjs-libraries/src/integrations/social/threads.provider.ts b/libraries/nestjs-libraries/src/integrations/social/threads.provider.ts index 37ed96fa..6ae04186 100644 --- a/libraries/nestjs-libraries/src/integrations/social/threads.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/threads.provider.ts @@ -13,6 +13,7 @@ import { capitalize, chunk } from 'lodash'; import { Plug } from '@gitroom/helpers/decorators/plug.decorator'; import { Integration } from '@prisma/client'; import { stripHtmlValidation } from '@gitroom/helpers/utils/strip.html.validation'; +import { TwitterApi } from 'twitter-api-v2'; export class ThreadsProvider extends SocialAbstract implements SocialProvider { identifier = 'threads'; @@ -23,6 +24,7 @@ export class ThreadsProvider extends SocialAbstract implements SocialProvider { 'threads_content_publish', 'threads_manage_replies', 'threads_manage_insights', + // 'threads_profile_discovery', ]; editor = 'normal' as const; @@ -413,8 +415,7 @@ export class ThreadsProvider extends SocialAbstract implements SocialProvider { { id: makeId(10), media: [], - message: - postDetails?.[0]?.settings?.thread_finisher, + message: postDetails?.[0]?.settings?.thread_finisher, settings: {}, }, lastReplyId, @@ -526,4 +527,29 @@ export class ThreadsProvider extends SocialAbstract implements SocialProvider { return false; } + + // override async mention( + // token: string, + // data: { query: string }, + // id: string, + // integration: Integration + // ) { + // const p = await ( + // await fetch( + // `https://graph.threads.net/v1.0/profile_lookup?username=${data.query}&access_token=${integration.token}` + // ) + // ).json(); + // + // return [ + // { + // id: String(p.id), + // label: p.name, + // image: p.profile_picture_url, + // }, + // ]; + // } + // + // mentionFormat(idOrHandle: string, name: string) { + // return `@${idOrHandle}`; + // } } From f2b96c27e01d289953ffd531672217dc0cb55a9d Mon Sep 17 00:00:00 2001 From: Nevo David Date: Fri, 1 Aug 2025 16:32:49 +0700 Subject: [PATCH 5/7] feat: linkedin fix --- .../src/integrations/social/linkedin.provider.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/nestjs-libraries/src/integrations/social/linkedin.provider.ts b/libraries/nestjs-libraries/src/integrations/social/linkedin.provider.ts index 98c2aba4..c3cb915b 100644 --- a/libraries/nestjs-libraries/src/integrations/social/linkedin.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/linkedin.provider.ts @@ -718,7 +718,7 @@ export class LinkedinProvider extends SocialAbstract implements SocialProvider { override async mention(token: string, data: { query: string }) { const { elements } = await ( - await fetch( + await this.fetch( `https://api.linkedin.com/v2/organizations?q=vanityName&vanityName=${encodeURIComponent( data.query )}&projection=(elements*(id,localizedName,logoV2(original~:playableStreams)))`, From 428932b328cadd161856f1319a6cfab95232dd91 Mon Sep 17 00:00:00 2001 From: Nevo David Date: Fri, 1 Aug 2025 16:33:54 +0700 Subject: [PATCH 6/7] feat: dynamic bluesky --- .../src/integrations/social/bluesky.provider.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/libraries/nestjs-libraries/src/integrations/social/bluesky.provider.ts b/libraries/nestjs-libraries/src/integrations/social/bluesky.provider.ts index 589103c4..83f437e1 100644 --- a/libraries/nestjs-libraries/src/integrations/social/bluesky.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/bluesky.provider.ts @@ -505,14 +505,14 @@ export class BlueskyProvider extends SocialAbstract implements SocialProvider { id: string, integration: Integration ) { - const agent = new BskyAgent({ - service: 'https://bsky.social', - }); - const body = JSON.parse( AuthService.fixedDecryption(integration.customInstanceDetails!) ); + const agent = new BskyAgent({ + service: body.service, + }); + await agent.login({ identifier: body.identifier, password: body.password, From f8dd1ae912ddef7d91cd554eee342edf5f124210 Mon Sep 17 00:00:00 2001 From: Nevo David Date: Fri, 1 Aug 2025 16:37:27 +0700 Subject: [PATCH 7/7] Feat: small fixes --- apps/frontend/src/components/new-launch/mention.component.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/apps/frontend/src/components/new-launch/mention.component.tsx b/apps/frontend/src/components/new-launch/mention.component.tsx index 1926d899..8de4fb55 100644 --- a/apps/frontend/src/components/new-launch/mention.component.tsx +++ b/apps/frontend/src/components/new-launch/mention.component.tsx @@ -96,7 +96,7 @@ const MentionList: FC = (props: any) => { className={`flex gap-[10px] w-full p-2 text-left rounded hover:bg-gray-100 ${ index === selectedIndex ? 'bg-blue-100' : '' }`} - key={index} + key={item.id || index} onClick={() => selectItem(index)} >