Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

📨 Notify users of proposal discussions #4849

Merged
merged 6 commits into from
May 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
-- AlterEnum
-- This migration adds more than one value to an enum.
-- With PostgreSQL versions 11 and earlier, this is not possible
-- in a single migration. This can be worked around by creating
-- multiple migrations, each migration adding only one value to
-- the enum.


ALTER TYPE "NotificationKind" ADD VALUE 'PROPOSAL_DISCUSSION_MENTION';
ALTER TYPE "NotificationKind" ADD VALUE 'PROPOSAL_DISCUSSION_REPLY';
ALTER TYPE "NotificationKind" ADD VALUE 'PROPOSAL_DISCUSSION_CREATOR';
ALTER TYPE "NotificationKind" ADD VALUE 'PROPOSAL_DISCUSSION_CONTRIBUTOR';
ALTER TYPE "NotificationKind" ADD VALUE 'PROPOSAL_DISCUSSION_ALL';
ALTER TYPE "NotificationKind" ADD VALUE 'PROPOSAL_ENTITY_DISCUSSION';
10 changes: 6 additions & 4 deletions packages/server/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -87,9 +87,11 @@ enum NotificationKind {
// PROPOSAL_VOTE_CREATOR

// Proposal: ProposalDiscussionPostCreatedEvent
// PROPOSAL_DISCUSSION_MENTION
// PROPOSAL_DISCUSSION_CREATOR
// PROPOSAL_DISCUSSION_CONTRIBUTOR
PROPOSAL_DISCUSSION_MENTION
PROPOSAL_DISCUSSION_REPLY
PROPOSAL_DISCUSSION_CREATOR
PROPOSAL_DISCUSSION_CONTRIBUTOR
PROPOSAL_DISCUSSION_ALL

// Referendum
ELECTION_ANNOUNCING_STARTED
Expand All @@ -108,7 +110,7 @@ enum NotificationKind {
// Proposal
// PROPOSAL_ENTITY_STATUS
// PROPOSAL_ENTITY_VOTE
// PROPOSAL_ENTITY_DISCUSSION
PROPOSAL_ENTITY_DISCUSSION
}

enum NotificationEmailStatus {
Expand Down
2 changes: 2 additions & 0 deletions packages/server/src/notifier/model/email/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
fromElectionVotingStartedNotification,
} from './election'
import { fromPostAddedNotification, fromThreadCreatedNotification } from './forum'
import { fromProposalPostCreatedNotification } from './proposal'
import { Notification, hasEmailAddress } from './utils'

export const createEmailNotifier =
Expand All @@ -25,6 +26,7 @@ export const createEmailNotifier =
const emailHandlers = [
fromPostAddedNotification,
fromThreadCreatedNotification,
fromProposalPostCreatedNotification,
fromElectionAnnouncingStartedNotification,
fromElectionVotingStartedNotification,
fromElectionRevealingStartedNotification,
Expand Down
68 changes: 68 additions & 0 deletions packages/server/src/notifier/model/email/proposal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { match } from 'ts-pattern'

import { PIONEER_URL } from '@/common/config'
import { renderPioneerEmail } from '@/common/email-templates/pioneer-email'

import { EmailFromNotificationFn } from './utils'
import { getProposalPost } from './utils/proposal'

export const fromProposalPostCreatedNotification: EmailFromNotificationFn = async ({ id, kind, entityId, member }) => {
if (
kind !== 'PROPOSAL_DISCUSSION_MENTION' &&
kind !== 'PROPOSAL_DISCUSSION_REPLY' &&
kind !== 'PROPOSAL_DISCUSSION_CREATOR' &&
kind !== 'PROPOSAL_DISCUSSION_CONTRIBUTOR' &&
kind !== 'PROPOSAL_DISCUSSION_ALL' &&
kind !== 'PROPOSAL_ENTITY_DISCUSSION'
) {
return
}

if (!entityId) {
throw Error(`Missing proposal discussion post id in notification ${kind}, with id: ${id}`)
}

const { author, proposal, proposalId } = await getProposalPost(entityId)

const emailSubject = `[Pioneer] proposal: ${proposal}`

const emailSummary: string = match(kind)
.with('PROPOSAL_DISCUSSION_MENTION', () => `${author} mentioned you regarding the proposal ${proposal}.`)
.with('PROPOSAL_DISCUSSION_REPLY', () => `${author} replied to your post regarding the proposal ${proposal}.`)
.with(
'PROPOSAL_DISCUSSION_CREATOR',
'PROPOSAL_DISCUSSION_CONTRIBUTOR',
'PROPOSAL_DISCUSSION_ALL',
'PROPOSAL_ENTITY_DISCUSSION',
() => `${author} posted regarding the proposal ${proposal}.`
)
.exhaustive()

const emailText: string = match(kind)
.with('PROPOSAL_DISCUSSION_MENTION', () => `${author} mentioned you regarding the proposal ${proposal}.`)
.with('PROPOSAL_DISCUSSION_REPLY', () => `${author} replied to your post regarding the proposal ${proposal}.`)
.with(
'PROPOSAL_DISCUSSION_CREATOR',
'PROPOSAL_DISCUSSION_CONTRIBUTOR',
'PROPOSAL_DISCUSSION_ALL',
'PROPOSAL_ENTITY_DISCUSSION',
() => `${author} posted regarding the proposal ${proposal}.`
)
.exhaustive()

const emailHtml = renderPioneerEmail({
memberHandle: member.name,
summary: emailSummary,
text: emailText,
button: {
label: 'See on Pioneer',
href: `${PIONEER_URL}/#/proposals/preview/${proposalId}?post=${entityId}`,
},
})

return {
subject: emailSubject,
html: emailHtml,
to: member.email,
}
}
28 changes: 28 additions & 0 deletions packages/server/src/notifier/model/email/utils/proposal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { request } from 'graphql-request'
import { memoize } from 'lodash'

import { QUERY_NODE_ENDPOINT } from '@/common/config'
import { GetProposalDiscussionPostDocument } from '@/common/queries'

interface ProposalDiscussionPost {
author: string
proposal: string
proposalId: string
}

export const getProposalPost = memoize(async (id: string): Promise<ProposalDiscussionPost> => {
const { proposalDiscussionPostByUniqueInput: post } = await request(
QUERY_NODE_ENDPOINT,
GetProposalDiscussionPostDocument,
{ id }
)
if (!post) {
throw Error(`Failed to fetch proposal discussion post ${id} on the QN`)
}

return {
author: post.author.handle,
proposal: post.discussionThread.proposal.title,
proposalId: post.discussionThread.proposal.id,
}
})
2 changes: 2 additions & 0 deletions packages/server/src/notifier/model/event/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
fromElectionVotingStartedEvent,
} from './election'
import { fromPostAddedEvent, fromThreadCreatedEvent } from './forum'
import { fromProposalPostAddedEvent } from './proposal'
import { buildEvents } from './utils/buildEvent'
import { ImplementedQNEvent } from './utils/types'

Expand All @@ -28,6 +29,7 @@ export const toNotificationEvents =
const notifEvent = match(event)
.with({ __typename: 'PostAddedEvent' }, (e) => fromPostAddedEvent(e, build, roles))
.with({ __typename: 'ThreadCreatedEvent' }, (e) => fromThreadCreatedEvent(e, build, roles))
.with({ __typename: 'ProposalDiscussionPostCreatedEvent' }, (e) => fromProposalPostAddedEvent(e, build, roles))
.with({ __typename: 'AnnouncingPeriodStartedEvent' }, (e) => fromElectionAnnouncingStartedEvent(e, build))
.with({ __typename: 'VotingPeriodStartedEvent' }, (e) => fromElectionVotingStartedEvent(e, build))
.with({ __typename: 'RevealingStageStartedEvent' }, (e) => fromElectionRevealingStartedEvent(e, build))
Expand Down
33 changes: 33 additions & 0 deletions packages/server/src/notifier/model/event/proposal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { pick, uniq } from 'lodash'

import {
GetCurrentRolesQuery,
ProposalDiscussionPostCreatedEventFieldsFragmentDoc,
useFragment,
} from '@/common/queries'

import { NotifEventFromQNEvent, isOlderThan, getMentionedMemberIds } from './utils'

export const fromProposalPostAddedEvent: NotifEventFromQNEvent<
'ProposalDiscussionPostCreatedEvent',
[GetCurrentRolesQuery]
> = async (event, buildEvents, roles) => {
const postCreatedEvent = useFragment(ProposalDiscussionPostCreatedEventFieldsFragmentDoc, event)
const post = postCreatedEvent.post

const mentionedMemberIds = getMentionedMemberIds(post.text, roles)
const repliedToMemberId = post.repliesTo && [Number(post.repliesTo.authorId)]
const earlierPosts = post.discussionThread.posts.filter(isOlderThan(post))
const earlierAuthors = uniq(earlierPosts.map((post) => Number(post.authorId)))

const eventData = pick(postCreatedEvent, 'inBlock', 'id')

return buildEvents(eventData, post.id, [post.authorId], ({ generalEvent, entityEvent }) => [
generalEvent('PROPOSAL_DISCUSSION_MENTION', mentionedMemberIds),
generalEvent('PROPOSAL_DISCUSSION_REPLY', repliedToMemberId ?? []),
generalEvent('PROPOSAL_DISCUSSION_CREATOR', [post.discussionThread.proposal.creatorId]),
generalEvent('PROPOSAL_DISCUSSION_CONTRIBUTOR', earlierAuthors),
entityEvent('PROPOSAL_ENTITY_DISCUSSION', post.discussionThread.proposal.id),
generalEvent('PROPOSAL_DISCUSSION_ALL', 'ANY'),
])
}
2 changes: 1 addition & 1 deletion packages/server/src/notifier/model/event/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ type Created = { createdAt: any }
export const isOlderThan =
<A extends Created>(a: A) =>
<B extends Created>(b: B): boolean =>
Date.parse(String(a)) > Date.parse(String(b))
Date.parse(String(a.createdAt)) > Date.parse(String(b.createdAt))

export const getMentionedMemberIds = (text: string, roles: GetCurrentRolesQuery): number[] =>
uniq(
Expand Down
21 changes: 13 additions & 8 deletions packages/server/src/notifier/model/subscriptionKinds.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@ export type EntitySubscriptionKind = (typeof EntitySubscriptionKind)[keyof typeo
export const EntitySubscriptionKind = extract(
'FORUM_THREAD_ENTITY_POST',
'FORUM_CATEGORY_ENTITY_POST',
'FORUM_CATEGORY_ENTITY_THREAD'
'FORUM_CATEGORY_ENTITY_THREAD',
// 'FORUM_WATCHED_CATEGORY_SUBCATEGORY',

// 'PROPOSAL_ENTITY_STATUS',
// 'PROPOSAL_ENTITY_VOTE',
// 'PROPOSAL_ENTITY_DISCUSSION'
'PROPOSAL_ENTITY_DISCUSSION'
)

export type GeneralSubscriptionKind = (typeof GeneralSubscriptionKind)[keyof typeof GeneralSubscriptionKind]
Expand All @@ -32,9 +32,11 @@ export const GeneralSubscriptionKind = extract(
// 'PROPOSAL_STATUS_CREATOR',
// 'PROPOSAL_VOTE_ALL',
// 'PROPOSAL_VOTE_CREATOR',
// 'PROPOSAL_DISCUSSION_MENTION',
// 'PROPOSAL_DISCUSSION_CREATOR',
// 'PROPOSAL_DISCUSSION_CONTRIBUTOR',
'PROPOSAL_DISCUSSION_MENTION',
'PROPOSAL_DISCUSSION_REPLY',
'PROPOSAL_DISCUSSION_CREATOR',
'PROPOSAL_DISCUSSION_CONTRIBUTOR',
'PROPOSAL_DISCUSSION_ALL',

'ELECTION_ANNOUNCING_STARTED',
'ELECTION_VOTING_STARTED',
Expand All @@ -49,11 +51,14 @@ const defaultSubscriptions: GeneralSubscriptionKind[] = [
'FORUM_THREAD_CREATOR',
'FORUM_THREAD_CONTRIBUTOR',
'FORUM_THREAD_MENTION',

// 'PROPOSAL_STATUS_CREATOR',
// 'PROPOSAL_VOTE_CREATOR',
// 'PROPOSAL_DISCUSSION_MENTION',
// 'PROPOSAL_DISCUSSION_CREATOR',
// 'PROPOSAL_DISCUSSION_CONTRIBUTOR',
'PROPOSAL_DISCUSSION_MENTION',
'PROPOSAL_DISCUSSION_REPLY',
'PROPOSAL_DISCUSSION_CREATOR',
'PROPOSAL_DISCUSSION_CONTRIBUTOR',

'ELECTION_ANNOUNCING_STARTED',
'ELECTION_VOTING_STARTED',
'ELECTION_REVEALING_STARTED',
Expand Down
13 changes: 13 additions & 0 deletions packages/server/src/notifier/queries/entities/proposal.graphql
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
query GetProposalDiscussionPost($id: ID!) {
proposalDiscussionPostByUniqueInput(where: { id: $id }) {
author {
handle
}
discussionThread {
proposal {
id
title
}
}
}
}
53 changes: 28 additions & 25 deletions packages/server/src/notifier/queries/events.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ fragment PostAddedEventFields on PostAddedEvent {
posts {
authorId
createdAt
text
}
categoryId
}
Expand Down Expand Up @@ -53,27 +52,30 @@ fragment ElectionRevealingStartedFields on RevealingStageStartedEvent {
inBlock
}

# fragment ProposalDiscussionPostCreatedEventFields on ProposalDiscussionPostCreatedEvent {
# __typename
# id
# inBlock
# post {
# id
# authorId
# text
# discussionThread {
# id
# proposal {
# id
# creatorId
# }
# posts {
# authorId
# text
# }
# }
# }
# }
fragment ProposalDiscussionPostCreatedEventFields on ProposalDiscussionPostCreatedEvent {
__typename
id
inBlock
post {
id
authorId
createdAt
text
repliesTo {
authorId
}
discussionThread {
proposal {
id # Users subscribe to proposals rather than proposal discussions
creatorId
}
posts {
authorId
createdAt
}
}
}
}

query GetNotificationEvents($from: Int, $exclude: [ID!]) {
events(
Expand All @@ -85,6 +87,7 @@ query GetNotificationEvents($from: Int, $exclude: [ID!]) {
VotingPeriodStartedEvent
RevealingStageStartedEvent
# PostTextUpdatedEvent
ProposalDiscussionPostCreatedEvent
]
inBlock_gte: $from
NOT: { id_in: $exclude }
Expand All @@ -106,8 +109,8 @@ query GetNotificationEvents($from: Int, $exclude: [ID!]) {
... on RevealingStageStartedEvent {
...ElectionRevealingStartedFields
}
# ... on ProposalDiscussionPostCreatedEvent {
# ...ProposalDiscussionPostCreatedEventFields
# }
... on ProposalDiscussionPostCreatedEvent {
...ProposalDiscussionPostCreatedEventFields
}
}
}
1 change: 1 addition & 0 deletions packages/server/test/_mocks/notifier/events/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './forum'
export * from './proposal'
Loading
Loading