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) {
|
for (const key of allKeys) {
|
||||||
if (key.checkValidity) {
|
if (key.checkValidity) {
|
||||||
const check = await 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') {
|
if (typeof check === 'string') {
|
||||||
toaster.show(check, 'warning');
|
toaster.show(check, 'warning');
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,10 @@ const finalInformation = {} as {
|
||||||
settings: () => object;
|
settings: () => object;
|
||||||
trigger: () => Promise<boolean>;
|
trigger: () => Promise<boolean>;
|
||||||
isValid: 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;
|
maximumCharacters?: number;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
@ -18,8 +21,11 @@ export const useValues = (
|
||||||
identifier: string,
|
identifier: string,
|
||||||
value: Array<{ id?: string; content: string; media?: Array<string> }>,
|
value: Array<{ id?: string; content: string; media?: Array<string> }>,
|
||||||
dto: any,
|
dto: any,
|
||||||
checkValidity?: (value: Array<Array<{path: string}>>) => Promise<string|true>,
|
checkValidity?: (
|
||||||
maximumCharacters?: number,
|
value: Array<Array<{ path: string }>>,
|
||||||
|
settings: any
|
||||||
|
) => Promise<string | true>,
|
||||||
|
maximumCharacters?: number
|
||||||
) => {
|
) => {
|
||||||
const resolver = useMemo(() => {
|
const resolver = useMemo(() => {
|
||||||
return classValidatorResolver(dto);
|
return classValidatorResolver(dto);
|
||||||
|
|
@ -43,8 +49,7 @@ export const useValues = (
|
||||||
finalInformation[integration].trigger = form.trigger;
|
finalInformation[integration].trigger = form.trigger;
|
||||||
|
|
||||||
if (checkValidity) {
|
if (checkValidity) {
|
||||||
finalInformation[integration].checkValidity =
|
finalInformation[integration].checkValidity = checkValidity;
|
||||||
checkValidity;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (maximumCharacters) {
|
if (maximumCharacters) {
|
||||||
|
|
|
||||||
|
|
@ -68,15 +68,16 @@ export const EditorWrapper: FC<{ children: ReactNode }> = ({ children }) => {
|
||||||
return children;
|
return children;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const withProvider = (
|
export const withProvider = function <T extends object>(
|
||||||
SettingsComponent: FC<{values?: any}> | null,
|
SettingsComponent: FC<{values?: any}> | null,
|
||||||
CustomPreviewComponent?: FC<{maximumCharacters?: number}>,
|
CustomPreviewComponent?: FC<{maximumCharacters?: number}>,
|
||||||
dto?: any,
|
dto?: any,
|
||||||
checkValidity?: (
|
checkValidity?: (
|
||||||
value: Array<Array<{ path: string }>>
|
value: Array<Array<{ path: string }>>,
|
||||||
|
settings: T
|
||||||
) => Promise<string | true>,
|
) => Promise<string | true>,
|
||||||
maximumCharacters?: number
|
maximumCharacters?: number
|
||||||
) => {
|
) {
|
||||||
return (props: {
|
return (props: {
|
||||||
identifier: string;
|
identifier: string;
|
||||||
id: 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 MediumProvider from "@gitroom/frontend/components/launches/providers/medium/medium.provider";
|
||||||
import HashnodeProvider from "@gitroom/frontend/components/launches/providers/hashnode/hashnode.provider";
|
import HashnodeProvider from "@gitroom/frontend/components/launches/providers/hashnode/hashnode.provider";
|
||||||
import FacebookProvider from '@gitroom/frontend/components/launches/providers/facebook/facebook.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 YoutubeProvider from '@gitroom/frontend/components/launches/providers/youtube/youtube.provider';
|
||||||
import TiktokProvider from '@gitroom/frontend/components/launches/providers/tiktok/tiktok.provider';
|
import TiktokProvider from '@gitroom/frontend/components/launches/providers/tiktok/tiktok.provider';
|
||||||
import PinterestProvider from '@gitroom/frontend/components/launches/providers/pinterest/pinterest.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 {
|
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);
|
const request = await fetch(url, options);
|
||||||
|
|
||||||
if (request.status === 200 || request.status === 201) {
|
if (request.status === 200 || request.status === 201) {
|
||||||
|
|
@ -40,7 +44,10 @@ export abstract class SocialAbstract {
|
||||||
return this.fetch(url, options, identifier);
|
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!);
|
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 { timer } from '@gitroom/helpers/utils/timer';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { SocialAbstract } from '@gitroom/nestjs-libraries/integrations/social.abstract';
|
import { SocialAbstract } from '@gitroom/nestjs-libraries/integrations/social.abstract';
|
||||||
|
import { InstagramDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/instagram.dto';
|
||||||
|
|
||||||
export class InstagramProvider
|
export class InstagramProvider
|
||||||
extends SocialAbstract
|
extends SocialAbstract
|
||||||
|
|
@ -203,10 +204,11 @@ export class InstagramProvider
|
||||||
async post(
|
async post(
|
||||||
id: string,
|
id: string,
|
||||||
accessToken: string,
|
accessToken: string,
|
||||||
postDetails: PostDetails[]
|
postDetails: PostDetails<InstagramDto>[]
|
||||||
): Promise<PostResponse[]> {
|
): Promise<PostResponse[]> {
|
||||||
const [firstPost, ...theRest] = postDetails;
|
const [firstPost, ...theRest] = postDetails;
|
||||||
|
console.log('in progress');
|
||||||
|
const isStory = firstPost.settings.post_type === 'story';
|
||||||
const medias = await Promise.all(
|
const medias = await Promise.all(
|
||||||
firstPost?.media?.map(async (m) => {
|
firstPost?.media?.map(async (m) => {
|
||||||
const caption =
|
const caption =
|
||||||
|
|
@ -218,18 +220,34 @@ export class InstagramProvider
|
||||||
const mediaType =
|
const mediaType =
|
||||||
m.url.indexOf('.mp4') > -1
|
m.url.indexOf('.mp4') > -1
|
||||||
? firstPost?.media?.length === 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`
|
: `video_url=${m.url}&media_type=VIDEO`
|
||||||
|
: isStory
|
||||||
|
? `image_url=${m.url}&media_type=STORIES`
|
||||||
: `image_url=${m.url}`;
|
: `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 (
|
const { id: photoId } = await (
|
||||||
await this.fetch(
|
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',
|
method: 'POST',
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
).json();
|
).json();
|
||||||
|
console.log('in progress2');
|
||||||
|
|
||||||
let status = 'IN_PROGRESS';
|
let status = 'IN_PROGRESS';
|
||||||
while (status === 'IN_PROGRESS') {
|
while (status === 'IN_PROGRESS') {
|
||||||
|
|
@ -241,6 +259,7 @@ export class InstagramProvider
|
||||||
await timer(3000);
|
await timer(3000);
|
||||||
status = status_code;
|
status = status_code;
|
||||||
}
|
}
|
||||||
|
console.log('in progress3');
|
||||||
|
|
||||||
return photoId;
|
return photoId;
|
||||||
}) || []
|
}) || []
|
||||||
|
|
@ -377,7 +396,11 @@ export class InstagramProvider
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
music(accessToken: string, data: {q: string}) {
|
music(accessToken: string, data: { q: string }) {
|
||||||
return this.fetch(`https://graph.facebook.com/v20.0/music/search?q=${encodeURIComponent(data.q)}&access_token=${accessToken}`);
|
return this.fetch(
|
||||||
|
`https://graph.facebook.com/v20.0/music/search?q=${encodeURIComponent(
|
||||||
|
data.q
|
||||||
|
)}&access_token=${accessToken}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue