diff --git a/apps/frontend/src/components/launches/add.edit.model.tsx b/apps/frontend/src/components/launches/add.edit.model.tsx index 58b4c4cf..807d249b 100644 --- a/apps/frontend/src/components/launches/add.edit.model.tsx +++ b/apps/frontend/src/components/launches/add.edit.model.tsx @@ -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'); diff --git a/apps/frontend/src/components/launches/helpers/use.values.ts b/apps/frontend/src/components/launches/helpers/use.values.ts index d156da9d..a863e776 100644 --- a/apps/frontend/src/components/launches/helpers/use.values.ts +++ b/apps/frontend/src/components/launches/helpers/use.values.ts @@ -8,7 +8,10 @@ const finalInformation = {} as { settings: () => object; trigger: () => Promise; isValid: boolean; - checkValidity?: (value: Array>) => Promise; + checkValidity?: ( + value: Array>, + settings: any + ) => Promise; maximumCharacters?: number; }; }; @@ -18,8 +21,11 @@ export const useValues = ( identifier: string, value: Array<{ id?: string; content: string; media?: Array }>, dto: any, - checkValidity?: (value: Array>) => Promise, - maximumCharacters?: number, + checkValidity?: ( + value: Array>, + settings: any + ) => Promise, + 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) { diff --git a/apps/frontend/src/components/launches/providers/high.order.provider.tsx b/apps/frontend/src/components/launches/providers/high.order.provider.tsx index 8249f0c7..a6d20952 100644 --- a/apps/frontend/src/components/launches/providers/high.order.provider.tsx +++ b/apps/frontend/src/components/launches/providers/high.order.provider.tsx @@ -68,15 +68,16 @@ export const EditorWrapper: FC<{ children: ReactNode }> = ({ children }) => { return children; }; -export const withProvider = ( +export const withProvider = function ( SettingsComponent: FC<{values?: any}> | null, CustomPreviewComponent?: FC<{maximumCharacters?: number}>, dto?: any, checkValidity?: ( - value: Array> + value: Array>, + settings: T ) => Promise, maximumCharacters?: number -) => { +) { return (props: { identifier: string; id: string; diff --git a/apps/frontend/src/components/launches/providers/instagram/instagram.collaborators.tsx b/apps/frontend/src/components/launches/providers/instagram/instagram.collaborators.tsx new file mode 100644 index 00000000..6312836c --- /dev/null +++ b/apps/frontend/src/components/launches/providers/instagram/instagram.collaborators.tsx @@ -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 ( + <> + + + {postCurrentType !== 'story' && ( + + )} + + ); +}; + +export default withProvider( + 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((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 +); diff --git a/apps/frontend/src/components/launches/providers/instagram/instagram.provider.tsx b/apps/frontend/src/components/launches/providers/instagram/instagram.provider.tsx deleted file mode 100644 index 8c12e352..00000000 --- a/apps/frontend/src/components/launches/providers/instagram/instagram.provider.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { withProvider } from '@gitroom/frontend/components/launches/providers/high.order.provider'; - -export default withProvider( - null, - undefined, - undefined, - async ([firstPost, ...otherPosts]) => { - if (!firstPost.length) { - return 'Instagram should have at least one media'; - } - - return true; - }, - 2200 -); diff --git a/apps/frontend/src/components/launches/providers/instagram/instagram.tags.tsx b/apps/frontend/src/components/launches/providers/instagram/instagram.tags.tsx new file mode 100644 index 00000000..b233923f --- /dev/null +++ b/apps/frontend/src/components/launches/providers/instagram/instagram.tags.tsx @@ -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([]); + const [suggestions, setSuggestions] = useState(''); + + 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 ( +
+
{label}
+ +
+ ); +}; diff --git a/apps/frontend/src/components/launches/providers/show.all.providers.tsx b/apps/frontend/src/components/launches/providers/show.all.providers.tsx index 0fece0eb..3b26f62b 100644 --- a/apps/frontend/src/components/launches/providers/show.all.providers.tsx +++ b/apps/frontend/src/components/launches/providers/show.all.providers.tsx @@ -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'; diff --git a/libraries/nestjs-libraries/src/dtos/posts/providers-settings/instagram.dto.ts b/libraries/nestjs-libraries/src/dtos/posts/providers-settings/instagram.dto.ts new file mode 100644 index 00000000..49e5cc4f --- /dev/null +++ b/libraries/nestjs-libraries/src/dtos/posts/providers-settings/instagram.dto.ts @@ -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[]; +} diff --git a/libraries/nestjs-libraries/src/integrations/social.abstract.ts b/libraries/nestjs-libraries/src/integrations/social.abstract.ts index e9fe0480..32ee8811 100644 --- a/libraries/nestjs-libraries/src/integrations/social.abstract.ts +++ b/libraries/nestjs-libraries/src/integrations/social.abstract.ts @@ -20,7 +20,11 @@ export class NotEnoughScopes { } export abstract class SocialAbstract { - async fetch(url: string, options: RequestInit = {}, identifier = ''): Promise { + async fetch( + url: string, + options: RequestInit = {}, + identifier = '' + ): Promise { 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!); } diff --git a/libraries/nestjs-libraries/src/integrations/social/instagram.provider.ts b/libraries/nestjs-libraries/src/integrations/social/instagram.provider.ts index c94f44f4..69d9ab23 100644 --- a/libraries/nestjs-libraries/src/integrations/social/instagram.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/instagram.provider.ts @@ -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[] ): Promise { 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; }) || [] @@ -376,4 +395,12 @@ 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}` + ); + } }