feat: instagram stories, add collaborators, infrastructure for settings in validation

This commit is contained in:
Nevo David 2024-12-04 19:27:43 +07:00
parent 69541c9eec
commit f9f5e6d486
10 changed files with 223 additions and 68 deletions

View File

@ -276,7 +276,8 @@ export const AddEditModal: FC<{
for (const key of allKeys) {
if (key.checkValidity) {
const check = await key.checkValidity(
key?.value.map((p: any) => p.image || [])
key?.value.map((p: any) => p.image || []),
key.settings
);
if (typeof check === 'string') {
toaster.show(check, 'warning');

View File

@ -8,7 +8,10 @@ const finalInformation = {} as {
settings: () => object;
trigger: () => Promise<boolean>;
isValid: boolean;
checkValidity?: (value: Array<Array<{path: string}>>) => Promise<string|true>;
checkValidity?: (
value: Array<Array<{ path: string }>>,
settings: any
) => Promise<string | true>;
maximumCharacters?: number;
};
};
@ -18,8 +21,11 @@ export const useValues = (
identifier: string,
value: Array<{ id?: string; content: string; media?: Array<string> }>,
dto: any,
checkValidity?: (value: Array<Array<{path: string}>>) => Promise<string|true>,
maximumCharacters?: number,
checkValidity?: (
value: Array<Array<{ path: string }>>,
settings: any
) => Promise<string | true>,
maximumCharacters?: number
) => {
const resolver = useMemo(() => {
return classValidatorResolver(dto);
@ -43,8 +49,7 @@ export const useValues = (
finalInformation[integration].trigger = form.trigger;
if (checkValidity) {
finalInformation[integration].checkValidity =
checkValidity;
finalInformation[integration].checkValidity = checkValidity;
}
if (maximumCharacters) {

View File

@ -68,15 +68,16 @@ export const EditorWrapper: FC<{ children: ReactNode }> = ({ children }) => {
return children;
};
export const withProvider = (
export const withProvider = function <T extends object>(
SettingsComponent: FC<{values?: any}> | null,
CustomPreviewComponent?: FC<{maximumCharacters?: number}>,
dto?: any,
checkValidity?: (
value: Array<Array<{ path: string }>>
value: Array<Array<{ path: string }>>,
settings: T
) => Promise<string | true>,
maximumCharacters?: number
) => {
) {
return (props: {
identifier: string;
id: string;

View File

@ -0,0 +1,89 @@
import { withProvider } from '@gitroom/frontend/components/launches/providers/high.order.provider';
import { FC } from 'react';
import { Select } from '@gitroom/react/form/select';
import { useSettings } from '@gitroom/frontend/components/launches/helpers/use.values';
import { InstagramDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/instagram.dto';
import { InstagramCollaboratorsTags } from '@gitroom/frontend/components/launches/providers/instagram/instagram.tags';
const postType = [
{
value: 'post',
label: 'Post / Reel',
},
{
value: 'story',
label: 'Story',
},
];
const InstagramCollaborators: FC<{ values?: any }> = (props) => {
const { watch, register, formState, control } = useSettings();
const postCurrentType = watch('post_type');
return (
<>
<Select
label="Post Type"
{...register('post_type', {
value: 'post',
})}
>
<option value="">Select Post Type...</option>
{postType.map((item) => (
<option key={item.value} value={item.value}>
{item.label}
</option>
))}
</Select>
{postCurrentType !== 'story' && (
<InstagramCollaboratorsTags
label="Collaborators (max 3) - accounts can't be private"
{...register('collaborators')}
/>
)}
</>
);
};
export default withProvider<InstagramDto>(
InstagramCollaborators,
undefined,
InstagramDto,
async ([firstPost, ...otherPosts], settings) => {
if (!firstPost.length) {
return 'Instagram should have at least one media';
}
if (firstPost.length > 1 && settings.post_type === 'story') {
return 'Instagram stories can only have one media';
}
const checkVideosLength = await Promise.all(
firstPost
.filter((f) => f.path.indexOf('mp4') > -1)
.flatMap((p) => p.path)
.map((p) => {
return new Promise<number>((res) => {
const video = document.createElement('video');
video.preload = 'metadata';
video.src = p;
video.addEventListener('loadedmetadata', () => {
res(video.duration);
});
});
})
);
for (const video of checkVideosLength) {
if (video > 60 && settings.post_type === 'story') {
return 'Instagram stories should be maximum 60 seconds';
}
if (video > 90 && settings.post_type === 'post') {
return 'Instagram reel should be maximum 90 seconds';
}
}
return true;
},
2200
);

View File

@ -1,50 +0,0 @@
import { withProvider } from '@gitroom/frontend/components/launches/providers/high.order.provider';
import { FC } from 'react';
import { Select } from '@gitroom/react/form/select';
import { useSettings } from '@gitroom/frontend/components/launches/helpers/use.values';
const postType = [
{
value: 'post',
label: 'Post',
},
{
value: 'story',
label: 'Story',
},
];
const InstagramProvider: FC<{ values?: any }> = (props) => {
const { watch, register, formState, control } = useSettings();
return (
<>
<Select
label="Post Type"
{...register('post_type', {
value: '',
})}
>
<option value="">Select Post Type...</option>
{postType.map((item) => (
<option key={item.value} value={item.value}>
{item.label}
</option>
))}
</Select>
</>
);
};
export default withProvider(
InstagramProvider,
undefined,
undefined,
async ([firstPost, ...otherPosts]) => {
if (!firstPost.length) {
return 'Instagram should have at least one media';
}
return true;
},
2200
);

View File

@ -0,0 +1,61 @@
import { FC, useCallback, useEffect, useMemo, useState } from 'react';
import { useSettings } from '@gitroom/frontend/components/launches/helpers/use.values';
import { ReactTags } from 'react-tag-autocomplete';
import interClass from '@gitroom/react/helpers/inter.font';
export const InstagramCollaboratorsTags: FC<{
name: string;
label: string;
onChange: (event: { target: { value: any[]; name: string } }) => void;
}> = (props) => {
const { onChange, name, label } = props;
const { getValues } = useSettings();
const [tagValue, setTagValue] = useState<any[]>([]);
const [suggestions, setSuggestions] = useState<string>('');
const onDelete = useCallback(
(tagIndex: number) => {
const modify = tagValue.filter((_, i) => i !== tagIndex);
setTagValue(modify);
onChange({ target: { value: modify, name } });
},
[tagValue]
);
const onAddition = useCallback(
(newTag: any) => {
if (tagValue.length >= 3) {
return;
}
const modify = [...tagValue, newTag];
setTagValue(modify);
onChange({ target: { value: modify, name } });
},
[tagValue]
);
useEffect(() => {
const settings = getValues()[props.name];
if (settings) {
setTagValue(settings);
}
}, []);
const suggestionsArray = useMemo(() => {
return [...tagValue, { label: suggestions, value: suggestions }].filter(f => f.label);
}, [suggestions, tagValue]);
return (
<div>
<div className={`${interClass} text-[14px] mb-[6px]`}>{label}</div>
<ReactTags
placeholderText="Add a tag"
suggestions={suggestionsArray}
selected={tagValue}
onAdd={onAddition}
onInput={setSuggestions}
onDelete={onDelete}
/>
</div>
);
};

View File

@ -7,7 +7,7 @@ import RedditProvider from "@gitroom/frontend/components/launches/providers/redd
import MediumProvider from "@gitroom/frontend/components/launches/providers/medium/medium.provider";
import HashnodeProvider from "@gitroom/frontend/components/launches/providers/hashnode/hashnode.provider";
import FacebookProvider from '@gitroom/frontend/components/launches/providers/facebook/facebook.provider';
import InstagramProvider from '@gitroom/frontend/components/launches/providers/instagram/instagram.provider';
import InstagramProvider from '@gitroom/frontend/components/launches/providers/instagram/instagram.collaborators';
import YoutubeProvider from '@gitroom/frontend/components/launches/providers/youtube/youtube.provider';
import TiktokProvider from '@gitroom/frontend/components/launches/providers/tiktok/tiktok.provider';
import PinterestProvider from '@gitroom/frontend/components/launches/providers/pinterest/pinterest.provider';

View File

@ -0,0 +1,18 @@
import { Type } from 'class-transformer';
import { IsArray, IsDefined, IsIn, IsString, ValidateNested } from 'class-validator';
export class Collaborators {
@IsDefined()
@IsString()
label: string;
}
export class InstagramDto {
@IsIn(['post', 'story'])
@IsDefined()
post_type: 'post' | 'story';
@Type(() => Collaborators)
@ValidateNested({ each: true })
@IsArray()
collaborators: Collaborators[];
}

View File

@ -20,7 +20,11 @@ export class NotEnoughScopes {
}
export abstract class SocialAbstract {
async fetch(url: string, options: RequestInit = {}, identifier = ''): Promise<Response> {
async fetch(
url: string,
options: RequestInit = {},
identifier = ''
): Promise<Response> {
const request = await fetch(url, options);
if (request.status === 200 || request.status === 201) {
@ -40,7 +44,10 @@ export abstract class SocialAbstract {
return this.fetch(url, options, identifier);
}
if (request.status === 401 || json.includes('OAuthException')) {
if (
request.status === 401 ||
(json.includes('OAuthException') && !json.includes("Unsupported format") && !json.includes('2207018'))
) {
throw new RefreshToken(identifier, json, options.body!);
}

View File

@ -9,6 +9,7 @@ import { makeId } from '@gitroom/nestjs-libraries/services/make.is';
import { timer } from '@gitroom/helpers/utils/timer';
import dayjs from 'dayjs';
import { SocialAbstract } from '@gitroom/nestjs-libraries/integrations/social.abstract';
import { InstagramDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/instagram.dto';
export class InstagramProvider
extends SocialAbstract
@ -203,10 +204,11 @@ export class InstagramProvider
async post(
id: string,
accessToken: string,
postDetails: PostDetails[]
postDetails: PostDetails<InstagramDto>[]
): Promise<PostResponse[]> {
const [firstPost, ...theRest] = postDetails;
console.log('in progress');
const isStory = firstPost.settings.post_type === 'story';
const medias = await Promise.all(
firstPost?.media?.map(async (m) => {
const caption =
@ -218,18 +220,34 @@ export class InstagramProvider
const mediaType =
m.url.indexOf('.mp4') > -1
? firstPost?.media?.length === 1
? `video_url=${m.url}&media_type=REELS`
? isStory
? `video_url=${m.url}&media_type=STORIES`
: `video_url=${m.url}&media_type=REELS`
: isStory
? `video_url=${m.url}&media_type=STORIES`
: `video_url=${m.url}&media_type=VIDEO`
: isStory
? `image_url=${m.url}&media_type=STORIES`
: `image_url=${m.url}`;
console.log('in progress1');
const collaborators =
firstPost?.settings?.collaborators?.length && !isStory
? `&collaborators=${JSON.stringify(
firstPost?.settings?.collaborators.map((p) => p.label)
)}`
: ``;
console.log(collaborators);
const { id: photoId } = await (
await this.fetch(
`https://graph.facebook.com/v20.0/${id}/media?${mediaType}${isCarousel}&access_token=${accessToken}${caption}`,
`https://graph.facebook.com/v20.0/${id}/media?${mediaType}${isCarousel}${collaborators}&access_token=${accessToken}${caption}`,
{
method: 'POST',
}
)
).json();
console.log('in progress2');
let status = 'IN_PROGRESS';
while (status === 'IN_PROGRESS') {
@ -241,6 +259,7 @@ export class InstagramProvider
await timer(3000);
status = status_code;
}
console.log('in progress3');
return photoId;
}) || []
@ -377,7 +396,11 @@ export class InstagramProvider
);
}
music(accessToken: string, data: {q: string}) {
return this.fetch(`https://graph.facebook.com/v20.0/music/search?q=${encodeURIComponent(data.q)}&access_token=${accessToken}`);
music(accessToken: string, data: { q: string }) {
return this.fetch(
`https://graph.facebook.com/v20.0/music/search?q=${encodeURIComponent(
data.q
)}&access_token=${accessToken}`
);
}
}