diff --git a/apps/frontend/src/components/launches/editor.tsx b/apps/frontend/src/components/launches/editor.tsx index 43fa32aa..1e470841 100644 --- a/apps/frontend/src/components/launches/editor.tsx +++ b/apps/frontend/src/components/launches/editor.tsx @@ -18,12 +18,14 @@ export const Editor = forwardRef< MDEditorProps & { order: number; totalPosts: number; + disabledCopilot?: boolean; } >( ( props: MDEditorProps & { order: number; totalPosts: number; + disabledCopilot?: boolean; }, ref: React.ForwardedRef ) => { @@ -34,6 +36,7 @@ export const Editor = forwardRef< const t = useT(); useCopilotReadable({ + ...(props.disabledCopilot ? { available: 'disabled' } : {}), description: 'Content of the post number ' + (props.order + 1), value: JSON.stringify({ content: props.value, @@ -42,6 +45,7 @@ export const Editor = forwardRef< }), }); useCopilotAction({ + ...(props.disabledCopilot ? { available: 'disabled' } : {}), name: 'editPost_' + props.order, description: `Edit the content of post number ${props.order}`, parameters: [ diff --git a/apps/frontend/src/components/launches/finisher/thread.finisher.tsx b/apps/frontend/src/components/launches/finisher/thread.finisher.tsx new file mode 100644 index 00000000..0de9b671 --- /dev/null +++ b/apps/frontend/src/components/launches/finisher/thread.finisher.tsx @@ -0,0 +1,63 @@ +import { Slider } from '@gitroom/react/form/slider'; +import clsx from 'clsx'; +import { useState } from 'react'; +import { Editor } from '@gitroom/frontend/components/launches/editor'; +import { useIntegration } from '@gitroom/frontend/components/launches/helpers/use.integration'; +import { useSettings } from '@gitroom/frontend/components/launches/helpers/use.values'; + +export const ThreadFinisher = () => { + const integration = useIntegration(); + const { register, watch, setValue } = useSettings(); + register('active_thread_finisher', { + value: false, + }); + + register('thread_finisher', { + value: `That's a wrap! + +If you enjoyed this thread: + +1. Follow me @${integration.integration?.display || integration.integration?.name} for more of these +2. RT the tweet below to share this thread with your audience`, + }); + + const slider = watch('active_thread_finisher'); + const value = watch('thread_finisher'); + + return ( +
+
+
Add a thread finisher
+
+ setValue('active_thread_finisher', p === 'on')} + fill={true} + /> +
+
+
+
+
+
+
+ setValue('thread_finisher', val)} + value={value} + height={150} + totalPosts={1} + order={1} + preview="edit" + /> +
+
+
+
+
+
+ ); +}; diff --git a/apps/frontend/src/components/launches/providers/threads/threads.provider.tsx b/apps/frontend/src/components/launches/providers/threads/threads.provider.tsx index 959f4f91..cde5fbd6 100644 --- a/apps/frontend/src/components/launches/providers/threads/threads.provider.tsx +++ b/apps/frontend/src/components/launches/providers/threads/threads.provider.tsx @@ -1,6 +1,11 @@ import { withProvider } from '@gitroom/frontend/components/launches/providers/high.order.provider'; +import { ThreadFinisher } from '@gitroom/frontend/components/launches/finisher/thread.finisher'; +const SettingsComponent = () => { + return ; +}; + export default withProvider( - null, + SettingsComponent, undefined, undefined, async () => { diff --git a/apps/frontend/src/components/launches/providers/x/x.provider.tsx b/apps/frontend/src/components/launches/providers/x/x.provider.tsx index 73a47ecd..d7f0d641 100644 --- a/apps/frontend/src/components/launches/providers/x/x.provider.tsx +++ b/apps/frontend/src/components/launches/providers/x/x.provider.tsx @@ -1,6 +1,12 @@ import { withProvider } from '@gitroom/frontend/components/launches/providers/high.order.provider'; +import { ThreadFinisher } from '@gitroom/frontend/components/launches/finisher/thread.finisher'; + +const SettingsComponent = () => { + return ; +}; + export default withProvider( - null, + SettingsComponent, undefined, undefined, async (posts) => { diff --git a/libraries/nestjs-libraries/src/integrations/social/threads.provider.ts b/libraries/nestjs-libraries/src/integrations/social/threads.provider.ts index 6bdc260a..b62f9c81 100644 --- a/libraries/nestjs-libraries/src/integrations/social/threads.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/threads.provider.ts @@ -34,6 +34,7 @@ export class ThreadsProvider extends SocialAbstract implements SocialProvider { const { id, name, + username, picture: { data: { url }, }, @@ -104,6 +105,7 @@ export class ThreadsProvider extends SocialAbstract implements SocialProvider { const { id, name, + username, picture: { data: { url }, }, @@ -116,7 +118,7 @@ export class ThreadsProvider extends SocialAbstract implements SocialProvider { refreshToken: access_token, expiresIn: dayjs().add(59, 'days').unix() - dayjs().unix(), picture: url, - username: '', + username: username, }; } @@ -179,8 +181,6 @@ export class ThreadsProvider extends SocialAbstract implements SocialProvider { access_token: accessToken, }); - console.log(mediaParams); - const { id: mediaId } = await ( await this.fetch( `https://graph.threads.net/v1.0/${userId}/threads?${mediaParams.toString()}`, @@ -243,7 +243,8 @@ export class ThreadsProvider extends SocialAbstract implements SocialProvider { userId: string, accessToken: string, message: string, - replyToId?: string + replyToId?: string, + quoteId?: string ): Promise { const form = new FormData(); form.append('media_type', 'TEXT'); @@ -254,6 +255,10 @@ export class ThreadsProvider extends SocialAbstract implements SocialProvider { form.append('reply_to_id', replyToId); } + if (quoteId) { + form.append('quote_post_id', quoteId); + } + const { id: contentId } = await ( await this.fetch(`https://graph.threads.net/v1.0/${userId}/threads`, { method: 'POST', @@ -293,7 +298,8 @@ export class ThreadsProvider extends SocialAbstract implements SocialProvider { userId: string, accessToken: string, postDetails: PostDetails, - replyToId?: string + replyToId?: string, + quoteId?: string ): Promise { // Handle content creation based on media type if (!postDetails.media || postDetails.media.length === 0) { @@ -302,7 +308,8 @@ export class ThreadsProvider extends SocialAbstract implements SocialProvider { userId, accessToken, postDetails.message, - replyToId + replyToId, + quoteId ); } else if (postDetails.media.length === 1) { // Single media content @@ -329,7 +336,10 @@ export class ThreadsProvider extends SocialAbstract implements SocialProvider { async post( userId: string, accessToken: string, - postDetails: PostDetails[] + postDetails: PostDetails<{ + active_thread_finisher: boolean; + thread_finisher: string; + }>[] ): Promise { if (!postDetails.length) { return []; @@ -392,6 +402,30 @@ export class ThreadsProvider extends SocialAbstract implements SocialProvider { }); } + if (postDetails?.[0]?.settings?.active_thread_finisher) { + try { + const replyContentId = await this.createThreadContent( + userId, + accessToken, + { + id: makeId(10), + media: [], + message: + postDetails?.[0]?.settings?.thread_finisher! + + '\n' + + responses[0].releaseURL, + settings: {}, + }, + lastReplyId, + threadId + ); + + await this.publishThread(userId, accessToken, replyContentId); + } catch (err) { + console.log(err); + } + } + return responses; } diff --git a/libraries/nestjs-libraries/src/integrations/social/x.provider.ts b/libraries/nestjs-libraries/src/integrations/social/x.provider.ts index 010fe79c..de4192b3 100644 --- a/libraries/nestjs-libraries/src/integrations/social/x.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/x.provider.ts @@ -9,13 +9,11 @@ import { import { lookup } from 'mime-types'; import sharp from 'sharp'; import { readOrFetch } from '@gitroom/helpers/utils/read.or.fetch'; -import removeMd from 'remove-markdown'; import { SocialAbstract } from '@gitroom/nestjs-libraries/integrations/social.abstract'; import { Plug } from '@gitroom/helpers/decorators/plug.decorator'; import { Integration } from '@prisma/client'; import { timer } from '@gitroom/helpers/utils/timer'; import { PostPlug } from '@gitroom/helpers/decorators/post.plug'; -import { number, string } from 'yup'; import dayjs from 'dayjs'; import { uniqBy } from 'lodash'; @@ -240,7 +238,10 @@ export class XProvider extends SocialAbstract implements SocialProvider { async post( id: string, accessToken: string, - postDetails: PostDetails[] + postDetails: PostDetails<{ + active_thread_finisher: boolean; + thread_finisher: string; + }>[] ): Promise { const [accessTokenSplit, accessSecretSplit] = accessToken.split(':'); const client = new TwitterApi({ @@ -312,6 +313,18 @@ export class XProvider extends SocialAbstract implements SocialProvider { }); } + if (postDetails?.[0]?.settings?.active_thread_finisher) { + try { + await client.v2.tweet({ + text: + postDetails?.[0]?.settings?.thread_finisher! + + '\n' + + ids[0].releaseURL, + reply: { in_reply_to_tweet_id: ids[ids.length - 1].postId }, + }); + } catch (err) {} + } + return ids.map((p) => ({ ...p, status: 'posted',