Merge pull request #775 from gitroomhq/feat/thread-finisher

Thread finisher
This commit is contained in:
Nevo David 2025-06-04 00:57:19 +07:00 committed by GitHub
commit 3dc5ea8476
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 137 additions and 12 deletions

View File

@ -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<RefMDEditor>
) => {
@ -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: [

View File

@ -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 (
<div className="flex flex-col gap-[10px] border-tableBorder border p-[15px] rounded-lg mb-5">
<div className="flex items-center">
<div className="flex-1">Add a thread finisher</div>
<div>
<Slider
value={slider ? 'on' : 'off'}
onChange={(p) => setValue('active_thread_finisher', p === 'on')}
fill={true}
/>
</div>
</div>
<div className="w-full mt-[40px]">
<div
className={clsx(
!slider && 'relative opacity-25 pointer-events-none editor'
)}
>
<div>
<div className="flex gap-[4px]">
<div className="flex-1 editor text-textColor">
<Editor
onChange={(val) => setValue('thread_finisher', val)}
value={value}
height={150}
totalPosts={1}
order={1}
preview="edit"
/>
</div>
</div>
</div>
</div>
</div>
</div>
);
};

View File

@ -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 <ThreadFinisher />;
};
export default withProvider(
null,
SettingsComponent,
undefined,
undefined,
async () => {

View File

@ -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 <ThreadFinisher />;
};
export default withProvider(
null,
SettingsComponent,
undefined,
undefined,
async (posts) => {

View File

@ -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<string> {
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<string> {
// 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<PostResponse[]> {
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;
}

View File

@ -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<PostResponse[]> {
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',