Skip to content

Commit

Permalink
🚀 Release 3.2.0 (#4797)
Browse files Browse the repository at this point in the history
  • Loading branch information
thesan authored Mar 7, 2024
2 parents d185fda + 298c121 commit 28824de
Show file tree
Hide file tree
Showing 33 changed files with 901 additions and 511 deletions.
13 changes: 11 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [3.2.0] - 2024-03-07

### Added
- Role mention.

### Fixed
- Proposal default order.

## [3.1.0] - 2024-02-27

### Added
Expand All @@ -31,7 +39,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Fixed
- Searching for members by id.
- General search box behavior.
- General search box behavior.
- Show members active roles only.
- Show creation date on member profiles.
- Fix opening creation for lead with separate role and controller accounts.
Expand Down Expand Up @@ -335,7 +343,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [0.1.1] - 2022-12-02

[unreleased]: https://github.com/Joystream/pioneer/compare/v3.1.0...HEAD
[unreleased]: https://github.com/Joystream/pioneer/compare/v3.2.0...HEAD
[3.2.0]: https://github.com/Joystream/pioneer/compare/v3.1.0...v3.2.0
[3.1.0]: https://github.com/Joystream/pioneer/compare/v3.0.0...v3.1.0
[3.0.0]: https://github.com/Joystream/pioneer/compare/v2.6.0...v3.0.0
[2.6.0]: https://github.com/Joystream/pioneer/compare/v2.5.0...v2.6.0
Expand Down
4 changes: 2 additions & 2 deletions packages/markdown-editor/dist/ckeditor.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion packages/markdown-editor/dist/ckeditor.js.map

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions packages/markdown-editor/src/MarkdownEditor.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@ function prepareLink({ type, itemId, addon }) {
case 'member': {
return `#mention?member-id=${itemId}`
}
case 'role': {
return `#mention?role=${itemId}`
}
case 'proposal': {
return `#mention?proposal-id=${itemId}`
}
Expand Down
2 changes: 1 addition & 1 deletion packages/server/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "server",
"version": "2.4.1",
"version": "3.2.0",
"license": "GPL-3.0-only",
"scripts": {
"prisma": "prisma",
Expand Down
48 changes: 28 additions & 20 deletions packages/server/src/notifier/createNotifications.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,30 @@
import { Prisma } from '@prisma/client'
import { Prisma, Subscription } from '@prisma/client'
import { request } from 'graphql-request'
import { clone, groupBy, isEqual, isObject, mapValues } from 'lodash'
import { info, verbose, warn } from 'npmlog'

import { QUERY_NODE_ENDPOINT, STARTING_BLOCK } from '@/common/config'
import { prisma } from '@/common/prisma'
import { GetNotificationEventsDocument } from '@/common/queries'
import { GetCurrentRolesDocument, GetNotificationEventsDocument } from '@/common/queries'
import { count, getTypename } from '@/common/utils'

import { toNotificationEvents } from './model/event'
import { notificationsFromEvent } from './model/notifications'
import { filterSubscriptions } from './model/subscriptionFilters'

interface ProgressDoc {
block: number
eventIds: string[]
}
const isProgressDoc = (consumed: any): consumed is ProgressDoc => isObject(consumed)
const defaultProgress: ProgressDoc = { block: STARTING_BLOCK, eventIds: [] }
const PROGRESS_KEY = { key: 'Progress' }
import { Notification, NotificationEvent, PotentialNotif, ProgressDocument } from './types'

export const createNotifications = async (): Promise<void> => {
// Check the last block that where processed
const { value: initialProgress } = (await prisma.store.findUnique({ where: PROGRESS_KEY })) ?? {}
const progress: ProgressDoc =
isProgressDoc(initialProgress) && initialProgress.block > STARTING_BLOCK ? clone(initialProgress) : defaultProgress
const progress: ProgressDocument =
isProgressDocument(initialProgress) && initialProgress.block > STARTING_BLOCK
? clone(initialProgress)
: defaultProgress

const allMembers = (await prisma.member.findMany()).map(({ id, receiveEmails }) => ({ id, receiveEmails }))

const qnRoles = await request(QUERY_NODE_ENDPOINT, GetCurrentRolesDocument)

/* eslint-disable-next-line no-constant-condition */
while (true) {
// Save the current process
Expand All @@ -39,33 +36,40 @@ export const createNotifications = async (): Promise<void> => {

// Fetch events from the query nodes and break if non are found
const qnVariables = { from: progress.block, exclude: progress.eventIds }
const qnData = await request(QUERY_NODE_ENDPOINT, GetNotificationEventsDocument, qnVariables)
const qnEvents = await request(QUERY_NODE_ENDPOINT, GetNotificationEventsDocument, qnVariables)
info(
'QN events',
`Received ${qnData.events.length} new events`,
mapValues(groupBy(qnData.events, getTypename), count),
`Received ${qnEvents.events.length} new events`,
mapValues(groupBy(qnEvents.events, getTypename), count),
`from block ${progress.block} onward excluding`,
progress.eventIds
)

if (qnData.events.length === 0) break
if (qnEvents.events.length === 0) break

// Generate the potential notification based on the query nodes data
const events = await Promise.all(qnData.events.map(toNotificationEvents(allMembers.map(({ id }) => id))))
const events: NotificationEvent[] = await Promise.all(
qnEvents.events.map(
toNotificationEvents(
allMembers.map(({ id }) => id),
qnRoles
)
)
)

// Update the progress
progress.block = Math.max(progress.block, ...events.map((event) => event.inBlock))
progress.eventIds = events.flatMap((event) => (event.inBlock === progress.block ? event.id : []))

const potentialNotifs = events.flatMap((event) => event.potentialNotifications)
const potentialNotifs: PotentialNotif[] = events.flatMap((event) => event.potentialNotifications)
if (potentialNotifs.length === 0) continue

// Fetch subscription related to the events
const subscriptionFilter = { OR: filterSubscriptions(potentialNotifs) }
const subscriptions = await prisma.subscription.findMany({ where: subscriptionFilter })
const subscriptions: Subscription[] = await prisma.subscription.findMany({ where: subscriptionFilter })

// Create and save new notifications
const notifications = events.flatMap(notificationsFromEvent(subscriptions, allMembers))
const notifications: Notification[] = events.flatMap(notificationsFromEvent(subscriptions, allMembers))
info('New notifications', 'Saving', notifications.length, 'new notifications')
verbose(
'New notifications',
Expand All @@ -79,3 +83,7 @@ export const createNotifications = async (): Promise<void> => {
}
}
}

const isProgressDocument = (consumed: any): consumed is ProgressDocument => isObject(consumed)
const defaultProgress: ProgressDocument = { block: STARTING_BLOCK, eventIds: [] }
const PROGRESS_KEY = { key: 'Progress' }
4 changes: 0 additions & 4 deletions packages/server/src/notifier/model/email/utils/forum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ interface ForumPost {
author: string
threadId: string
thread: string
text: string
}
const cachedForumPosts: { [id: string]: ForumPost } = {}
export const getForumPost = async (id: string): Promise<ForumPost> => {
Expand All @@ -21,7 +20,6 @@ export const getForumPost = async (id: string): Promise<ForumPost> => {
author: post.author.handle,
threadId: post.thread.id,
thread: post.thread.title,
text: post.text,
}
}

Expand All @@ -31,7 +29,6 @@ export const getForumPost = async (id: string): Promise<ForumPost> => {
interface ForumThread {
author: string
title: string
text?: string
}
const cachedForumThreads: { [id: string]: ForumThread } = {}
export const getForumThread = async (id: string): Promise<ForumThread> => {
Expand All @@ -44,7 +41,6 @@ export const getForumThread = async (id: string): Promise<ForumThread> => {
cachedForumThreads[id] = {
author: thread.author.handle,
title: thread.title,
text: thread.initialPost?.text,
}
}

Expand Down
23 changes: 18 additions & 5 deletions packages/server/src/notifier/model/event/forum.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,23 @@
import { pick, uniq } from 'lodash'

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

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

export const fromPostAddedEvent: NotifEventFromQNEvent<'PostAddedEvent'> = async (event, buildEvents) => {
export const fromPostAddedEvent: NotifEventFromQNEvent<'PostAddedEvent', [GetCurrentRolesQuery]> = async (
event,
buildEvents,
roles
) => {
const postAddedEvent = useFragment(PostAddedEventFieldsFragmentDoc, event)
const post = postAddedEvent.post

const mentionedMemberIds = getMentionedMemberIds(post.text)
const mentionedMemberIds = getMentionedMemberIds(post.text, roles)
const repliedToMemberId = post.repliesTo && [Number(post.repliesTo.authorId)]
const earlierPosts = post.thread.posts.filter(isOlderThan(post))
const earlierAuthors = uniq(earlierPosts.map((post) => Number(post.authorId)))
Expand All @@ -27,11 +36,15 @@ export const fromPostAddedEvent: NotifEventFromQNEvent<'PostAddedEvent'> = async
])
}

export const fromThreadCreatedEvent: NotifEventFromQNEvent<'ThreadCreatedEvent'> = async (event, buildEvents) => {
export const fromThreadCreatedEvent: NotifEventFromQNEvent<'ThreadCreatedEvent', [GetCurrentRolesQuery]> = async (
event,
buildEvents,
roles
) => {
const threadCreatedEvent = useFragment(ThreadCreatedEventFieldsFragmentDoc, event)
const thread = threadCreatedEvent.thread

const mentionedMemberIds = getMentionedMemberIds(threadCreatedEvent.text)
const mentionedMemberIds = getMentionedMemberIds(threadCreatedEvent.text, roles)
const parentCategoryIds = await getParentCategories(thread.categoryId)

const eventData = pick(threadCreatedEvent, 'inBlock', 'id')
Expand Down
12 changes: 6 additions & 6 deletions packages/server/src/notifier/model/event/index.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,23 @@
import { match } from 'ts-pattern'

import { GetNotificationEventsQuery } from '@/common/queries'
import { GetCurrentRolesQuery, GetNotificationEventsQuery } from '@/common/queries'
import { NotificationEvent } from '@/notifier/types'

import {
fromElectionAnnouncingStartedEvent,
fromElectionRevealingStartedEvent,
fromElectionVotingStartedEvent,
} from './election'
import { fromPostAddedEvent, fromThreadCreatedEvent } from './forum'
import { NotificationEvent } from './utils'
import { buildEvents } from './utils/buildEvent'
import { ImplementedQNEvent } from './utils/types'

export { NotificationEvent, PotentialNotif, isGeneralPotentialNotif, isEntityPotentialNotif } from './utils'
export { isGeneralPotentialNotif, isEntityPotentialNotif } from './utils'

type AnyQNEvent = GetNotificationEventsQuery['events'][0]

export const toNotificationEvents =
(allMemberIds: number[]) =>
(allMemberIds: number[], roles: GetCurrentRolesQuery) =>
async (anyEvent: AnyQNEvent): Promise<NotificationEvent> => {
// NOTE: The conversion to ImplementedQNEvent assumes that the QN will only return
// events with fragments defined in the codegen document.
Expand All @@ -26,8 +26,8 @@ export const toNotificationEvents =
const build = buildEvents(allMemberIds, event)

const notifEvent = match(event)
.with({ __typename: 'PostAddedEvent' }, (e) => fromPostAddedEvent(e, build))
.with({ __typename: 'ThreadCreatedEvent' }, (e) => fromThreadCreatedEvent(e, build))
.with({ __typename: 'PostAddedEvent' }, (e) => fromPostAddedEvent(e, build, roles))
.with({ __typename: 'ThreadCreatedEvent' }, (e) => fromThreadCreatedEvent(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
3 changes: 2 additions & 1 deletion packages/server/src/notifier/model/event/utils/buildEvent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ import { verbose } from 'npmlog'

import { getTypename } from '@/common/utils'
import { isDefaultSubscription } from '@/notifier/model/subscriptionKinds'
import { NotificationEvent, PotentialNotif } from '@/notifier/types'

import { BuildEvents, ImplementedQNEvent, NotificationEvent, NotifsBuilder, PotentialNotif } from './types'
import { BuildEvents, ImplementedQNEvent, NotifsBuilder } from './types'

export const buildEvents =
(allMemberIds: number[], event: ImplementedQNEvent): BuildEvents =>
Expand Down
58 changes: 50 additions & 8 deletions packages/server/src/notifier/model/event/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,65 @@
import { uniq } from 'lodash'

import { EntitiyPotentialNotif, GeneralPotentialNotif, PotentialNotif } from './types'

import { GetCurrentRolesQuery } from '@/common/queries'
import { EntityPotentialNotif, GeneralPotentialNotif, PotentialNotif } from '@/notifier/types'
export { getParentCategories } from './getParentCategories'
export { NotifEventFromQNEvent, NotificationEvent, PotentialNotif } from './types'
export { NotifEventFromQNEvent } from './types'

export const isGeneralPotentialNotif = (p: PotentialNotif): p is GeneralPotentialNotif => 'relatedMembers' in p
export const isEntityPotentialNotif = (p: PotentialNotif): p is EntitiyPotentialNotif => 'relatedEntityId' in p
export const isEntityPotentialNotif = (p: PotentialNotif): p is EntityPotentialNotif => 'relatedEntityId' in p

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))

export const getMentionedMemberIds = (text: string): number[] =>
export const getMentionedMemberIds = (text: string, roles: GetCurrentRolesQuery): number[] =>
uniq(
Array.from(text.matchAll(/\[@[-.0-9A-Z\\_a-z]+\]\(#mention\?member-id=(\d+)\)/g)).flatMap((match) => {
const id = match[1] && Number(match[1])
return !id || isNaN(id) ? [] : Number(id)
Array.from(text.matchAll(/\[@[^\]\n]*\]\(#mention\?(member-id|role)=([^)\n]+?)\)/g)).flatMap((match) => {
const type = match[1]
const id = match[2]
if (!type || !id) return []

if (type === 'member-id') {
const memberId = Number(id)
return isNaN(memberId) ? [] : memberId
}

if (type !== 'role') return []

const councilIds = roles.electedCouncils.at(0)?.councilMembers.map((member) => Number(member.memberId)) ?? []
const workers = roles.workers
const leads = workers.filter((worker) => worker.isLead)
const [role, groupId] = id.split('_')

switch (role) {
case 'dao':
return [...councilIds, ...roles.workers.map((worker) => Number(worker.membershipId))]

case 'council':
return councilIds

case 'workers': {
if (!groupId) {
return []
}
return workers.filter((worker) => worker.groupId === groupId).map((worker) => Number(worker.membershipId))
}

case 'leads':
return leads.map((lead) => Number(lead.membershipId))

case 'lead': {
if (!groupId) {
return []
}
const leadId = leads.find((lead) => lead.groupId === groupId)?.membershipId ?? []
return leadId ? [Number(leadId)] : []
}

default:
return []
}
})
)
26 changes: 5 additions & 21 deletions packages/server/src/notifier/model/event/utils/types.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,12 @@
import { GetNotificationEventsQuery } from '@/common/queries'
import { DocWithFragments } from '@/common/utils/types'
import { EntitySubscriptionKind, GeneralSubscriptionKind } from '@/notifier/model/subscriptionKinds'
import { EntityPotentialNotif, GeneralPotentialNotif, NotificationEvent } from '@/notifier/types'

export type ImplementedQNEvent = DocWithFragments<Required<GetNotificationEventsQuery['events'][0]>>
type QNEvent<T extends ImplementedQNEvent['__typename']> = { __typename: T } & ImplementedQNEvent

type GeneralEventParams = {
kind: GeneralSubscriptionKind
relatedMembers: 'ANY' | { ids: number[] }
isDefault: boolean
}
type EntityEventParams = { kind: EntitySubscriptionKind; relatedEntityId: string }
export type GeneralPotentialNotif = { priority: number } & GeneralEventParams
export type EntitiyPotentialNotif = { priority: number } & EntityEventParams

export type PotentialNotif = GeneralPotentialNotif | EntitiyPotentialNotif

type PartialNotif = Omit<GeneralPotentialNotif, 'priority'> | Omit<EntitiyPotentialNotif, 'priority'>

export interface NotificationEvent {
id: string
inBlock: number
entityId: string
potentialNotifications: PotentialNotif[]
}
type PartialNotif = Omit<GeneralPotentialNotif, 'priority'> | Omit<EntityPotentialNotif, 'priority'>

export interface NotifsBuilder {
generalEvent: (kind: GeneralSubscriptionKind, members: 'ANY' | (number | string)[]) => PartialNotif | []
Expand All @@ -37,7 +20,8 @@ export type BuildEvents = (
build: (b: NotifsBuilder) => (PartialNotif | [])[]
) => NotificationEvent

export type NotifEventFromQNEvent<T extends ImplementedQNEvent['__typename']> = (
export type NotifEventFromQNEvent<T extends ImplementedQNEvent['__typename'], Args extends any[] = []> = (
event: QNEvent<T>,
buildEvents: BuildEvents
buildEvents: BuildEvents,
...args: Args
) => Promise<NotificationEvent>
Loading

0 comments on commit 28824de

Please sign in to comment.