Skip to content

Commit 608c399

Browse files
authored
Merge pull request #3313 from SeedCompany/notifications
2 parents febff99 + 65f63ee commit 608c399

29 files changed

+899
-50
lines changed

dbschema/migrations/00008-m146fzf.edgeql

Lines changed: 35 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dbschema/notifications.esdl

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
module default {
2+
abstract type Notification extending Mixin::Audited {
3+
readAt := .currentRecipient.readAt;
4+
unread := not exists .currentRecipient.readAt;
5+
single currentRecipient := assert_single((
6+
select .recipients filter .user = global currentUser
7+
));
8+
recipients := .<notification[is Notification::Recipient];
9+
}
10+
}
11+
12+
module Notification {
13+
type Recipient {
14+
required notification: default::Notification {
15+
on target delete delete source;
16+
};
17+
required user: default::User {
18+
on target delete delete source;
19+
};
20+
21+
readAt: datetime;
22+
}
23+
24+
type System extending default::Notification {
25+
required message: str;
26+
}
27+
abstract type Comment extending default::Notification {
28+
required comment: Comments::Comment {
29+
on target delete delete source;
30+
};
31+
}
32+
type CommentViaMention extending Comment;
33+
type CommentViaMembership extending Comment;
34+
}

src/app.module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { FilmModule } from './components/film/film.module';
1818
import { FundingAccountModule } from './components/funding-account/funding-account.module';
1919
import { LanguageModule } from './components/language/language.module';
2020
import { LocationModule } from './components/location/location.module';
21+
import { SystemNotificationModule } from './components/notification-system/system-notification.module';
2122
import { NotificationModule } from './components/notifications/notification.module';
2223
import { OrganizationModule } from './components/organization/organization.module';
2324
import { PartnerModule } from './components/partner/partner.module';
@@ -91,6 +92,7 @@ if (process.env.NODE_ENV !== 'production') {
9192
PromptsModule,
9293
PnpExtractionResultModule,
9394
NotificationModule,
95+
SystemNotificationModule,
9496
],
9597
})
9698
export class AppModule {}

src/common/markdown.scalar.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11
import { CustomScalar, Scalar } from '@nestjs/graphql';
22
import { GraphQLError, Kind, ValueNode } from 'graphql';
33

