feat: mentions
This commit is contained in:
parent
2756f28d72
commit
449e2acab1
|
|
@ -37,6 +37,7 @@ import {
|
|||
AuthorizationActions,
|
||||
Sections,
|
||||
} from '@gitroom/backend/services/auth/permissions/permission.exception.class';
|
||||
import { uniqBy } from 'lodash';
|
||||
|
||||
@ApiTags('Integrations')
|
||||
@Controller('/integrations')
|
||||
|
|
@ -246,11 +247,59 @@ export class IntegrationsController {
|
|||
) {
|
||||
return this._integrationService.setTimes(org.id, id, body);
|
||||
}
|
||||
|
||||
@Post('/mentions')
|
||||
async mentions(
|
||||
@GetOrgFromRequest() org: Organization,
|
||||
@Body() body: IntegrationFunctionDto
|
||||
) {
|
||||
const getIntegration = await this._integrationService.getIntegrationById(
|
||||
org.id,
|
||||
body.id
|
||||
);
|
||||
if (!getIntegration) {
|
||||
throw new Error('Invalid integration');
|
||||
}
|
||||
|
||||
const list = await this._integrationService.getMentions(
|
||||
getIntegration.providerIdentifier,
|
||||
body?.data?.query
|
||||
);
|
||||
|
||||
let newList = [];
|
||||
try {
|
||||
newList = await this.functionIntegration(org, body);
|
||||
} catch (err) {}
|
||||
|
||||
if (newList.length) {
|
||||
await this._integrationService.insertMentions(
|
||||
getIntegration.providerIdentifier,
|
||||
newList.map((p: any) => ({
|
||||
name: p.label,
|
||||
username: p.id,
|
||||
image: p.image,
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
return uniqBy(
|
||||
[
|
||||
...list.map((p) => ({
|
||||
id: p.username,
|
||||
image: p.image,
|
||||
label: p.name,
|
||||
})),
|
||||
...newList,
|
||||
],
|
||||
(p) => p.id
|
||||
);
|
||||
}
|
||||
|
||||
@Post('/function')
|
||||
async functionIntegration(
|
||||
@GetOrgFromRequest() org: Organization,
|
||||
@Body() body: IntegrationFunctionDto
|
||||
) {
|
||||
): Promise<any> {
|
||||
const getIntegration = await this._integrationService.getIntegrationById(
|
||||
org.id,
|
||||
body.id
|
||||
|
|
@ -266,8 +315,10 @@ export class IntegrationsController {
|
|||
throw new Error('Invalid provider');
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
if (integrationProvider[body.name]) {
|
||||
try {
|
||||
// @ts-ignore
|
||||
const load = await integrationProvider[body.name](
|
||||
getIntegration.token,
|
||||
body.data,
|
||||
|
|
|
|||
|
|
@ -511,23 +511,6 @@ export const Editor: FC<{
|
|||
[props.value, id]
|
||||
);
|
||||
|
||||
const addLinkedinTag = useCallback((text: string) => {
|
||||
const id = text.split('(')[1].split(')')[0];
|
||||
const name = text.split('[')[1].split(']')[0];
|
||||
|
||||
editorRef?.current?.editor
|
||||
.chain()
|
||||
.focus()
|
||||
.insertContent({
|
||||
type: 'mention',
|
||||
attrs: {
|
||||
linkedinId: id,
|
||||
label: name,
|
||||
},
|
||||
})
|
||||
.run();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="relative bg-bigStrip" id={id}>
|
||||
|
|
@ -559,9 +542,6 @@ export const Editor: FC<{
|
|||
>
|
||||
{'\uD83D\uDE00'}
|
||||
</div>
|
||||
{identifier === 'linkedin' || identifier === 'linkedin-page' ? (
|
||||
<LinkedinCompanyPop addText={addLinkedinTag} />
|
||||
) : null}
|
||||
<div className="relative">
|
||||
<div className="absolute z-[200] top-[35px] -start-[50px]">
|
||||
<EmojiPicker
|
||||
|
|
@ -696,7 +676,7 @@ export const OnlyEditor = forwardRef<
|
|||
}
|
||||
|
||||
try {
|
||||
const load = await fetch('/integrations/function', {
|
||||
const load = await fetch('/integrations/mentions', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
name: 'mention',
|
||||
|
|
|
|||
|
|
@ -73,6 +73,10 @@ const MentionList: FC = (props: any) => {
|
|||
},
|
||||
}));
|
||||
|
||||
if (props?.stop) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="dropdown-menu bg-white border border-gray-200 rounded-lg shadow-lg max-h-60 overflow-y-auto p-2">
|
||||
{props?.items?.none ? (
|
||||
|
|
@ -84,22 +88,26 @@ const MentionList: FC = (props: any) => {
|
|||
Loading...
|
||||
</div>
|
||||
) : props?.items ? (
|
||||
props.items.map((item: any, index: any) => (
|
||||
<button
|
||||
className={`flex gap-[10px] w-full p-2 text-left rounded hover:bg-gray-100 ${
|
||||
index === selectedIndex ? 'bg-blue-100' : ''
|
||||
}`}
|
||||
key={index}
|
||||
onClick={() => selectItem(index)}
|
||||
>
|
||||
<img
|
||||
src={item.image}
|
||||
alt={item.label}
|
||||
className="w-[30px] h-[30px] rounded-full object-cover"
|
||||
/>
|
||||
<div className="flex-1 text-gray-800">{item.label}</div>
|
||||
</button>
|
||||
))
|
||||
props.items.length === 0 ? (
|
||||
<div className="p-2 text-gray-500 text-center">No results found</div>
|
||||
) : (
|
||||
props.items.map((item: any, index: any) => (
|
||||
<button
|
||||
className={`flex gap-[10px] w-full p-2 text-left rounded hover:bg-gray-100 ${
|
||||
index === selectedIndex ? 'bg-blue-100' : ''
|
||||
}`}
|
||||
key={index}
|
||||
onClick={() => selectItem(index)}
|
||||
>
|
||||
<img
|
||||
src={item.image}
|
||||
alt={item.label}
|
||||
className="w-[30px] h-[30px] rounded-full object-cover"
|
||||
/>
|
||||
<div className="flex-1 text-gray-800">{item.label}</div>
|
||||
</button>
|
||||
))
|
||||
)
|
||||
) : (
|
||||
<div className="p-2 text-gray-500 text-center">Loading...</div>
|
||||
)}
|
||||
|
|
@ -142,11 +150,12 @@ export const suggestion = (
|
|||
return {
|
||||
items: async ({ query }: { query: string }) => {
|
||||
if (!query || query.length < 2) {
|
||||
component.updateProps({ loading: true, stop: true });
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
component.updateProps({ loading: true });
|
||||
component.updateProps({ loading: true, stop: false });
|
||||
const result = await debouncedLoadList(query);
|
||||
console.log(result);
|
||||
return result;
|
||||
|
|
@ -169,7 +178,7 @@ export const suggestion = (
|
|||
},
|
||||
editor: props.editor,
|
||||
});
|
||||
component.updateProps({ ...props, loading: true });
|
||||
component.updateProps({ ...props, loading: true, stop: false });
|
||||
updatePosition(props.editor, component.element);
|
||||
},
|
||||
onStart: (props: any) => {
|
||||
|
|
@ -212,7 +221,7 @@ export const suggestion = (
|
|||
newQuery.length >= 2 &&
|
||||
(!props.items || props.items.length === 0);
|
||||
|
||||
component.updateProps({ ...props, loading: false });
|
||||
component.updateProps({ ...props, loading: false, stop: false });
|
||||
|
||||
if (!props.clientRect) {
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -135,7 +135,8 @@ export const stripHtmlValidation = (
|
|||
type: 'none' | 'normal' | 'markdown' | 'html',
|
||||
value: string,
|
||||
replaceBold = false,
|
||||
none = false
|
||||
none = false,
|
||||
convertMentionFunction?: (idOrHandle: string, name: string) => string,
|
||||
): string => {
|
||||
if (type === 'html') {
|
||||
return striptags(value, [
|
||||
|
|
@ -171,18 +172,16 @@ export const stripHtmlValidation = (
|
|||
}
|
||||
|
||||
if (replaceBold) {
|
||||
const processedHtml = convertLinkedinMention(
|
||||
const processedHtml = convertMention(
|
||||
convertToAscii(
|
||||
html
|
||||
.replace(/<ul>/, "\n<ul>")
|
||||
.replace(/<\/ul>\n/, "</ul>")
|
||||
.replace(
|
||||
/<li.*?>([.\s\S]*?)<\/li.*?>/gm,
|
||||
(match, p1) => {
|
||||
.replace(/<ul>/, '\n<ul>')
|
||||
.replace(/<\/ul>\n/, '</ul>')
|
||||
.replace(/<li.*?>([.\s\S]*?)<\/li.*?>/gm, (match, p1) => {
|
||||
return `<li><p>- ${p1.replace(/\n/gm, '')}\n</p></li>`;
|
||||
}
|
||||
)
|
||||
)
|
||||
})
|
||||
),
|
||||
convertMentionFunction
|
||||
);
|
||||
|
||||
return striptags(processedHtml, ['h1', 'h2', 'h3']);
|
||||
|
|
@ -192,11 +191,18 @@ export const stripHtmlValidation = (
|
|||
return striptags(html, ['ul', 'li', 'h1', 'h2', 'h3']);
|
||||
};
|
||||
|
||||
export const convertLinkedinMention = (value: string) => {
|
||||
export const convertMention = (
|
||||
value: string,
|
||||
process?: (idOrHandle: string, name: string) => string
|
||||
) => {
|
||||
if (!process) {
|
||||
return value;
|
||||
}
|
||||
|
||||
return value.replace(
|
||||
/<span.*?data-mention-id="(.*?)".*?>(.*?)<\/span>/gi,
|
||||
(match, id, name) => {
|
||||
return `@[${name.replace('@', '')}](${id})`;
|
||||
return `<span>` + process(id, name) + `</span>`;
|
||||
}
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -15,9 +15,56 @@ export class IntegrationRepository {
|
|||
private _posts: PrismaRepository<'post'>,
|
||||
private _plugs: PrismaRepository<'plugs'>,
|
||||
private _exisingPlugData: PrismaRepository<'exisingPlugData'>,
|
||||
private _customers: PrismaRepository<'customer'>
|
||||
private _customers: PrismaRepository<'customer'>,
|
||||
private _mentions: PrismaRepository<'mentions'>
|
||||
) {}
|
||||
|
||||
getMentions(platform: string, q: string) {
|
||||
return this._mentions.model.mentions.findMany({
|
||||
where: {
|
||||
platform,
|
||||
OR: [
|
||||
{
|
||||
name: {
|
||||
contains: q,
|
||||
mode: 'insensitive',
|
||||
},
|
||||
},
|
||||
{
|
||||
username: {
|
||||
contains: q,
|
||||
mode: 'insensitive',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
orderBy: {
|
||||
name: 'asc',
|
||||
},
|
||||
take: 100,
|
||||
select: {
|
||||
name: true,
|
||||
username: true,
|
||||
image: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
insertMentions(
|
||||
platform: string,
|
||||
mentions: { name: string; username: string; image: string }[]
|
||||
) {
|
||||
return this._mentions.model.mentions.createMany({
|
||||
data: mentions.map((mention) => ({
|
||||
platform,
|
||||
name: mention.name,
|
||||
username: mention.username,
|
||||
image: mention.image,
|
||||
})),
|
||||
skipDuplicates: true,
|
||||
});
|
||||
}
|
||||
|
||||
updateProviderSettings(org: string, id: string, settings: string) {
|
||||
return this._integration.model.integration.update({
|
||||
where: {
|
||||
|
|
|
|||
|
|
@ -46,6 +46,17 @@ export class IntegrationService {
|
|||
return true;
|
||||
}
|
||||
|
||||
getMentions(platform: string, q: string) {
|
||||
return this._integrationRepository.getMentions(platform, q);
|
||||
}
|
||||
|
||||
insertMentions(
|
||||
platform: string,
|
||||
mentions: { name: string; username: string; image: string }[]
|
||||
) {
|
||||
return this._integrationRepository.insertMentions(platform, mentions);
|
||||
}
|
||||
|
||||
async setTimes(
|
||||
orgId: string,
|
||||
integrationId: string,
|
||||
|
|
@ -163,7 +174,11 @@ export class IntegrationService {
|
|||
await this.informAboutRefreshError(orgId, integration);
|
||||
}
|
||||
|
||||
async informAboutRefreshError(orgId: string, integration: Integration, err = '') {
|
||||
async informAboutRefreshError(
|
||||
orgId: string,
|
||||
integration: Integration,
|
||||
err = ''
|
||||
) {
|
||||
await this._notificationService.inAppNotification(
|
||||
orgId,
|
||||
`Could not refresh your ${integration.providerIdentifier} channel ${err}`,
|
||||
|
|
|
|||
|
|
@ -394,7 +394,7 @@ export class PostsRepository {
|
|||
where: {
|
||||
orgId: orgId,
|
||||
name: {
|
||||
in: tags.map((tag) => tag.label).filter(f => f),
|
||||
in: tags.map((tag) => tag.label).filter((f) => f),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -378,7 +378,9 @@ export class PostsService {
|
|||
return post;
|
||||
}
|
||||
|
||||
const ids = (extract || []).map((e) => e.replace('(post:', '').replace(')', ''));
|
||||
const ids = (extract || []).map((e) =>
|
||||
e.replace('(post:', '').replace(')', '')
|
||||
);
|
||||
const urls = await this._postRepository.getPostUrls(orgId, ids);
|
||||
const newPlainText = ids.reduce((acc, value) => {
|
||||
const findUrl = urls?.find?.((u) => u.id === value)?.releaseURL || '';
|
||||
|
|
@ -467,7 +469,13 @@ export class PostsService {
|
|||
await Promise.all(
|
||||
(newPosts || []).map(async (p) => ({
|
||||
id: p.id,
|
||||
message: stripHtmlValidation(getIntegration.editor, p.content, true),
|
||||
message: stripHtmlValidation(
|
||||
getIntegration.editor,
|
||||
p.content,
|
||||
true,
|
||||
false,
|
||||
getIntegration.mentionFormat
|
||||
),
|
||||
settings: JSON.parse(p.settings || '{}'),
|
||||
media: await this.updateMedia(
|
||||
p.id,
|
||||
|
|
@ -535,7 +543,12 @@ export class PostsService {
|
|||
throw err;
|
||||
}
|
||||
|
||||
throw new BadBody(integration.providerIdentifier, JSON.stringify(err), {} as any, '');
|
||||
throw new BadBody(
|
||||
integration.providerIdentifier,
|
||||
JSON.stringify(err),
|
||||
{} as any,
|
||||
''
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -658,6 +658,18 @@ model Errors {
|
|||
@@index([createdAt])
|
||||
}
|
||||
|
||||
model Mentions {
|
||||
name String
|
||||
username String
|
||||
platform String
|
||||
image String
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@id([name, username, platform, image])
|
||||
@@index([createdAt])
|
||||
}
|
||||
|
||||
enum OrderStatus {
|
||||
PENDING
|
||||
ACCEPTED
|
||||
|
|
|
|||
|
|
@ -519,13 +519,17 @@ export class BlueskyProvider extends SocialAbstract implements SocialProvider {
|
|||
});
|
||||
|
||||
const list = await agent.searchActors({
|
||||
q: d.query
|
||||
q: d.query,
|
||||
});
|
||||
|
||||
return list.data.actors.map(p => ({
|
||||
return list.data.actors.map((p) => ({
|
||||
label: p.displayName,
|
||||
id: p.handle,
|
||||
image: p.avatar
|
||||
}))
|
||||
image: p.avatar,
|
||||
}));
|
||||
}
|
||||
|
||||
mentionFormat(idOrHandle: string, name: string) {
|
||||
return `@${idOrHandle}`;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -716,7 +716,7 @@ export class LinkedinProvider extends SocialAbstract implements SocialProvider {
|
|||
});
|
||||
}
|
||||
|
||||
async mention(token: string, data: { query: string }) {
|
||||
override async mention(token: string, data: { query: string }) {
|
||||
const { elements } = await (
|
||||
await fetch(
|
||||
`https://api.linkedin.com/v2/organizations?q=vanityName&vanityName=${encodeURIComponent(
|
||||
|
|
@ -739,4 +739,8 @@ export class LinkedinProvider extends SocialAbstract implements SocialProvider {
|
|||
image: p.logoV2?.['original~']?.elements?.[0]?.identifiers?.[0]?.identifier || '',
|
||||
}));
|
||||
}
|
||||
|
||||
mentionFormat(idOrHandle: string, name: string) {
|
||||
return `@[${name.replace('@', '')}](urn:li:organization:${idOrHandle})`;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -135,4 +135,5 @@ export interface SocialProvider
|
|||
mention?: (
|
||||
token: string, data: { query: string }, id: string, integration: Integration
|
||||
) => Promise<{ id: string; label: string; image: string }[] | {none: true}>;
|
||||
mentionFormat?(idOrHandle: string, name: string): string;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -526,4 +526,8 @@ export class XProvider extends SocialAbstract implements SocialProvider {
|
|||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
mentionFormat(idOrHandle: string, name: string) {
|
||||
return `@${idOrHandle}`;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue