From 47d168a29b237002455c5e7f381d49a8a6e95ace Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Wed, 31 Jan 2024 15:01:27 -0800 Subject: [PATCH] Add notifications for document and collection access (#6460) * Add notification for added to document * Add notifications for document and collection access * Add notification delay * fix: Collection notifications not appearing * Add notification settings --- app/models/Notification.ts | 29 ++++- app/scenes/Settings/Notifications.tsx | 17 +++ .../server/tasks/DeliverWebhookTask.ts | 4 +- .../templates/CollectionSharedEmail.tsx | 102 ++++++++++++++++++ .../emails/templates/DocumentSharedEmail.tsx | 98 +++++++++++++++++ server/models/Collection.ts | 10 ++ server/models/Document.ts | 10 +- server/models/Event.ts | 2 + server/models/Notification.ts | 5 + server/queues/processors/EmailsProcessor.ts | 34 ++++++ .../processors/NotificationsProcessor.ts | 30 ++++++ .../CollectionAddUserNotificationsTask.ts | 32 ++++++ .../tasks/DocumentAddUserNotificationsTask.ts | 32 ++++++ server/routes/api/collections/collections.ts | 28 ++--- server/routes/api/documents/documents.ts | 8 +- server/types.ts | 17 ++- shared/i18n/locales/en_US/translation.json | 6 ++ shared/types.ts | 4 + 18 files changed, 437 insertions(+), 31 deletions(-) create mode 100644 server/emails/templates/CollectionSharedEmail.tsx create mode 100644 server/emails/templates/DocumentSharedEmail.tsx create mode 100644 server/queues/tasks/CollectionAddUserNotificationsTask.ts create mode 100644 server/queues/tasks/DocumentAddUserNotificationsTask.ts diff --git a/app/models/Notification.ts b/app/models/Notification.ts index 4e27e04c5..2c6c66b23 100644 --- a/app/models/Notification.ts +++ b/app/models/Notification.ts @@ -7,6 +7,7 @@ import { documentPath, settingsPath, } from "~/utils/routeHelpers"; +import Collection from "./Collection"; import Comment from "./Comment"; import Document from "./Document"; import User from "./User"; @@ -57,6 +58,14 @@ class Notification extends Model { */ collectionId?: string; + /** + * The collection that the notification is associated with. + */ + @Relation(() => Collection, { onDelete: "cascade" }) + collection?: Collection; + + commentId?: string; + /** * The comment that the notification is associated with. */ @@ -114,6 +123,10 @@ class Notification extends Model { return t("mentioned you in"); case NotificationEventType.CreateComment: return t("left a comment on"); + case NotificationEventType.AddUserToDocument: + return t("shared"); + case NotificationEventType.AddUserToCollection: + return t("invited you to"); default: return this.event; } @@ -126,7 +139,13 @@ class Notification extends Model { * @returns The subject */ get subject() { - return this.document?.title; + if (this.documentId) { + return this.document?.title ?? "a document"; + } + if (this.collectionId) { + return this.collection?.name ?? "a collection"; + } + return "Unknown"; } /** @@ -142,20 +161,22 @@ class Notification extends Model { case NotificationEventType.CreateRevision: { return this.document ? documentPath(this.document) : ""; } + case NotificationEventType.AddUserToCollection: case NotificationEventType.CreateCollection: { const collection = this.collectionId ? this.store.rootStore.documents.get(this.collectionId) : undefined; - return collection ? collectionPath(collection.url) : ""; + return collection ? collectionPath(collection.path) : ""; } + case NotificationEventType.AddUserToDocument: case NotificationEventType.MentionedInDocument: { - return this.document?.url; + return this.document?.path; } case NotificationEventType.MentionedInComment: case NotificationEventType.CreateComment: { return this.document && this.comment ? commentPath(this.document, this.comment) - : this.document?.url; + : this.document?.path; } case NotificationEventType.InviteAccepted: { return settingsPath("members"); diff --git a/app/scenes/Settings/Notifications.tsx b/app/scenes/Settings/Notifications.tsx index 264313f15..5071c10b6 100644 --- a/app/scenes/Settings/Notifications.tsx +++ b/app/scenes/Settings/Notifications.tsx @@ -5,6 +5,7 @@ import { CheckboxIcon, CollectionIcon, CommentIcon, + DocumentIcon, EditIcon, EmailIcon, PublishIcon, @@ -80,6 +81,22 @@ function Notifications() { "Receive a notification when someone you invited creates an account" ), }, + { + event: NotificationEventType.AddUserToDocument, + icon: , + title: t("Invited to document"), + description: t( + "Receive a notification when a document is shared with you" + ), + }, + { + event: NotificationEventType.AddUserToCollection, + icon: , + title: t("Invited to collection"), + description: t( + "Receive a notification when you are given access to a collection" + ), + }, { event: NotificationEventType.ExportCompleted, icon: , diff --git a/plugins/webhooks/server/tasks/DeliverWebhookTask.ts b/plugins/webhooks/server/tasks/DeliverWebhookTask.ts index 5d8856970..7528da435 100644 --- a/plugins/webhooks/server/tasks/DeliverWebhookTask.ts +++ b/plugins/webhooks/server/tasks/DeliverWebhookTask.ts @@ -450,7 +450,7 @@ export default class DeliverWebhookTask extends BaseTask { event, subscription, payload: { - id: event.data.membershipId, + id: event.modelId, model: model && presentMembership(model), collection: model && presentCollection(model.collection!), user: model && presentUser(model.user), @@ -477,7 +477,7 @@ export default class DeliverWebhookTask extends BaseTask { event, subscription, payload: { - id: event.data.membershipId, + id: event.modelId, model: model && presentCollectionGroupMembership(model), collection: model && presentCollection(model.collection!), group: model && presentGroup(model.group), diff --git a/server/emails/templates/CollectionSharedEmail.tsx b/server/emails/templates/CollectionSharedEmail.tsx new file mode 100644 index 000000000..00efabdb1 --- /dev/null +++ b/server/emails/templates/CollectionSharedEmail.tsx @@ -0,0 +1,102 @@ +import * as React from "react"; +import { CollectionPermission } from "@shared/types"; +import { Collection, UserMembership } from "@server/models"; +import BaseEmail, { EmailProps } from "./BaseEmail"; +import Body from "./components/Body"; +import Button from "./components/Button"; +import EmailTemplate from "./components/EmailLayout"; +import Header from "./components/Header"; +import Heading from "./components/Heading"; + +type InputProps = EmailProps & { + userId: string; + collectionId: string; + actorName: string; + teamUrl: string; +}; + +type BeforeSend = { + collection: Collection; + membership: UserMembership; +}; + +type Props = InputProps & BeforeSend; + +/** + * Email sent to a user when someone adds them to a collection. + */ +export default class CollectionSharedEmail extends BaseEmail< + InputProps, + BeforeSend +> { + protected async beforeSend({ userId, collectionId }: InputProps) { + const collection = await Collection.findByPk(collectionId); + if (!collection) { + return false; + } + + const membership = await UserMembership.findOne({ + where: { + collectionId, + userId, + }, + }); + if (!membership) { + return false; + } + + return { collection, membership }; + } + + protected subject({ actorName, collection }: Props) { + return `${actorName} invited you to the “${collection.name}” collection`; + } + + protected preview({ actorName }: Props): string { + return `${actorName} invited you to a collection`; + } + + protected fromName({ actorName }: Props) { + return actorName; + } + + protected renderAsText({ actorName, teamUrl, collection }: Props): string { + return ` +${actorName} invited you to the “${collection.name}” collection. + +View Document: ${teamUrl}${collection.path} +`; + } + + protected render(props: Props) { + const { collection, membership, actorName, teamUrl } = props; + const collectionUrl = `${teamUrl}${collection.path}?ref=notification-email`; + + const permission = + membership.permission === CollectionPermission.ReadWrite + ? "view and edit" + : CollectionPermission.Admin + ? "manage" + : "view"; + + return ( + +
+ + + {collection.name} +

+ {actorName} invited you to {permission} documents in the{" "} + {collection.name} collection. +

+

+ +

+ + + ); + } +} diff --git a/server/emails/templates/DocumentSharedEmail.tsx b/server/emails/templates/DocumentSharedEmail.tsx new file mode 100644 index 000000000..4dcd4d628 --- /dev/null +++ b/server/emails/templates/DocumentSharedEmail.tsx @@ -0,0 +1,98 @@ +import * as React from "react"; +import { DocumentPermission } from "@shared/types"; +import { Document, UserMembership } from "@server/models"; +import BaseEmail, { EmailProps } from "./BaseEmail"; +import Body from "./components/Body"; +import Button from "./components/Button"; +import EmailTemplate from "./components/EmailLayout"; +import Header from "./components/Header"; +import Heading from "./components/Heading"; + +type InputProps = EmailProps & { + userId: string; + documentId: string; + actorName: string; + teamUrl: string; +}; + +type BeforeSend = { + document: Document; + membership: UserMembership; +}; + +type Props = InputProps & BeforeSend; + +/** + * Email sent to a user when someone adds them to a document. + */ +export default class DocumentSharedEmail extends BaseEmail< + InputProps, + BeforeSend +> { + protected async beforeSend({ documentId, userId }: InputProps) { + const document = await Document.unscoped().findByPk(documentId); + if (!document) { + return false; + } + + const membership = await UserMembership.findOne({ + where: { + documentId, + userId, + }, + }); + if (!membership) { + return false; + } + + return { document, membership }; + } + + protected subject({ actorName, document }: Props) { + return `${actorName} shared “${document.title}” with you`; + } + + protected preview({ actorName }: Props): string { + return `${actorName} shared a document`; + } + + protected fromName({ actorName }: Props) { + return actorName; + } + + protected renderAsText({ actorName, teamUrl, document }: Props): string { + return ` +${actorName} shared “${document.title}” with you. + +View Document: ${teamUrl}${document.path} +`; + } + + protected render(props: Props) { + const { document, membership, actorName, teamUrl } = props; + const documentUrl = `${teamUrl}${document.path}?ref=notification-email`; + + const permission = + membership.permission === DocumentPermission.ReadWrite ? "edit" : "view"; + + return ( + +
+ + + {document.title} +

+ {actorName} invited you to {permission} the{" "} + {document.title} document. +

+

+ +

+ + + ); + } +} diff --git a/server/models/Collection.ts b/server/models/Collection.ts index 1158bb5c0..a2ae45716 100644 --- a/server/models/Collection.ts +++ b/server/models/Collection.ts @@ -231,7 +231,17 @@ class Collection extends ParanoidModel< // getters + /** + * The frontend path to this collection. + * + * @deprecated Use `path` instead. + */ get url(): string { + return this.path; + } + + /** The frontend path to this collection. */ + get path(): string { if (!this.name) { return `/collection/untitled-${this.urlId}`; } diff --git a/server/models/Document.ts b/server/models/Document.ts index 15a12c2a7..4fe0d406e 100644 --- a/server/models/Document.ts +++ b/server/models/Document.ts @@ -310,6 +310,11 @@ class Document extends ParanoidModel< * @deprecated Use `path` instead. */ get url() { + return this.path; + } + + /** The frontend path to this document. */ + get path() { if (!this.title) { return `/doc/untitled-${this.urlId}`; } @@ -317,11 +322,6 @@ class Document extends ParanoidModel< return `/doc/${slugifiedTitle}-${this.urlId}`; } - /** The frontend path to this document. */ - get path() { - return this.url; - } - get tasks() { return getTasks(this.text || ""); } diff --git a/server/models/Event.ts b/server/models/Event.ts index 27b3deb0e..47fbe8c9d 100644 --- a/server/models/Event.ts +++ b/server/models/Event.ts @@ -137,6 +137,8 @@ class Event extends IdModel< "collections.delete", "collections.move", "collections.permission_changed", + "collections.add_user", + "collections.remove_user", "documents.publish", "documents.unpublish", "documents.archive", diff --git a/server/models/Notification.ts b/server/models/Notification.ts index f8645ecf4..2f76b6b15 100644 --- a/server/models/Notification.ts +++ b/server/models/Notification.ts @@ -72,12 +72,15 @@ import Fix from "./decorators/Fix"; include: [ { association: "document", + required: false, }, { association: "comment", + required: false, }, { association: "actor", + required: false, }, ], })) @@ -181,7 +184,9 @@ class Notification extends Model< userId: model.userId, modelId: model.id, teamId: model.teamId, + commentId: model.commentId, documentId: model.documentId, + collectionId: model.collectionId, actorId: model.actorId, }; diff --git a/server/queues/processors/EmailsProcessor.ts b/server/queues/processors/EmailsProcessor.ts index f5f2355ac..f46b98c82 100644 --- a/server/queues/processors/EmailsProcessor.ts +++ b/server/queues/processors/EmailsProcessor.ts @@ -1,9 +1,11 @@ import { NotificationEventType } from "@shared/types"; import CollectionCreatedEmail from "@server/emails/templates/CollectionCreatedEmail"; +import CollectionSharedEmail from "@server/emails/templates/CollectionSharedEmail"; import CommentCreatedEmail from "@server/emails/templates/CommentCreatedEmail"; import CommentMentionedEmail from "@server/emails/templates/CommentMentionedEmail"; import DocumentMentionedEmail from "@server/emails/templates/DocumentMentionedEmail"; import DocumentPublishedOrUpdatedEmail from "@server/emails/templates/DocumentPublishedOrUpdatedEmail"; +import DocumentSharedEmail from "@server/emails/templates/DocumentSharedEmail"; import { Notification } from "@server/models"; import { Event, NotificationEvent } from "@server/types"; import BaseProcessor from "./BaseProcessor"; @@ -23,6 +25,10 @@ export default class EmailsProcessor extends BaseProcessor { const notificationId = notification.id; + if (notification.user.isSuspended) { + return; + } + switch (notification.event) { case NotificationEventType.UpdateDocument: case NotificationEventType.PublishDocument: { @@ -41,6 +47,34 @@ export default class EmailsProcessor extends BaseProcessor { return; } + case NotificationEventType.AddUserToDocument: { + await new DocumentSharedEmail( + { + to: notification.user.email, + userId: notification.userId, + documentId: notification.documentId, + teamUrl: notification.team.url, + actorName: notification.actor.name, + }, + { notificationId } + ).schedule(); + return; + } + + case NotificationEventType.AddUserToCollection: { + await new CollectionSharedEmail( + { + to: notification.user.email, + userId: notification.userId, + collectionId: notification.collectionId, + teamUrl: notification.team.url, + actorName: notification.actor.name, + }, + { notificationId } + ).schedule(); + return; + } + case NotificationEventType.MentionedInDocument: { await new DocumentMentionedEmail( { diff --git a/server/queues/processors/NotificationsProcessor.ts b/server/queues/processors/NotificationsProcessor.ts index 3e0026cc4..b4cc4e967 100644 --- a/server/queues/processors/NotificationsProcessor.ts +++ b/server/queues/processors/NotificationsProcessor.ts @@ -5,10 +5,14 @@ import { Event, DocumentEvent, CommentEvent, + CollectionUserEvent, + DocumentUserEvent, } from "@server/types"; +import CollectionAddUserNotificationsTask from "../tasks/CollectionAddUserNotificationsTask"; import CollectionCreatedNotificationsTask from "../tasks/CollectionCreatedNotificationsTask"; import CommentCreatedNotificationsTask from "../tasks/CommentCreatedNotificationsTask"; import CommentUpdatedNotificationsTask from "../tasks/CommentUpdatedNotificationsTask"; +import DocumentAddUserNotificationsTask from "../tasks/DocumentAddUserNotificationsTask"; import DocumentPublishedNotificationsTask from "../tasks/DocumentPublishedNotificationsTask"; import RevisionCreatedNotificationsTask from "../tasks/RevisionCreatedNotificationsTask"; import BaseProcessor from "./BaseProcessor"; @@ -16,8 +20,10 @@ import BaseProcessor from "./BaseProcessor"; export default class NotificationsProcessor extends BaseProcessor { static applicableEvents: Event["name"][] = [ "documents.publish", + "documents.add_user", "revisions.create", "collections.create", + "collections.add_user", "comments.create", "comments.update", ]; @@ -26,10 +32,14 @@ export default class NotificationsProcessor extends BaseProcessor { switch (event.name) { case "documents.publish": return this.documentPublished(event); + case "documents.add_user": + return this.documentAddUser(event); case "revisions.create": return this.revisionCreated(event); case "collections.create": return this.collectionCreated(event); + case "collections.add_user": + return this.collectionAddUser(event); case "comments.create": return this.commentCreated(event); case "comments.update": @@ -51,6 +61,16 @@ export default class NotificationsProcessor extends BaseProcessor { await DocumentPublishedNotificationsTask.schedule(event); } + async documentAddUser(event: DocumentUserEvent) { + if (!event.data.isNew || event.userId === event.actorId) { + return; + } + + await DocumentAddUserNotificationsTask.schedule(event, { + delay: Minute, + }); + } + async revisionCreated(event: RevisionEvent) { await RevisionCreatedNotificationsTask.schedule(event); } @@ -68,6 +88,16 @@ export default class NotificationsProcessor extends BaseProcessor { await CollectionCreatedNotificationsTask.schedule(event); } + async collectionAddUser(event: CollectionUserEvent) { + if (!event.data.isNew || event.userId === event.actorId) { + return; + } + + await CollectionAddUserNotificationsTask.schedule(event, { + delay: Minute, + }); + } + async commentCreated(event: CommentEvent) { await CommentCreatedNotificationsTask.schedule(event, { delay: Minute, diff --git a/server/queues/tasks/CollectionAddUserNotificationsTask.ts b/server/queues/tasks/CollectionAddUserNotificationsTask.ts new file mode 100644 index 000000000..1bbf45723 --- /dev/null +++ b/server/queues/tasks/CollectionAddUserNotificationsTask.ts @@ -0,0 +1,32 @@ +import { NotificationEventType } from "@shared/types"; +import { Notification, User } from "@server/models"; +import { CollectionUserEvent } from "@server/types"; +import BaseTask, { TaskPriority } from "./BaseTask"; + +export default class CollectionAddUserNotificationsTask extends BaseTask { + public async perform(event: CollectionUserEvent) { + const recipient = await User.findByPk(event.userId); + if (!recipient) { + return; + } + + if ( + !recipient.isSuspended && + recipient.subscribedToEventType(NotificationEventType.AddUserToCollection) + ) { + await Notification.create({ + event: NotificationEventType.AddUserToCollection, + userId: event.userId, + actorId: event.actorId, + teamId: event.teamId, + collectionId: event.collectionId, + }); + } + } + + public get options() { + return { + priority: TaskPriority.Background, + }; + } +} diff --git a/server/queues/tasks/DocumentAddUserNotificationsTask.ts b/server/queues/tasks/DocumentAddUserNotificationsTask.ts new file mode 100644 index 000000000..a41676f37 --- /dev/null +++ b/server/queues/tasks/DocumentAddUserNotificationsTask.ts @@ -0,0 +1,32 @@ +import { NotificationEventType } from "@shared/types"; +import { Notification, User } from "@server/models"; +import { DocumentUserEvent } from "@server/types"; +import BaseTask, { TaskPriority } from "./BaseTask"; + +export default class DocumentAddUserNotificationsTask extends BaseTask { + public async perform(event: DocumentUserEvent) { + const recipient = await User.findByPk(event.userId); + if (!recipient) { + return; + } + + if ( + !recipient.isSuspended && + recipient.subscribedToEventType(NotificationEventType.AddUserToDocument) + ) { + await Notification.create({ + event: NotificationEventType.AddUserToDocument, + userId: event.userId, + actorId: event.actorId, + teamId: event.teamId, + documentId: event.documentId, + }); + } + } + + public get options() { + return { + priority: TaskPriority.Background, + }; + } +} diff --git a/server/routes/api/collections/collections.ts b/server/routes/api/collections/collections.ts index 59ae19da2..9da61f774 100644 --- a/server/routes/api/collections/collections.ts +++ b/server/routes/api/collections/collections.ts @@ -382,6 +382,7 @@ router.post( router.post( "collections.add_user", auth(), + rateLimiter(RateLimiterStrategy.OneHundredPerHour), transaction(), validate(T.CollectionsAddUserSchema), async (ctx: APIContext) => { @@ -397,28 +398,20 @@ router.post( const user = await User.findByPk(userId); authorize(actor, "read", user); - let membership = await UserMembership.findOne({ + const [membership, isNew] = await UserMembership.findOrCreate({ where: { collectionId: id, userId, }, + defaults: { + permission: permission || user.defaultCollectionPermission, + createdById: actor.id, + }, transaction, lock: transaction.LOCK.UPDATE, }); - if (!membership) { - membership = await UserMembership.create( - { - collectionId: id, - userId, - permission: permission || user.defaultCollectionPermission, - createdById: actor.id, - }, - { - transaction, - } - ); - } else if (permission) { + if (permission) { membership.permission = permission; await membership.save({ transaction }); } @@ -427,12 +420,13 @@ router.post( { name: "collections.add_user", userId, + modelId: membership.id, collectionId: collection.id, teamId: collection.teamId, actorId: actor.id, data: { - name: user.name, - membershipId: membership.id, + isNew, + permission: membership.permission, }, ip: ctx.request.ip, }, @@ -482,12 +476,12 @@ router.post( { name: "collections.remove_user", userId, + modelId: membership.id, collectionId: collection.id, teamId: collection.teamId, actorId: actor.id, data: { name: user.name, - membershipId: membership.id, }, ip: ctx.request.ip, }, diff --git a/server/routes/api/documents/documents.ts b/server/routes/api/documents/documents.ts index 07dc3a909..44eb19c18 100644 --- a/server/routes/api/documents/documents.ts +++ b/server/routes/api/documents/documents.ts @@ -1475,6 +1475,7 @@ router.post( "documents.add_user", auth(), validate(T.DocumentsAddUserSchema), + rateLimiter(RateLimiterStrategy.OneHundredPerHour), transaction(), async (ctx: APIContext) => { const { auth, transaction } = ctx.state; @@ -1521,7 +1522,7 @@ router.post( UserMemberships.length ? UserMemberships[0].index : null ); - const [membership] = await UserMembership.findOrCreate({ + const [membership, isNew] = await UserMembership.findOrCreate({ where: { documentId: id, userId, @@ -1553,6 +1554,11 @@ router.post( teamId: document.teamId, actorId: actor.id, ip: ctx.request.ip, + data: { + title: document.title, + isNew, + permission: membership.permission, + }, }, { transaction, diff --git a/server/types.ts b/server/types.ts index 396de2424..00acfa8fd 100644 --- a/server/types.ts +++ b/server/types.ts @@ -7,6 +7,7 @@ import { NavigationNode, Client, CollectionPermission, + DocumentPermission, } from "@shared/types"; import { BaseSchema } from "@server/routes/api/schema"; import { AccountProvisionerResult } from "./commands/accountProvisioner"; @@ -209,15 +210,19 @@ export type FileOperationEvent = BaseEvent & { export type CollectionUserEvent = BaseEvent & { name: "collections.add_user" | "collections.remove_user"; userId: string; + modelId: string; collectionId: string; - data: { name: string; membershipId: string }; + data: { + isNew?: boolean; + permission?: CollectionPermission; + }; }; export type CollectionGroupEvent = BaseEvent & { name: "collections.add_group" | "collections.remove_group"; collectionId: string; modelId: string; - data: { name: string; membershipId: string }; + data: { name: string }; }; export type DocumentUserEvent = BaseEvent & { @@ -225,6 +230,11 @@ export type DocumentUserEvent = BaseEvent & { userId: string; modelId: string; documentId: string; + data: { + title: string; + isNew?: boolean; + permission?: DocumentPermission; + }; }; export type CollectionEvent = BaseEvent & @@ -381,7 +391,10 @@ export type NotificationEvent = BaseEvent & { modelId: string; teamId: string; userId: string; + actorId: string; + commentId?: string; documentId?: string; + collectionId?: string; }; export type Event = diff --git a/shared/i18n/locales/en_US/translation.json b/shared/i18n/locales/en_US/translation.json index 398d91693..3309e64ef 100644 --- a/shared/i18n/locales/en_US/translation.json +++ b/shared/i18n/locales/en_US/translation.json @@ -459,6 +459,8 @@ "created the collection": "created the collection", "mentioned you in": "mentioned you in", "left a comment on": "left a comment on", + "shared": "shared", + "invited you to": "invited you to", "API token created": "API token created", "Name your token something that will help you to remember it's use in the future, for example \"local development\", \"production\", or \"continuous integration\".": "Name your token something that will help you to remember it's use in the future, for example \"local development\", \"production\", or \"continuous integration\".", "The document archive is empty at the moment.": "The document archive is empty at the moment.", @@ -858,6 +860,10 @@ "Receive a notification whenever a new collection is created": "Receive a notification whenever a new collection is created", "Invite accepted": "Invite accepted", "Receive a notification when someone you invited creates an account": "Receive a notification when someone you invited creates an account", + "Invited to document": "Invited to document", + "Receive a notification when a document is shared with you": "Receive a notification when a document is shared with you", + "Invited to collection": "Invited to collection", + "Receive a notification when you are given access to a collection": "Receive a notification when you are given access to a collection", "Export completed": "Export completed", "Receive a notification when an export you requested has been completed": "Receive a notification when an export you requested has been completed", "Getting started": "Getting started", diff --git a/shared/types.ts b/shared/types.ts index 61707e81f..64e972b04 100644 --- a/shared/types.ts +++ b/shared/types.ts @@ -203,6 +203,8 @@ export type CollectionSort = { export enum NotificationEventType { PublishDocument = "documents.publish", UpdateDocument = "documents.update", + AddUserToDocument = "documents.add_user", + AddUserToCollection = "collections.add_user", CreateRevision = "revisions.create", CreateCollection = "collections.create", CreateComment = "comments.create", @@ -239,6 +241,8 @@ export const NotificationEventDefaults = { [NotificationEventType.Onboarding]: true, [NotificationEventType.Features]: true, [NotificationEventType.ExportCompleted]: true, + [NotificationEventType.AddUserToDocument]: true, + [NotificationEventType.AddUserToCollection]: true, }; export enum UnfurlType {