4-
@Scalar('InlineMarkdown')
5-
export class InlineMarkdownScalar
6-
implements CustomScalar<string, string | null>
7-
{
8-
description = 'A string that holds inline Markdown formatted text';
4+
@Scalar('Markdown')
5+
export class MarkdownScalar implements CustomScalar<string, string | null> {
6+
description = 'A string that holds Markdown formatted text';
97

108
parseLiteral(ast: ValueNode): string | null {
119
if (ast.kind !== Kind.STRING) {
@@ -22,3 +20,8 @@ export class InlineMarkdownScalar
2220
return value;
2321
}
2422
}
23+
24+
@Scalar('InlineMarkdown')
25+
export class InlineMarkdownScalar extends MarkdownScalar {
26+
description = 'A string that holds inline Markdown formatted text';
27+
}

src/common/scalars.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { CustomScalar } from '@nestjs/graphql';
33
import { GraphQLScalarType } from 'graphql';
44
import UploadScalar from 'graphql-upload/GraphQLUpload.mjs';
55
import { DateScalar, DateTimeScalar } from './luxon.graphql';
6-
import { InlineMarkdownScalar } from './markdown.scalar';
6+
import { InlineMarkdownScalar, MarkdownScalar } from './markdown.scalar';
77
import { RichTextScalar } from './rich-text.scalar';
88
import { UrlScalar } from './url.field';
99

@@ -16,5 +16,6 @@ export const getRegisteredScalars = (): Scalar[] => [
1616
RichTextScalar,
1717
UploadScalar,
1818
UrlScalar,
19+
MarkdownScalar,
1920
InlineMarkdownScalar,
2021
];

src/components/comments/comment.module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,13 @@ import { CommentResolver } from './comment.resolver';
1010
import { CommentService } from './comment.service';
1111
import { CommentableResolver } from './commentable.resolver';
1212
import { CreateCommentResolver } from './create-comment.resolver';
13+
import { CommentViaMentionNotificationModule } from './mention-notification/comment-via-mention-notification.module';
1314

1415
@Module({
1516
imports: [
1617
forwardRef(() => UserModule),
1718
forwardRef(() => AuthorizationModule),
19+
CommentViaMentionNotificationModule,
1820
],
1921
providers: [
2022
CreateCommentResolver,

src/components/comments/comment.service.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Injectable } from '@nestjs/common';
2+
import { difference } from 'lodash';
23
import {
34
ID,
45
InvalidIdForTypeException,
@@ -26,6 +27,7 @@ import {
2627
CreateCommentInput,
2728
UpdateCommentInput,
2829
} from './dto';
30+
import { CommentViaMentionNotificationService } from './mention-notification/comment-via-mention-notification.service';
2931

3032
type CommentableRef = ID | BaseNode | Commentable;
3133

@@ -36,6 +38,7 @@ export class CommentService {
3638
private readonly privileges: Privileges,
3739
private readonly resources: ResourceLoader,
3840
private readonly resourcesHost: ResourcesHost,
41+
private readonly mentionNotificationService: CommentViaMentionNotificationService,
3942
) {}
4043

4144
async create(input: CreateCommentInput, session: Session) {
@@ -45,12 +48,13 @@ export class CommentService {
4548
);
4649
perms.verifyCan('create');
4750

51+
let dto;
4852
try {
4953
const result = await this.repo.create(input, session);
5054
if (!result) {
5155
throw new ServerException('Failed to create comment');
5256
}
53-
return await this.readOne(result.id, session);
57+
dto = await this.repo.readOne(result.id);
5458
} catch (exception) {
5559
if (
5660
input.threadId &&
@@ -64,6 +68,11 @@ export class CommentService {
6468

6569
throw new ServerException('Failed to create comment', exception);
6670
}
71+
72+
const mentionees = this.mentionNotificationService.extract(dto);
73+
await this.mentionNotificationService.notify(mentionees, dto);
74+
75+
return this.secureComment(dto, session);
6776
}
6877

6978
async getPermissionsFromResource(resource: CommentableRef, session: Session) {
@@ -134,7 +143,14 @@ export class CommentService {
134143
this.privileges.for(session, Comment, object).verifyChanges(changes);
135144
await this.repo.update(object, changes);
136145

137-
return await this.readOne(input.id, session);
146+
const updated = await this.repo.readOne(object.id);
147+
148+
const prevMentionees = this.mentionNotificationService.extract(object);
149+
const nowMentionees = this.mentionNotificationService.extract(updated);
150+
const newMentionees = difference(prevMentionees, nowMentionees);
151+
await this.mentionNotificationService.notify(newMentionees, updated);
152+
153+
return this.secureComment(updated, session);
138154
}
139155

140156
async delete(id: ID, session: Session): Promise<void> {
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { ObjectType } from '@nestjs/graphql';
2+
import { keys as keysOf } from 'ts-transformer-keys';
3+
import { SecuredProps } from '~/common';
4+
import { e } from '~/core/edgedb';
5+
import { LinkTo, RegisterResource } from '~/core/resources';
6+
import { Notification } from '../../notifications';
7+
8+
@RegisterResource({ db: e.Notification.CommentViaMention })
9+
@ObjectType({
10+
implements: [Notification],
11+
})
12+
export class CommentViaMentionNotification extends Notification {
13+
static readonly Props = keysOf<CommentViaMentionNotification>();
14+
static readonly SecuredProps =
15+
keysOf<SecuredProps<CommentViaMentionNotification>>();
16+
17+
readonly comment: LinkTo<'Comment'>;
18+
}
19+
20+
declare module '~/core/resources/map' {
21+
interface ResourceMap {
22+
CommentMentionedNotification: typeof CommentViaMentionNotification;
23+
}
24+
interface ResourceDBMap {
25+
CommentMentionedNotification: typeof e.Notification.CommentViaMention;
26+
}
27+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { Module } from '@nestjs/common';
2+
import { NotificationModule } from '../../notifications';
3+
import { CommentViaMentionNotificationResolver } from './comment-via-mention-notification.resolver';
4+
import { CommentViaMentionNotificationService } from './comment-via-mention-notification.service';
5+
import { CommentViaMentionNotificationStrategy } from './comment-via-mention-notification.strategy';
6+
7+
@Module({
8+
imports: [NotificationModule],
9+
providers: [
10+
CommentViaMentionNotificationResolver,
11+
CommentViaMentionNotificationStrategy,
12+
CommentViaMentionNotificationService,
13+
],
14+
exports: [CommentViaMentionNotificationService],
15+
})
16+
export class CommentViaMentionNotificationModule {}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { Parent, ResolveField, Resolver } from '@nestjs/graphql';
2+
import { Loader, LoaderOf } from '~/core';
3+
import { CommentLoader } from '../comment.loader';
4+
import { Comment } from '../dto';
5+
import { CommentViaMentionNotification as Notification } from './comment-via-mention-notification.dto';
6+
7+
@Resolver(Notification)
8+
export class CommentViaMentionNotificationResolver {
9+
@ResolveField(() => Comment)
10+
async comment(
11+
@Parent() { comment }: Notification,
12+
@Loader(CommentLoader) comments: LoaderOf<CommentLoader>,
13+
) {
14+
return await comments.load(comment.id);
15+
}
16+
}

0 commit comments

Comments
 (0)