From 831c93677fa02faaa823f2d48e12dce75477e1d0 Mon Sep 17 00:00:00 2001 From: Saurabhkmr98 Date: Tue, 18 Feb 2025 20:42:18 +0530 Subject: [PATCH 01/11] feat: inbox settings backend apis + web UI and store changes --- apiserver/plane/app/serializers/__init__.py | 6 +- .../plane/app/serializers/notification.py | 7 +- apiserver/plane/app/urls/notification.py | 11 ++ apiserver/plane/app/views/__init__.py | 1 + .../plane/app/views/notification/base.py | 81 +++++++++ apiserver/plane/app/views/workspace/home.py | 33 +++- apiserver/plane/db/models/__init__.py | 7 +- apiserver/plane/db/models/notification.py | 48 +++++ packages/constants/src/notification.ts | 73 +++++++- .../i18n/src/locales/en/translations.json | 21 +++ packages/types/src/index.d.ts | 1 + packages/types/src/notification.d.ts | 22 +++ .../notifications/{ => (list)}/layout.tsx | 0 .../notifications/{ => (list)}/page.tsx | 0 .../notifications/settings/header.tsx | 44 +++++ .../notifications/settings/layout.tsx | 24 +++ .../notifications/settings/page.tsx | 44 +++++ .../inbox/settings/content-header.tsx | 17 ++ .../inbox/settings/content-wrapper.tsx | 31 ++++ web/core/components/inbox/settings/index.ts | 5 + web/core/components/inbox/settings/root.tsx | 47 +++++ .../inbox/settings/update-setting.tsx | 51 ++++++ web/core/constants/fetch-keys.ts | 4 + web/core/hooks/store/notifications/index.ts | 1 + .../use-workspace-notification-settings.ts | 12 ++ ...workspace-notification-settings.service.ts | 46 +++++ .../workspace-notification-settings.store.ts | 165 ++++++++++++++++++ web/core/store/root.store.ts | 4 + 28 files changed, 801 insertions(+), 5 deletions(-) create mode 100644 packages/types/src/notification.d.ts rename web/app/[workspaceSlug]/(projects)/notifications/{ => (list)}/layout.tsx (100%) rename web/app/[workspaceSlug]/(projects)/notifications/{ => (list)}/page.tsx (100%) create mode 100644 web/app/[workspaceSlug]/(projects)/notifications/settings/header.tsx create mode 100644 web/app/[workspaceSlug]/(projects)/notifications/settings/layout.tsx create mode 100644 web/app/[workspaceSlug]/(projects)/notifications/settings/page.tsx create mode 100644 web/core/components/inbox/settings/content-header.tsx create mode 100644 web/core/components/inbox/settings/content-wrapper.tsx create mode 100644 web/core/components/inbox/settings/index.ts create mode 100644 web/core/components/inbox/settings/root.tsx create mode 100644 web/core/components/inbox/settings/update-setting.tsx create mode 100644 web/core/hooks/store/notifications/use-workspace-notification-settings.ts create mode 100644 web/core/services/workspace-notification-settings.service.ts create mode 100644 web/core/store/notifications/workspace-notification-settings.store.ts diff --git a/apiserver/plane/app/serializers/__init__.py b/apiserver/plane/app/serializers/__init__.py index 479f08d5a31..d024fca94da 100644 --- a/apiserver/plane/app/serializers/__init__.py +++ b/apiserver/plane/app/serializers/__init__.py @@ -115,7 +115,11 @@ from .analytic import AnalyticViewSerializer -from .notification import NotificationSerializer, UserNotificationPreferenceSerializer +from .notification import ( + NotificationSerializer, + UserNotificationPreferenceSerializer, + WorkspaceUserNotificationPreferenceSerializer, +) from .exporter import ExporterHistorySerializer diff --git a/apiserver/plane/app/serializers/notification.py b/apiserver/plane/app/serializers/notification.py index 58007ec26c4..ac054fd58c9 100644 --- a/apiserver/plane/app/serializers/notification.py +++ b/apiserver/plane/app/serializers/notification.py @@ -1,7 +1,7 @@ # Module imports from .base import BaseSerializer from .user import UserLiteSerializer -from plane.db.models import Notification, UserNotificationPreference +from plane.db.models import Notification, UserNotificationPreference, WorkspaceUserNotificationPreference # Third Party imports from rest_framework import serializers @@ -22,3 +22,8 @@ class UserNotificationPreferenceSerializer(BaseSerializer): class Meta: model = UserNotificationPreference fields = "__all__" + +class WorkspaceUserNotificationPreferenceSerializer(BaseSerializer): + class Meta: + model = WorkspaceUserNotificationPreference + fields = "__all__" \ No newline at end of file diff --git a/apiserver/plane/app/urls/notification.py b/apiserver/plane/app/urls/notification.py index cd5647ea4fa..6df44bafaf2 100644 --- a/apiserver/plane/app/urls/notification.py +++ b/apiserver/plane/app/urls/notification.py @@ -6,6 +6,7 @@ UnreadNotificationEndpoint, MarkAllReadNotificationViewSet, UserNotificationPreferenceEndpoint, + WorkspaceUserNotificationPreferenceEndpoint, ) @@ -47,4 +48,14 @@ UserNotificationPreferenceEndpoint.as_view(), name="user-notification-preferences", ), + path( + "workspaces//user-notification-preferences//", + WorkspaceUserNotificationPreferenceEndpoint.as_view(), + name="workspace-user-notification-preference", + ), + path( + "workspaces//user-notification-preferences/", + WorkspaceUserNotificationPreferenceEndpoint.as_view(), + name="workspace-user-notification-preference", + ), ] diff --git a/apiserver/plane/app/views/__init__.py b/apiserver/plane/app/views/__init__.py index 684179d9045..785192d8aee 100644 --- a/apiserver/plane/app/views/__init__.py +++ b/apiserver/plane/app/views/__init__.py @@ -199,6 +199,7 @@ NotificationViewSet, UnreadNotificationEndpoint, UserNotificationPreferenceEndpoint, + WorkspaceUserNotificationPreferenceEndpoint, ) from .exporter.base import ExportIssuesEndpoint diff --git a/apiserver/plane/app/views/notification/base.py b/apiserver/plane/app/views/notification/base.py index d2aa1a02d7b..df488c3907c 100644 --- a/apiserver/plane/app/views/notification/base.py +++ b/apiserver/plane/app/views/notification/base.py @@ -9,6 +9,7 @@ from plane.app.serializers import ( NotificationSerializer, UserNotificationPreferenceSerializer, + WorkspaceUserNotificationPreferenceSerializer ) from plane.db.models import ( Issue, @@ -17,6 +18,8 @@ Notification, UserNotificationPreference, WorkspaceMember, + Workspace, + WorkspaceUserNotificationPreference ) from plane.utils.paginator import BasePaginator from plane.app.permissions import allow_permission, ROLE @@ -360,3 +363,81 @@ def patch(self, request): serializer.save() return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + + +class WorkspaceUserNotificationPreferenceEndpoint(BaseAPIView): + model = WorkspaceUserNotificationPreference + serializer_class = WorkspaceUserNotificationPreferenceSerializer + + @allow_permission( + allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE" + ) + def get(self, request, slug): + workspace = Workspace.objects.get(slug=slug) + get_notification_preferences = ( + WorkspaceUserNotificationPreference.objects.filter( + workspace=workspace, user=request.user + ) + ) + + create_transports = [] + + transports = [ + transport + for transport, _ in WorkspaceUserNotificationPreference.TransportChoices.choices + ] + + for transport in transports: + if transport not in get_notification_preferences.values_list( + "transport", flat=True + ): + create_transports.append(transport) + + + notification_preferences = ( + WorkspaceUserNotificationPreference.objects.bulk_create( + [ + WorkspaceUserNotificationPreference( + workspace=workspace, + user=request.user, + transport=transport, + ) + for transport in create_transports + ] + ) + ) + + notification_preferences = WorkspaceUserNotificationPreference.objects.filter( + workspace=workspace, user=request.user + ) + + return Response( + WorkspaceUserNotificationPreferenceSerializer( + notification_preferences, many=True + ).data, + status=status.HTTP_200_OK, + ) + + @allow_permission( + allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE" + ) + def patch(self, request, slug, transport): + notification_preference = WorkspaceUserNotificationPreference.objects.filter( + transport=transport, workspace__slug=slug, user=request.user + ).first() + + if notification_preference: + serializer = WorkspaceUserNotificationPreferenceSerializer( + notification_preference, data=request.data, partial=True + ) + + if serializer.is_valid(): + serializer.save() + return Response(serializer.data, status=status.HTTP_200_OK) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + return Response( + {"detail": "Workspace notification preference not found"}, + status=status.HTTP_404_NOT_FOUND, + ) diff --git a/apiserver/plane/app/views/workspace/home.py b/apiserver/plane/app/views/workspace/home.py index 5ee9b0a39f1..a69007aab8a 100644 --- a/apiserver/plane/app/views/workspace/home.py +++ b/apiserver/plane/app/views/workspace/home.py @@ -2,7 +2,7 @@ from ..base import BaseAPIView from plane.db.models.workspace import WorkspaceHomePreference from plane.app.permissions import allow_permission, ROLE -from plane.db.models import Workspace +from plane.db.models import Workspace, WorkspaceUserNotificationPreference from plane.app.serializers.workspace import WorkspaceHomePreferenceSerializer # Third party imports @@ -59,6 +59,37 @@ def get(self, request, slug): user=request.user, workspace_id=workspace.id ) + # Notification preference get or create + workspace = Workspace.objects.get(slug=slug) + get_notification_preferences = ( + WorkspaceUserNotificationPreference.objects.filter( + workspace=workspace, user=request.user + ) + ) + + create_transports = [] + + transports = [ + transport + for transport, _ in WorkspaceUserNotificationPreference.TransportChoices.choices + ] + + + for transport in transports: + if transport not in get_notification_preferences.values_list( + "transport", flat=True + ): + create_transports.append(transport) + + _ = WorkspaceUserNotificationPreference.objects.bulk_create( + [ + WorkspaceUserNotificationPreference( + workspace=workspace, user=request.user, transport=transport + ) + for transport in create_transports + ] + ) + return Response( preference.values("key", "is_enabled", "config", "sort_order"), status=status.HTTP_200_OK, diff --git a/apiserver/plane/db/models/__init__.py b/apiserver/plane/db/models/__init__.py index 8e20a8d6714..becaa1e6267 100644 --- a/apiserver/plane/db/models/__init__.py +++ b/apiserver/plane/db/models/__init__.py @@ -45,7 +45,12 @@ IssueDescriptionVersion, ) from .module import Module, ModuleIssue, ModuleLink, ModuleMember, ModuleUserProperties -from .notification import EmailNotificationLog, Notification, UserNotificationPreference +from .notification import ( + EmailNotificationLog, + Notification, + UserNotificationPreference, + WorkspaceUserNotificationPreference, +) from .page import Page, PageLabel, PageLog, ProjectPage, PageVersion from .project import ( Project, diff --git a/apiserver/plane/db/models/notification.py b/apiserver/plane/db/models/notification.py index 2847c07cf0f..1b0f1a0e7a2 100644 --- a/apiserver/plane/db/models/notification.py +++ b/apiserver/plane/db/models/notification.py @@ -123,3 +123,51 @@ class Meta: verbose_name_plural = "Email Notification Logs" db_table = "email_notification_logs" ordering = ("-created_at",) + +class WorkspaceUserNotificationPreference(BaseModel): + class TransportChoices(models.TextChoices): + EMAIL = "EMAIL", "Email" + IN_APP = "IN_APP", "In App" + + workspace = models.ForeignKey( + "db.Workspace", + related_name="workspace_notification_preference", + on_delete=models.CASCADE, + ) + user = models.ForeignKey( + "db.User", + related_name="workspace_notification_preference", + on_delete=models.CASCADE, + ) + transport = models.CharField(max_length=50) + + # task updates + work_item_property_updates_enabled = models.BooleanField(default=False) + status_updates_enabled = models.BooleanField(default=False) + priority_updates_enabled = models.BooleanField(default=False) + assignee_updates_enabled = models.BooleanField(default=False) + start_due_date_updates_enabled = models.BooleanField(default=False) + module_updates_enabled = models.BooleanField(default=False) + cycle_updates_enabled = models.BooleanField(default=False) + + # comment updates + mentioned_comments_updates_enabled = models.BooleanField(default=False) + new_comments_updates_enabled = models.BooleanField(default=False) + reaction_comments_updates_enabled = models.BooleanField(default=False) + + class Meta: + unique_together = ["workspace", "user", "transport", "deleted_at"] + constraints = [ + models.UniqueConstraint( + fields=["workspace", "user", "transport"], + condition=models.Q(deleted_at__isnull=True), + name="notification_preferences_unique_workspace_user_transport_when_deleted_at_null", + ) + ] + verbose_name = "Workspace User Notification Preference" + verbose_name_plural = "Workspace User Notification Preferences" + db_table = "workspace_user_notification_preferences" + ordering = ("-created_at",) + + def __str__(self): + return f"{self.workspace} - {self.user} Notification Preferences" diff --git a/packages/constants/src/notification.ts b/packages/constants/src/notification.ts index cb267c4ad1f..e4296188ed0 100644 --- a/packages/constants/src/notification.ts +++ b/packages/constants/src/notification.ts @@ -1,4 +1,4 @@ -import { TUnreadNotificationsCount } from "@plane/types"; +import { TUnreadNotificationsCount, TNotificationSettings } from "@plane/types"; export enum ENotificationTab { ALL = "all", @@ -135,3 +135,74 @@ export const allTimeIn30MinutesInterval12HoursFormat: Array<{ { label: "11:00", value: "11:00" }, { label: "11:30", value: "11:30" }, ]; + + +export enum ENotificationSettingsKey { + WORK_ITEM_PROPERTY_UPDATES = "work_item_property_updates", + STATUS_UPDATES = "status_updates", + PRIORITY_UPDATES = "priority_updates", + ASSIGNEE_UPDATES = "assignee_updates", + START_DUE_DATE_UPDATES = "start_due_date_updates", + MODULE_UPDATES = "module_updates", + CYCLE_UPDATES = "cycle_updates", + MENTIONED_COMMENTS_UPDATES = "mentioned_comments_updates", + NEW_COMMENTS_UPDATES = "new_comments_updates", + REACTION_COMMENT_UPDATES = "reaction_comment_updates", +} + + +export enum EWorkspaceNotificationTransport { + EMAIL = "EMAIL", + IN_APP = "IN_APP", +} + +export const TASK_UPDATES_NOTIFICATION_SETTINGS: TNotificationSettings[] = [ + { + key: ENotificationSettingsKey.WORK_ITEM_PROPERTY_UPDATES, + i18n_title: "notification_settings.work_item_property_title", + }, + { + key: ENotificationSettingsKey.STATUS_UPDATES, + i18n_title: "notification_settings.status_title", + }, + { + key: ENotificationSettingsKey.PRIORITY_UPDATES, + i18n_title: "notification_settings.priority_title", + }, + { + key: ENotificationSettingsKey.ASSIGNEE_UPDATES, + i18n_title: "notification_settings.assignee_title", + }, + { + key: ENotificationSettingsKey.START_DUE_DATE_UPDATES, + i18n_title: "notification_settings.due_date_title", + }, + { + key: ENotificationSettingsKey.MODULE_UPDATES, + i18n_title: "notification_settings.module_title", + }, + { + key: ENotificationSettingsKey.CYCLE_UPDATES, + i18n_title: "notification_settings.cycle_title", + } +] + +export const COMMENT_NOTIFICATION_SETTINGS: TNotificationSettings[] = [ + { + key: ENotificationSettingsKey.MENTIONED_COMMENTS_UPDATES, + i18n_title: "notification_settings.mentioned_comments_title", + }, + { + key: ENotificationSettingsKey.NEW_COMMENTS_UPDATES, + i18n_title: "notification_settings.new_comments_title", + }, + { + key: ENotificationSettingsKey.REACTION_COMMENT_UPDATES, + i18n_title: "notification_settings.reaction_comments_title", + }, +] + +export const NOTIFICATION_SETTINGS: TNotificationSettings[] = [ + ...TASK_UPDATES_NOTIFICATION_SETTINGS, + ...COMMENT_NOTIFICATION_SETTINGS +] \ No newline at end of file diff --git a/packages/i18n/src/locales/en/translations.json b/packages/i18n/src/locales/en/translations.json index a330fe8e684..d2697dcabce 100644 --- a/packages/i18n/src/locales/en/translations.json +++ b/packages/i18n/src/locales/en/translations.json @@ -2178,5 +2178,26 @@ "module": { "label": "{count, plural, one {Module} other {Modules}}", "no_module": "No module" + }, + + "notification_settings": { + "page_label": "{workspace} - Inbox settings", + "inbox_settings": "Inbox settings", + "inbox_settings_description": "Toggle these ON or OFF for the workspace", + "advanced_settings": "Advanced settings", + "in_plane": "In Plane", + "email": "Email", + "task_updates": "Task updates", + "comments": "Comments", + "work_item_property_title": "Update on any property of work item", + "status_title": "Status update", + "priority_title": "Priority update", + "assignee_title": "Assignee update", + "due_date_title": "Start/Due date update", + "module_title": "Module update", + "cycle_title": "Cycle update", + "mentioned_comments_title": "Comments I'm @mentioned in", + "new_comments_title": "New comments", + "reaction_comments_title": "Reactions" } } diff --git a/packages/types/src/index.d.ts b/packages/types/src/index.d.ts index 9ec3846b7c5..1ad24302d4c 100644 --- a/packages/types/src/index.d.ts +++ b/packages/types/src/index.d.ts @@ -40,3 +40,4 @@ export * from "./epics"; export * from "./charts"; export * from "./home"; export * from "./stickies"; +export * from "./notification"; \ No newline at end of file diff --git a/packages/types/src/notification.d.ts b/packages/types/src/notification.d.ts new file mode 100644 index 00000000000..2175bac0c5f --- /dev/null +++ b/packages/types/src/notification.d.ts @@ -0,0 +1,22 @@ +export type TNotificationSettings = { + i18n_title: string, + key: string +} + +export type TWorkspaceNotificationTransport = "EMAIL" | "IN_APP" | "PUSH" | "SLACK"; + +export type TWorkspaceUserNotification = { + workspace: string, + user: string, + transport: TWorkspaceNotificationTransport, + work_item_property_updates_enabled: boolean, + status_updates_enabled: boolean, + priority_updates_enabled: boolean, + assignee_updates_enabled: boolean, + start_due_date_updates_enabled: boolean, + module_updates_enabled: boolean, + cycle_updates_enabled: boolean, + mentioned_comments_updates_enabled: boolean, + new_comments_updates_enabled: boolean, + reaction_comments_updates_enabled: boolean +} \ No newline at end of file diff --git a/web/app/[workspaceSlug]/(projects)/notifications/layout.tsx b/web/app/[workspaceSlug]/(projects)/notifications/(list)/layout.tsx similarity index 100% rename from web/app/[workspaceSlug]/(projects)/notifications/layout.tsx rename to web/app/[workspaceSlug]/(projects)/notifications/(list)/layout.tsx diff --git a/web/app/[workspaceSlug]/(projects)/notifications/page.tsx b/web/app/[workspaceSlug]/(projects)/notifications/(list)/page.tsx similarity index 100% rename from web/app/[workspaceSlug]/(projects)/notifications/page.tsx rename to web/app/[workspaceSlug]/(projects)/notifications/(list)/page.tsx diff --git a/web/app/[workspaceSlug]/(projects)/notifications/settings/header.tsx b/web/app/[workspaceSlug]/(projects)/notifications/settings/header.tsx new file mode 100644 index 00000000000..90890d496f7 --- /dev/null +++ b/web/app/[workspaceSlug]/(projects)/notifications/settings/header.tsx @@ -0,0 +1,44 @@ +"use client"; + +import { FC } from "react"; +import { observer } from "mobx-react"; +import { Inbox, Settings } from "lucide-react"; +// ui +import { useTranslation } from "@plane/i18n"; +import { Breadcrumbs, Header } from "@plane/ui"; +// components +import { BreadcrumbLink } from "@/components/common"; +// hooks +import { useWorkspace } from "@/hooks/store"; + +export const NotificationsSettingsHeader: FC = observer(() => { + const { currentWorkspace, loader } = useWorkspace(); + const { t } = useTranslation(); + + return ( +
+ + + } + /> + } + /> + } /> + } + /> + + +
+ ); +}); diff --git a/web/app/[workspaceSlug]/(projects)/notifications/settings/layout.tsx b/web/app/[workspaceSlug]/(projects)/notifications/settings/layout.tsx new file mode 100644 index 00000000000..2f01d0a5db0 --- /dev/null +++ b/web/app/[workspaceSlug]/(projects)/notifications/settings/layout.tsx @@ -0,0 +1,24 @@ +"use client" + +import { FC, ReactNode } from "react" +// plane ui +import { AppHeader } from "@/components/core" +import { NotificationsSettingsHeader } from "./header"; + + +export interface INotificationsSettingsLayoutProps { + children: ReactNode; +} + + +const NotificationsSettingsLayout: FC = (props) => { + const { children } = props + return ( + <> + } /> + {children} + + ) +} + +export default NotificationsSettingsLayout; \ No newline at end of file diff --git a/web/app/[workspaceSlug]/(projects)/notifications/settings/page.tsx b/web/app/[workspaceSlug]/(projects)/notifications/settings/page.tsx new file mode 100644 index 00000000000..5e490975ef8 --- /dev/null +++ b/web/app/[workspaceSlug]/(projects)/notifications/settings/page.tsx @@ -0,0 +1,44 @@ +"use client"; + +import { observer } from "mobx-react"; +// components +import useSWR from "swr"; +import { useTranslation } from "@plane/i18n"; +import { PageHead } from "@/components/core"; +// hooks +import { InboxSettingsContentHeader, InboxSettingsRoot, InboxSettingContentWrapper } from "@/components/inbox/settings"; +import { EmailSettingsLoader } from "@/components/ui"; +import { NOTIFICATION_SETTINGS } from "@/constants/fetch-keys"; +import { useWorkspaceNotificationSettings } from "@/hooks/store"; + +const NotificationsSettingsPage = observer(() => { + // store hooks + const { workspace: currentWorkspace, fetchWorkspaceUserNotificationSettings } = useWorkspaceNotificationSettings(); + const { t } = useTranslation(); + // derived values + const pageTitle = currentWorkspace?.name + ? t("notification_settings.page_label", { workspace: currentWorkspace.name }) + : undefined; + + + const { data, isLoading } = useSWR(currentWorkspace?.slug ? NOTIFICATION_SETTINGS(currentWorkspace?.slug) : null, () => fetchWorkspaceUserNotificationSettings()); + + if (!data || isLoading) { + return ; + } + + return ( + <> + + + + + + + ); +}); + +export default NotificationsSettingsPage; diff --git a/web/core/components/inbox/settings/content-header.tsx b/web/core/components/inbox/settings/content-header.tsx new file mode 100644 index 00000000000..bc07917d292 --- /dev/null +++ b/web/core/components/inbox/settings/content-header.tsx @@ -0,0 +1,17 @@ +"use client"; +import React, { FC } from "react"; + +type Props = { + title: string; + description?: string; +}; + +export const InboxSettingsContentHeader: FC = (props) => { + const { title, description } = props; + return ( +
+
{title}
+ {description &&
{description}
} +
+ ); +}; diff --git a/web/core/components/inbox/settings/content-wrapper.tsx b/web/core/components/inbox/settings/content-wrapper.tsx new file mode 100644 index 00000000000..080cb31773b --- /dev/null +++ b/web/core/components/inbox/settings/content-wrapper.tsx @@ -0,0 +1,31 @@ +"use client"; +import React, { FC } from "react"; +// helpers +import { SidebarHamburgerToggle } from "@/components/core"; +import { cn } from "@/helpers/common.helper"; + + +type Props = { + children: React.ReactNode; + className?: string; +}; + +export const InboxSettingContentWrapper: FC = (props) => { + const { children, className = "" } = props; + return ( +
+
+ +
+ +
+ {children} +
+
+ ); +}; diff --git a/web/core/components/inbox/settings/index.ts b/web/core/components/inbox/settings/index.ts new file mode 100644 index 00000000000..ab5ca770cde --- /dev/null +++ b/web/core/components/inbox/settings/index.ts @@ -0,0 +1,5 @@ +export * from "./content-header" +export * from "./root" +export * from "./content-wrapper" +export * from "./content-header" +export * from "./update-setting" \ No newline at end of file diff --git a/web/core/components/inbox/settings/root.tsx b/web/core/components/inbox/settings/root.tsx new file mode 100644 index 00000000000..b4328ead504 --- /dev/null +++ b/web/core/components/inbox/settings/root.tsx @@ -0,0 +1,47 @@ +"use client"; + +import { FC } from "react"; +import { COMMENT_NOTIFICATION_SETTINGS, TASK_UPDATES_NOTIFICATION_SETTINGS } from "@plane/constants"; +import { useTranslation } from "@plane/i18n"; +import { InboxSettingUpdate } from "./update-setting"; + + +export const InboxSettingsRoot: FC = (props) => { + const { } = props; + const { t } = useTranslation(); + + return ( + <> +
+
+ {t("notification_settings.task_updates")} +
+
+
{t("notification_settings.advanced_settings")}
+
{t("notification_settings.in_plane")}
+
{t("notification_settings.email")}
+
+ { + TASK_UPDATES_NOTIFICATION_SETTINGS?.map((item, index) => ( + + )) + } +
+
+
+ {t("notification_settings.comments")} +
+
+
{t("notification_settings.advanced_settings")}
+
{t("notification_settings.in_plane")}
+
{t("notification_settings.email")}
+
+ { + COMMENT_NOTIFICATION_SETTINGS?.map((item, index) => ( + + )) + } +
+ + ); +}; \ No newline at end of file diff --git a/web/core/components/inbox/settings/update-setting.tsx b/web/core/components/inbox/settings/update-setting.tsx new file mode 100644 index 00000000000..55022e50b86 --- /dev/null +++ b/web/core/components/inbox/settings/update-setting.tsx @@ -0,0 +1,51 @@ +"use client" + +import { FC, useState } from "react"; +import { observer } from "mobx-react"; +import { EWorkspaceNotificationTransport } from "@plane/constants"; +import { useTranslation } from "@plane/i18n"; +import { ToggleSwitch } from "@plane/ui"; +import { useWorkspaceNotificationSettings } from "@/hooks/store"; + +type InboxSettingUpdateProps = { + settings_key: string; + title: string; +} + +export const InboxSettingUpdate: FC = observer((props: InboxSettingUpdateProps) => { + const { title, settings_key } = props; + + const [isChecked, setIsChecked] = useState(false); + + const { t } = useTranslation() + + const { workspace: currentWorkspace, getNotificationSettingsForTransport } = useWorkspaceNotificationSettings(); + + + console.log("check tracked data", getNotificationSettingsForTransport(EWorkspaceNotificationTransport.EMAIL)) + return ( +
+
+ {t(title)} +
+
+ { + setIsChecked(newValue); + }} + size="md" + /> +
+
+ { + setIsChecked(newValue); + }} + size="md" + /> +
+
+ ); +}) \ No newline at end of file diff --git a/web/core/constants/fetch-keys.ts b/web/core/constants/fetch-keys.ts index ec5de760f6f..0bfbf58817c 100644 --- a/web/core/constants/fetch-keys.ts +++ b/web/core/constants/fetch-keys.ts @@ -273,3 +273,7 @@ export const COMMENT_REACTION_LIST = (workspaceSlug: string, projectId: string, export const API_TOKENS_LIST = (workspaceSlug: string) => `API_TOKENS_LIST_${workspaceSlug.toUpperCase()}`; export const API_TOKEN_DETAILS = (workspaceSlug: string, tokenId: string) => `API_TOKEN_DETAILS_${workspaceSlug.toUpperCase()}_${tokenId.toUpperCase()}`; + +// notification settings +export const NOTIFICATION_SETTINGS = (workspaceSlug: string) => + `NOTIFICATION_SETTINGS_${workspaceSlug.toUpperCase()}`; \ No newline at end of file diff --git a/web/core/hooks/store/notifications/index.ts b/web/core/hooks/store/notifications/index.ts index 07bcca1cf09..fab3afacb25 100644 --- a/web/core/hooks/store/notifications/index.ts +++ b/web/core/hooks/store/notifications/index.ts @@ -1,2 +1,3 @@ export * from "./use-workspace-notifications"; export * from "./use-notification"; +export * from './use-workspace-notification-settings'; diff --git a/web/core/hooks/store/notifications/use-workspace-notification-settings.ts b/web/core/hooks/store/notifications/use-workspace-notification-settings.ts new file mode 100644 index 00000000000..24031fd1ca1 --- /dev/null +++ b/web/core/hooks/store/notifications/use-workspace-notification-settings.ts @@ -0,0 +1,12 @@ +import { useContext } from "react"; +// mobx store +import { StoreContext } from "@/lib/store-context"; +// mobx store +import { IWorkspaceNotificationSettingsStore } from "@/store/notifications/workspace-notification-settings.store"; + +export const useWorkspaceNotificationSettings = (): IWorkspaceNotificationSettingsStore => { + const context = useContext(StoreContext); + if (context === undefined) throw new Error("useNotification must be used within StoreProvider"); + + return context.workspaceNotificationSettings; +}; diff --git a/web/core/services/workspace-notification-settings.service.ts b/web/core/services/workspace-notification-settings.service.ts new file mode 100644 index 00000000000..d81dcf7c767 --- /dev/null +++ b/web/core/services/workspace-notification-settings.service.ts @@ -0,0 +1,46 @@ +/* eslint-disable no-useless-catch */ + +import type { + TWorkspaceUserNotification, + TWorkspaceNotificationTransport, +} from "@plane/types"; +// helpers +import { API_BASE_URL } from "@/helpers/common.helper"; +// services +import { APIService } from "@/services/api.service"; + +export class WorkspaceNotificationSettingsService extends APIService { + constructor() { + super(API_BASE_URL); + } + + async fetchNotificationSettings(workspaceSlug: string): Promise { + try { + const { data } = await this.get(`/api/workspaces/${workspaceSlug}/user-notification-preferences/`); + return data || undefined; + } catch (error) { + throw error; + } + } + + + async updateNotificationSettings( + workspaceSlug: string, + transport: TWorkspaceNotificationTransport, + payload: Partial + ): Promise { + try { + const { data } = await this.patch( + `/api/workspaces/${workspaceSlug}/user-notification-preferences/${transport}/`, + payload + ); + return data || undefined; + } catch (error) { + throw error; + } + } +} + +const workspaceNotificationSettingService = new WorkspaceNotificationSettingsService(); + +export default workspaceNotificationSettingService; diff --git a/web/core/store/notifications/workspace-notification-settings.store.ts b/web/core/store/notifications/workspace-notification-settings.store.ts new file mode 100644 index 00000000000..18102e427ba --- /dev/null +++ b/web/core/store/notifications/workspace-notification-settings.store.ts @@ -0,0 +1,165 @@ +import set from "lodash/set"; +import { action, autorun, computed, makeObservable, observable, runInAction } from "mobx"; +import { IUser, IWorkspace, TWorkspaceNotificationTransport, TWorkspaceUserNotification } from "@plane/types"; +// plane web services +// plane web root store +import { WorkspaceNotificationSettingsService } from "@/services/workspace-notification-settings.service"; +import { CoreRootStore } from "../root.store"; + +export interface IWorkspaceNotificationSettingsStore { + // observables + error: object; + user: IUser | undefined; + workspace: IWorkspace | undefined; + settings: Record>; // workspaceSlug -> transport -> settings + // computed functions + notificationSettingsForWorkspace: Record | undefined; + getNotificationSettingsForTransport: ( + transport: TWorkspaceNotificationTransport + ) => TWorkspaceUserNotification | undefined; + // helper actions + fetchWorkspaceUserNotificationSettings: () => Promise; + updateWorkspaceUserNotificationSettings: ( + transport: TWorkspaceNotificationTransport, + settings: Partial + ) => Promise; +} + +export class WorkspaceNotificationSettingsStore implements IWorkspaceNotificationSettingsStore { + // observables + error: object = {}; + user: IUser | undefined = undefined; + workspace: IWorkspace | undefined = undefined; + settings: Record> = {}; + settingService: WorkspaceNotificationSettingsService; + + constructor(public store: CoreRootStore) { + makeObservable(this, { + // observables + error: observable, + user: observable, + workspace: observable, + settings: observable, + // //computed + notificationSettingsForWorkspace: computed, + getNotificationSettingsForTransport: computed, + // actions + fetchWorkspaceUserNotificationSettings: action, + updateWorkspaceUserNotificationSettings: action, + }); + + + autorun(() => { + const { + workspaceRoot: { currentWorkspace }, + user: { data: currentUser }, + } = this.store; + + if ( + currentWorkspace && + currentUser && + (!this.workspace || + !this.user || + this.workspace?.id !== currentWorkspace?.id || + this.user?.id !== currentUser?.id) + ) { + this.user = currentUser; + this.workspace = currentWorkspace; + } + }); + + this.settingService = new WorkspaceNotificationSettingsService(); + } + + // computed functions + /** + * @description get project ids by workspace slug + * @param { string } workspaceSlug + * @returns { string[] | undefined } + */ + get notificationSettingsForWorkspace() { + const workspaceSlug = this.store.workspaceRoot?.currentWorkspace?.slug; + if (!workspaceSlug) { + return; + } + return this.settings[workspaceSlug]; + } + + + /** + * @description get notification settings for the workspace for a transport + * @param { TWorkspaceNotificationTransport } transport + * @returns { TWorkspaceUserNotification } + */ + + getNotificationSettingsForTransport(transport: TWorkspaceNotificationTransport) { + const workspaceSlug = this.store.workspaceRoot?.currentWorkspace?.slug; + if (!workspaceSlug || !transport) { + return; + } + return this.settings[workspaceSlug]?.[transport] || undefined; + } + + // helper actions + /** + * @description handle states + * @returns { TWorkspaceUserNotification[] | undefined } + */ + fetchWorkspaceUserNotificationSettings = async (): Promise => { + + console.log("inside fetchWorkspace") + const workspaceSlug = this.store.workspaceRoot.currentWorkspace?.slug; + if (!workspaceSlug) return undefined; + + this.error = {}; + try { + const notificationSettings = await this.settingService.fetchNotificationSettings(workspaceSlug) + if (notificationSettings) { + runInAction(() => { + notificationSettings.forEach((state) => { + const { transport } = state; + set(this.settings, [workspaceSlug, transport], state); + }); + }); + } + return notificationSettings; + } catch (error) { + runInAction(() => { + this.error = error as unknown as object; + }); + throw error; + } + }; + + /** + * @description - updates user notification settings for a transport + * @param transport + * @param settings + * @returns { TWorkspaceNotificationTransport } + */ + updateWorkspaceUserNotificationSettings = async ( + transport: TWorkspaceNotificationTransport, + settings: Partial): Promise => { + + const workspaceSlug = this.store.workspaceRoot.currentWorkspace?.slug; + if (!workspaceSlug || !transport || !settings) { + return undefined; + } + + try { + const notificationSetting = await this.settingService.updateNotificationSettings(workspaceSlug, transport, settings) + if (notificationSetting) { + runInAction(() => { + set(this.settings, [workspaceSlug, transport], notificationSetting) + }) + } + return notificationSetting; + } catch (error) { + runInAction(() => { + this.error = error as unknown as object; + }); + throw error; + } + + }; +} diff --git a/web/core/store/root.store.ts b/web/core/store/root.store.ts index 03f1acdf497..ef7e04e9772 100644 --- a/web/core/store/root.store.ts +++ b/web/core/store/root.store.ts @@ -20,6 +20,7 @@ import { IMemberRootStore, MemberRootStore } from "./member"; import { IModuleStore, ModulesStore } from "./module.store"; import { IModuleFilterStore, ModuleFilterStore } from "./module_filter.store"; import { IMultipleSelectStore, MultipleSelectStore } from "./multiple_select.store"; +import { IWorkspaceNotificationSettingsStore, WorkspaceNotificationSettingsStore } from "./notifications/workspace-notification-settings.store"; import { IWorkspaceNotificationStore, WorkspaceNotificationStore } from "./notifications/workspace-notifications.store"; import { IProjectPageStore, ProjectPageStore } from "./pages/project-page.store"; import { IProjectRootStore, ProjectRootStore } from "./project"; @@ -58,6 +59,7 @@ export class CoreRootStore { projectEstimate: IProjectEstimateStore; multipleSelect: IMultipleSelectStore; workspaceNotification: IWorkspaceNotificationStore; + workspaceNotificationSettings: IWorkspaceNotificationSettingsStore; favorite: IFavoriteStore; transient: ITransientStore; stickyStore: IStickyStore; @@ -86,6 +88,7 @@ export class CoreRootStore { this.projectInbox = new ProjectInboxStore(this); this.projectPages = new ProjectPageStore(this as unknown as RootStore); this.projectEstimate = new ProjectEstimateStore(this); + this.workspaceNotificationSettings = new WorkspaceNotificationSettingsStore(this); this.workspaceNotification = new WorkspaceNotificationStore(this); this.favorite = new FavoriteStore(this); this.transient = new TransientStore(); @@ -118,6 +121,7 @@ export class CoreRootStore { this.projectPages = new ProjectPageStore(this as unknown as RootStore); this.multipleSelect = new MultipleSelectStore(); this.projectEstimate = new ProjectEstimateStore(this); + this.workspaceNotificationSettings = new WorkspaceNotificationSettingsStore(this); this.workspaceNotification = new WorkspaceNotificationStore(this); this.favorite = new FavoriteStore(this); this.transient = new TransientStore(); From 6b5d007241f2c16f4df363182258401460578766 Mon Sep 17 00:00:00 2001 From: Saurabhkmr98 Date: Tue, 18 Feb 2025 20:45:55 +0530 Subject: [PATCH 02/11] fix: computed functions --- .../workspace-notification-settings.store.ts | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/web/core/store/notifications/workspace-notification-settings.store.ts b/web/core/store/notifications/workspace-notification-settings.store.ts index 18102e427ba..4b1c354a206 100644 --- a/web/core/store/notifications/workspace-notification-settings.store.ts +++ b/web/core/store/notifications/workspace-notification-settings.store.ts @@ -1,5 +1,6 @@ import set from "lodash/set"; import { action, autorun, computed, makeObservable, observable, runInAction } from "mobx"; +import { computedFn } from "mobx-utils"; import { IUser, IWorkspace, TWorkspaceNotificationTransport, TWorkspaceUserNotification } from "@plane/types"; // plane web services // plane web root store @@ -13,7 +14,7 @@ export interface IWorkspaceNotificationSettingsStore { workspace: IWorkspace | undefined; settings: Record>; // workspaceSlug -> transport -> settings // computed functions - notificationSettingsForWorkspace: Record | undefined; + notificationSettingsForWorkspace: () => Record | undefined; getNotificationSettingsForTransport: ( transport: TWorkspaceNotificationTransport ) => TWorkspaceUserNotification | undefined; @@ -40,9 +41,6 @@ export class WorkspaceNotificationSettingsStore implements IWorkspaceNotificatio user: observable, workspace: observable, settings: observable, - // //computed - notificationSettingsForWorkspace: computed, - getNotificationSettingsForTransport: computed, // actions fetchWorkspaceUserNotificationSettings: action, updateWorkspaceUserNotificationSettings: action, @@ -77,13 +75,13 @@ export class WorkspaceNotificationSettingsStore implements IWorkspaceNotificatio * @param { string } workspaceSlug * @returns { string[] | undefined } */ - get notificationSettingsForWorkspace() { + notificationSettingsForWorkspace = computedFn(() => { const workspaceSlug = this.store.workspaceRoot?.currentWorkspace?.slug; if (!workspaceSlug) { return; } return this.settings[workspaceSlug]; - } + }); /** @@ -92,13 +90,14 @@ export class WorkspaceNotificationSettingsStore implements IWorkspaceNotificatio * @returns { TWorkspaceUserNotification } */ - getNotificationSettingsForTransport(transport: TWorkspaceNotificationTransport) { + getNotificationSettingsForTransport = computedFn((transport: TWorkspaceNotificationTransport) => { const workspaceSlug = this.store.workspaceRoot?.currentWorkspace?.slug; if (!workspaceSlug || !transport) { return; } - return this.settings[workspaceSlug]?.[transport] || undefined; - } + const notificationSettingsForTransport = this.settings[workspaceSlug][transport] || undefined; + return notificationSettingsForTransport; + }); // helper actions /** From 266de8c55f65f2ef84568994a3ab80a7316aa978 Mon Sep 17 00:00:00 2001 From: Saurabhkmr98 Date: Wed, 19 Feb 2025 13:52:41 +0530 Subject: [PATCH 03/11] integrated API with UI --- packages/constants/src/notification.ts | 21 +++--- .../i18n/src/locales/en/translations.json | 4 +- .../i18n/src/locales/es/translations.json | 23 ++++++ .../i18n/src/locales/fr/translations.json | 24 ++++++ .../i18n/src/locales/ja/translations.json | 23 ++++++ .../i18n/src/locales/zh-CN/translations.json | 23 ++++++ packages/types/src/notification.d.ts | 8 +- web/core/components/inbox/settings/index.ts | 2 +- web/core/components/inbox/settings/root.tsx | 6 +- .../inbox/settings/update-setting-row.tsx | 36 +++++++++ .../inbox/settings/update-setting.tsx | 73 ++++++++++--------- ...workspace-notification-settings.service.ts | 4 +- .../workspace-notification-settings.store.ts | 19 ++--- 13 files changed, 202 insertions(+), 64 deletions(-) create mode 100644 web/core/components/inbox/settings/update-setting-row.tsx diff --git a/packages/constants/src/notification.ts b/packages/constants/src/notification.ts index e4296188ed0..148f81acb47 100644 --- a/packages/constants/src/notification.ts +++ b/packages/constants/src/notification.ts @@ -138,19 +138,18 @@ export const allTimeIn30MinutesInterval12HoursFormat: Array<{ export enum ENotificationSettingsKey { - WORK_ITEM_PROPERTY_UPDATES = "work_item_property_updates", - STATUS_UPDATES = "status_updates", - PRIORITY_UPDATES = "priority_updates", - ASSIGNEE_UPDATES = "assignee_updates", - START_DUE_DATE_UPDATES = "start_due_date_updates", - MODULE_UPDATES = "module_updates", - CYCLE_UPDATES = "cycle_updates", - MENTIONED_COMMENTS_UPDATES = "mentioned_comments_updates", - NEW_COMMENTS_UPDATES = "new_comments_updates", - REACTION_COMMENT_UPDATES = "reaction_comment_updates", + WORK_ITEM_PROPERTY_UPDATES = "work_item_property_updates_enabled", + STATUS_UPDATES = "status_updates_enabled", + PRIORITY_UPDATES = "priority_updates_enabled", + ASSIGNEE_UPDATES = "assignee_updates_enabled", + START_DUE_DATE_UPDATES = "start_due_date_updates_enabled", + MODULE_UPDATES = "module_updates_enabled", + CYCLE_UPDATES = "cycle_updates_enabled", + MENTIONED_COMMENTS_UPDATES = "mentioned_comments_updates_enabled", + NEW_COMMENTS_UPDATES = "new_comments_updates_enabled", + REACTION_COMMENT_UPDATES = "reaction_comments_updates_enabled", } - export enum EWorkspaceNotificationTransport { EMAIL = "EMAIL", IN_APP = "IN_APP", diff --git a/packages/i18n/src/locales/en/translations.json b/packages/i18n/src/locales/en/translations.json index d2697dcabce..14b3f7e04df 100644 --- a/packages/i18n/src/locales/en/translations.json +++ b/packages/i18n/src/locales/en/translations.json @@ -2198,6 +2198,8 @@ "cycle_title": "Cycle update", "mentioned_comments_title": "Comments I'm @mentioned in", "new_comments_title": "New comments", - "reaction_comments_title": "Reactions" + "reaction_comments_title": "Reactions", + "setting_updated_successfully": "Setting updated successfully", + "failed_to_update_setting": "Failed to update setting" } } diff --git a/packages/i18n/src/locales/es/translations.json b/packages/i18n/src/locales/es/translations.json index 0a81c1f32e6..4ccab4a003a 100644 --- a/packages/i18n/src/locales/es/translations.json +++ b/packages/i18n/src/locales/es/translations.json @@ -2347,5 +2347,28 @@ "module": { "label": "{count, plural, one {Módulo} other {Módulos}}", "no_module": "Sin módulo" + }, + + "notification_settings": { + "page_label": "{workspace} - Configuración de la bandeja de entrada", + "inbox_settings": "Configuración de la bandeja de entrada", + "inbox_settings_description": "Activa o desactiva estas opciones para el espacio de trabajo", + "advanced_settings": "Configuración avanzada", + "in_plane": "En Plane", + "email": "Correo electrónico", + "task_updates": "Actualizaciones de tareas", + "comments": "Comentarios", + "work_item_property_title": "Actualización de cualquier propiedad del elemento de trabajo", + "status_title": "Actualización de estado", + "priority_title": "Actualización de prioridad", + "assignee_title": "Actualización de asignado", + "due_date_title": "Actualización de fecha de inicio/vencimiento", + "module_title": "Actualización de módulo", + "cycle_title": "Actualización de ciclo", + "mentioned_comments_title": "Comentarios en los que estoy @mencionado", + "new_comments_title": "Nuevos comentarios", + "reaction_comments_title": "Reacciones", + "setting_updated_successfully": "Configuración actualizada correctamente", + "failed_to_update_setting": "No se pudo actualizar la configuración" } } diff --git a/packages/i18n/src/locales/fr/translations.json b/packages/i18n/src/locales/fr/translations.json index c23ae214855..17a6df80cbe 100644 --- a/packages/i18n/src/locales/fr/translations.json +++ b/packages/i18n/src/locales/fr/translations.json @@ -2347,5 +2347,29 @@ "module": { "label": "{count, plural, one {Module} other {Modules}}", "no_module": "Pas de module" + }, + + "notification_settings": { + "page_label": "{workspace} - Paramètres de la boîte de réception", + "inbox_settings": "Paramètres de la boîte de réception", + "inbox_settings_description": "Activez ou désactivez ces options pour l'espace de travail", + "advanced_settings": "Paramètres avancés", + "in_plane": "Dans Plane", + "email": "E-mail", + "task_updates": "Mises à jour des tâches", + "comments": "Commentaires", + "work_item_property_title": "Mise à jour de toute propriété de l'élément de travail", + "status_title": "Mise à jour du statut", + "priority_title": "Mise à jour de la priorité", + "assignee_title": "Mise à jour de l'assigné", + "due_date_title": "Mise à jour de la date de début/d'échéance", + "module_title": "Mise à jour du module", + "cycle_title": "Mise à jour du cycle", + "mentioned_comments_title": "Commentaires où je suis @mentionné", + "new_comments_title": "Nouveaux commentaires", + "reaction_comments_title": "Réactions", + "setting_updated_successfully": "Paramètre mis à jour avec succès", + "failed_to_update_setting": "Échec de la mise à jour du paramètre" } + } diff --git a/packages/i18n/src/locales/ja/translations.json b/packages/i18n/src/locales/ja/translations.json index af5e951dce0..4a4c1b0f200 100644 --- a/packages/i18n/src/locales/ja/translations.json +++ b/packages/i18n/src/locales/ja/translations.json @@ -2347,5 +2347,28 @@ "module": { "label": "{count, plural, one {モジュール} other {モジュール}}", "no_module": "モジュールなし" + }, + + "notification_settings": { + "page_label": "{workspace} - 受信トレイ設定", + "inbox_settings": "受信トレイ設定", + "inbox_settings_description": "ワークスペースのためにこれらをオンまたはオフに切り替えます", + "advanced_settings": "詳細設定", + "in_plane": "Plane内", + "email": "メール", + "task_updates": "タスクの更新", + "comments": "コメント", + "work_item_property_title": "作業項目のプロパティの更新", + "status_title": "ステータスの更新", + "priority_title": "優先度の更新", + "assignee_title": "担当者の更新", + "due_date_title": "開始日/期日の更新", + "module_title": "モジュールの更新", + "cycle_title": "サイクルの更新", + "mentioned_comments_title": "自分が@メンションされたコメント", + "new_comments_title": "新しいコメント", + "reaction_comments_title": "リアクション", + "setting_updated_successfully": "設定が正常に更新されました", + "failed_to_update_setting": "設定の更新に失敗しました" } } diff --git a/packages/i18n/src/locales/zh-CN/translations.json b/packages/i18n/src/locales/zh-CN/translations.json index f25155216e5..85c3184941e 100644 --- a/packages/i18n/src/locales/zh-CN/translations.json +++ b/packages/i18n/src/locales/zh-CN/translations.json @@ -2347,5 +2347,28 @@ "module": { "label": "{count, plural, one {模块} other {模块}}", "no_module": "无模块" + }, + + "notification_settings": { + "page_label": "{workspace} - 收件箱设置", + "inbox_settings": "收件箱设置", + "inbox_settings_description": "为工作空间打开或关闭这些选项", + "advanced_settings": "高级设置", + "in_plane": "在 Plane", + "email": "电子邮件", + "task_updates": "任务更新", + "comments": "评论", + "work_item_property_title": "工作项属性更新", + "status_title": "状态更新", + "priority_title": "优先级更新", + "assignee_title": "负责人更新", + "due_date_title": "开始/截止日期更新", + "module_title": "模块更新", + "cycle_title": "周期更新", + "mentioned_comments_title": "我被@提及的评论", + "new_comments_title": "新评论", + "reaction_comments_title": "反应", + "setting_updated_successfully": "设置已成功更新", + "failed_to_update_setting": "设置更新失败" } } diff --git a/packages/types/src/notification.d.ts b/packages/types/src/notification.d.ts index 2175bac0c5f..27e11d2e43a 100644 --- a/packages/types/src/notification.d.ts +++ b/packages/types/src/notification.d.ts @@ -1,14 +1,14 @@ +import { ENotificationSettingsKey, EWorkspaceNotificationTransport } from "@plane/constants"; + export type TNotificationSettings = { i18n_title: string, - key: string + key: ENotificationSettingsKey } -export type TWorkspaceNotificationTransport = "EMAIL" | "IN_APP" | "PUSH" | "SLACK"; - export type TWorkspaceUserNotification = { workspace: string, user: string, - transport: TWorkspaceNotificationTransport, + transport: EWorkspaceNotificationTransport, work_item_property_updates_enabled: boolean, status_updates_enabled: boolean, priority_updates_enabled: boolean, diff --git a/web/core/components/inbox/settings/index.ts b/web/core/components/inbox/settings/index.ts index ab5ca770cde..ab30ec426cc 100644 --- a/web/core/components/inbox/settings/index.ts +++ b/web/core/components/inbox/settings/index.ts @@ -2,4 +2,4 @@ export * from "./content-header" export * from "./root" export * from "./content-wrapper" export * from "./content-header" -export * from "./update-setting" \ No newline at end of file +export * from "./update-setting-row" \ No newline at end of file diff --git a/web/core/components/inbox/settings/root.tsx b/web/core/components/inbox/settings/root.tsx index b4328ead504..6b02790ba4b 100644 --- a/web/core/components/inbox/settings/root.tsx +++ b/web/core/components/inbox/settings/root.tsx @@ -3,7 +3,7 @@ import { FC } from "react"; import { COMMENT_NOTIFICATION_SETTINGS, TASK_UPDATES_NOTIFICATION_SETTINGS } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; -import { InboxSettingUpdate } from "./update-setting"; +import { InboxSettingUpdateRow } from "./update-setting-row"; export const InboxSettingsRoot: FC = (props) => { @@ -23,7 +23,7 @@ export const InboxSettingsRoot: FC = (props) => { { TASK_UPDATES_NOTIFICATION_SETTINGS?.map((item, index) => ( - + )) } @@ -38,7 +38,7 @@ export const InboxSettingsRoot: FC = (props) => { { COMMENT_NOTIFICATION_SETTINGS?.map((item, index) => ( - + )) } diff --git a/web/core/components/inbox/settings/update-setting-row.tsx b/web/core/components/inbox/settings/update-setting-row.tsx new file mode 100644 index 00000000000..aa94733a2f6 --- /dev/null +++ b/web/core/components/inbox/settings/update-setting-row.tsx @@ -0,0 +1,36 @@ +"use client" + +import { FC, useState } from "react"; +import { observer } from "mobx-react"; +import { ENotificationSettingsKey, EWorkspaceNotificationTransport } from "@plane/constants"; +import { useTranslation } from "@plane/i18n"; +import { useWorkspaceNotificationSettings } from "@/hooks/store"; +import { InboxSettingUpdate } from "./update-setting"; + +type InboxSettingUpdateRowProps = { + settings_key: ENotificationSettingsKey; + title: string; +} + +export const InboxSettingUpdateRow: FC = observer((props: InboxSettingUpdateRowProps) => { + const { title, settings_key } = props; + + const { t } = useTranslation() + + const { getNotificationSettingsForTransport } = useWorkspaceNotificationSettings(); + + console.log("check tracked data", getNotificationSettingsForTransport(EWorkspaceNotificationTransport.EMAIL)) + return ( +
+
+ {t(title)} +
+
+ +
+
+ +
+
+ ); +}) \ No newline at end of file diff --git a/web/core/components/inbox/settings/update-setting.tsx b/web/core/components/inbox/settings/update-setting.tsx index 55022e50b86..4b275917fa9 100644 --- a/web/core/components/inbox/settings/update-setting.tsx +++ b/web/core/components/inbox/settings/update-setting.tsx @@ -1,51 +1,58 @@ "use client" -import { FC, useState } from "react"; +import { FC } from "react"; import { observer } from "mobx-react"; -import { EWorkspaceNotificationTransport } from "@plane/constants"; +import { ENotificationSettingsKey, EWorkspaceNotificationTransport } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; -import { ToggleSwitch } from "@plane/ui"; +import { setToast, TOAST_TYPE, ToggleSwitch } from "@plane/ui"; import { useWorkspaceNotificationSettings } from "@/hooks/store"; type InboxSettingUpdateProps = { - settings_key: string; - title: string; + settings_key: ENotificationSettingsKey; + transport: EWorkspaceNotificationTransport; } export const InboxSettingUpdate: FC = observer((props: InboxSettingUpdateProps) => { - const { title, settings_key } = props; - - const [isChecked, setIsChecked] = useState(false); - + const { transport, settings_key } = props; const { t } = useTranslation() - const { workspace: currentWorkspace, getNotificationSettingsForTransport } = useWorkspaceNotificationSettings(); + const { getNotificationSettingsForTransport, updateWorkspaceUserNotificationSettings } = useWorkspaceNotificationSettings(); + + const notificationSettings = getNotificationSettingsForTransport(transport); + + const handleChange = async (value: boolean) => { + try { + await updateWorkspaceUserNotificationSettings(transport, { + [settings_key]: value, + }); + setToast({ + title: t("success"), + type: TOAST_TYPE.SUCCESS, + message: t("notification_settings.setting_updated_successfully"), + }) + } catch (error) { + setToast({ + title: t("error"), + type: TOAST_TYPE.ERROR, + message: t("notification_settings.failed_to_update_setting"), + }) + } + } + + if (!notificationSettings) { + return null; + } console.log("check tracked data", getNotificationSettingsForTransport(EWorkspaceNotificationTransport.EMAIL)) return ( -
-
- {t(title)} -
-
- { - setIsChecked(newValue); - }} - size="md" - /> -
-
- { - setIsChecked(newValue); - }} - size="md" - /> -
-
+ { + handleChange(newValue); + }} + size="md" + /> + ); }) \ No newline at end of file diff --git a/web/core/services/workspace-notification-settings.service.ts b/web/core/services/workspace-notification-settings.service.ts index d81dcf7c767..9ce037415c3 100644 --- a/web/core/services/workspace-notification-settings.service.ts +++ b/web/core/services/workspace-notification-settings.service.ts @@ -1,8 +1,8 @@ /* eslint-disable no-useless-catch */ +import { EWorkspaceNotificationTransport } from "@plane/constants"; import type { TWorkspaceUserNotification, - TWorkspaceNotificationTransport, } from "@plane/types"; // helpers import { API_BASE_URL } from "@/helpers/common.helper"; @@ -26,7 +26,7 @@ export class WorkspaceNotificationSettingsService extends APIService { async updateNotificationSettings( workspaceSlug: string, - transport: TWorkspaceNotificationTransport, + transport: EWorkspaceNotificationTransport, payload: Partial ): Promise { try { diff --git a/web/core/store/notifications/workspace-notification-settings.store.ts b/web/core/store/notifications/workspace-notification-settings.store.ts index 4b1c354a206..952b87d60da 100644 --- a/web/core/store/notifications/workspace-notification-settings.store.ts +++ b/web/core/store/notifications/workspace-notification-settings.store.ts @@ -1,7 +1,8 @@ import set from "lodash/set"; import { action, autorun, computed, makeObservable, observable, runInAction } from "mobx"; import { computedFn } from "mobx-utils"; -import { IUser, IWorkspace, TWorkspaceNotificationTransport, TWorkspaceUserNotification } from "@plane/types"; +import { EWorkspaceNotificationTransport } from "@plane/constants"; +import { IUser, IWorkspace, TWorkspaceUserNotification } from "@plane/types"; // plane web services // plane web root store import { WorkspaceNotificationSettingsService } from "@/services/workspace-notification-settings.service"; @@ -12,16 +13,16 @@ export interface IWorkspaceNotificationSettingsStore { error: object; user: IUser | undefined; workspace: IWorkspace | undefined; - settings: Record>; // workspaceSlug -> transport -> settings + settings: Record>; // workspaceSlug -> transport -> settings // computed functions notificationSettingsForWorkspace: () => Record | undefined; getNotificationSettingsForTransport: ( - transport: TWorkspaceNotificationTransport + transport: EWorkspaceNotificationTransport ) => TWorkspaceUserNotification | undefined; // helper actions fetchWorkspaceUserNotificationSettings: () => Promise; updateWorkspaceUserNotificationSettings: ( - transport: TWorkspaceNotificationTransport, + transport: EWorkspaceNotificationTransport, settings: Partial ) => Promise; } @@ -31,7 +32,7 @@ export class WorkspaceNotificationSettingsStore implements IWorkspaceNotificatio error: object = {}; user: IUser | undefined = undefined; workspace: IWorkspace | undefined = undefined; - settings: Record> = {}; + settings: Record> = {}; settingService: WorkspaceNotificationSettingsService; constructor(public store: CoreRootStore) { @@ -86,11 +87,11 @@ export class WorkspaceNotificationSettingsStore implements IWorkspaceNotificatio /** * @description get notification settings for the workspace for a transport - * @param { TWorkspaceNotificationTransport } transport + * @param { EWorkspaceNotificationTransport } transport * @returns { TWorkspaceUserNotification } */ - getNotificationSettingsForTransport = computedFn((transport: TWorkspaceNotificationTransport) => { + getNotificationSettingsForTransport = computedFn((transport: EWorkspaceNotificationTransport) => { const workspaceSlug = this.store.workspaceRoot?.currentWorkspace?.slug; if (!workspaceSlug || !transport) { return; @@ -134,10 +135,10 @@ export class WorkspaceNotificationSettingsStore implements IWorkspaceNotificatio * @description - updates user notification settings for a transport * @param transport * @param settings - * @returns { TWorkspaceNotificationTransport } + * @returns { EWorkspaceNotificationTransport } */ updateWorkspaceUserNotificationSettings = async ( - transport: TWorkspaceNotificationTransport, + transport: EWorkspaceNotificationTransport, settings: Partial): Promise => { const workspaceSlug = this.store.workspaceRoot.currentWorkspace?.slug; From 561816027f6cd8fb880eceaff3b7efedea9180ab Mon Sep 17 00:00:00 2001 From: Saurabhkmr98 Date: Wed, 19 Feb 2025 16:15:46 +0530 Subject: [PATCH 04/11] remove console logs --- web/core/components/inbox/settings/update-setting.tsx | 1 - .../store/notifications/workspace-notification-settings.store.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/web/core/components/inbox/settings/update-setting.tsx b/web/core/components/inbox/settings/update-setting.tsx index 4b275917fa9..59462b343d0 100644 --- a/web/core/components/inbox/settings/update-setting.tsx +++ b/web/core/components/inbox/settings/update-setting.tsx @@ -44,7 +44,6 @@ export const InboxSettingUpdate: FC = observer((props: return null; } - console.log("check tracked data", getNotificationSettingsForTransport(EWorkspaceNotificationTransport.EMAIL)) return ( => { - console.log("inside fetchWorkspace") const workspaceSlug = this.store.workspaceRoot.currentWorkspace?.slug; if (!workspaceSlug) return undefined; From b22f5f8820860adc7895a3987239a2d2d7a1b72e Mon Sep 17 00:00:00 2001 From: Saurabhkmr98 Date: Mon, 24 Feb 2025 17:11:01 +0530 Subject: [PATCH 05/11] notification task updates for user preference model changes --- apiserver/plane/app/serializers/__init__.py | 3 +- .../plane/app/serializers/notification.py | 7 +- .../plane/app/views/notification/base.py | 25 +- apiserver/plane/app/views/workspace/home.py | 10 +- apiserver/plane/bgtasks/notification_task.py | 249 ++++++++++-------- apiserver/plane/db/models/__init__.py | 2 +- apiserver/plane/db/models/notification.py | 77 ++---- apiserver/plane/db/models/user.py | 17 -- packages/constants/src/notification.ts | 42 ++- .../i18n/src/locales/en/translations.json | 2 +- .../i18n/src/locales/fr/translations.json | 2 +- .../i18n/src/locales/ja/translations.json | 2 +- packages/types/src/notification.d.ts | 18 +- .../inbox/settings/update-setting-row.tsx | 4 +- .../workspace-notification-settings.store.ts | 2 +- 15 files changed, 207 insertions(+), 255 deletions(-) diff --git a/apiserver/plane/app/serializers/__init__.py b/apiserver/plane/app/serializers/__init__.py index d024fca94da..98722f53a8c 100644 --- a/apiserver/plane/app/serializers/__init__.py +++ b/apiserver/plane/app/serializers/__init__.py @@ -117,8 +117,7 @@ from .notification import ( NotificationSerializer, - UserNotificationPreferenceSerializer, - WorkspaceUserNotificationPreferenceSerializer, + UserNotificationPreferenceSerializer ) from .exporter import ExporterHistorySerializer diff --git a/apiserver/plane/app/serializers/notification.py b/apiserver/plane/app/serializers/notification.py index ac054fd58c9..58007ec26c4 100644 --- a/apiserver/plane/app/serializers/notification.py +++ b/apiserver/plane/app/serializers/notification.py @@ -1,7 +1,7 @@ # Module imports from .base import BaseSerializer from .user import UserLiteSerializer -from plane.db.models import Notification, UserNotificationPreference, WorkspaceUserNotificationPreference +from plane.db.models import Notification, UserNotificationPreference # Third Party imports from rest_framework import serializers @@ -22,8 +22,3 @@ class UserNotificationPreferenceSerializer(BaseSerializer): class Meta: model = UserNotificationPreference fields = "__all__" - -class WorkspaceUserNotificationPreferenceSerializer(BaseSerializer): - class Meta: - model = WorkspaceUserNotificationPreference - fields = "__all__" \ No newline at end of file diff --git a/apiserver/plane/app/views/notification/base.py b/apiserver/plane/app/views/notification/base.py index df488c3907c..c6ff6f2b37d 100644 --- a/apiserver/plane/app/views/notification/base.py +++ b/apiserver/plane/app/views/notification/base.py @@ -8,8 +8,7 @@ from plane.app.serializers import ( NotificationSerializer, - UserNotificationPreferenceSerializer, - WorkspaceUserNotificationPreferenceSerializer + UserNotificationPreferenceSerializer ) from plane.db.models import ( Issue, @@ -19,7 +18,7 @@ UserNotificationPreference, WorkspaceMember, Workspace, - WorkspaceUserNotificationPreference + NotificationTransportChoices ) from plane.utils.paginator import BasePaginator from plane.app.permissions import allow_permission, ROLE @@ -367,8 +366,8 @@ def patch(self, request): class WorkspaceUserNotificationPreferenceEndpoint(BaseAPIView): - model = WorkspaceUserNotificationPreference - serializer_class = WorkspaceUserNotificationPreferenceSerializer + model = UserNotificationPreference + serializer_class = UserNotificationPreferenceSerializer @allow_permission( allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE" @@ -376,7 +375,7 @@ class WorkspaceUserNotificationPreferenceEndpoint(BaseAPIView): def get(self, request, slug): workspace = Workspace.objects.get(slug=slug) get_notification_preferences = ( - WorkspaceUserNotificationPreference.objects.filter( + UserNotificationPreference.objects.filter( workspace=workspace, user=request.user ) ) @@ -385,7 +384,7 @@ def get(self, request, slug): transports = [ transport - for transport, _ in WorkspaceUserNotificationPreference.TransportChoices.choices + for transport, _ in NotificationTransportChoices.choices ] for transport in transports: @@ -396,9 +395,9 @@ def get(self, request, slug): notification_preferences = ( - WorkspaceUserNotificationPreference.objects.bulk_create( + UserNotificationPreference.objects.bulk_create( [ - WorkspaceUserNotificationPreference( + UserNotificationPreference( workspace=workspace, user=request.user, transport=transport, @@ -408,12 +407,12 @@ def get(self, request, slug): ) ) - notification_preferences = WorkspaceUserNotificationPreference.objects.filter( + notification_preferences = UserNotificationPreference.objects.filter( workspace=workspace, user=request.user ) return Response( - WorkspaceUserNotificationPreferenceSerializer( + UserNotificationPreferenceSerializer( notification_preferences, many=True ).data, status=status.HTTP_200_OK, @@ -423,12 +422,12 @@ def get(self, request, slug): allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE" ) def patch(self, request, slug, transport): - notification_preference = WorkspaceUserNotificationPreference.objects.filter( + notification_preference = UserNotificationPreference.objects.filter( transport=transport, workspace__slug=slug, user=request.user ).first() if notification_preference: - serializer = WorkspaceUserNotificationPreferenceSerializer( + serializer = UserNotificationPreferenceSerializer( notification_preference, data=request.data, partial=True ) diff --git a/apiserver/plane/app/views/workspace/home.py b/apiserver/plane/app/views/workspace/home.py index a69007aab8a..46bda35670f 100644 --- a/apiserver/plane/app/views/workspace/home.py +++ b/apiserver/plane/app/views/workspace/home.py @@ -2,7 +2,7 @@ from ..base import BaseAPIView from plane.db.models.workspace import WorkspaceHomePreference from plane.app.permissions import allow_permission, ROLE -from plane.db.models import Workspace, WorkspaceUserNotificationPreference +from plane.db.models import Workspace, UserNotificationPreference, NotificationTransportChoices from plane.app.serializers.workspace import WorkspaceHomePreferenceSerializer # Third party imports @@ -62,7 +62,7 @@ def get(self, request, slug): # Notification preference get or create workspace = Workspace.objects.get(slug=slug) get_notification_preferences = ( - WorkspaceUserNotificationPreference.objects.filter( + UserNotificationPreference.objects.filter( workspace=workspace, user=request.user ) ) @@ -71,7 +71,7 @@ def get(self, request, slug): transports = [ transport - for transport, _ in WorkspaceUserNotificationPreference.TransportChoices.choices + for transport, _ in NotificationTransportChoices.choices ] @@ -81,9 +81,9 @@ def get(self, request, slug): ): create_transports.append(transport) - _ = WorkspaceUserNotificationPreference.objects.bulk_create( + _ = UserNotificationPreference.objects.bulk_create( [ - WorkspaceUserNotificationPreference( + UserNotificationPreference( workspace=workspace, user=request.user, transport=transport ) for transport in create_transports diff --git a/apiserver/plane/bgtasks/notification_task.py b/apiserver/plane/bgtasks/notification_task.py index e58344bbf2e..86b8ed67e28 100644 --- a/apiserver/plane/bgtasks/notification_task.py +++ b/apiserver/plane/bgtasks/notification_task.py @@ -19,6 +19,7 @@ IssueActivity, UserNotificationPreference, ProjectMember, + NotificationTransportChoices ) from django.db.models import Subquery @@ -340,6 +341,7 @@ def notifications( ).values_list("assignee", flat=True) issue_subscribers = list(set(issue_subscribers) - {uuid.UUID(actor_id)}) + issue_workspace_id = project.workspace_id for subscriber in issue_subscribers: if issue.created_by_id and issue.created_by_id == subscriber: @@ -352,7 +354,11 @@ def notifications( else: sender = "in_app:issue_activities:subscribed" - preference = UserNotificationPreference.objects.get(user_id=subscriber) + preference = UserNotificationPreference.objects.filter( + user_id=subscriber, workspace_id=issue_workspace_id + ) + email_preference = preference.filter(transport="EMAIL").first() + in_app_preference = preference.filter(transport="IN_APP").first() for issue_activity in issue_activities_created: # If activity done in blocking then blocked by email should not go @@ -365,29 +371,32 @@ def notifications( # Check if the value should be sent or not send_email = False - if ( - issue_activity.get("field") == "state" - and preference.state_change - ): - send_email = True - elif ( - issue_activity.get("field") == "state" - and preference.issue_completed - and State.objects.filter( - project_id=project_id, - pk=issue_activity.get("new_identifier"), - group="completed", - ).exists() - ): - send_email = True + send_in_app = False + if issue_activity.get("field") == "state": + send_email = True if email_preference.state_change else False + send_in_app = True if in_app_preference.state_change else False + elif issue_activity.get("field") == "comment": + send_email = True if email_preference.comment else False + send_in_app = True if in_app_preference.comment else False + elif issue_activity.get("field") == "priority": + send_email = True if email_preference.priority else False + send_in_app = True if in_app_preference.priority else False + elif issue_activity.get("field") == "assignee": + send_email = True if email_preference.assignee else False + send_in_app = True if in_app_preference.assignee else False elif ( - issue_activity.get("field") == "comment" and preference.comment + issue_activity.get("field") == "start_date" + or issue_activity.get("field") == "target_date" ): - send_email = True - elif preference.property_change: - send_email = True + send_email = True if email_preference.start_due_date else False + send_in_app = ( + True if in_app_preference.start_due_date else False + ) else: - send_email = False + send_email = True if email_preference.property_change else False + send_in_app = ( + True if in_app_preference.property_change else False + ) # If activity is of issue comment fetch the comment issue_comment = ( @@ -402,51 +411,52 @@ def notifications( ) # Create in app notification - bulk_notifications.append( - Notification( - workspace=project.workspace, - sender=sender, - triggered_by_id=actor_id, - receiver_id=subscriber, - entity_identifier=issue_id, - entity_name="issue", - project=project, - title=issue_activity.get("comment"), - data={ - "issue": { - "id": str(issue_id), - "name": str(issue.name), - "identifier": str(issue.project.identifier), - "sequence_id": issue.sequence_id, - "state_name": issue.state.name, - "state_group": issue.state.group, - }, - "issue_activity": { - "id": str(issue_activity.get("id")), - "verb": str(issue_activity.get("verb")), - "field": str(issue_activity.get("field")), - "actor": str(issue_activity.get("actor_id")), - "new_value": str(issue_activity.get("new_value")), - "old_value": str(issue_activity.get("old_value")), - "issue_comment": str( - issue_comment.comment_stripped - if issue_comment is not None - else "" - ), - "old_identifier": ( - str(issue_activity.get("old_identifier")) - if issue_activity.get("old_identifier") - else None - ), - "new_identifier": ( - str(issue_activity.get("new_identifier")) - if issue_activity.get("new_identifier") - else None - ), + if send_in_app: + bulk_notifications.append( + Notification( + workspace=project.workspace, + sender=sender, + triggered_by_id=actor_id, + receiver_id=subscriber, + entity_identifier=issue_id, + entity_name="issue", + project=project, + title=issue_activity.get("comment"), + data={ + "issue": { + "id": str(issue_id), + "name": str(issue.name), + "identifier": str(issue.project.identifier), + "sequence_id": issue.sequence_id, + "state_name": issue.state.name, + "state_group": issue.state.group, + }, + "issue_activity": { + "id": str(issue_activity.get("id")), + "verb": str(issue_activity.get("verb")), + "field": str(issue_activity.get("field")), + "actor": str(issue_activity.get("actor_id")), + "new_value": str(issue_activity.get("new_value")), + "old_value": str(issue_activity.get("old_value")), + "issue_comment": str( + issue_comment.comment_stripped + if issue_comment is not None + else "" + ), + "old_identifier": ( + str(issue_activity.get("old_identifier")) + if issue_activity.get("old_identifier") + else None + ), + "new_identifier": ( + str(issue_activity.get("new_identifier")) + if issue_activity.get("new_identifier") + else None + ), + }, }, - }, + ) ) - ) # Create email notification if send_email: bulk_email_logs.append( @@ -521,9 +531,12 @@ def notifications( for mention_id in comment_mentions: if mention_id != actor_id: - preference = UserNotificationPreference.objects.get( - user_id=mention_id + preference = UserNotificationPreference.objects.filter( + user_id=mention_id, + workspace_id=issue_workspace_id, ) + email_preference = preference.filter(transport=NotificationTransportChoices.EMAIL[0]).first() + in_app_preference = preference.filter(transport=NotificationTransportChoices.IN_APP[0]).first() for issue_activity in issue_activities_created: notification = create_mention_notification( project=project, @@ -536,7 +549,7 @@ def notifications( ) # check for email notifications - if preference.mention: + if email_preference.mention: bulk_email_logs.append( EmailNotificationLog( triggered_by_id=actor_id, @@ -590,63 +603,68 @@ def notifications( }, ) ) - bulk_notifications.append(notification) + if in_app_preference.mention: + bulk_notifications.append(notification) for mention_id in new_mentions: if mention_id != actor_id: - preference = UserNotificationPreference.objects.get( - user_id=mention_id + preference = UserNotificationPreference.objects.filter( + user_id=mention_id, + workspace_id=issue_workspace_id, ) + email_preference = preference.filter(transport=NotificationTransportChoices.EMAIL[0]).first() + in_app_preference = preference.filter(transport=NotificationTransportChoices.IN_APP[0]).first() if ( last_activity is not None and last_activity.field == "description" and actor_id == str(last_activity.actor_id) ): - bulk_notifications.append( - Notification( - workspace=project.workspace, - sender="in_app:issue_activities:mentioned", - triggered_by_id=actor_id, - receiver_id=mention_id, - entity_identifier=issue_id, - entity_name="issue", - project=project, - message=f"You have been mentioned in the issue {issue.name}", - data={ - "issue": { - "id": str(issue_id), - "name": str(issue.name), - "identifier": str(issue.project.identifier), - "sequence_id": issue.sequence_id, - "state_name": issue.state.name, - "state_group": issue.state.group, - "project_id": str(issue.project.id), - "workspace_slug": str( - issue.project.workspace.slug - ), - }, - "issue_activity": { - "id": str(last_activity.id), - "verb": str(last_activity.verb), - "field": str(last_activity.field), - "actor": str(last_activity.actor_id), - "new_value": str(last_activity.new_value), - "old_value": str(last_activity.old_value), - "old_identifier": ( - str(issue_activity.get("old_identifier")) - if issue_activity.get("old_identifier") - else None - ), - "new_identifier": ( - str(issue_activity.get("new_identifier")) - if issue_activity.get("new_identifier") - else None - ), + if in_app_preference.mention: + bulk_notifications.append( + Notification( + workspace=project.workspace, + sender="in_app:issue_activities:mentioned", + triggered_by_id=actor_id, + receiver_id=mention_id, + entity_identifier=issue_id, + entity_name="issue", + project=project, + message=f"You have been mentioned in the issue {issue.name}", + data={ + "issue": { + "id": str(issue_id), + "name": str(issue.name), + "identifier": str(issue.project.identifier), + "sequence_id": issue.sequence_id, + "state_name": issue.state.name, + "state_group": issue.state.group, + "project_id": str(issue.project.id), + "workspace_slug": str( + issue.project.workspace.slug + ), + }, + "issue_activity": { + "id": str(last_activity.id), + "verb": str(last_activity.verb), + "field": str(last_activity.field), + "actor": str(last_activity.actor_id), + "new_value": str(last_activity.new_value), + "old_value": str(last_activity.old_value), + "old_identifier": ( + str(issue_activity.get("old_identifier")) + if issue_activity.get("old_identifier") + else None + ), + "new_identifier": ( + str(issue_activity.get("new_identifier")) + if issue_activity.get("new_identifier") + else None + ), + }, }, - }, + ) ) - ) - if preference.mention: + if email_preference.mention: bulk_email_logs.append( EmailNotificationLog( triggered_by_id=actor_id, @@ -701,7 +719,7 @@ def notifications( issue_id=issue_id, activity=issue_activity, ) - if preference.mention: + if email_preference.mention: bulk_email_logs.append( EmailNotificationLog( triggered_by_id=actor_id, @@ -761,7 +779,8 @@ def notifications( }, ) ) - bulk_notifications.append(notification) + if in_app_preference.mention: + bulk_notifications.append(notification) # save new mentions for the particular issue and remove the mentions that has been deleted from the description update_mentions_for_issue( diff --git a/apiserver/plane/db/models/__init__.py b/apiserver/plane/db/models/__init__.py index becaa1e6267..17c15f821f3 100644 --- a/apiserver/plane/db/models/__init__.py +++ b/apiserver/plane/db/models/__init__.py @@ -49,7 +49,7 @@ EmailNotificationLog, Notification, UserNotificationPreference, - WorkspaceUserNotificationPreference, + NotificationTransportChoices ) from .page import Page, PageLabel, PageLog, ProjectPage, PageVersion from .project import ( diff --git a/apiserver/plane/db/models/notification.py b/apiserver/plane/db/models/notification.py index 1b0f1a0e7a2..afa62f7a525 100644 --- a/apiserver/plane/db/models/notification.py +++ b/apiserver/plane/db/models/notification.py @@ -66,7 +66,6 @@ class UserNotificationPreference(BaseModel): "db.Workspace", on_delete=models.CASCADE, related_name="workspace_notification_preferences", - null=True, ) # project project = models.ForeignKey( @@ -76,14 +75,28 @@ class UserNotificationPreference(BaseModel): null=True, ) - # preference fields - property_change = models.BooleanField(default=True) - state_change = models.BooleanField(default=True) - comment = models.BooleanField(default=True) - mention = models.BooleanField(default=True) - issue_completed = models.BooleanField(default=True) + transport = models.CharField(max_length=50, default="EMAIL") + # task updates + property_change = models.BooleanField(default=False) + state_change = models.BooleanField(default=False) + issue_completed = models.BooleanField(default=False) + priority = models.BooleanField(default=False) + assignee = models.BooleanField(default=False) + start_due_date = models.BooleanField(default=False) + # comments fields + comment = models.BooleanField(default=False) + mention = models.BooleanField(default=False) + comment_reactions = models.BooleanField(default=False) class Meta: + unique_together = ["workspace", "user", "transport", "deleted_at"] + constraints = [ + models.UniqueConstraint( + fields=["workspace", "user", "transport"], + condition=models.Q(deleted_at__isnull=True), + name="notification_preferences_unique_workspace_user_transport_when_deleted_at_null", + ) + ] verbose_name = "UserNotificationPreference" verbose_name_plural = "UserNotificationPreferences" db_table = "user_notification_preferences" @@ -124,50 +137,6 @@ class Meta: db_table = "email_notification_logs" ordering = ("-created_at",) -class WorkspaceUserNotificationPreference(BaseModel): - class TransportChoices(models.TextChoices): - EMAIL = "EMAIL", "Email" - IN_APP = "IN_APP", "In App" - - workspace = models.ForeignKey( - "db.Workspace", - related_name="workspace_notification_preference", - on_delete=models.CASCADE, - ) - user = models.ForeignKey( - "db.User", - related_name="workspace_notification_preference", - on_delete=models.CASCADE, - ) - transport = models.CharField(max_length=50) - - # task updates - work_item_property_updates_enabled = models.BooleanField(default=False) - status_updates_enabled = models.BooleanField(default=False) - priority_updates_enabled = models.BooleanField(default=False) - assignee_updates_enabled = models.BooleanField(default=False) - start_due_date_updates_enabled = models.BooleanField(default=False) - module_updates_enabled = models.BooleanField(default=False) - cycle_updates_enabled = models.BooleanField(default=False) - - # comment updates - mentioned_comments_updates_enabled = models.BooleanField(default=False) - new_comments_updates_enabled = models.BooleanField(default=False) - reaction_comments_updates_enabled = models.BooleanField(default=False) - - class Meta: - unique_together = ["workspace", "user", "transport", "deleted_at"] - constraints = [ - models.UniqueConstraint( - fields=["workspace", "user", "transport"], - condition=models.Q(deleted_at__isnull=True), - name="notification_preferences_unique_workspace_user_transport_when_deleted_at_null", - ) - ] - verbose_name = "Workspace User Notification Preference" - verbose_name_plural = "Workspace User Notification Preferences" - db_table = "workspace_user_notification_preferences" - ordering = ("-created_at",) - - def __str__(self): - return f"{self.workspace} - {self.user} Notification Preferences" +class NotificationTransportChoices(models.TextChoices): + EMAIL = "EMAIL", "Email" + IN_APP = "IN_APP", "In App" \ No newline at end of file diff --git a/apiserver/plane/db/models/user.py b/apiserver/plane/db/models/user.py index a7ac5251e8d..60ce7a33768 100644 --- a/apiserver/plane/db/models/user.py +++ b/apiserver/plane/db/models/user.py @@ -231,20 +231,3 @@ class Meta: verbose_name_plural = "Accounts" db_table = "accounts" ordering = ("-created_at",) - - -@receiver(post_save, sender=User) -def create_user_notification(sender, instance, created, **kwargs): - # create preferences - if created and not instance.is_bot: - # Module imports - from plane.db.models import UserNotificationPreference - - UserNotificationPreference.objects.create( - user=instance, - property_change=False, - state_change=False, - comment=False, - mention=False, - issue_completed=False, - ) diff --git a/packages/constants/src/notification.ts b/packages/constants/src/notification.ts index 148f81acb47..3f002730963 100644 --- a/packages/constants/src/notification.ts +++ b/packages/constants/src/notification.ts @@ -138,16 +138,14 @@ export const allTimeIn30MinutesInterval12HoursFormat: Array<{ export enum ENotificationSettingsKey { - WORK_ITEM_PROPERTY_UPDATES = "work_item_property_updates_enabled", - STATUS_UPDATES = "status_updates_enabled", - PRIORITY_UPDATES = "priority_updates_enabled", - ASSIGNEE_UPDATES = "assignee_updates_enabled", - START_DUE_DATE_UPDATES = "start_due_date_updates_enabled", - MODULE_UPDATES = "module_updates_enabled", - CYCLE_UPDATES = "cycle_updates_enabled", - MENTIONED_COMMENTS_UPDATES = "mentioned_comments_updates_enabled", - NEW_COMMENTS_UPDATES = "new_comments_updates_enabled", - REACTION_COMMENT_UPDATES = "reaction_comments_updates_enabled", + PROPERTY_CHANGE = "property_change", + STATE_CHANGE = "state_change", + PRIORITY = "priority", + ASSIGNEE = "assignee", + START_DUE_DATE = "start_due_date", + COMMENTS = "comment", + MENTIONED_COMMENTS = "mention", + COMMENT_REACTIONS = "comment_reactions", } export enum EWorkspaceNotificationTransport { @@ -157,46 +155,38 @@ export enum EWorkspaceNotificationTransport { export const TASK_UPDATES_NOTIFICATION_SETTINGS: TNotificationSettings[] = [ { - key: ENotificationSettingsKey.WORK_ITEM_PROPERTY_UPDATES, + key: ENotificationSettingsKey.PROPERTY_CHANGE, i18n_title: "notification_settings.work_item_property_title", }, { - key: ENotificationSettingsKey.STATUS_UPDATES, + key: ENotificationSettingsKey.STATE_CHANGE, i18n_title: "notification_settings.status_title", }, { - key: ENotificationSettingsKey.PRIORITY_UPDATES, + key: ENotificationSettingsKey.PRIORITY, i18n_title: "notification_settings.priority_title", }, { - key: ENotificationSettingsKey.ASSIGNEE_UPDATES, + key: ENotificationSettingsKey.ASSIGNEE, i18n_title: "notification_settings.assignee_title", }, { - key: ENotificationSettingsKey.START_DUE_DATE_UPDATES, + key: ENotificationSettingsKey.START_DUE_DATE, i18n_title: "notification_settings.due_date_title", - }, - { - key: ENotificationSettingsKey.MODULE_UPDATES, - i18n_title: "notification_settings.module_title", - }, - { - key: ENotificationSettingsKey.CYCLE_UPDATES, - i18n_title: "notification_settings.cycle_title", } ] export const COMMENT_NOTIFICATION_SETTINGS: TNotificationSettings[] = [ { - key: ENotificationSettingsKey.MENTIONED_COMMENTS_UPDATES, + key: ENotificationSettingsKey.MENTIONED_COMMENTS, i18n_title: "notification_settings.mentioned_comments_title", }, { - key: ENotificationSettingsKey.NEW_COMMENTS_UPDATES, + key: ENotificationSettingsKey.COMMENTS, i18n_title: "notification_settings.new_comments_title", }, { - key: ENotificationSettingsKey.REACTION_COMMENT_UPDATES, + key: ENotificationSettingsKey.COMMENT_REACTIONS, i18n_title: "notification_settings.reaction_comments_title", }, ] diff --git a/packages/i18n/src/locales/en/translations.json b/packages/i18n/src/locales/en/translations.json index 14b3f7e04df..d70ad7a8108 100644 --- a/packages/i18n/src/locales/en/translations.json +++ b/packages/i18n/src/locales/en/translations.json @@ -2190,7 +2190,7 @@ "task_updates": "Task updates", "comments": "Comments", "work_item_property_title": "Update on any property of work item", - "status_title": "Status update", + "status_title": "State update", "priority_title": "Priority update", "assignee_title": "Assignee update", "due_date_title": "Start/Due date update", diff --git a/packages/i18n/src/locales/fr/translations.json b/packages/i18n/src/locales/fr/translations.json index 17a6df80cbe..dd480b0ffa6 100644 --- a/packages/i18n/src/locales/fr/translations.json +++ b/packages/i18n/src/locales/fr/translations.json @@ -2359,7 +2359,7 @@ "task_updates": "Mises à jour des tâches", "comments": "Commentaires", "work_item_property_title": "Mise à jour de toute propriété de l'élément de travail", - "status_title": "Mise à jour du statut", + "status_title": "Mise à jour de l'état", "priority_title": "Mise à jour de la priorité", "assignee_title": "Mise à jour de l'assigné", "due_date_title": "Mise à jour de la date de début/d'échéance", diff --git a/packages/i18n/src/locales/ja/translations.json b/packages/i18n/src/locales/ja/translations.json index 4a4c1b0f200..9ec96e38192 100644 --- a/packages/i18n/src/locales/ja/translations.json +++ b/packages/i18n/src/locales/ja/translations.json @@ -2359,7 +2359,7 @@ "task_updates": "タスクの更新", "comments": "コメント", "work_item_property_title": "作業項目のプロパティの更新", - "status_title": "ステータスの更新", + "status_title": "状態の更新", "priority_title": "優先度の更新", "assignee_title": "担当者の更新", "due_date_title": "開始日/期日の更新", diff --git a/packages/types/src/notification.d.ts b/packages/types/src/notification.d.ts index 27e11d2e43a..e881148f454 100644 --- a/packages/types/src/notification.d.ts +++ b/packages/types/src/notification.d.ts @@ -9,14 +9,12 @@ export type TWorkspaceUserNotification = { workspace: string, user: string, transport: EWorkspaceNotificationTransport, - work_item_property_updates_enabled: boolean, - status_updates_enabled: boolean, - priority_updates_enabled: boolean, - assignee_updates_enabled: boolean, - start_due_date_updates_enabled: boolean, - module_updates_enabled: boolean, - cycle_updates_enabled: boolean, - mentioned_comments_updates_enabled: boolean, - new_comments_updates_enabled: boolean, - reaction_comments_updates_enabled: boolean + property_change: boolean, + state_change: boolean, + priority: boolean, + assignee: boolean, + start_due_date: boolean, + comment: boolean, + mention: boolean, + comment_reactions: boolean } \ No newline at end of file diff --git a/web/core/components/inbox/settings/update-setting-row.tsx b/web/core/components/inbox/settings/update-setting-row.tsx index aa94733a2f6..69832ac513e 100644 --- a/web/core/components/inbox/settings/update-setting-row.tsx +++ b/web/core/components/inbox/settings/update-setting-row.tsx @@ -26,10 +26,10 @@ export const InboxSettingUpdateRow: FC = observer((p {t(title)}
- +
- +
); diff --git a/web/core/store/notifications/workspace-notification-settings.store.ts b/web/core/store/notifications/workspace-notification-settings.store.ts index 83ee3936e26..7580a3f5e87 100644 --- a/web/core/store/notifications/workspace-notification-settings.store.ts +++ b/web/core/store/notifications/workspace-notification-settings.store.ts @@ -1,5 +1,5 @@ import set from "lodash/set"; -import { action, autorun, computed, makeObservable, observable, runInAction } from "mobx"; +import { action, autorun, makeObservable, observable, runInAction } from "mobx"; import { computedFn } from "mobx-utils"; import { EWorkspaceNotificationTransport } from "@plane/constants"; import { IUser, IWorkspace, TWorkspaceUserNotification } from "@plane/types"; From 77253c97a693b1b7016ba3ce35814d50b2041903 Mon Sep 17 00:00:00 2001 From: Saurabhkmr98 Date: Mon, 24 Feb 2025 19:34:58 +0530 Subject: [PATCH 06/11] remove console logs --- web/core/components/inbox/settings/root.tsx | 5 ++--- web/core/components/inbox/settings/update-setting-row.tsx | 6 +----- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/web/core/components/inbox/settings/root.tsx b/web/core/components/inbox/settings/root.tsx index 6b02790ba4b..de7c17505ec 100644 --- a/web/core/components/inbox/settings/root.tsx +++ b/web/core/components/inbox/settings/root.tsx @@ -6,8 +6,7 @@ import { useTranslation } from "@plane/i18n"; import { InboxSettingUpdateRow } from "./update-setting-row"; -export const InboxSettingsRoot: FC = (props) => { - const { } = props; +export const InboxSettingsRoot: FC = () => { const { t } = useTranslation(); return ( @@ -22,7 +21,7 @@ export const InboxSettingsRoot: FC = (props) => {
{t("notification_settings.email")}
{ - TASK_UPDATES_NOTIFICATION_SETTINGS?.map((item, index) => ( + TASK_UPDATES_NOTIFICATION_SETTINGS?.map((item) => ( )) } diff --git a/web/core/components/inbox/settings/update-setting-row.tsx b/web/core/components/inbox/settings/update-setting-row.tsx index 69832ac513e..ba60e32ddf5 100644 --- a/web/core/components/inbox/settings/update-setting-row.tsx +++ b/web/core/components/inbox/settings/update-setting-row.tsx @@ -1,10 +1,9 @@ "use client" -import { FC, useState } from "react"; +import { FC } from "react"; import { observer } from "mobx-react"; import { ENotificationSettingsKey, EWorkspaceNotificationTransport } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; -import { useWorkspaceNotificationSettings } from "@/hooks/store"; import { InboxSettingUpdate } from "./update-setting"; type InboxSettingUpdateRowProps = { @@ -17,9 +16,6 @@ export const InboxSettingUpdateRow: FC = observer((p const { t } = useTranslation() - const { getNotificationSettingsForTransport } = useWorkspaceNotificationSettings(); - - console.log("check tracked data", getNotificationSettingsForTransport(EWorkspaceNotificationTransport.EMAIL)) return (
From 6cd1a33577046d0df0c1a07673f320e90dc8a150 Mon Sep 17 00:00:00 2001 From: Saurabhkmr98 Date: Tue, 25 Feb 2025 13:28:49 +0530 Subject: [PATCH 07/11] replace flex with grid layout for setting UI customisation --- packages/types/src/index.d.ts | 2 +- web/core/components/inbox/settings/root.tsx | 16 ++++++++-------- .../inbox/settings/update-setting-row.tsx | 8 ++++---- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/packages/types/src/index.d.ts b/packages/types/src/index.d.ts index 1ad24302d4c..3f9cb1243b3 100644 --- a/packages/types/src/index.d.ts +++ b/packages/types/src/index.d.ts @@ -40,4 +40,4 @@ export * from "./epics"; export * from "./charts"; export * from "./home"; export * from "./stickies"; -export * from "./notification"; \ No newline at end of file +export * from "./notification"; diff --git a/web/core/components/inbox/settings/root.tsx b/web/core/components/inbox/settings/root.tsx index de7c17505ec..8aee3e24043 100644 --- a/web/core/components/inbox/settings/root.tsx +++ b/web/core/components/inbox/settings/root.tsx @@ -15,10 +15,10 @@ export const InboxSettingsRoot: FC = () => {
{t("notification_settings.task_updates")}
-
-
{t("notification_settings.advanced_settings")}
-
{t("notification_settings.in_plane")}
-
{t("notification_settings.email")}
+
+
{t("notification_settings.advanced_settings")}
+
{t("notification_settings.in_plane")}
+
{t("notification_settings.email")}
{ TASK_UPDATES_NOTIFICATION_SETTINGS?.map((item) => ( @@ -30,10 +30,10 @@ export const InboxSettingsRoot: FC = () => {
{t("notification_settings.comments")}
-
-
{t("notification_settings.advanced_settings")}
-
{t("notification_settings.in_plane")}
-
{t("notification_settings.email")}
+
+
{t("notification_settings.advanced_settings")}
+
{t("notification_settings.in_plane")}
+
{t("notification_settings.email")}
{ COMMENT_NOTIFICATION_SETTINGS?.map((item, index) => ( diff --git a/web/core/components/inbox/settings/update-setting-row.tsx b/web/core/components/inbox/settings/update-setting-row.tsx index ba60e32ddf5..1edd3b02082 100644 --- a/web/core/components/inbox/settings/update-setting-row.tsx +++ b/web/core/components/inbox/settings/update-setting-row.tsx @@ -17,14 +17,14 @@ export const InboxSettingUpdateRow: FC = observer((p const { t } = useTranslation() return ( -
-
+
+
{t(title)}
-
+
-
+
From 5590ccc572f80f604f3d913b436977895e4901ac Mon Sep 17 00:00:00 2001 From: Saurabhkmr98 Date: Tue, 25 Feb 2025 16:23:36 +0530 Subject: [PATCH 08/11] conserve old notification models for revert logic --- apiserver/plane/app/serializers/__init__.py | 3 +- .../plane/app/serializers/notification.py | 7 +- .../plane/app/views/notification/base.py | 22 ++--- apiserver/plane/app/views/workspace/home.py | 8 +- apiserver/plane/bgtasks/notification_task.py | 9 ++- apiserver/plane/db/models/__init__.py | 3 +- apiserver/plane/db/models/notification.py | 80 ++++++++++++++----- apiserver/plane/db/models/user.py | 16 ++++ 8 files changed, 106 insertions(+), 42 deletions(-) diff --git a/apiserver/plane/app/serializers/__init__.py b/apiserver/plane/app/serializers/__init__.py index 98722f53a8c..ae2ac1deb00 100644 --- a/apiserver/plane/app/serializers/__init__.py +++ b/apiserver/plane/app/serializers/__init__.py @@ -117,7 +117,8 @@ from .notification import ( NotificationSerializer, - UserNotificationPreferenceSerializer + UserNotificationPreferenceSerializer, + WorkspaceUserNotificationPreferenceSerializer ) from .exporter import ExporterHistorySerializer diff --git a/apiserver/plane/app/serializers/notification.py b/apiserver/plane/app/serializers/notification.py index 58007ec26c4..ac054fd58c9 100644 --- a/apiserver/plane/app/serializers/notification.py +++ b/apiserver/plane/app/serializers/notification.py @@ -1,7 +1,7 @@ # Module imports from .base import BaseSerializer from .user import UserLiteSerializer -from plane.db.models import Notification, UserNotificationPreference +from plane.db.models import Notification, UserNotificationPreference, WorkspaceUserNotificationPreference # Third Party imports from rest_framework import serializers @@ -22,3 +22,8 @@ class UserNotificationPreferenceSerializer(BaseSerializer): class Meta: model = UserNotificationPreference fields = "__all__" + +class WorkspaceUserNotificationPreferenceSerializer(BaseSerializer): + class Meta: + model = WorkspaceUserNotificationPreference + fields = "__all__" \ No newline at end of file diff --git a/apiserver/plane/app/views/notification/base.py b/apiserver/plane/app/views/notification/base.py index c6ff6f2b37d..0249ef1cb03 100644 --- a/apiserver/plane/app/views/notification/base.py +++ b/apiserver/plane/app/views/notification/base.py @@ -8,7 +8,8 @@ from plane.app.serializers import ( NotificationSerializer, - UserNotificationPreferenceSerializer + UserNotificationPreferenceSerializer, + WorkspaceUserNotificationPreferenceSerializer ) from plane.db.models import ( Issue, @@ -18,6 +19,7 @@ UserNotificationPreference, WorkspaceMember, Workspace, + WorkspaceUserNotificationPreference, NotificationTransportChoices ) from plane.utils.paginator import BasePaginator @@ -366,8 +368,8 @@ def patch(self, request): class WorkspaceUserNotificationPreferenceEndpoint(BaseAPIView): - model = UserNotificationPreference - serializer_class = UserNotificationPreferenceSerializer + model = WorkspaceUserNotificationPreference + serializer_class = WorkspaceUserNotificationPreferenceSerializer @allow_permission( allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE" @@ -375,7 +377,7 @@ class WorkspaceUserNotificationPreferenceEndpoint(BaseAPIView): def get(self, request, slug): workspace = Workspace.objects.get(slug=slug) get_notification_preferences = ( - UserNotificationPreference.objects.filter( + WorkspaceUserNotificationPreference.objects.filter( workspace=workspace, user=request.user ) ) @@ -395,9 +397,9 @@ def get(self, request, slug): notification_preferences = ( - UserNotificationPreference.objects.bulk_create( + WorkspaceUserNotificationPreference.objects.bulk_create( [ - UserNotificationPreference( + WorkspaceUserNotificationPreference( workspace=workspace, user=request.user, transport=transport, @@ -407,12 +409,12 @@ def get(self, request, slug): ) ) - notification_preferences = UserNotificationPreference.objects.filter( + notification_preferences = WorkspaceUserNotificationPreference.objects.filter( workspace=workspace, user=request.user ) return Response( - UserNotificationPreferenceSerializer( + WorkspaceUserNotificationPreferenceSerializer( notification_preferences, many=True ).data, status=status.HTTP_200_OK, @@ -422,12 +424,12 @@ def get(self, request, slug): allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE" ) def patch(self, request, slug, transport): - notification_preference = UserNotificationPreference.objects.filter( + notification_preference = WorkspaceUserNotificationPreference.objects.filter( transport=transport, workspace__slug=slug, user=request.user ).first() if notification_preference: - serializer = UserNotificationPreferenceSerializer( + serializer = WorkspaceUserNotificationPreferenceSerializer( notification_preference, data=request.data, partial=True ) diff --git a/apiserver/plane/app/views/workspace/home.py b/apiserver/plane/app/views/workspace/home.py index 46bda35670f..170d2ea07e3 100644 --- a/apiserver/plane/app/views/workspace/home.py +++ b/apiserver/plane/app/views/workspace/home.py @@ -2,7 +2,7 @@ from ..base import BaseAPIView from plane.db.models.workspace import WorkspaceHomePreference from plane.app.permissions import allow_permission, ROLE -from plane.db.models import Workspace, UserNotificationPreference, NotificationTransportChoices +from plane.db.models import Workspace, WorkspaceUserNotificationPreference, NotificationTransportChoices from plane.app.serializers.workspace import WorkspaceHomePreferenceSerializer # Third party imports @@ -62,7 +62,7 @@ def get(self, request, slug): # Notification preference get or create workspace = Workspace.objects.get(slug=slug) get_notification_preferences = ( - UserNotificationPreference.objects.filter( + WorkspaceUserNotificationPreference.objects.filter( workspace=workspace, user=request.user ) ) @@ -81,9 +81,9 @@ def get(self, request, slug): ): create_transports.append(transport) - _ = UserNotificationPreference.objects.bulk_create( + _ = WorkspaceUserNotificationPreference.objects.bulk_create( [ - UserNotificationPreference( + WorkspaceUserNotificationPreference( workspace=workspace, user=request.user, transport=transport ) for transport in create_transports diff --git a/apiserver/plane/bgtasks/notification_task.py b/apiserver/plane/bgtasks/notification_task.py index 86b8ed67e28..803db791022 100644 --- a/apiserver/plane/bgtasks/notification_task.py +++ b/apiserver/plane/bgtasks/notification_task.py @@ -19,7 +19,8 @@ IssueActivity, UserNotificationPreference, ProjectMember, - NotificationTransportChoices + NotificationTransportChoices, + WorkspaceUserNotificationPreference ) from django.db.models import Subquery @@ -354,7 +355,7 @@ def notifications( else: sender = "in_app:issue_activities:subscribed" - preference = UserNotificationPreference.objects.filter( + preference = WorkspaceUserNotificationPreference.objects.filter( user_id=subscriber, workspace_id=issue_workspace_id ) email_preference = preference.filter(transport="EMAIL").first() @@ -531,7 +532,7 @@ def notifications( for mention_id in comment_mentions: if mention_id != actor_id: - preference = UserNotificationPreference.objects.filter( + preference = WorkspaceUserNotificationPreference.objects.filter( user_id=mention_id, workspace_id=issue_workspace_id, ) @@ -608,7 +609,7 @@ def notifications( for mention_id in new_mentions: if mention_id != actor_id: - preference = UserNotificationPreference.objects.filter( + preference = WorkspaceUserNotificationPreference.objects.filter( user_id=mention_id, workspace_id=issue_workspace_id, ) diff --git a/apiserver/plane/db/models/__init__.py b/apiserver/plane/db/models/__init__.py index 17c15f821f3..296d0aed1cb 100644 --- a/apiserver/plane/db/models/__init__.py +++ b/apiserver/plane/db/models/__init__.py @@ -49,7 +49,8 @@ EmailNotificationLog, Notification, UserNotificationPreference, - NotificationTransportChoices + NotificationTransportChoices, + WorkspaceUserNotificationPreference ) from .page import Page, PageLabel, PageLog, ProjectPage, PageVersion from .project import ( diff --git a/apiserver/plane/db/models/notification.py b/apiserver/plane/db/models/notification.py index afa62f7a525..4738a4a1bd0 100644 --- a/apiserver/plane/db/models/notification.py +++ b/apiserver/plane/db/models/notification.py @@ -66,6 +66,7 @@ class UserNotificationPreference(BaseModel): "db.Workspace", on_delete=models.CASCADE, related_name="workspace_notification_preferences", + null=True, ) # project project = models.ForeignKey( @@ -75,28 +76,14 @@ class UserNotificationPreference(BaseModel): null=True, ) - transport = models.CharField(max_length=50, default="EMAIL") + # preference fields + property_change = models.BooleanField(default=True) + state_change = models.BooleanField(default=True) + comment = models.BooleanField(default=True) + mention = models.BooleanField(default=True) + issue_completed = models.BooleanField(default=True) - # task updates - property_change = models.BooleanField(default=False) - state_change = models.BooleanField(default=False) - issue_completed = models.BooleanField(default=False) - priority = models.BooleanField(default=False) - assignee = models.BooleanField(default=False) - start_due_date = models.BooleanField(default=False) - # comments fields - comment = models.BooleanField(default=False) - mention = models.BooleanField(default=False) - comment_reactions = models.BooleanField(default=False) class Meta: - unique_together = ["workspace", "user", "transport", "deleted_at"] - constraints = [ - models.UniqueConstraint( - fields=["workspace", "user", "transport"], - condition=models.Q(deleted_at__isnull=True), - name="notification_preferences_unique_workspace_user_transport_when_deleted_at_null", - ) - ] verbose_name = "UserNotificationPreference" verbose_name_plural = "UserNotificationPreferences" db_table = "user_notification_preferences" @@ -106,7 +93,6 @@ def __str__(self): """Return the user""" return f"<{self.user}>" - class EmailNotificationLog(BaseModel): # receiver receiver = models.ForeignKey( @@ -136,6 +122,58 @@ class Meta: verbose_name_plural = "Email Notification Logs" db_table = "email_notification_logs" ordering = ("-created_at",) + +class WorkspaceUserNotificationPreference(BaseModel): + # user it is related to + user = models.ForeignKey( + settings.AUTH_USER_MODEL, + on_delete=models.CASCADE, + related_name="user_workspace_notification_preferences", + ) + # workspace if it is applicable + workspace = models.ForeignKey( + "db.Workspace", + on_delete=models.CASCADE, + related_name="workspace_user_notification_preferences", + ) + # project + project = models.ForeignKey( + "db.Project", + on_delete=models.CASCADE, + related_name="project_user_notification_preferences", + null=True, + ) + + transport = models.CharField(max_length=50, default="EMAIL") + + # task updates + property_change = models.BooleanField(default=False) + state_change = models.BooleanField(default=False) + priority = models.BooleanField(default=False) + assignee = models.BooleanField(default=False) + start_due_date = models.BooleanField(default=False) + # comments fields + comment = models.BooleanField(default=False) + mention = models.BooleanField(default=False) + comment_reactions = models.BooleanField(default=False) + class Meta: + unique_together = ["workspace", "user", "transport", "deleted_at"] + constraints = [ + models.UniqueConstraint( + fields=["workspace", "user", "transport"], + condition=models.Q(deleted_at__isnull=True), + name="notification_preferences_unique_workspace_user_transport_when_deleted_at_null", + ) + ] + verbose_name = "Workspace User Notification Preference" + verbose_name_plural = "Workspace User Notification Preferences" + db_table = "workspace_user_notification_preferences" + ordering = ("-created_at",) + + def __str__(self): + """Return the user""" + return f"<{self.user}>" + class NotificationTransportChoices(models.TextChoices): EMAIL = "EMAIL", "Email" diff --git a/apiserver/plane/db/models/user.py b/apiserver/plane/db/models/user.py index 60ce7a33768..cf6cd5b0c48 100644 --- a/apiserver/plane/db/models/user.py +++ b/apiserver/plane/db/models/user.py @@ -231,3 +231,19 @@ class Meta: verbose_name_plural = "Accounts" db_table = "accounts" ordering = ("-created_at",) + +@receiver(post_save, sender=User) +def create_user_notification(sender, instance, created, **kwargs): + # create preferences + if created and not instance.is_bot: + # Module imports + from plane.db.models import UserNotificationPreference + + UserNotificationPreference.objects.create( + user=instance, + property_change=False, + state_change=False, + comment=False, + mention=False, + issue_completed=False, + ) From a2717ae43b44978984e9f4c77ac5f844aef33ea1 Mon Sep 17 00:00:00 2001 From: Saurabhkmr98 Date: Tue, 25 Feb 2025 16:26:25 +0530 Subject: [PATCH 09/11] remove spaces --- apiserver/plane/db/models/user.py | 1 + 1 file changed, 1 insertion(+) diff --git a/apiserver/plane/db/models/user.py b/apiserver/plane/db/models/user.py index cf6cd5b0c48..a7ac5251e8d 100644 --- a/apiserver/plane/db/models/user.py +++ b/apiserver/plane/db/models/user.py @@ -232,6 +232,7 @@ class Meta: db_table = "accounts" ordering = ("-created_at",) + @receiver(post_save, sender=User) def create_user_notification(sender, instance, created, **kwargs): # create preferences From 75916ce1e2aded22c5a93897a74aa26602cebd8c Mon Sep 17 00:00:00 2001 From: Saurabhkmr98 Date: Tue, 25 Feb 2025 19:39:11 +0530 Subject: [PATCH 10/11] fixes for null state --- packages/i18n/src/locales/en/translations.json | 1 + packages/i18n/src/locales/es/translations.json | 1 + packages/i18n/src/locales/fr/translations.json | 1 + packages/i18n/src/locales/ja/translations.json | 1 + packages/i18n/src/locales/zh-CN/translations.json | 1 + web/core/components/inbox/index.ts | 1 + web/core/components/inbox/settings/index.ts | 3 ++- web/core/components/inbox/settings/update-setting.tsx | 5 +---- web/core/constants/fetch-keys.ts | 2 ++ 9 files changed, 11 insertions(+), 5 deletions(-) diff --git a/packages/i18n/src/locales/en/translations.json b/packages/i18n/src/locales/en/translations.json index d70ad7a8108..1d38aa19553 100644 --- a/packages/i18n/src/locales/en/translations.json +++ b/packages/i18n/src/locales/en/translations.json @@ -2187,6 +2187,7 @@ "advanced_settings": "Advanced settings", "in_plane": "In Plane", "email": "Email", + "slack": "Slack", "task_updates": "Task updates", "comments": "Comments", "work_item_property_title": "Update on any property of work item", diff --git a/packages/i18n/src/locales/es/translations.json b/packages/i18n/src/locales/es/translations.json index 4ccab4a003a..f20229ae0cc 100644 --- a/packages/i18n/src/locales/es/translations.json +++ b/packages/i18n/src/locales/es/translations.json @@ -2356,6 +2356,7 @@ "advanced_settings": "Configuración avanzada", "in_plane": "En Plane", "email": "Correo electrónico", + "slack": "Slack", "task_updates": "Actualizaciones de tareas", "comments": "Comentarios", "work_item_property_title": "Actualización de cualquier propiedad del elemento de trabajo", diff --git a/packages/i18n/src/locales/fr/translations.json b/packages/i18n/src/locales/fr/translations.json index dd480b0ffa6..c33d2051a8b 100644 --- a/packages/i18n/src/locales/fr/translations.json +++ b/packages/i18n/src/locales/fr/translations.json @@ -2356,6 +2356,7 @@ "advanced_settings": "Paramètres avancés", "in_plane": "Dans Plane", "email": "E-mail", + "slack": "Slack", "task_updates": "Mises à jour des tâches", "comments": "Commentaires", "work_item_property_title": "Mise à jour de toute propriété de l'élément de travail", diff --git a/packages/i18n/src/locales/ja/translations.json b/packages/i18n/src/locales/ja/translations.json index 9ec96e38192..755a61db6c1 100644 --- a/packages/i18n/src/locales/ja/translations.json +++ b/packages/i18n/src/locales/ja/translations.json @@ -2356,6 +2356,7 @@ "advanced_settings": "詳細設定", "in_plane": "Plane内", "email": "メール", + "slack": "Slack", "task_updates": "タスクの更新", "comments": "コメント", "work_item_property_title": "作業項目のプロパティの更新", diff --git a/packages/i18n/src/locales/zh-CN/translations.json b/packages/i18n/src/locales/zh-CN/translations.json index 85c3184941e..54ce4ebeed3 100644 --- a/packages/i18n/src/locales/zh-CN/translations.json +++ b/packages/i18n/src/locales/zh-CN/translations.json @@ -2356,6 +2356,7 @@ "advanced_settings": "高级设置", "in_plane": "在 Plane", "email": "电子邮件", + "slack": "Slack", "task_updates": "任务更新", "comments": "评论", "work_item_property_title": "工作项属性更新", diff --git a/web/core/components/inbox/index.ts b/web/core/components/inbox/index.ts index 8b05b565ff4..97c558275b6 100644 --- a/web/core/components/inbox/index.ts +++ b/web/core/components/inbox/index.ts @@ -4,3 +4,4 @@ export * from "./sidebar"; export * from "./inbox-filter"; export * from "./content"; export * from "./inbox-issue-status"; +export * from "./settings"; diff --git a/web/core/components/inbox/settings/index.ts b/web/core/components/inbox/settings/index.ts index ab30ec426cc..34db8319f13 100644 --- a/web/core/components/inbox/settings/index.ts +++ b/web/core/components/inbox/settings/index.ts @@ -2,4 +2,5 @@ export * from "./content-header" export * from "./root" export * from "./content-wrapper" export * from "./content-header" -export * from "./update-setting-row" \ No newline at end of file +export * from "./update-setting-row" +export * from "./update-setting" \ No newline at end of file diff --git a/web/core/components/inbox/settings/update-setting.tsx b/web/core/components/inbox/settings/update-setting.tsx index 59462b343d0..83ce277cb3d 100644 --- a/web/core/components/inbox/settings/update-setting.tsx +++ b/web/core/components/inbox/settings/update-setting.tsx @@ -40,13 +40,10 @@ export const InboxSettingUpdate: FC = observer((props: } } - if (!notificationSettings) { - return null; - } return ( { handleChange(newValue); }} diff --git a/web/core/constants/fetch-keys.ts b/web/core/constants/fetch-keys.ts index 0bfbf58817c..d6467ae58a8 100644 --- a/web/core/constants/fetch-keys.ts +++ b/web/core/constants/fetch-keys.ts @@ -221,6 +221,8 @@ export const GITHUB_REPOSITORY_INFO = (workspaceSlug: string, repoName: string) // slack-project-integration export const SLACK_CHANNEL_INFO = (workspaceSlug: string, projectId: string) => `SLACK_CHANNEL_INFO_${workspaceSlug.toString().toUpperCase()}_${projectId.toUpperCase()}`; +export const SLACK_USER_CONNECTION_STATUS = (workspaceSlug: string) => + `SLACK_USER_CONNECTION_STATUS_${workspaceSlug.toString().toUpperCase()}`; // Pages export const RECENT_PAGES_LIST = (projectId: string) => `RECENT_PAGES_LIST_${projectId.toUpperCase()}`; From 05c7dc51db8c4704abe43846d35e1fe1e54d3ba8 Mon Sep 17 00:00:00 2001 From: Saurabhkmr98 Date: Thu, 27 Feb 2025 16:34:24 +0530 Subject: [PATCH 11/11] notification task code refactor + copy changes for UI --- apiserver/plane/bgtasks/notification_task.py | 1222 +++++++++-------- packages/constants/src/notification.ts | 8 + .../i18n/src/locales/en/translations.json | 26 +- .../i18n/src/locales/es/translations.json | 32 +- .../i18n/src/locales/fr/translations.json | 34 +- .../i18n/src/locales/ja/translations.json | 30 +- .../i18n/src/locales/zh-CN/translations.json | 30 +- packages/types/src/notification.d.ts | 1 + web/core/components/inbox/settings/root.tsx | 22 +- .../inbox/settings/update-setting-row.tsx | 14 +- 10 files changed, 814 insertions(+), 605 deletions(-) diff --git a/apiserver/plane/bgtasks/notification_task.py b/apiserver/plane/bgtasks/notification_task.py index 803db791022..792f2399dd3 100644 --- a/apiserver/plane/bgtasks/notification_task.py +++ b/apiserver/plane/bgtasks/notification_task.py @@ -12,15 +12,13 @@ User, IssueAssignee, Issue, - State, EmailNotificationLog, Notification, IssueComment, IssueActivity, - UserNotificationPreference, ProjectMember, NotificationTransportChoices, - WorkspaceUserNotificationPreference + WorkspaceUserNotificationPreference, ) from django.db.models import Subquery @@ -209,6 +207,578 @@ def create_mention_notification( ) +def process_mentions( + requested_data, + current_instance, + project_id, + issue_id, + project_members, + issue_activities_created, +): + """ + Process mentions from issue data and comments. + Returns new mentions, removed mentions, and subscribers. + """ + # Get new mentions from the newer instance + new_mentions = get_new_mentions( + requested_instance=requested_data, current_instance=current_instance + ) + new_mentions = list(set(new_mentions) & {str(member) for member in project_members}) + removed_mention = get_removed_mentions( + requested_instance=requested_data, current_instance=current_instance + ) + + comment_mentions = [] + all_comment_mentions = [] + + # Get New Subscribers from the mentions of the newer instance + requested_mentions = extract_mentions(issue_instance=requested_data) + mention_subscribers = extract_mentions_as_subscribers( + project_id=project_id, issue_id=issue_id, mentions=requested_mentions + ) + + # Process comment mentions + for issue_activity in issue_activities_created: + issue_comment = issue_activity.get("issue_comment") + issue_comment_new_value = issue_activity.get("new_value") + issue_comment_old_value = issue_activity.get("old_value") + if issue_comment is not None: + all_comment_mentions = all_comment_mentions + extract_comment_mentions( + issue_comment_new_value + ) + + new_comment_mentions = get_new_comment_mentions( + old_value=issue_comment_old_value, new_value=issue_comment_new_value + ) + comment_mentions = comment_mentions + new_comment_mentions + comment_mentions = [ + mention + for mention in comment_mentions + if UUID(mention) in set(project_members) + ] + + comment_mention_subscribers = extract_mentions_as_subscribers( + project_id=project_id, issue_id=issue_id, mentions=all_comment_mentions + ) + + return { + "new_mentions": new_mentions, + "removed_mention": removed_mention, + "comment_mentions": comment_mentions, + "mention_subscribers": mention_subscribers, + "comment_mention_subscribers": comment_mention_subscribers, + } + + +def create_in_app_notifications( + issue, + project, + actor_id, + issue_activities_created, + issue_subscribers, + issue_assignees, + new_mentions, + comment_mentions, + last_activity, + issue_workspace_id, +): + """ + Create in-app notifications for issue activities and mentions. + Returns a list of Notification objects to be bulk created. + """ + bulk_notifications = [] + + # Process notifications for subscribers + for subscriber in issue_subscribers: + if issue.created_by_id and issue.created_by_id == subscriber: + sender = "in_app:issue_activities:created" + elif ( + subscriber in issue_assignees and issue.created_by_id not in issue_assignees + ): + sender = "in_app:issue_activities:assigned" + else: + sender = "in_app:issue_activities:subscribed" + + # Get user notification preferences for in-app + in_app_preference = WorkspaceUserNotificationPreference.objects.filter( + user_id=subscriber, + workspace_id=issue_workspace_id, + transport=NotificationTransportChoices.IN_APP[0], + ).first() + + for issue_activity in issue_activities_created: + # Skip if activity is not for this issue + if issue_activity.get("issue_detail").get("id") != issue.id: + continue + + # Skip description updates + if issue_activity.get("field") == "description": + continue + + # Check if notification should be sent based on preferences + send_in_app = should_send_notification( + in_app_preference, issue_activity.get("field") + ) + + if not send_in_app: + continue + + # Get issue comment if relevant + issue_comment = get_issue_comment_for_activity( + issue_activity, issue.id, project.id, project.workspace_id + ) + + # Create in-app notification + bulk_notifications.append( + create_activity_notification( + project=project, + issue=issue, + sender=sender, + actor_id=actor_id, + subscriber=subscriber, + issue_activity=issue_activity, + issue_comment=issue_comment, + ) + ) + + # Process comment mention notifications + actor = User.objects.get(pk=actor_id) + for mention_id in comment_mentions: + if mention_id != actor_id: + in_app_preference = WorkspaceUserNotificationPreference.objects.filter( + user_id=mention_id, + workspace_id=issue_workspace_id, + transport=NotificationTransportChoices.IN_APP[0], + ).first() + + if in_app_preference.mention: + for issue_activity in issue_activities_created: + notification = create_mention_notification( + project=project, + issue=issue, + notification_comment=f"{actor.display_name} has mentioned you in a comment in issue {issue.name}", + actor_id=actor_id, + mention_id=mention_id, + issue_id=issue.id, + activity=issue_activity, + ) + bulk_notifications.append(notification) + + # Process issue mention notifications + for mention_id in new_mentions: + if mention_id != actor_id: + in_app_preference = WorkspaceUserNotificationPreference.objects.filter( + user_id=mention_id, + workspace_id=issue_workspace_id, + transport=NotificationTransportChoices.IN_APP[0], + ).first() + + if not in_app_preference.mention: + continue + + if ( + last_activity is not None + and last_activity.field == "description" + and actor_id == str(last_activity.actor_id) + ): + bulk_notifications.append( + Notification( + workspace=project.workspace, + sender="in_app:issue_activities:mentioned", + triggered_by_id=actor_id, + receiver_id=mention_id, + entity_identifier=issue.id, + entity_name="issue", + project=project, + message=f"You have been mentioned in the issue {issue.name}", + data=create_notification_data(issue, last_activity), + ) + ) + else: + for issue_activity in issue_activities_created: + notification = create_mention_notification( + project=project, + issue=issue, + notification_comment=f"You have been mentioned in the issue {issue.name}", + actor_id=actor_id, + mention_id=mention_id, + issue_id=issue.id, + activity=issue_activity, + ) + bulk_notifications.append(notification) + + return bulk_notifications + + +def create_email_notifications( + issue, + project, + actor_id, + issue_activities_created, + issue_subscribers, + issue_assignees, + new_mentions, + comment_mentions, + last_activity, + issue_workspace_id, +): + """ + Create email notifications for issue activities and mentions. + Returns a list of EmailNotificationLog objects to be bulk created. + """ + bulk_email_logs = [] + + # Process notifications for subscribers + for subscriber in issue_subscribers: + # Get user notification preferences for email + email_preference = WorkspaceUserNotificationPreference.objects.filter( + user_id=subscriber, + workspace_id=issue_workspace_id, + transport=NotificationTransportChoices.EMAIL[0], + ).first() + + for issue_activity in issue_activities_created: + # Skip if activity is not for this issue + if issue_activity.get("issue_detail").get("id") != issue.id: + continue + + # Skip description updates + if issue_activity.get("field") == "description": + continue + + # Check if notification should be sent based on preferences + send_email = should_send_notification( + email_preference, issue_activity.get("field") + ) + + if not send_email: + continue + + # Get issue comment if relevant + issue_comment = get_issue_comment_for_activity( + issue_activity, issue.id, project.id, project.workspace_id + ) + + # Create email notification log + bulk_email_logs.append( + create_email_notification_log( + issue=issue, + actor_id=actor_id, + subscriber=subscriber, + issue_activity=issue_activity, + issue_comment=issue_comment, + ) + ) + + # Process comment mention notifications + for mention_id in comment_mentions: + if mention_id != actor_id: + email_preference = WorkspaceUserNotificationPreference.objects.filter( + user_id=mention_id, + workspace_id=issue_workspace_id, + transport=NotificationTransportChoices.EMAIL[0], + ).first() + + if email_preference.mention: + for issue_activity in issue_activities_created: + bulk_email_logs.append( + create_mention_email_log( + issue=issue, + actor_id=actor_id, + mention_id=mention_id, + issue_activity=issue_activity, + field="mention", + ) + ) + + # Process issue mention notifications + for mention_id in new_mentions: + if mention_id != actor_id: + email_preference = WorkspaceUserNotificationPreference.objects.filter( + user_id=mention_id, + workspace_id=issue_workspace_id, + transport=NotificationTransportChoices.EMAIL[0], + ).first() + + if not email_preference.mention: + continue + + if ( + last_activity is not None + and last_activity.field == "description" + and actor_id == str(last_activity.actor_id) + ): + bulk_email_logs.append( + EmailNotificationLog( + triggered_by_id=actor_id, + receiver_id=mention_id, + entity_identifier=issue.id, + entity_name="issue", + data=create_email_data_from_activity( + issue, last_activity, field="mention" + ), + ) + ) + else: + for issue_activity in issue_activities_created: + bulk_email_logs.append( + create_mention_email_log( + issue=issue, + actor_id=actor_id, + mention_id=mention_id, + issue_activity=issue_activity, + field="mention", + ) + ) + + return bulk_email_logs + + +def should_send_notification(preference, field): + """ + Determine if notification should be sent based on user preferences and activity field. + """ + if field == "state": + return preference.state_change + elif field == "comment": + return preference.comment + elif field == "priority": + return preference.priority + elif field == "assignee": + return preference.assignee + elif field == "start_date" or field == "target_date": + return preference.start_due_date + else: + return preference.property_change + + +def get_issue_comment_for_activity(issue_activity, issue_id, project_id, workspace_id): + """ + Fetch issue comment for an activity if it exists. + """ + if issue_activity.get("issue_comment"): + return IssueComment.objects.filter( + id=issue_activity.get("issue_comment"), + issue_id=issue_id, + project_id=project_id, + workspace_id=workspace_id, + ).first() + return None + + +def create_activity_notification( + project, issue, sender, actor_id, subscriber, issue_activity, issue_comment +): + """ + Create a Notification object for an issue activity. + """ + return Notification( + workspace=project.workspace, + sender=sender, + triggered_by_id=actor_id, + receiver_id=subscriber, + entity_identifier=issue.id, + entity_name="issue", + project=project, + title=issue_activity.get("comment"), + data={ + "issue": { + "id": str(issue.id), + "name": str(issue.name), + "identifier": str(issue.project.identifier), + "sequence_id": issue.sequence_id, + "state_name": issue.state.name, + "state_group": issue.state.group, + }, + "issue_activity": { + "id": str(issue_activity.get("id")), + "verb": str(issue_activity.get("verb")), + "field": str(issue_activity.get("field")), + "actor": str(issue_activity.get("actor_id")), + "new_value": str(issue_activity.get("new_value")), + "old_value": str(issue_activity.get("old_value")), + "issue_comment": str( + issue_comment.comment_stripped if issue_comment is not None else "" + ), + "old_identifier": ( + str(issue_activity.get("old_identifier")) + if issue_activity.get("old_identifier") + else None + ), + "new_identifier": ( + str(issue_activity.get("new_identifier")) + if issue_activity.get("new_identifier") + else None + ), + }, + }, + ) + + +def create_email_notification_log( + issue, actor_id, subscriber, issue_activity, issue_comment +): + """ + Create an EmailNotificationLog object for an issue activity. + """ + return EmailNotificationLog( + triggered_by_id=actor_id, + receiver_id=subscriber, + entity_identifier=issue.id, + entity_name="issue", + data={ + "issue": { + "id": str(issue.id), + "name": str(issue.name), + "identifier": str(issue.project.identifier), + "project_id": str(issue.project.id), + "workspace_slug": str(issue.project.workspace.slug), + "sequence_id": issue.sequence_id, + "state_name": issue.state.name, + "state_group": issue.state.group, + }, + "issue_activity": { + "id": str(issue_activity.get("id")), + "verb": str(issue_activity.get("verb")), + "field": str(issue_activity.get("field")), + "actor": str(issue_activity.get("actor_id")), + "new_value": str(issue_activity.get("new_value")), + "old_value": str(issue_activity.get("old_value")), + "issue_comment": str( + issue_comment.comment_stripped if issue_comment is not None else "" + ), + "old_identifier": ( + str(issue_activity.get("old_identifier")) + if issue_activity.get("old_identifier") + else None + ), + "new_identifier": ( + str(issue_activity.get("new_identifier")) + if issue_activity.get("new_identifier") + else None + ), + "activity_time": issue_activity.get("created_at"), + }, + }, + ) + + +def create_mention_email_log(issue, actor_id, mention_id, issue_activity, field): + """ + Create an EmailNotificationLog for a mention notification. + """ + return EmailNotificationLog( + triggered_by_id=actor_id, + receiver_id=mention_id, + entity_identifier=issue.id, + entity_name="issue", + data={ + "issue": { + "id": str(issue.id), + "name": str(issue.name), + "identifier": str(issue.project.identifier), + "sequence_id": issue.sequence_id, + "state_name": issue.state.name, + "state_group": issue.state.group, + "project_id": str(issue.project.id), + "workspace_slug": str(issue.project.workspace.slug), + }, + "issue_activity": { + "id": str(issue_activity.get("id")), + "verb": str(issue_activity.get("verb")), + "field": str(field), + "actor": str(issue_activity.get("actor_id")), + "new_value": str(issue_activity.get("new_value")), + "old_value": str(issue_activity.get("old_value")), + "old_identifier": ( + str(issue_activity.get("old_identifier")) + if issue_activity.get("old_identifier") + else None + ), + "new_identifier": ( + str(issue_activity.get("new_identifier")) + if issue_activity.get("new_identifier") + else None + ), + "activity_time": issue_activity.get("created_at"), + }, + }, + ) + + +def create_notification_data(issue, activity): + """ + Create a standard data structure for notifications. + """ + return { + "issue": { + "id": str(issue.id), + "name": str(issue.name), + "identifier": str(issue.project.identifier), + "sequence_id": issue.sequence_id, + "state_name": issue.state.name, + "state_group": issue.state.group, + "project_id": str(issue.project.id), + "workspace_slug": str(issue.project.workspace.slug), + }, + "issue_activity": { + "id": str(activity.id), + "verb": str(activity.verb), + "field": str(activity.field), + "actor": str(activity.actor_id), + "new_value": str(activity.new_value), + "old_value": str(activity.old_value), + "old_identifier": ( + str(activity.get("old_identifier")) + if activity.get("old_identifier") + else None + ), + "new_identifier": ( + str(activity.get("new_identifier")) + if activity.get("new_identifier") + else None + ), + }, + } + + +def create_email_data_from_activity(issue, activity, field=None): + """ + Create a standard data structure for email notifications. + """ + return { + "issue": { + "id": str(issue.id), + "name": str(issue.name), + "identifier": str(issue.project.identifier), + "sequence_id": issue.sequence_id, + "state_name": issue.state.name, + "state_group": issue.state.group, + "project_id": str(issue.project.id), + "workspace_slug": str(issue.project.workspace.slug), + }, + "issue_activity": { + "id": str(activity.id), + "verb": str(activity.verb), + "field": str(field or activity.field), + "actor": str(activity.actor_id), + "new_value": str(activity.new_value), + "old_value": str(activity.old_value), + "old_identifier": ( + str(activity.get("old_identifier")) + if activity.get("old_identifier") + else None + ), + "new_identifier": ( + str(activity.get("new_identifier")) + if activity.get("new_identifier") + else None + ), + "activity_time": str(activity.created_at), + }, + } + + @shared_task def notifications( type, @@ -226,7 +796,9 @@ def notifications( if issue_activities_created is not None else None ) - if type not in [ + + # Skip processing for certain activity types + if type in [ "cycle.activity.created", "cycle.activity.deleted", "module.activity.created", @@ -241,560 +813,120 @@ def notifications( "issue_draft.activity.updated", "issue_draft.activity.deleted", ]: - # Create Notifications - bulk_notifications = [] - bulk_email_logs = [] - - """ - Mention Tasks - 1. Perform Diffing and Extract the mentions, that mention notification needs to be sent - 2. From the latest set of mentions, extract the users which are not a subscribers & make them subscribers - """ - - # get the list of active project members - project_members = ProjectMember.objects.filter( - project_id=project_id, is_active=True - ).values_list("member_id", flat=True) - - # Get new mentions from the newer instance - new_mentions = get_new_mentions( - requested_instance=requested_data, current_instance=current_instance - ) - new_mentions = list( - set(new_mentions) & {str(member) for member in project_members} - ) - removed_mention = get_removed_mentions( - requested_instance=requested_data, current_instance=current_instance - ) - - comment_mentions = [] - all_comment_mentions = [] - - # Get New Subscribers from the mentions of the newer instance - requested_mentions = extract_mentions(issue_instance=requested_data) - mention_subscribers = extract_mentions_as_subscribers( - project_id=project_id, issue_id=issue_id, mentions=requested_mentions - ) - - for issue_activity in issue_activities_created: - issue_comment = issue_activity.get("issue_comment") - issue_comment_new_value = issue_activity.get("new_value") - issue_comment_old_value = issue_activity.get("old_value") - if issue_comment is not None: - # TODO: Maybe save the comment mentions, so that in future, we can filter out the issues based on comment mentions as well. - - all_comment_mentions = ( - all_comment_mentions - + extract_comment_mentions(issue_comment_new_value) - ) + return + + # Get project and issue information + project = Project.objects.get(pk=project_id) + issue = Issue.objects.filter(pk=issue_id).first() + issue_workspace_id = project.workspace_id + + # Get project members + project_members = ProjectMember.objects.filter( + project_id=project_id, is_active=True + ).values_list("member_id", flat=True) + + # Handle mentions + mention_data = process_mentions( + requested_data=requested_data, + current_instance=current_instance, + project_id=project_id, + issue_id=issue_id, + project_members=project_members, + issue_activities_created=issue_activities_created, + ) - new_comment_mentions = get_new_comment_mentions( - old_value=issue_comment_old_value, - new_value=issue_comment_new_value, - ) - comment_mentions = comment_mentions + new_comment_mentions - comment_mentions = [ - mention - for mention in comment_mentions - if UUID(mention) in set(project_members) - ] - - comment_mention_subscribers = extract_mentions_as_subscribers( - project_id=project_id, issue_id=issue_id, mentions=all_comment_mentions - ) - """ - We will not send subscription activity notification to the below mentioned user sets - - Those who have been newly mentioned in the issue description, we will send mention notification to them. - - When the activity is a comment_created and there exist a mention in the comment, then we have to send the "mention_in_comment" notification - - When the activity is a comment_updated and there exist a mention change, then also we have to send the "mention_in_comment" notification - """ - - # ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- # - issue_subscribers = list( - IssueSubscriber.objects.filter( - project_id=project_id, - issue_id=issue_id, - subscriber__in=Subquery(project_members), + new_mentions = mention_data["new_mentions"] + removed_mention = mention_data["removed_mention"] + comment_mentions = mention_data["comment_mentions"] + mention_subscribers = mention_data["mention_subscribers"] + comment_mention_subscribers = mention_data["comment_mention_subscribers"] + + # Add the actor as a subscriber if needed + if subscriber: + try: + _ = IssueSubscriber.objects.get_or_create( + project_id=project_id, issue_id=issue_id, subscriber_id=actor_id ) - .exclude( - subscriber_id__in=list(new_mentions + comment_mentions + [actor_id]) - ) - .values_list("subscriber", flat=True) - ) - - issue = Issue.objects.filter(pk=issue_id).first() - - if subscriber: - # add the user to issue subscriber - try: - _ = IssueSubscriber.objects.get_or_create( - project_id=project_id, issue_id=issue_id, subscriber_id=actor_id - ) - except Exception: - pass - - project = Project.objects.get(pk=project_id) + except Exception: + pass - issue_assignees = IssueAssignee.objects.filter( - issue_id=issue_id, + # Get issue subscribers excluding mentioned users and actor + issue_subscribers = list( + IssueSubscriber.objects.filter( project_id=project_id, - assignee__in=Subquery(project_members), - ).values_list("assignee", flat=True) - - issue_subscribers = list(set(issue_subscribers) - {uuid.UUID(actor_id)}) - issue_workspace_id = project.workspace_id - - for subscriber in issue_subscribers: - if issue.created_by_id and issue.created_by_id == subscriber: - sender = "in_app:issue_activities:created" - elif ( - subscriber in issue_assignees - and issue.created_by_id not in issue_assignees - ): - sender = "in_app:issue_activities:assigned" - else: - sender = "in_app:issue_activities:subscribed" - - preference = WorkspaceUserNotificationPreference.objects.filter( - user_id=subscriber, workspace_id=issue_workspace_id - ) - email_preference = preference.filter(transport="EMAIL").first() - in_app_preference = preference.filter(transport="IN_APP").first() + issue_id=issue_id, + subscriber__in=Subquery(project_members), + ) + .exclude( + subscriber_id__in=list(new_mentions + comment_mentions + [actor_id]) + ) + .values_list("subscriber", flat=True) + ) - for issue_activity in issue_activities_created: - # If activity done in blocking then blocked by email should not go - if issue_activity.get("issue_detail").get("id") != issue_id: - continue - - # Do not send notification for description update - if issue_activity.get("field") == "description": - continue - - # Check if the value should be sent or not - send_email = False - send_in_app = False - if issue_activity.get("field") == "state": - send_email = True if email_preference.state_change else False - send_in_app = True if in_app_preference.state_change else False - elif issue_activity.get("field") == "comment": - send_email = True if email_preference.comment else False - send_in_app = True if in_app_preference.comment else False - elif issue_activity.get("field") == "priority": - send_email = True if email_preference.priority else False - send_in_app = True if in_app_preference.priority else False - elif issue_activity.get("field") == "assignee": - send_email = True if email_preference.assignee else False - send_in_app = True if in_app_preference.assignee else False - elif ( - issue_activity.get("field") == "start_date" - or issue_activity.get("field") == "target_date" - ): - send_email = True if email_preference.start_due_date else False - send_in_app = ( - True if in_app_preference.start_due_date else False - ) - else: - send_email = True if email_preference.property_change else False - send_in_app = ( - True if in_app_preference.property_change else False - ) + issue_assignees = IssueAssignee.objects.filter( + issue_id=issue_id, + project_id=project_id, + assignee__in=Subquery(project_members), + ).values_list("assignee", flat=True) - # If activity is of issue comment fetch the comment - issue_comment = ( - IssueComment.objects.filter( - id=issue_activity.get("issue_comment"), - issue_id=issue_id, - project_id=project_id, - workspace_id=project.workspace_id, - ).first() - if issue_activity.get("issue_comment") - else None - ) + issue_subscribers = list(set(issue_subscribers) - {uuid.UUID(actor_id)}) - # Create in app notification - if send_in_app: - bulk_notifications.append( - Notification( - workspace=project.workspace, - sender=sender, - triggered_by_id=actor_id, - receiver_id=subscriber, - entity_identifier=issue_id, - entity_name="issue", - project=project, - title=issue_activity.get("comment"), - data={ - "issue": { - "id": str(issue_id), - "name": str(issue.name), - "identifier": str(issue.project.identifier), - "sequence_id": issue.sequence_id, - "state_name": issue.state.name, - "state_group": issue.state.group, - }, - "issue_activity": { - "id": str(issue_activity.get("id")), - "verb": str(issue_activity.get("verb")), - "field": str(issue_activity.get("field")), - "actor": str(issue_activity.get("actor_id")), - "new_value": str(issue_activity.get("new_value")), - "old_value": str(issue_activity.get("old_value")), - "issue_comment": str( - issue_comment.comment_stripped - if issue_comment is not None - else "" - ), - "old_identifier": ( - str(issue_activity.get("old_identifier")) - if issue_activity.get("old_identifier") - else None - ), - "new_identifier": ( - str(issue_activity.get("new_identifier")) - if issue_activity.get("new_identifier") - else None - ), - }, - }, - ) - ) - # Create email notification - if send_email: - bulk_email_logs.append( - EmailNotificationLog( - triggered_by_id=actor_id, - receiver_id=subscriber, - entity_identifier=issue_id, - entity_name="issue", - data={ - "issue": { - "id": str(issue_id), - "name": str(issue.name), - "identifier": str(issue.project.identifier), - "project_id": str(issue.project.id), - "workspace_slug": str( - issue.project.workspace.slug - ), - "sequence_id": issue.sequence_id, - "state_name": issue.state.name, - "state_group": issue.state.group, - }, - "issue_activity": { - "id": str(issue_activity.get("id")), - "verb": str(issue_activity.get("verb")), - "field": str(issue_activity.get("field")), - "actor": str(issue_activity.get("actor_id")), - "new_value": str( - issue_activity.get("new_value") - ), - "old_value": str( - issue_activity.get("old_value") - ), - "issue_comment": str( - issue_comment.comment_stripped - if issue_comment is not None - else "" - ), - "old_identifier": ( - str(issue_activity.get("old_identifier")) - if issue_activity.get("old_identifier") - else None - ), - "new_identifier": ( - str(issue_activity.get("new_identifier")) - if issue_activity.get("new_identifier") - else None - ), - "activity_time": issue_activity.get( - "created_at" - ), - }, - }, - ) - ) + # Add Mentioned users as Issue Subscribers + IssueSubscriber.objects.bulk_create( + mention_subscribers + comment_mention_subscribers, + batch_size=100, + ignore_conflicts=True, + ) - # ----------------------------------------------------------------------------------------------------------------- # + # Update mentions for the issue + update_mentions_for_issue( + issue=issue, + project=project, + new_mentions=new_mentions, + removed_mention=removed_mention, + ) - # Add Mentioned as Issue Subscribers - IssueSubscriber.objects.bulk_create( - mention_subscribers + comment_mention_subscribers, - batch_size=100, - ignore_conflicts=True, - ) + # Create and send notifications for each transport type + last_activity = ( + IssueActivity.objects.filter(issue_id=issue_id) + .order_by("-created_at") + .first() + ) - last_activity = ( - IssueActivity.objects.filter(issue_id=issue_id) - .order_by("-created_at") - .first() - ) + # Process in-app notifications + in_app_notifications = create_in_app_notifications( + issue=issue, + project=project, + actor_id=actor_id, + issue_activities_created=issue_activities_created, + issue_subscribers=issue_subscribers, + issue_assignees=issue_assignees, + new_mentions=new_mentions, + comment_mentions=comment_mentions, + last_activity=last_activity, + issue_workspace_id=issue_workspace_id, + ) - actor = User.objects.get(pk=actor_id) + # Process email notifications + email_notifications = create_email_notifications( + issue=issue, + project=project, + actor_id=actor_id, + issue_activities_created=issue_activities_created, + issue_subscribers=issue_subscribers, + issue_assignees=issue_assignees, + new_mentions=new_mentions, + comment_mentions=comment_mentions, + last_activity=last_activity, + issue_workspace_id=issue_workspace_id, + ) - for mention_id in comment_mentions: - if mention_id != actor_id: - preference = WorkspaceUserNotificationPreference.objects.filter( - user_id=mention_id, - workspace_id=issue_workspace_id, - ) - email_preference = preference.filter(transport=NotificationTransportChoices.EMAIL[0]).first() - in_app_preference = preference.filter(transport=NotificationTransportChoices.IN_APP[0]).first() - for issue_activity in issue_activities_created: - notification = create_mention_notification( - project=project, - issue=issue, - notification_comment=f"{actor.display_name} has mentioned you in a comment in issue {issue.name}", - actor_id=actor_id, - mention_id=mention_id, - issue_id=issue_id, - activity=issue_activity, - ) + # Bulk create notifications for each transport type + Notification.objects.bulk_create(in_app_notifications, batch_size=100) + EmailNotificationLog.objects.bulk_create( + email_notifications, batch_size=100, ignore_conflicts=True + ) - # check for email notifications - if email_preference.mention: - bulk_email_logs.append( - EmailNotificationLog( - triggered_by_id=actor_id, - receiver_id=mention_id, - entity_identifier=issue_id, - entity_name="issue", - data={ - "issue": { - "id": str(issue_id), - "name": str(issue.name), - "identifier": str(issue.project.identifier), - "sequence_id": issue.sequence_id, - "state_name": issue.state.name, - "state_group": issue.state.group, - "project_id": str(issue.project.id), - "workspace_slug": str( - issue.project.workspace.slug - ), - }, - "issue_activity": { - "id": str(issue_activity.get("id")), - "verb": str(issue_activity.get("verb")), - "field": str("mention"), - "actor": str( - issue_activity.get("actor_id") - ), - "new_value": str( - issue_activity.get("new_value") - ), - "old_value": str( - issue_activity.get("old_value") - ), - "old_identifier": ( - str( - issue_activity.get("old_identifier") - ) - if issue_activity.get("old_identifier") - else None - ), - "new_identifier": ( - str( - issue_activity.get("new_identifier") - ) - if issue_activity.get("new_identifier") - else None - ), - "activity_time": issue_activity.get( - "created_at" - ), - }, - }, - ) - ) - if in_app_preference.mention: - bulk_notifications.append(notification) - - for mention_id in new_mentions: - if mention_id != actor_id: - preference = WorkspaceUserNotificationPreference.objects.filter( - user_id=mention_id, - workspace_id=issue_workspace_id, - ) - email_preference = preference.filter(transport=NotificationTransportChoices.EMAIL[0]).first() - in_app_preference = preference.filter(transport=NotificationTransportChoices.IN_APP[0]).first() - if ( - last_activity is not None - and last_activity.field == "description" - and actor_id == str(last_activity.actor_id) - ): - if in_app_preference.mention: - bulk_notifications.append( - Notification( - workspace=project.workspace, - sender="in_app:issue_activities:mentioned", - triggered_by_id=actor_id, - receiver_id=mention_id, - entity_identifier=issue_id, - entity_name="issue", - project=project, - message=f"You have been mentioned in the issue {issue.name}", - data={ - "issue": { - "id": str(issue_id), - "name": str(issue.name), - "identifier": str(issue.project.identifier), - "sequence_id": issue.sequence_id, - "state_name": issue.state.name, - "state_group": issue.state.group, - "project_id": str(issue.project.id), - "workspace_slug": str( - issue.project.workspace.slug - ), - }, - "issue_activity": { - "id": str(last_activity.id), - "verb": str(last_activity.verb), - "field": str(last_activity.field), - "actor": str(last_activity.actor_id), - "new_value": str(last_activity.new_value), - "old_value": str(last_activity.old_value), - "old_identifier": ( - str(issue_activity.get("old_identifier")) - if issue_activity.get("old_identifier") - else None - ), - "new_identifier": ( - str(issue_activity.get("new_identifier")) - if issue_activity.get("new_identifier") - else None - ), - }, - }, - ) - ) - if email_preference.mention: - bulk_email_logs.append( - EmailNotificationLog( - triggered_by_id=actor_id, - receiver_id=subscriber, - entity_identifier=issue_id, - entity_name="issue", - data={ - "issue": { - "id": str(issue_id), - "name": str(issue.name), - "identifier": str(issue.project.identifier), - "sequence_id": issue.sequence_id, - "state_name": issue.state.name, - "state_group": issue.state.group, - }, - "issue_activity": { - "id": str(last_activity.id), - "verb": str(last_activity.verb), - "field": "mention", - "actor": str(last_activity.actor_id), - "new_value": str(last_activity.new_value), - "old_value": str(last_activity.old_value), - "old_identifier": ( - str( - issue_activity.get("old_identifier") - ) - if issue_activity.get("old_identifier") - else None - ), - "new_identifier": ( - str( - issue_activity.get("new_identifier") - ) - if issue_activity.get("new_identifier") - else None - ), - "activity_time": str( - last_activity.created_at - ), - }, - }, - ) - ) - else: - for issue_activity in issue_activities_created: - notification = create_mention_notification( - project=project, - issue=issue, - notification_comment=f"You have been mentioned in the issue {issue.name}", - actor_id=actor_id, - mention_id=mention_id, - issue_id=issue_id, - activity=issue_activity, - ) - if email_preference.mention: - bulk_email_logs.append( - EmailNotificationLog( - triggered_by_id=actor_id, - receiver_id=subscriber, - entity_identifier=issue_id, - entity_name="issue", - data={ - "issue": { - "id": str(issue_id), - "name": str(issue.name), - "identifier": str( - issue.project.identifier - ), - "sequence_id": issue.sequence_id, - "state_name": issue.state.name, - "state_group": issue.state.group, - }, - "issue_activity": { - "id": str(issue_activity.get("id")), - "verb": str(issue_activity.get("verb")), - "field": str("mention"), - "actor": str( - issue_activity.get("actor_id") - ), - "new_value": str( - issue_activity.get("new_value") - ), - "old_value": str( - issue_activity.get("old_value") - ), - "old_identifier": ( - str( - issue_activity.get( - "old_identifier" - ) - ) - if issue_activity.get( - "old_identifier" - ) - else None - ), - "new_identifier": ( - str( - issue_activity.get( - "new_identifier" - ) - ) - if issue_activity.get( - "new_identifier" - ) - else None - ), - "activity_time": issue_activity.get( - "created_at" - ), - }, - }, - ) - ) - if in_app_preference.mention: - bulk_notifications.append(notification) - - # save new mentions for the particular issue and remove the mentions that has been deleted from the description - update_mentions_for_issue( - issue=issue, - project=project, - new_mentions=new_mentions, - removed_mention=removed_mention, - ) - # Bulk create notifications - Notification.objects.bulk_create(bulk_notifications, batch_size=100) - EmailNotificationLog.objects.bulk_create( - bulk_email_logs, batch_size=100, ignore_conflicts=True - ) return except Exception as e: print(e) diff --git a/packages/constants/src/notification.ts b/packages/constants/src/notification.ts index 3f002730963..2c9ca9d3734 100644 --- a/packages/constants/src/notification.ts +++ b/packages/constants/src/notification.ts @@ -157,22 +157,27 @@ export const TASK_UPDATES_NOTIFICATION_SETTINGS: TNotificationSettings[] = [ { key: ENotificationSettingsKey.PROPERTY_CHANGE, i18n_title: "notification_settings.work_item_property_title", + i18n_subtitle: "notification_settings.work_item_property_subtitle", }, { key: ENotificationSettingsKey.STATE_CHANGE, i18n_title: "notification_settings.status_title", + i18n_subtitle: "notification_settings.status_subtitle", }, { key: ENotificationSettingsKey.PRIORITY, i18n_title: "notification_settings.priority_title", + i18n_subtitle: "notification_settings.priority_subtitle", }, { key: ENotificationSettingsKey.ASSIGNEE, i18n_title: "notification_settings.assignee_title", + i18n_subtitle: "notification_settings.assignee_subtitle", }, { key: ENotificationSettingsKey.START_DUE_DATE, i18n_title: "notification_settings.due_date_title", + i18n_subtitle: "notification_settings.due_date_subtitle", } ] @@ -180,14 +185,17 @@ export const COMMENT_NOTIFICATION_SETTINGS: TNotificationSettings[] = [ { key: ENotificationSettingsKey.MENTIONED_COMMENTS, i18n_title: "notification_settings.mentioned_comments_title", + i18n_subtitle: "notification_settings.mentioned_comments_subtitle", }, { key: ENotificationSettingsKey.COMMENTS, i18n_title: "notification_settings.new_comments_title", + i18n_subtitle: "notification_settings.new_comments_subtitle", }, { key: ENotificationSettingsKey.COMMENT_REACTIONS, i18n_title: "notification_settings.reaction_comments_title", + i18n_subtitle: "notification_settings.reaction_comments_subtitle", }, ] diff --git a/packages/i18n/src/locales/en/translations.json b/packages/i18n/src/locales/en/translations.json index 1d38aa19553..0f03c3ed397 100644 --- a/packages/i18n/src/locales/en/translations.json +++ b/packages/i18n/src/locales/en/translations.json @@ -2183,23 +2183,33 @@ "notification_settings": { "page_label": "{workspace} - Inbox settings", "inbox_settings": "Inbox settings", - "inbox_settings_description": "Toggle these ON or OFF for the workspace", + "inbox_settings_description": "Customize how you receive notifications for activities in your workspace. Your changes are saved automatically.", "advanced_settings": "Advanced settings", "in_plane": "In Plane", "email": "Email", "slack": "Slack", - "task_updates": "Task updates", + "task_updates": "Work item updates", + "task_updates_subtitle": "Get notified when work items in your workspace are updated.", "comments": "Comments", - "work_item_property_title": "Update on any property of work item", - "status_title": "State update", - "priority_title": "Priority update", - "assignee_title": "Assignee update", - "due_date_title": "Start/Due date update", + "comments_subtitle": "Stay updated on discussions in your workspace.", + "work_item_property_title": "Update on any property of the work item", + "work_item_property_subtitle": "Get notified when work items in your workspace are updated.", + "status_title": "State change", + "status_subtitle": "When a work item's state is updated.", + "priority_title": "Priority change", + "priority_subtitle": "When a work item's priority level is adjusted.", + "assignee_title": "Assignee change", + "assignee_subtitle": "When a work item is assigned or reassigned to someone.", + "due_date_title": "Date change", + "due_date_subtitle": "When a work item's start or due date is updated.", "module_title": "Module update", "cycle_title": "Cycle update", - "mentioned_comments_title": "Comments I'm @mentioned in", + "mentioned_comments_title": "Mentions", + "mentioned_comments_subtitle": "When someone mentions you in a comment.", "new_comments_title": "New comments", + "new_comments_subtitle": "When a new comment is added to a task you’re following.", "reaction_comments_title": "Reactions", + "reaction_comments_subtitle": "Get notified when someone reacts to your comments or tasks with an emoji.", "setting_updated_successfully": "Setting updated successfully", "failed_to_update_setting": "Failed to update setting" } diff --git a/packages/i18n/src/locales/es/translations.json b/packages/i18n/src/locales/es/translations.json index f20229ae0cc..858b631f3b2 100644 --- a/packages/i18n/src/locales/es/translations.json +++ b/packages/i18n/src/locales/es/translations.json @@ -2350,26 +2350,36 @@ }, "notification_settings": { - "page_label": "{workspace} - Configuración de la bandeja de entrada", - "inbox_settings": "Configuración de la bandeja de entrada", - "inbox_settings_description": "Activa o desactiva estas opciones para el espacio de trabajo", + "page_label": "{workspace} - Configuración de bandeja de entrada", + "inbox_settings": "Configuración de bandeja de entrada", + "inbox_settings_description": "Personaliza cómo recibes notificaciones para actividades en tu espacio de trabajo. Tus cambios se guardan automáticamente.", "advanced_settings": "Configuración avanzada", "in_plane": "En Plane", "email": "Correo electrónico", "slack": "Slack", - "task_updates": "Actualizaciones de tareas", + "task_updates": "Actualizaciones de elementos de trabajo", + "task_updates_subtitle": "Recibe notificaciones cuando se actualicen elementos de trabajo en tu espacio de trabajo.", "comments": "Comentarios", + "comments_subtitle": "Mantente actualizado sobre las discusiones en tu espacio de trabajo.", "work_item_property_title": "Actualización de cualquier propiedad del elemento de trabajo", - "status_title": "Actualización de estado", - "priority_title": "Actualización de prioridad", - "assignee_title": "Actualización de asignado", - "due_date_title": "Actualización de fecha de inicio/vencimiento", + "work_item_property_subtitle": "Recibe notificaciones cuando se actualicen elementos de trabajo en tu espacio de trabajo.", + "status_title": "Cambio de estado", + "status_subtitle": "Cuando se actualiza el estado de un elemento de trabajo.", + "priority_title": "Cambio de prioridad", + "priority_subtitle": "Cuando se ajusta el nivel de prioridad de un elemento de trabajo.", + "assignee_title": "Cambio de asignación", + "assignee_subtitle": "Cuando un elemento de trabajo es asignado o reasignado a alguien.", + "due_date_title": "Cambio de fecha", + "due_date_subtitle": "Cuando se actualiza la fecha de inicio o vencimiento de un elemento de trabajo.", "module_title": "Actualización de módulo", "cycle_title": "Actualización de ciclo", - "mentioned_comments_title": "Comentarios en los que estoy @mencionado", + "mentioned_comments_title": "Menciones", + "mentioned_comments_subtitle": "Cuando alguien te menciona en un comentario.", "new_comments_title": "Nuevos comentarios", + "new_comments_subtitle": "Cuando se agrega un nuevo comentario a una tarea que sigues.", "reaction_comments_title": "Reacciones", - "setting_updated_successfully": "Configuración actualizada correctamente", - "failed_to_update_setting": "No se pudo actualizar la configuración" + "reaction_comments_subtitle": "Recibe notificaciones cuando alguien reacciona a tus comentarios o tareas con un emoji.", + "setting_updated_successfully": "Configuración actualizada con éxito", + "failed_to_update_setting": "Error al actualizar la configuración" } } diff --git a/packages/i18n/src/locales/fr/translations.json b/packages/i18n/src/locales/fr/translations.json index c33d2051a8b..97109023b1d 100644 --- a/packages/i18n/src/locales/fr/translations.json +++ b/packages/i18n/src/locales/fr/translations.json @@ -2350,25 +2350,35 @@ }, "notification_settings": { - "page_label": "{workspace} - Paramètres de la boîte de réception", - "inbox_settings": "Paramètres de la boîte de réception", - "inbox_settings_description": "Activez ou désactivez ces options pour l'espace de travail", + "page_label": "{workspace} - Paramètres de boîte de réception", + "inbox_settings": "Paramètres de boîte de réception", + "inbox_settings_description": "Personnalisez la façon dont vous recevez des notifications pour les activités dans votre espace de travail. Vos modifications sont enregistrées automatiquement.", "advanced_settings": "Paramètres avancés", "in_plane": "Dans Plane", "email": "E-mail", "slack": "Slack", - "task_updates": "Mises à jour des tâches", + "task_updates": "Mises à jour des éléments de travail", + "task_updates_subtitle": "Soyez notifié lorsque des éléments de travail dans votre espace de travail sont mis à jour.", "comments": "Commentaires", - "work_item_property_title": "Mise à jour de toute propriété de l'élément de travail", - "status_title": "Mise à jour de l'état", - "priority_title": "Mise à jour de la priorité", - "assignee_title": "Mise à jour de l'assigné", - "due_date_title": "Mise à jour de la date de début/d'échéance", - "module_title": "Mise à jour du module", - "cycle_title": "Mise à jour du cycle", - "mentioned_comments_title": "Commentaires où je suis @mentionné", + "comments_subtitle": "Restez informé des discussions dans votre espace de travail.", + "work_item_property_title": "Mise à jour de n'importe quelle propriété de l'élément de travail", + "work_item_property_subtitle": "Soyez notifié lorsque des éléments de travail dans votre espace de travail sont mis à jour.", + "status_title": "Changement d'état", + "status_subtitle": "Lorsque l'état d'un élément de travail est mis à jour.", + "priority_title": "Changement de priorité", + "priority_subtitle": "Lorsque le niveau de priorité d'un élément de travail est ajusté.", + "assignee_title": "Changement d'assignation", + "assignee_subtitle": "Lorsqu'un élément de travail est assigné ou réassigné à quelqu'un.", + "due_date_title": "Changement de date", + "due_date_subtitle": "Lorsque la date de début ou d'échéance d'un élément de travail est mise à jour.", + "module_title": "Mise à jour de module", + "cycle_title": "Mise à jour de cycle", + "mentioned_comments_title": "Mentions", + "mentioned_comments_subtitle": "Lorsque quelqu'un vous mentionne dans un commentaire.", "new_comments_title": "Nouveaux commentaires", + "new_comments_subtitle": "Lorsqu'un nouveau commentaire est ajouté à une tâche que vous suivez.", "reaction_comments_title": "Réactions", + "reaction_comments_subtitle": "Soyez notifié lorsque quelqu'un réagit à vos commentaires ou tâches avec un emoji.", "setting_updated_successfully": "Paramètre mis à jour avec succès", "failed_to_update_setting": "Échec de la mise à jour du paramètre" } diff --git a/packages/i18n/src/locales/ja/translations.json b/packages/i18n/src/locales/ja/translations.json index 755a61db6c1..4cb09ecf4b9 100644 --- a/packages/i18n/src/locales/ja/translations.json +++ b/packages/i18n/src/locales/ja/translations.json @@ -2352,23 +2352,33 @@ "notification_settings": { "page_label": "{workspace} - 受信トレイ設定", "inbox_settings": "受信トレイ設定", - "inbox_settings_description": "ワークスペースのためにこれらをオンまたはオフに切り替えます", + "inbox_settings_description": "ワークスペースでのアクティビティに関する通知の受信方法をカスタマイズします。変更は自動的に保存されます。", "advanced_settings": "詳細設定", "in_plane": "Plane内", "email": "メール", "slack": "Slack", - "task_updates": "タスクの更新", + "task_updates": "作業項目の更新", + "task_updates_subtitle": "ワークスペース内の作業項目が更新されたときに通知を受け取ります。", "comments": "コメント", - "work_item_property_title": "作業項目のプロパティの更新", - "status_title": "状態の更新", - "priority_title": "優先度の更新", - "assignee_title": "担当者の更新", - "due_date_title": "開始日/期日の更新", - "module_title": "モジュールの更新", - "cycle_title": "サイクルの更新", - "mentioned_comments_title": "自分が@メンションされたコメント", + "comments_subtitle": "ワークスペース内のディスカッションに関する最新情報を入手します。", + "work_item_property_title": "作業項目のあらゆるプロパティの更新", + "work_item_property_subtitle": "ワークスペース内の作業項目が更新されたときに通知を受け取ります。", + "status_title": "状態変更", + "status_subtitle": "作業項目の状態が更新されたとき。", + "priority_title": "優先度変更", + "priority_subtitle": "作業項目の優先度レベルが調整されたとき。", + "assignee_title": "担当者変更", + "assignee_subtitle": "作業項目が誰かに割り当てられたり再割り当てされたとき。", + "due_date_title": "日付変更", + "due_date_subtitle": "作業項目の開始日または期日が更新されたとき。", + "module_title": "モジュール更新", + "cycle_title": "サイクル更新", + "mentioned_comments_title": "メンション", + "mentioned_comments_subtitle": "誰かがコメントであなたに言及したとき。", "new_comments_title": "新しいコメント", + "new_comments_subtitle": "フォローしているタスクに新しいコメントが追加されたとき。", "reaction_comments_title": "リアクション", + "reaction_comments_subtitle": "誰かがあなたのコメントやタスクに絵文字でリアクションしたときに通知を受け取ります。", "setting_updated_successfully": "設定が正常に更新されました", "failed_to_update_setting": "設定の更新に失敗しました" } diff --git a/packages/i18n/src/locales/zh-CN/translations.json b/packages/i18n/src/locales/zh-CN/translations.json index 54ce4ebeed3..3d9c690c0af 100644 --- a/packages/i18n/src/locales/zh-CN/translations.json +++ b/packages/i18n/src/locales/zh-CN/translations.json @@ -2352,24 +2352,34 @@ "notification_settings": { "page_label": "{workspace} - 收件箱设置", "inbox_settings": "收件箱设置", - "inbox_settings_description": "为工作空间打开或关闭这些选项", + "inbox_settings_description": "自定义如何接收工作空间活动的通知。您的更改会自动保存。", "advanced_settings": "高级设置", - "in_plane": "在 Plane", + "in_plane": "在Plane中", "email": "电子邮件", "slack": "Slack", - "task_updates": "任务更新", + "task_updates": "工作项更新", + "task_updates_subtitle": "当您工作空间中的工作项被更新时获得通知。", "comments": "评论", - "work_item_property_title": "工作项属性更新", - "status_title": "状态更新", - "priority_title": "优先级更新", - "assignee_title": "负责人更新", - "due_date_title": "开始/截止日期更新", + "comments_subtitle": "随时了解您工作空间中的讨论。", + "work_item_property_title": "工作项任何属性的更新", + "work_item_property_subtitle": "当您工作空间中的工作项被更新时获得通知。", + "status_title": "状态变更", + "status_subtitle": "当工作项的状态被更新时。", + "priority_title": "优先级变更", + "priority_subtitle": "当工作项的优先级被调整时。", + "assignee_title": "负责人变更", + "assignee_subtitle": "当工作项被分配或重新分配给某人时。", + "due_date_title": "日期变更", + "due_date_subtitle": "当工作项的开始日期或截止日期被更新时。", "module_title": "模块更新", "cycle_title": "周期更新", - "mentioned_comments_title": "我被@提及的评论", + "mentioned_comments_title": "提及", + "mentioned_comments_subtitle": "当有人在评论中提及您时。", "new_comments_title": "新评论", + "new_comments_subtitle": "当您关注的任务添加了新评论时。", "reaction_comments_title": "反应", - "setting_updated_successfully": "设置已成功更新", + "reaction_comments_subtitle": "当有人用表情符号对您的评论或任务做出反应时获得通知。", + "setting_updated_successfully": "设置更新成功", "failed_to_update_setting": "设置更新失败" } } diff --git a/packages/types/src/notification.d.ts b/packages/types/src/notification.d.ts index e881148f454..6e6d9177b7d 100644 --- a/packages/types/src/notification.d.ts +++ b/packages/types/src/notification.d.ts @@ -2,6 +2,7 @@ import { ENotificationSettingsKey, EWorkspaceNotificationTransport } from "@plan export type TNotificationSettings = { i18n_title: string, + i18n_subtitle?: string, key: ENotificationSettingsKey } diff --git a/web/core/components/inbox/settings/root.tsx b/web/core/components/inbox/settings/root.tsx index 8aee3e24043..569bf0164cd 100644 --- a/web/core/components/inbox/settings/root.tsx +++ b/web/core/components/inbox/settings/root.tsx @@ -12,8 +12,13 @@ export const InboxSettingsRoot: FC = () => { return ( <>
-
- {t("notification_settings.task_updates")} +
+
+ {t("notification_settings.task_updates")} +
+
+ {t("notification_settings.task_updates_subtitle")} +
{t("notification_settings.advanced_settings")}
@@ -22,13 +27,18 @@ export const InboxSettingsRoot: FC = () => {
{ TASK_UPDATES_NOTIFICATION_SETTINGS?.map((item) => ( - + )) }
-
- {t("notification_settings.comments")} +
+
+ {t("notification_settings.comments")} +
+
+ {t("notification_settings.comments_subtitle")} +
{t("notification_settings.advanced_settings")}
@@ -37,7 +47,7 @@ export const InboxSettingsRoot: FC = () => {
{ COMMENT_NOTIFICATION_SETTINGS?.map((item, index) => ( - + )) }
diff --git a/web/core/components/inbox/settings/update-setting-row.tsx b/web/core/components/inbox/settings/update-setting-row.tsx index 1edd3b02082..8e5bd31d605 100644 --- a/web/core/components/inbox/settings/update-setting-row.tsx +++ b/web/core/components/inbox/settings/update-setting-row.tsx @@ -9,17 +9,25 @@ import { InboxSettingUpdate } from "./update-setting"; type InboxSettingUpdateRowProps = { settings_key: ENotificationSettingsKey; title: string; + subtitle?: string } export const InboxSettingUpdateRow: FC = observer((props: InboxSettingUpdateRowProps) => { - const { title, settings_key } = props; + const { title, subtitle, settings_key } = props; const { t } = useTranslation() return (
-
- {t(title)} +
+
+ {t(title)} +
+ { + subtitle &&
+ {t(subtitle)} +
+ }