diff --git a/backend/src/database/repositories/organizationRepository.ts b/backend/src/database/repositories/organizationRepository.ts index f196cf042f..2165e16799 100644 --- a/backend/src/database/repositories/organizationRepository.ts +++ b/backend/src/database/repositories/organizationRepository.ts @@ -903,8 +903,14 @@ class OrganizationRepository { dateEnd: new Date(Math.max.apply(null, endDates)).toISOString(), memberId: memberOrganization.memberId, organizationId: toOrganizationId, - title: foundIntersectingRoles.length > 0 ? foundIntersectingRoles[0].title : memberOrganization.title, - source: foundIntersectingRoles.length > 0 ? foundIntersectingRoles[0].source : memberOrganization.source, + title: + foundIntersectingRoles.length > 0 + ? foundIntersectingRoles[0].title + : memberOrganization.title, + source: + foundIntersectingRoles.length > 0 + ? foundIntersectingRoles[0].source + : memberOrganization.source, }) // we'll delete all roles that intersect with incoming org member roles and create a merged role diff --git a/backend/src/serverless/microservices/nodejs/automation/workers/slack/newActivityBlocks.ts b/backend/src/serverless/microservices/nodejs/automation/workers/slack/newActivityBlocks.ts index e056c9bb25..0ca5777f88 100644 --- a/backend/src/serverless/microservices/nodejs/automation/workers/slack/newActivityBlocks.ts +++ b/backend/src/serverless/microservices/nodejs/automation/workers/slack/newActivityBlocks.ts @@ -1,7 +1,10 @@ import htmlToMrkdwn from 'html-to-mrkdwn-ts' -import { integrationLabel } from '@crowd/types' +import { integrationLabel, integrationProfileUrl } from '@crowd/types' import { API_CONFIG } from '../../../../../../conf' +const defaultAvatarUrl = + 'https://uploads-ssl.webflow.com/635150609746eee5c60c4aac/6502afc9d75946873c1efa93_image%20(292).png' + const computeEngagementLevel = (score) => { if (score <= 1) { return 'Silent' @@ -47,45 +50,61 @@ const truncateText = (text: string, characters: number = 60): string => { } export const newActivityBlocks = (activity) => { - const display = htmlToMrkdwn(replaceHeadline(`${activity.display.default}`)) + const display = htmlToMrkdwn(replaceHeadline(activity.display.default)) const reach = activity.member.reach?.[activity.platform] || activity.member.reach?.total + + const { member } = activity const memberProperties = [] - if (activity.member.attributes.jobTitle?.default) { - memberProperties.push(activity.member.attributes.jobTitle?.default) + if (member.attributes.jobTitle?.default) { + memberProperties.push(`*💼 Job title:* ${member.attributes.jobTitle?.default}`) + } + if (member.organizations.length > 0) { + const orgs = member.organizations.map( + (org) => + `<${`${API_CONFIG.frontendUrl}/organizations/${org.id}`}|${org.name || org.displayName}>`, + ) + memberProperties.push(`*🏢 Organization:* ${orgs.join(' | ')}`) } if (reach > 0) { - memberProperties.push(`${reach} followers`) + memberProperties.push(`*👥 Reach:* ${reach} followers`) + } + if (member.attributes?.location?.default) { + memberProperties.push(`*📍 Location:* ${member.attributes?.location?.default}`) + } + if (member.emails.length > 0) { + const [email] = member.emails + memberProperties.push(`*✉️ Email:* `) } const engagementLevel = computeEngagementLevel(activity.member.score || activity.engagement) if (engagementLevel.length > 0) { - memberProperties.push(`*Engagement level:* ${engagementLevel}`) + memberProperties.push(`*📊 Engagement level:* ${engagementLevel}`) } if (activity.member.activeOn) { const platforms = activity.member.activeOn .map((platform) => integrationLabel[platform] || platform) .join(' | ') - memberProperties.push(`*Active on:* ${platforms}`) + memberProperties.push(`*💬 Active on:* ${platforms}`) } + const profiles = Object.keys(member.username) + .map((p) => { + const username = (member.username?.[p] || []).length > 0 ? member.username[p][0] : null + const url = + member.attributes?.url?.[p] || (username && integrationProfileUrl[p](username)) || null + return { + platform: p, + url, + } + }) + .filter((p) => !!p.url) + return { blocks: [ { type: 'section', text: { type: 'mrkdwn', - text: ':satellite_antenna: *New activity*', - }, - }, - { - type: 'divider', - }, - { - type: 'section', - text: { - type: 'mrkdwn', - text: `*<${API_CONFIG.frontendUrl}/members/${activity.member.id}|${ - activity.member.displayName - }>* \n *${truncateText(display.text)}*`, + text: `*<${API_CONFIG.frontendUrl}/members/${activity.member.id}|${activity.member.displayName}>* *${display.text}*`, }, ...(activity.url ? { @@ -105,37 +124,72 @@ export const newActivityBlocks = (activity) => { } : {}), }, + ...(activity.title || activity.body + ? [ + { + type: 'section', + text: { + type: 'mrkdwn', + text: `>${ + activity.title && activity.title !== activity.display.default + ? `*${truncateText(htmlToMrkdwn(activity.title).text, 120).replaceAll( + '\n', + '\n>', + )}* \n>` + : '' + }${truncateText(htmlToMrkdwn(activity.body).text, 260).replaceAll('\n', '\n>')}`, + }, + }, + ] + : []), + ...(memberProperties.length > 0 + ? [ + { + type: 'divider', + }, + { + type: 'section', + text: { + type: 'mrkdwn', + text: memberProperties.join('\n'), + }, + accessory: { + type: 'image', + image_url: member.attributes?.avatarUrl?.default ?? defaultAvatarUrl, + alt_text: 'computer thumbnail', + }, + }, + ] + : []), { - type: 'context', + type: 'actions', elements: [ { - type: 'mrkdwn', - text: memberProperties.join(' • '), + type: 'button', + text: { + type: 'plain_text', + text: 'View in crowd.dev', + emoji: true, + }, + url: `${API_CONFIG.frontendUrl}/members/${member.id}`, }, + ...(profiles.length > 0 + ? [ + { + type: 'overflow', + options: profiles.map(({ platform, url }) => ({ + text: { + type: 'plain_text', + text: `${integrationLabel[platform] ?? platform} profile`, + emoji: true, + }, + url, + })), + }, + ] + : []), ], }, ], - ...(activity.title || activity.body - ? { - attachments: [ - { - color: '#eeeeee', - blocks: [ - { - type: 'section', - text: { - type: 'mrkdwn', - text: `${ - activity.title && activity.title !== activity.display.default - ? `*${htmlToMrkdwn(activity.title).text}* \n ` - : '' - }${htmlToMrkdwn(activity.body).text}`, - }, - }, - ], - }, - ], - } - : {}), } } diff --git a/backend/src/serverless/microservices/nodejs/automation/workers/slack/newMemberBlocks.ts b/backend/src/serverless/microservices/nodejs/automation/workers/slack/newMemberBlocks.ts index 50a386bfa0..87d5cd71f1 100644 --- a/backend/src/serverless/microservices/nodejs/automation/workers/slack/newMemberBlocks.ts +++ b/backend/src/serverless/microservices/nodejs/automation/workers/slack/newMemberBlocks.ts @@ -1,19 +1,49 @@ -import { integrationLabel } from '@crowd/types' +import { integrationLabel, integrationProfileUrl } from '@crowd/types' import { API_CONFIG } from '../../../../../../conf' +const defaultAvatarUrl = + 'https://uploads-ssl.webflow.com/635150609746eee5c60c4aac/6502afc9d75946873c1efa93_image%20(292).png' + export const newMemberBlocks = (member) => { const platforms = member.activeOn const reach = platforms && platforms.length > 0 ? member.reach?.[platforms[0]] : member.reach?.total + const details = [] + if (member.attributes.jobTitle?.default) { + details.push(`*💼 Job title:* ${member.attributes.jobTitle?.default}`) + } + if (member.organizations.length > 0) { + const orgs = member.organizations.map( + (org) => + `<${`${API_CONFIG.frontendUrl}/organizations/${org.id}`}|${org.name || org.displayName}>`, + ) + details.push(`*🏢 Organization:* ${orgs.join(' | ')}`) + } + if (reach > 0) { + details.push(`*👥 Reach:* ${reach} followers`) + } + if (member.attributes?.location?.default) { + details.push(`*📍 Location:* ${member.attributes?.location?.default}`) + } + if (member.emails.length > 0) { + const [email] = member.emails + details.push(`*✉️ Email:* `) + } + const profiles = Object.keys(member.username) + .filter((p) => !platforms.includes(p)) + .map((p) => { + const username = (member.username?.[p] || []).length > 0 ? member.username[p][0] : null + const url = + member.attributes?.url?.[p] || (username && integrationProfileUrl[p](username)) || null + return { + platform: p, + url, + } + }) + .filter((p) => !!p.url) + return { blocks: [ - { - type: 'section', - text: { - type: 'mrkdwn', - text: ':tada: *New member*', - }, - }, { type: 'header', text: { @@ -37,85 +67,22 @@ export const newMemberBlocks = (member) => { }, ] : []), - { - type: 'divider', - }, - ...(member.attributes.jobTitle?.default - ? [ - { - type: 'section', - fields: [ - { - type: 'mrkdwn', - text: '*Title/Role:*', - }, - { - type: 'mrkdwn', - text: member.attributes.jobTitle?.default || '-', - }, - ], - }, - { - type: 'divider', - }, - ] - : []), - ...(member.organizations.length > 0 - ? [ - { - type: 'section', - fields: [ - { - type: 'mrkdwn', - text: '*Organization:*', - }, - { - type: 'mrkdwn', - text: `<${`${API_CONFIG.frontendUrl}/organizations/${member.organizations[0].id}`}|${ - member.organizations[0].name - }>`, - }, - ], - }, - { - type: 'divider', - }, - ] - : []), - ...(reach > 0 + ...(details.length > 0 ? [ - { - type: 'section', - fields: [ - { - type: 'mrkdwn', - text: '*Followers:*', - }, - { - type: 'mrkdwn', - text: reach > 0 ? `${reach}` : '-', - }, - ], - }, { type: 'divider', }, - ] - : []), - ...(member.attributes?.location?.default - ? [ { type: 'section', - fields: [ - { - type: 'mrkdwn', - text: '*Location:*', - }, - { - type: 'mrkdwn', - text: member.attributes?.location?.default || '-', - }, - ], + text: { + type: 'mrkdwn', + text: details.length > 0 ? details.join('\n') : '\n', + }, + accessory: { + type: 'image', + image_url: member.attributes?.avatarUrl?.default ?? defaultAvatarUrl, + alt_text: 'computer thumbnail', + }, }, { type: 'divider', @@ -139,12 +106,27 @@ export const newMemberBlocks = (member) => { type: 'button', text: { type: 'plain_text', - text: `View ${integrationLabel[platform]} profile`, + text: `${integrationLabel[platform] ?? platform} profile`, emoji: true, }, url: member.attributes?.url?.[platform], })) .filter((action) => !!action.url), + ...(profiles.length > 0 + ? [ + { + type: 'overflow', + options: profiles.map(({ platform, url }) => ({ + text: { + type: 'plain_text', + text: `${integrationLabel[platform] ?? platform} profile`, + emoji: true, + }, + url, + })), + }, + ] + : []), ], }, ], diff --git a/services/libs/types/src/enums/platforms.ts b/services/libs/types/src/enums/platforms.ts index e588c39e41..eef48254c7 100644 --- a/services/libs/types/src/enums/platforms.ts +++ b/services/libs/types/src/enums/platforms.ts @@ -58,3 +58,22 @@ export const integrationLabel: Record = { [IntegrationType.GIT]: 'Git', [IntegrationType.HUBSPOT]: 'HubSpot', } + +// Backup url from username if profile url not present in member.attributes.url +export const integrationProfileUrl: Record string | null> = { + [IntegrationType.DEVTO]: (username) => `https://dev.to/${username}`, + [IntegrationType.SLACK]: (username) => `https://slack.com/${username}`, + [IntegrationType.REDDIT]: (username) => `https://reddit.com/user/${username}`, + [IntegrationType.DISCORD]: (username) => `https://discord.com/${username}`, + [IntegrationType.GITHUB]: (username) => `https://github.com/${username}`, + [IntegrationType.TWITTER]: (username) => `https://twitter.com/${username}`, + [IntegrationType.TWITTER_REACH]: (username) => `https://twitter.com/${username}`, + [IntegrationType.HACKER_NEWS]: (username) => `https://news.ycombinator.com/user?id=${username}`, + [IntegrationType.LINKEDIN]: (username) => + !username?.includes('private-') ? `https://linkedin.com/in/${username}` : null, + [IntegrationType.CROWD]: () => null, + [IntegrationType.STACKOVERFLOW]: (username) => `https://stackoverflow.com/users/${username}`, + [IntegrationType.DISCOURSE]: () => null, + [IntegrationType.GIT]: () => null, + [IntegrationType.HUBSPOT]: () => null, +}