Skip to content

Commit 653f787

Browse files
committed
feat: long tweet for X + add additional settings per provider
1 parent 4806b82 commit 653f787

File tree

14 files changed

+331
-70
lines changed

14 files changed

+331
-70
lines changed

apps/backend/src/api/routes/integrations.controller.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,11 +106,24 @@ export class IntegrationsController {
106106
changeProfilePicture: !!findIntegration?.changeProfilePicture,
107107
changeNickName: !!findIntegration?.changeNickname,
108108
customer: p.customer,
109+
additionalSettings: p.additionalSettings || '[]',
109110
};
110111
}),
111112
};
112113
}
113114

115+
@Post('/:id/settings')
116+
async updateProviderSettings(
117+
@GetOrgFromRequest() org: Organization,
118+
@Param('id') id: string,
119+
@Body('additionalSettings') body: string
120+
) {
121+
if (typeof body !== 'string') {
122+
throw new Error('Invalid body');
123+
}
124+
125+
await this._integrationService.updateProviderSettings(org.id, id, body);
126+
}
114127
@Post('/:id/nickname')
115128
async setNickname(
116129
@GetOrgFromRequest() org: Organization,
@@ -257,13 +270,14 @@ export class IntegrationsController {
257270
return load;
258271
} catch (err) {
259272
if (err instanceof RefreshToken) {
260-
const { accessToken, refreshToken, expiresIn } =
273+
const { accessToken, refreshToken, expiresIn, additionalSettings } =
261274
await integrationProvider.refreshToken(
262275
getIntegration.refreshToken
263276
);
264277

265278
if (accessToken) {
266279
await this._integrationService.createOrUpdateIntegration(
280+
additionalSettings,
267281
!!integrationProvider.oneTimeToken,
268282
getIntegration.organizationId,
269283
getIntegration.name,
@@ -346,6 +360,7 @@ export class IntegrationsController {
346360
}
347361

348362
return this._integrationService.createOrUpdateIntegration(
363+
undefined,
349364
true,
350365
org.id,
351366
name,
@@ -413,6 +428,7 @@ export class IntegrationsController {
413428
name,
414429
picture,
415430
username,
431+
additionalSettings,
416432
// eslint-disable-next-line no-async-promise-executor
417433
} = await new Promise<AuthTokenDetails>(async (res) => {
418434
const auth = await integrationProvider.authenticate(
@@ -432,6 +448,7 @@ export class IntegrationsController {
432448
name: '',
433449
picture: '',
434450
username: '',
451+
additionalSettings: [],
435452
});
436453
}
437454

@@ -470,6 +487,7 @@ export class IntegrationsController {
470487
}
471488
}
472489
return this._integrationService.createOrUpdateIntegration(
490+
additionalSettings,
473491
!!integrationProvider.oneTimeToken,
474492
org.id,
475493
validName.trim(),

apps/frontend/src/components/launches/calendar.context.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ export interface Integrations {
6060
type: string;
6161
picture: string;
6262
changeProfilePicture: boolean;
63+
additionalSettings: string;
6364
changeNickName: boolean;
6465
time: { time: number }[];
6566
customer?: {

apps/frontend/src/components/launches/menu/menu.tsx

Lines changed: 59 additions & 8 deletions
Large diffs are not rendered by default.

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

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ export const withProvider = function <T extends object>(
8787
value: Array<Array<{ path: string }>>,
8888
settings: T
8989
) => Promise<string | true>,
90-
maximumCharacters?: number
90+
maximumCharacters?: number | ((settings: any) => number)
9191
) {
9292
return (props: {
9393
identifier: string;
@@ -155,7 +155,11 @@ export const withProvider = function <T extends object>(
155155
editInPlace ? InPlaceValue : props.value,
156156
dto,
157157
checkValidity,
158-
maximumCharacters
158+
!maximumCharacters
159+
? undefined
160+
: typeof maximumCharacters === 'number'
161+
? maximumCharacters
162+
: maximumCharacters(JSON.parse(integration?.additionalSettings || '[]'))
159163
);
160164

161165
// change editor value
@@ -348,10 +352,12 @@ export const withProvider = function <T extends object>(
348352
);
349353

350354
const getInternalPlugs = useCallback(async () => {
351-
return (await fetch(`/integrations/${props.identifier}/internal-plugs`)).json();
355+
return (
356+
await fetch(`/integrations/${props.identifier}/internal-plugs`)
357+
).json();
352358
}, [props.identifier]);
353359

354-
const {data} = useSWR(`internal-${props.identifier}`, getInternalPlugs);
360+
const { data } = useSWR(`internal-${props.identifier}`, getInternalPlugs);
355361

356362
// this is a trick to prevent the data from being deleted, yet we don't render the elements
357363
if (!props.show) {
@@ -423,7 +429,8 @@ export const withProvider = function <T extends object>(
423429
<div>
424430
<div className="flex gap-[4px]">
425431
<div className="flex-1 text-textColor editor">
426-
{(integration?.identifier === 'linkedin' || integration?.identifier === 'linkedin-page') && (
432+
{(integration?.identifier === 'linkedin' ||
433+
integration?.identifier === 'linkedin-page') && (
427434
<Button
428435
className="mb-[5px]"
429436
onClick={tagPersonOrCompany(
@@ -527,7 +534,7 @@ export const withProvider = function <T extends object>(
527534
<div className={clsx('mt-[20px]', showTab !== 2 && 'hidden')}>
528535
<Component values={editInPlace ? InPlaceValue : props.value} />
529536
{data?.internalPlugs?.length && (
530-
<InternalChannels plugs={data?.internalPlugs} />
537+
<InternalChannels plugs={data?.internalPlugs} />
531538
)}
532539
</div>
533540
)}
@@ -546,11 +553,23 @@ export const withProvider = function <T extends object>(
546553
.join('').length ? (
547554
CustomPreviewComponent ? (
548555
<CustomPreviewComponent
549-
maximumCharacters={maximumCharacters}
556+
maximumCharacters={
557+
!maximumCharacters
558+
? undefined
559+
: typeof maximumCharacters === 'number'
560+
? maximumCharacters
561+
: maximumCharacters(JSON.parse(integration?.additionalSettings || '[]'))
562+
}
550563
/>
551564
) : (
552565
<GeneralPreviewComponent
553-
maximumCharacters={maximumCharacters}
566+
maximumCharacters={
567+
!maximumCharacters
568+
? undefined
569+
: typeof maximumCharacters === 'number'
570+
? maximumCharacters
571+
: maximumCharacters(JSON.parse(integration?.additionalSettings || '[]'))
572+
}
554573
/>
555574
)
556575
) : (

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,13 @@ export default withProvider(
2626
}
2727
return true;
2828
},
29-
280
29+
(settings) => {
30+
if (settings?.[0]?.value) {
31+
console.log(4000);
32+
return 4000;
33+
}
34+
return 280;
35+
}
3036
);
3137

3238
const checkVideoDuration = async (url: string): Promise<boolean> => {
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import { TopTitle } from '@gitroom/frontend/components/launches/helpers/top.title.component';
2+
import React, { FC, useCallback, useMemo, useState } from 'react';
3+
import { useModals } from '@mantine/modals';
4+
import { Integration } from '@prisma/client';
5+
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
6+
import { Button } from '@gitroom/react/form/button';
7+
import { Slider } from '@gitroom/react/form/slider';
8+
9+
export const Element: FC<{ setting: any; onChange: (value: any) => void }> = (
10+
props
11+
) => {
12+
const { setting, onChange } = props;
13+
const [value, setValue] = useState(setting.value);
14+
15+
return (
16+
<div className="flex flex-col gap-[10px]">
17+
<div>{setting.title}</div>
18+
<div className="text-[14px]">{setting.description}</div>
19+
<Slider
20+
value={value === true ? 'on' : 'off'}
21+
onChange={() => {
22+
setValue(!value);
23+
onChange(!value);
24+
}}
25+
fill={true}
26+
/>
27+
</div>
28+
);
29+
};
30+
31+
export const SettingsModal: FC<{
32+
integration: Integration & { customer?: { id: string; name: string } };
33+
onClose: () => void;
34+
}> = (props) => {
35+
const fetch = useFetch();
36+
const { onClose, integration } = props;
37+
const modal = useModals();
38+
const [values, setValues] = useState(
39+
JSON.parse(integration?.additionalSettings || '[]')
40+
);
41+
42+
const changeValue = useCallback(
43+
(index: number) => (value: any) => {
44+
const newValues = [...values];
45+
newValues[index].value = value;
46+
setValues(newValues);
47+
},
48+
[values]
49+
);
50+
51+
const save = useCallback(async () => {
52+
await fetch(`/integrations/${integration.id}/settings`, {
53+
method: 'POST',
54+
body: JSON.stringify({ additionalSettings: JSON.stringify(values) }),
55+
});
56+
57+
modal.closeAll();
58+
onClose();
59+
}, [values, integration]);
60+
61+
return (
62+
<div className="rounded-[4px] border border-customColor6 bg-sixth px-[16px] pb-[16px] relative w-full">
63+
<TopTitle title={`Additional Settings`} />
64+
<button
65+
className="outline-none absolute right-[20px] top-[20px] mantine-UnstyledButton-root mantine-ActionIcon-root hover:bg-tableBorder cursor-pointer mantine-Modal-close mantine-1dcetaa"
66+
type="button"
67+
onClick={() => modal.closeAll()}
68+
>
69+
<svg
70+
viewBox="0 0 15 15"
71+
fill="none"
72+
xmlns="http://www.w3.org/2000/svg"
73+
width="16"
74+
height="16"
75+
>
76+
<path
77+
d="M11.7816 4.03157C12.0062 3.80702 12.0062 3.44295 11.7816 3.2184C11.5571 2.99385 11.193 2.99385 10.9685 3.2184L7.50005 6.68682L4.03164 3.2184C3.80708 2.99385 3.44301 2.99385 3.21846 3.2184C2.99391 3.44295 2.99391 3.80702 3.21846 4.03157L6.68688 7.49999L3.21846 10.9684C2.99391 11.193 2.99391 11.557 3.21846 11.7816C3.44301 12.0061 3.80708 12.0061 4.03164 11.7816L7.50005 8.31316L10.9685 11.7816C11.193 12.0061 11.5571 12.0061 11.7816 11.7816C12.0062 11.557 12.0062 11.193 11.7816 10.9684L8.31322 7.49999L11.7816 4.03157Z"
78+
fill="currentColor"
79+
fillRule="evenodd"
80+
clipRule="evenodd"
81+
></path>
82+
</svg>
83+
</button>
84+
85+
<div className="mt-[16px]">
86+
{values.map((setting: any, index: number) => (
87+
<Element key={setting.title} setting={setting} onChange={changeValue(index)} />
88+
))}
89+
</div>
90+
91+
<div className="my-[16px] flex gap-[10px]">
92+
<Button onClick={save}>Save</Button>
93+
</div>
94+
</div>
95+
);
96+
};

libraries/helpers/src/utils/count.length.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import twitter from 'twitter-text';
44
export const textSlicer = (
55
integrationType: string,
66
end: number,
7-
text: string
7+
text: string,
88
): {start: number, end: number} => {
99
if (integrationType !== 'x') {
1010
return {
@@ -13,7 +13,21 @@ export const textSlicer = (
1313
}
1414
}
1515

16-
const {validRangeEnd, valid} = twitter.parseTweet(text);
16+
const {validRangeEnd, valid} = twitter.parseTweet(text, {
17+
version: 3,
18+
maxWeightedTweetLength: end,
19+
scale: 100,
20+
defaultWeight: 200,
21+
emojiParsingEnabled: true,
22+
transformedURLLength: 23,
23+
ranges: [
24+
{ start: 0, end: 4351, weight: 100 },
25+
{ start: 8192, end: 8205, weight: 100 },
26+
{ start: 8208, end: 8223, weight: 100 },
27+
{ start: 8242, end: 8247, weight: 100 }
28+
]
29+
});
30+
1731
return {
1832
start: 0,
1933
end: valid ? end : validRangeEnd

libraries/nestjs-libraries/src/database/prisma/integrations/integration.repository.ts

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,18 @@ export class IntegrationRepository {
1818
private _customers: PrismaRepository<'customer'>
1919
) {}
2020

21+
updateProviderSettings(org: string, id: string, settings: string) {
22+
return this._integration.model.integration.update({
23+
where: {
24+
id,
25+
organizationId: org,
26+
},
27+
data: {
28+
additionalSettings: settings,
29+
},
30+
});
31+
}
32+
2133
async setTimes(org: string, id: string, times: IntegrationTimeDto) {
2234
return this._integration.model.integration.update({
2335
select: {
@@ -94,6 +106,15 @@ export class IntegrationRepository {
94106
}
95107

96108
async createOrUpdateIntegration(
109+
additionalSettings:
110+
| {
111+
title: string;
112+
description: string;
113+
type: 'checkbox' | 'text' | 'textarea';
114+
value: any;
115+
regex?: string;
116+
}[]
117+
| undefined,
97118
oneTimeToken: boolean,
98119
org: string,
99120
name: string,
@@ -144,6 +165,9 @@ export class IntegrationRepository {
144165
refreshNeeded: false,
145166
rootInternalId: internalId.split('_').pop(),
146167
...(customInstanceDetails ? { customInstanceDetails } : {}),
168+
additionalSettings: additionalSettings
169+
? JSON.stringify(additionalSettings)
170+
: '[]',
147171
},
148172
update: {
149173
type: type as any,
@@ -168,17 +192,28 @@ export class IntegrationRepository {
168192
});
169193

170194
if (oneTimeToken) {
195+
const rootId =
196+
(
197+
await this._integration.model.integration.findFirst({
198+
where: {
199+
organizationId: org,
200+
internalId: internalId,
201+
},
202+
})
203+
)?.rootInternalId || internalId.split('_').pop()!;
204+
171205
await this._integration.model.integration.updateMany({
172206
where: {
173207
id: {
174208
not: upsert.id,
175209
},
176210
organizationId: org,
177-
rootInternalId: internalId.split('_').pop(),
211+
rootInternalId: rootId,
178212
},
179213
data: {
180214
token,
181215
refreshToken,
216+
refreshNeeded: false,
182217
...(expiresIn
183218
? { tokenExpiration: new Date(Date.now() + expiresIn * 1000) }
184219
: {}),

0 commit comments

Comments
 (0)