Skip to content

Commit 70eebd5

Browse files
authored
Merge pull request #468 from gitroomhq/feat/instagram
instagram stories, add collaborators, infrastructure for settings in validation
2 parents 31ba8a7 + f9f5e6d commit 70eebd5

File tree

10 files changed

+225
-31
lines changed

10 files changed

+225
-31
lines changed

apps/frontend/src/components/launches/add.edit.model.tsx

+2-1
Original file line numberDiff line numberDiff line change
@@ -276,7 +276,8 @@ export const AddEditModal: FC<{
276276
for (const key of allKeys) {
277277
if (key.checkValidity) {
278278
const check = await key.checkValidity(
279-
key?.value.map((p: any) => p.image || [])
279+
key?.value.map((p: any) => p.image || []),
280+
key.settings
280281
);
281282
if (typeof check === 'string') {
282283
toaster.show(check, 'warning');

apps/frontend/src/components/launches/helpers/use.values.ts

+10-5
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@ const finalInformation = {} as {
88
settings: () => object;
99
trigger: () => Promise<boolean>;
1010
isValid: boolean;
11-
checkValidity?: (value: Array<Array<{path: string}>>) => Promise<string|true>;
11+
checkValidity?: (
12+
value: Array<Array<{ path: string }>>,
13+
settings: any
14+
) => Promise<string | true>;
1215
maximumCharacters?: number;
1316
};
1417
};
@@ -18,8 +21,11 @@ export const useValues = (
1821
identifier: string,
1922
value: Array<{ id?: string; content: string; media?: Array<string> }>,
2023
dto: any,
21-
checkValidity?: (value: Array<Array<{path: string}>>) => Promise<string|true>,
22-
maximumCharacters?: number,
24+
checkValidity?: (
25+
value: Array<Array<{ path: string }>>,
26+
settings: any
27+
) => Promise<string | true>,
28+
maximumCharacters?: number
2329
) => {
2430
const resolver = useMemo(() => {
2531
return classValidatorResolver(dto);
@@ -43,8 +49,7 @@ export const useValues = (
4349
finalInformation[integration].trigger = form.trigger;
4450

4551
if (checkValidity) {
46-
finalInformation[integration].checkValidity =
47-
checkValidity;
52+
finalInformation[integration].checkValidity = checkValidity;
4853
}
4954

5055
if (maximumCharacters) {

apps/frontend/src/components/launches/providers/high.order.provider.tsx

+4-3
Original file line numberDiff line numberDiff line change
@@ -68,15 +68,16 @@ export const EditorWrapper: FC<{ children: ReactNode }> = ({ children }) => {
6868
return children;
6969
};
7070

71-
export const withProvider = (
71+
export const withProvider = function <T extends object>(
7272
SettingsComponent: FC<{values?: any}> | null,
7373
CustomPreviewComponent?: FC<{maximumCharacters?: number}>,
7474
dto?: any,
7575
checkValidity?: (
76-
value: Array<Array<{ path: string }>>
76+
value: Array<Array<{ path: string }>>,
77+
settings: T
7778
) => Promise<string | true>,
7879
maximumCharacters?: number
79-
) => {
80+
) {
8081
return (props: {
8182
identifier: string;
8283
id: string;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import { withProvider } from '@gitroom/frontend/components/launches/providers/high.order.provider';
2+
import { FC } from 'react';
3+
import { Select } from '@gitroom/react/form/select';
4+
import { useSettings } from '@gitroom/frontend/components/launches/helpers/use.values';
5+
import { InstagramDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/instagram.dto';
6+
import { InstagramCollaboratorsTags } from '@gitroom/frontend/components/launches/providers/instagram/instagram.tags';
7+
8+
const postType = [
9+
{
10+
value: 'post',
11+
label: 'Post / Reel',
12+
},
13+
{
14+
value: 'story',
15+
label: 'Story',
16+
},
17+
];
18+
const InstagramCollaborators: FC<{ values?: any }> = (props) => {
19+
const { watch, register, formState, control } = useSettings();
20+
const postCurrentType = watch('post_type');
21+
return (
22+
<>
23+
<Select
24+
label="Post Type"
25+
{...register('post_type', {
26+
value: 'post',
27+
})}
28+
>
29+
<option value="">Select Post Type...</option>
30+
{postType.map((item) => (
31+
<option key={item.value} value={item.value}>
32+
{item.label}
33+
</option>
34+
))}
35+
</Select>
36+
37+
{postCurrentType !== 'story' && (
38+
<InstagramCollaboratorsTags
39+
label="Collaborators (max 3) - accounts can't be private"
40+
{...register('collaborators')}
41+
/>
42+
)}
43+
</>
44+
);
45+
};
46+
47+
export default withProvider<InstagramDto>(
48+
InstagramCollaborators,
49+
undefined,
50+
InstagramDto,
51+
async ([firstPost, ...otherPosts], settings) => {
52+
if (!firstPost.length) {
53+
return 'Instagram should have at least one media';
54+
}
55+
56+
if (firstPost.length > 1 && settings.post_type === 'story') {
57+
return 'Instagram stories can only have one media';
58+
}
59+
60+
const checkVideosLength = await Promise.all(
61+
firstPost
62+
.filter((f) => f.path.indexOf('mp4') > -1)
63+
.flatMap((p) => p.path)
64+
.map((p) => {
65+
return new Promise<number>((res) => {
66+
const video = document.createElement('video');
67+
video.preload = 'metadata';
68+
video.src = p;
69+
video.addEventListener('loadedmetadata', () => {
70+
res(video.duration);
71+
});
72+
});
73+
})
74+
);
75+
76+
for (const video of checkVideosLength) {
77+
if (video > 60 && settings.post_type === 'story') {
78+
return 'Instagram stories should be maximum 60 seconds';
79+
}
80+
81+
if (video > 90 && settings.post_type === 'post') {
82+
return 'Instagram reel should be maximum 90 seconds';
83+
}
84+
}
85+
86+
return true;
87+
},
88+
2200
89+
);

apps/frontend/src/components/launches/providers/instagram/instagram.provider.tsx

-15
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { FC, useCallback, useEffect, useMemo, useState } from 'react';
2+
import { useSettings } from '@gitroom/frontend/components/launches/helpers/use.values';
3+
import { ReactTags } from 'react-tag-autocomplete';
4+
import interClass from '@gitroom/react/helpers/inter.font';
5+
6+
export const InstagramCollaboratorsTags: FC<{
7+
name: string;
8+
label: string;
9+
onChange: (event: { target: { value: any[]; name: string } }) => void;
10+
}> = (props) => {
11+
const { onChange, name, label } = props;
12+
const { getValues } = useSettings();
13+
const [tagValue, setTagValue] = useState<any[]>([]);
14+
const [suggestions, setSuggestions] = useState<string>('');
15+
16+
const onDelete = useCallback(
17+
(tagIndex: number) => {
18+
const modify = tagValue.filter((_, i) => i !== tagIndex);
19+
setTagValue(modify);
20+
onChange({ target: { value: modify, name } });
21+
},
22+
[tagValue]
23+
);
24+
25+
const onAddition = useCallback(
26+
(newTag: any) => {
27+
if (tagValue.length >= 3) {
28+
return;
29+
}
30+
const modify = [...tagValue, newTag];
31+
setTagValue(modify);
32+
onChange({ target: { value: modify, name } });
33+
},
34+
[tagValue]
35+
);
36+
37+
useEffect(() => {
38+
const settings = getValues()[props.name];
39+
if (settings) {
40+
setTagValue(settings);
41+
}
42+
}, []);
43+
44+
const suggestionsArray = useMemo(() => {
45+
return [...tagValue, { label: suggestions, value: suggestions }].filter(f => f.label);
46+
}, [suggestions, tagValue]);
47+
48+
return (
49+
<div>
50+
<div className={`${interClass} text-[14px] mb-[6px]`}>{label}</div>
51+
<ReactTags
52+
placeholderText="Add a tag"
53+
suggestions={suggestionsArray}
54+
selected={tagValue}
55+
onAdd={onAddition}
56+
onInput={setSuggestions}
57+
onDelete={onDelete}
58+
/>
59+
</div>
60+
);
61+
};

apps/frontend/src/components/launches/providers/show.all.providers.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import RedditProvider from "@gitroom/frontend/components/launches/providers/redd
77
import MediumProvider from "@gitroom/frontend/components/launches/providers/medium/medium.provider";
88
import HashnodeProvider from "@gitroom/frontend/components/launches/providers/hashnode/hashnode.provider";
99
import FacebookProvider from '@gitroom/frontend/components/launches/providers/facebook/facebook.provider';
10-
import InstagramProvider from '@gitroom/frontend/components/launches/providers/instagram/instagram.provider';
10+
import InstagramProvider from '@gitroom/frontend/components/launches/providers/instagram/instagram.collaborators';
1111
import YoutubeProvider from '@gitroom/frontend/components/launches/providers/youtube/youtube.provider';
1212
import TiktokProvider from '@gitroom/frontend/components/launches/providers/tiktok/tiktok.provider';
1313
import PinterestProvider from '@gitroom/frontend/components/launches/providers/pinterest/pinterest.provider';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { Type } from 'class-transformer';
2+
import { IsArray, IsDefined, IsIn, IsString, ValidateNested } from 'class-validator';
3+
4+
export class Collaborators {
5+
@IsDefined()
6+
@IsString()
7+
label: string;
8+
}
9+
export class InstagramDto {
10+
@IsIn(['post', 'story'])
11+
@IsDefined()
12+
post_type: 'post' | 'story';
13+
14+
@Type(() => Collaborators)
15+
@ValidateNested({ each: true })
16+
@IsArray()
17+
collaborators: Collaborators[];
18+
}

libraries/nestjs-libraries/src/integrations/social.abstract.ts

+9-2
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,11 @@ export class NotEnoughScopes {
2020
}
2121

2222
export abstract class SocialAbstract {
23-
async fetch(url: string, options: RequestInit = {}, identifier = ''): Promise<Response> {
23+
async fetch(
24+
url: string,
25+
options: RequestInit = {},
26+
identifier = ''
27+
): Promise<Response> {
2428
const request = await fetch(url, options);
2529

2630
if (request.status === 200 || request.status === 201) {
@@ -40,7 +44,10 @@ export abstract class SocialAbstract {
4044
return this.fetch(url, options, identifier);
4145
}
4246

43-
if (request.status === 401 || json.includes('OAuthException')) {
47+
if (
48+
request.status === 401 ||
49+
(json.includes('OAuthException') && !json.includes("Unsupported format") && !json.includes('2207018'))
50+
) {
4451
throw new RefreshToken(identifier, json, options.body!);
4552
}
4653

libraries/nestjs-libraries/src/integrations/social/instagram.provider.ts

+31-4
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { makeId } from '@gitroom/nestjs-libraries/services/make.is';
99
import { timer } from '@gitroom/helpers/utils/timer';
1010
import dayjs from 'dayjs';
1111
import { SocialAbstract } from '@gitroom/nestjs-libraries/integrations/social.abstract';
12+
import { InstagramDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/instagram.dto';
1213

1314
export class InstagramProvider
1415
extends SocialAbstract
@@ -203,10 +204,11 @@ export class InstagramProvider
203204
async post(
204205
id: string,
205206
accessToken: string,
206-
postDetails: PostDetails[]
207+
postDetails: PostDetails<InstagramDto>[]
207208
): Promise<PostResponse[]> {
208209
const [firstPost, ...theRest] = postDetails;
209-
210+
console.log('in progress');
211+
const isStory = firstPost.settings.post_type === 'story';
210212
const medias = await Promise.all(
211213
firstPost?.media?.map(async (m) => {
212214
const caption =
@@ -218,18 +220,34 @@ export class InstagramProvider
218220
const mediaType =
219221
m.url.indexOf('.mp4') > -1
220222
? firstPost?.media?.length === 1
221-
? `video_url=${m.url}&media_type=REELS`
223+
? isStory
224+
? `video_url=${m.url}&media_type=STORIES`
225+
: `video_url=${m.url}&media_type=REELS`
226+
: isStory
227+
? `video_url=${m.url}&media_type=STORIES`
222228
: `video_url=${m.url}&media_type=VIDEO`
229+
: isStory
230+
? `image_url=${m.url}&media_type=STORIES`
223231
: `image_url=${m.url}`;
232+
console.log('in progress1');
233+
234+
const collaborators =
235+
firstPost?.settings?.collaborators?.length && !isStory
236+
? `&collaborators=${JSON.stringify(
237+
firstPost?.settings?.collaborators.map((p) => p.label)
238+
)}`
239+
: ``;
224240

241+
console.log(collaborators);
225242
const { id: photoId } = await (
226243
await this.fetch(
227-
`https://graph.facebook.com/v20.0/${id}/media?${mediaType}${isCarousel}&access_token=${accessToken}${caption}`,
244+
`https://graph.facebook.com/v20.0/${id}/media?${mediaType}${isCarousel}${collaborators}&access_token=${accessToken}${caption}`,
228245
{
229246
method: 'POST',
230247
}
231248
)
232249
).json();
250+
console.log('in progress2');
233251

234252
let status = 'IN_PROGRESS';
235253
while (status === 'IN_PROGRESS') {
@@ -241,6 +259,7 @@ export class InstagramProvider
241259
await timer(3000);
242260
status = status_code;
243261
}
262+
console.log('in progress3');
244263

245264
return photoId;
246265
}) || []
@@ -376,4 +395,12 @@ export class InstagramProvider
376395
})) || []
377396
);
378397
}
398+
399+
music(accessToken: string, data: { q: string }) {
400+
return this.fetch(
401+
`https://graph.facebook.com/v20.0/music/search?q=${encodeURIComponent(
402+
data.q
403+
)}&access_token=${accessToken}`
404+
);
405+
}
379406
}

0 commit comments

Comments
 (0)