feat: instagram stories, add collaborators, infrastructure for settings in validation
This commit is contained in:
parent
69541c9eec
commit
f9f5e6d486
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
);
|
||||
|
|
@ -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
|
||||
);
|
||||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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[];
|
||||
}
|
||||
|
|
@ -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!);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue