diff --git a/apiserver/plane/app/serializers/__init__.py b/apiserver/plane/app/serializers/__init__.py index 623071573c4..9d3bda31417 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 7baba9bb075..838590aca92 100644 --- a/apiserver/plane/app/views/__init__.py +++ b/apiserver/plane/app/views/__init__.py @@ -203,6 +203,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..0249ef1cb03 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,9 @@ Notification, UserNotificationPreference, WorkspaceMember, + Workspace, + WorkspaceUserNotificationPreference, + NotificationTransportChoices ) from plane.utils.paginator import BasePaginator from plane.app.permissions import allow_permission, ROLE @@ -360,3 +364,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 NotificationTransportChoices.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..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 +from plane.db.models import Workspace, WorkspaceUserNotificationPreference, NotificationTransportChoices 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 NotificationTransportChoices.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/bgtasks/notification_task.py b/apiserver/plane/bgtasks/notification_task.py index e58344bbf2e..792f2399dd3 100644 --- a/apiserver/plane/bgtasks/notification_task.py +++ b/apiserver/plane/bgtasks/notification_task.py @@ -12,13 +12,13 @@ User, IssueAssignee, Issue, - State, EmailNotificationLog, Notification, IssueComment, IssueActivity, - UserNotificationPreference, ProjectMember, + NotificationTransportChoices, + WorkspaceUserNotificationPreference, ) from django.db.models import Subquery @@ -207,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, @@ -224,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", @@ -239,542 +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)}) - - 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" + 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) + ) - preference = UserNotificationPreference.objects.get(user_id=subscriber) + issue_assignees = IssueAssignee.objects.filter( + issue_id=issue_id, + project_id=project_id, + assignee__in=Subquery(project_members), + ).values_list("assignee", 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 - 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 - elif ( - issue_activity.get("field") == "comment" and preference.comment - ): - send_email = True - elif preference.property_change: - send_email = True - else: - send_email = False - - # 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 - 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 = UserNotificationPreference.objects.get( - user_id=mention_id - ) - 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 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" - ), - }, - }, - ) - ) - bulk_notifications.append(notification) - - for mention_id in new_mentions: - if mention_id != actor_id: - preference = UserNotificationPreference.objects.get( - user_id=mention_id - ) - 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 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 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" - ), - }, - }, - ) - ) - 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/apiserver/plane/db/models/__init__.py b/apiserver/plane/db/models/__init__.py index 3cf46c91946..fdbe39dd3ba 100644 --- a/apiserver/plane/db/models/__init__.py +++ b/apiserver/plane/db/models/__init__.py @@ -44,7 +44,13 @@ IssueDescriptionVersion, ) from .module import Module, ModuleIssue, ModuleLink, ModuleMember, ModuleUserProperties -from .notification import EmailNotificationLog, Notification, UserNotificationPreference +from .notification import ( + EmailNotificationLog, + Notification, + UserNotificationPreference, + NotificationTransportChoices, + 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..4738a4a1bd0 100644 --- a/apiserver/plane/db/models/notification.py +++ b/apiserver/plane/db/models/notification.py @@ -93,7 +93,6 @@ def __str__(self): """Return the user""" return f"<{self.user}>" - class EmailNotificationLog(BaseModel): # receiver receiver = models.ForeignKey( @@ -123,3 +122,59 @@ 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" + IN_APP = "IN_APP", "In App" \ No newline at end of file diff --git a/packages/constants/src/notification.ts b/packages/constants/src/notification.ts index cb267c4ad1f..2c9ca9d3734 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,71 @@ export const allTimeIn30MinutesInterval12HoursFormat: Array<{ { label: "11:00", value: "11:00" }, { label: "11:30", value: "11:30" }, ]; + + +export enum ENotificationSettingsKey { + 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 { + EMAIL = "EMAIL", + IN_APP = "IN_APP", +} + +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", + } +] + +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", + }, +] + +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/cs/translations.json b/packages/i18n/src/locales/cs/translations.json index 0dff29fc7e5..e54c1a5e93f 100644 --- a/packages/i18n/src/locales/cs/translations.json +++ b/packages/i18n/src/locales/cs/translations.json @@ -2435,5 +2435,39 @@ "last_edited_by": "Naposledy upraveno uživatelem", "previously_edited_by": "Dříve upraveno uživatelem", "edited_by": "Upraveno uživatelem" + }, + + "notification_settings": { + "page_label": "{workspace} - Nastavení doručené pošty", + "inbox_settings": "Nastavení doručené pošty", + "inbox_settings_description": "Přizpůsobte si, jak dostáváte oznámení o aktivitách ve vašem pracovním prostoru. Vaše změny jsou automaticky uloženy.", + "advanced_settings": "Pokročilá nastavení", + "in_plane": "V Plane", + "email": "Email", + "slack": "Slack", + "task_updates": "Aktualizace pracovních položek", + "task_updates_subtitle": "Dostávejte oznámení, když jsou aktualizovány pracovní položky ve vašem pracovním prostoru.", + "comments": "Komentáře", + "comments_subtitle": "Buďte informováni o diskusích ve vašem pracovním prostoru.", + "work_item_property_title": "Aktualizace jakékoli vlastnosti pracovního položky", + "work_item_property_subtitle": "Dostávejte oznámení, když jsou aktualizovány pracovní položky ve vašem pracovním prostoru.", + "status_title": "Změna stavu", + "status_subtitle": "Když je aktualizován stav pracovního položky.", + "priority_title": "Změna priority", + "priority_subtitle": "Když je upravena priorita pracovního položky.", + "assignee_title": "Změna přiřazení", + "assignee_subtitle": "Když je pracovní položka přiřazena nebo přeřazena někomu.", + "due_date_title": "Změna data", + "due_date_subtitle": "Když je aktualizováno datum zahájení nebo splatnosti pracovního položky.", + "module_title": "Aktualizace modulu", + "cycle_title": "Aktualizace cyklu", + "mentioned_comments_title": "Zmínky", + "mentioned_comments_subtitle": "Když vás někdo zmíní v komentáři.", + "new_comments_title": "Nové komentáře", + "new_comments_subtitle": "Když je přidán nový komentář k úkolu, který sledujete.", + "reaction_comments_title": "Reakce", + "reaction_comments_subtitle": "Dostávejte oznámení, když někdo reaguje na vaše komentáře nebo úkoly pomocí emoji.", + "setting_updated_successfully": "Nastavení bylo úspěšně aktualizováno", + "failed_to_update_setting": "Nepodařilo se aktualizovat nastavení" } } diff --git a/packages/i18n/src/locales/de/translations.json b/packages/i18n/src/locales/de/translations.json index 97334611b88..d84d34dca3d 100644 --- a/packages/i18n/src/locales/de/translations.json +++ b/packages/i18n/src/locales/de/translations.json @@ -2393,5 +2393,39 @@ "last_edited_by": "Zuletzt bearbeitet von", "previously_edited_by": "Zuvor bearbeitet von", "edited_by": "Bearbeitet von" + }, + + "notification_settings": { + "page_label": "{workspace} - Posteingangseinstellungen", + "inbox_settings": "Posteingangseinstellungen", + "inbox_settings_description": "Passen Sie an, wie Sie Benachrichtigungen für Aktivitäten in Ihrem Arbeitsbereich erhalten. Ihre Änderungen werden automatisch gespeichert.", + "advanced_settings": "Erweiterte Einstellungen", + "in_plane": "In Plane", + "email": "E-Mail", + "slack": "Slack", + "task_updates": "Aktualisierungen von Arbeitselementen", + "task_updates_subtitle": "Erhalten Sie Benachrichtigungen, wenn Arbeitselemente in Ihrem Arbeitsbereich aktualisiert werden.", + "comments": "Kommentare", + "comments_subtitle": "Bleiben Sie über Diskussionen in Ihrem Arbeitsbereich auf dem Laufenden.", + "work_item_property_title": "Aktualisierung einer Eigenschaft des Arbeitselements", + "work_item_property_subtitle": "Erhalten Sie Benachrichtigungen, wenn Arbeitselemente in Ihrem Arbeitsbereich aktualisiert werden.", + "status_title": "Statusänderung", + "status_subtitle": "Wenn der Status eines Arbeitselements aktualisiert wird.", + "priority_title": "Prioritätsänderung", + "priority_subtitle": "Wenn das Prioritätsniveau eines Arbeitselements angepasst wird.", + "assignee_title": "Zuweisungsänderung", + "assignee_subtitle": "Wenn ein Arbeitselement jemandem zugewiesen oder neu zugewiesen wird.", + "due_date_title": "Datumsänderung", + "due_date_subtitle": "Wenn das Start- oder Fälligkeitsdatum eines Arbeitselements aktualisiert wird.", + "module_title": "Modulaktualisierung", + "cycle_title": "Zyklusaktualisierung", + "mentioned_comments_title": "Erwähnungen", + "mentioned_comments_subtitle": "Wenn jemand Sie in einem Kommentar erwähnt.", + "new_comments_title": "Neue Kommentare", + "new_comments_subtitle": "Wenn ein neuer Kommentar zu einer Aufgabe hinzugefügt wird, die Sie verfolgen.", + "reaction_comments_title": "Reaktionen", + "reaction_comments_subtitle": "Erhalten Sie Benachrichtigungen, wenn jemand mit einem Emoji auf Ihre Kommentare oder Aufgaben reagiert.", + "setting_updated_successfully": "Einstellung erfolgreich aktualisiert", + "failed_to_update_setting": "Fehler beim Aktualisieren der Einstellung" } } diff --git a/packages/i18n/src/locales/en/translations.json b/packages/i18n/src/locales/en/translations.json index d2a891fc6e1..b7922fee09e 100644 --- a/packages/i18n/src/locales/en/translations.json +++ b/packages/i18n/src/locales/en/translations.json @@ -2270,5 +2270,39 @@ "last_edited_by": "Last edited by", "previously_edited_by": "Previously edited by", "edited_by": "Edited by" + }, + + "notification_settings": { + "page_label": "{workspace} - Inbox settings", + "inbox_settings": "Inbox settings", + "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": "Work item updates", + "task_updates_subtitle": "Get notified when work items in your workspace are updated.", + "comments": "Comments", + "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": "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 9470a8c7aaf..b0b6d5dfd59 100644 --- a/packages/i18n/src/locales/es/translations.json +++ b/packages/i18n/src/locales/es/translations.json @@ -2439,5 +2439,39 @@ "last_edited_by": "Última edición por", "previously_edited_by": "Editado anteriormente por", "edited_by": "Editado por" + }, + + "notification_settings": { + "page_label": "{workspace} - Configuración de la bandeja de entrada", + "inbox_settings": "Configuración de la bandeja de entrada", + "inbox_settings_description": "Personaliza cómo recibes notificaciones de 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 elementos de trabajo", + "task_updates_subtitle": "Recibe notificaciones cuando se actualicen los 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", + "work_item_property_subtitle": "Recibe notificaciones cuando se actualicen los 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 asignado", + "assignee_subtitle": "Cuando un elemento de trabajo se asigna o reasigna 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 del módulo", + "cycle_title": "Actualización del ciclo", + "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", + "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 27d6f54c85e..552b256575d 100644 --- a/packages/i18n/src/locales/fr/translations.json +++ b/packages/i18n/src/locales/fr/translations.json @@ -2437,5 +2437,40 @@ "last_edited_by": "Dernière modification par", "previously_edited_by": "Précédemment modifié par", "edited_by": "Modifié par" + }, + + "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": "Personnalisez la façon dont vous recevez les 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": "Email", + "slack": "Slack", + "task_updates": "Mises à jour des éléments de travail", + "task_updates_subtitle": "Recevez une notification lorsque les éléments de travail dans votre espace de travail sont mis à jour.", + "comments": "Commentaires", + "comments_subtitle": "Restez informé des discussions dans votre espace de travail.", + "work_item_property_title": "Mise à jour de toute propriété de l'élément de travail", + "work_item_property_subtitle": "Recevez une notification lorsque les éléments de travail dans votre espace de travail sont mis à jour.", + "status_title": "Changement d'état", + "status_subtitle": "Lorsqu'un élément de travail change d'état.", + "priority_title": "Changement de priorité", + "priority_subtitle": "Lorsqu'un niveau de priorité d'un élément de travail est ajusté.", + "assignee_title": "Changement de responsable", + "assignee_subtitle": "Lorsqu'un élément de travail est assigné ou réassigné à quelqu'un.", + "due_date_title": "Changement de date", + "due_date_subtitle": "Lorsqu'une date de début ou d'échéance d'un élément de travail est mise à jour.", + "module_title": "Mise à jour du module", + "cycle_title": "Mise à jour du cycle", + "mentioned_comments_title": "Mentions", + "mentioned_comments_subtitle": "Lorsqu'une personne 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": "Recevez une notification 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/id/translations.json b/packages/i18n/src/locales/id/translations.json index 22ebae1c0cf..7ba07a2bac2 100644 --- a/packages/i18n/src/locales/id/translations.json +++ b/packages/i18n/src/locales/id/translations.json @@ -2431,5 +2431,39 @@ "last_edited_by": "Terakhir disunting oleh", "previously_edited_by": "Sebelumnya disunting oleh", "edited_by": "Disunting oleh" + }, + + "notification_settings": { + "page_label": "{workspace} - Pengaturan kotak masuk", + "inbox_settings": "Pengaturan kotak masuk", + "inbox_settings_description": "Sesuaikan cara Anda menerima notifikasi untuk aktivitas di ruang kerja Anda. Perubahan Anda disimpan secara otomatis.", + "advanced_settings": "Pengaturan lanjutan", + "in_plane": "Di Plane", + "email": "Email", + "slack": "Slack", + "task_updates": "Pembaruan item kerja", + "task_updates_subtitle": "Dapatkan notifikasi ketika item kerja di ruang kerja Anda diperbarui.", + "comments": "Komentar", + "comments_subtitle": "Tetap terinformasi tentang diskusi di ruang kerja Anda.", + "work_item_property_title": "Pembaruan properti item kerja", + "work_item_property_subtitle": "Dapatkan notifikasi ketika item kerja di ruang kerja Anda diperbarui.", + "status_title": "Perubahan status", + "status_subtitle": "Ketika status item kerja diperbarui.", + "priority_title": "Perubahan prioritas", + "priority_subtitle": "Ketika tingkat prioritas item kerja disesuaikan.", + "assignee_title": "Perubahan penugasan", + "assignee_subtitle": "Ketika item kerja ditugaskan atau ditugaskan ulang kepada seseorang.", + "due_date_title": "Perubahan tanggal", + "due_date_subtitle": "Ketika tanggal mulai atau jatuh tempo item kerja diperbarui.", + "module_title": "Pembaruan modul", + "cycle_title": "Pembaruan siklus", + "mentioned_comments_title": "Penyebutan", + "mentioned_comments_subtitle": "Ketika seseorang menyebut Anda dalam komentar.", + "new_comments_title": "Komentar baru", + "new_comments_subtitle": "Ketika komentar baru ditambahkan ke tugas yang Anda ikuti.", + "reaction_comments_title": "Reaksi", + "reaction_comments_subtitle": "Dapatkan notifikasi ketika seseorang bereaksi terhadap komentar atau tugas Anda dengan emoji.", + "setting_updated_successfully": "Pengaturan berhasil diperbarui", + "failed_to_update_setting": "Gagal memperbarui pengaturan" } } diff --git a/packages/i18n/src/locales/it/translations.json b/packages/i18n/src/locales/it/translations.json index 4cfe9c2b5cd..9e97698d343 100644 --- a/packages/i18n/src/locales/it/translations.json +++ b/packages/i18n/src/locales/it/translations.json @@ -2436,5 +2436,39 @@ "last_edited_by": "Ultima modifica di", "previously_edited_by": "Precedentemente modificato da", "edited_by": "Modificato da" + }, + + "notification_settings": { + "page_label": "{workspace} - Impostazioni della casella di posta", + "inbox_settings": "Impostazioni della casella di posta", + "inbox_settings_description": "Personalizza come ricevi le notifiche per le attività nel tuo spazio di lavoro. Le tue modifiche vengono salvate automaticamente.", + "advanced_settings": "Impostazioni avanzate", + "in_plane": "In Plane", + "email": "Email", + "slack": "Slack", + "task_updates": "Aggiornamenti degli elementi di lavoro", + "task_updates_subtitle": "Ricevi una notifica quando gli elementi di lavoro nel tuo spazio di lavoro vengono aggiornati.", + "comments": "Commenti", + "comments_subtitle": "Rimani aggiornato sulle discussioni nel tuo spazio di lavoro.", + "work_item_property_title": "Aggiornamento di qualsiasi proprietà dell'elemento di lavoro", + "work_item_property_subtitle": "Ricevi una notifica quando gli elementi di lavoro nel tuo spazio di lavoro vengono aggiornati.", + "status_title": "Cambio di stato", + "status_subtitle": "Quando lo stato di un elemento di lavoro viene aggiornato.", + "priority_title": "Cambio di priorità", + "priority_subtitle": "Quando il livello di priorità di un elemento di lavoro viene regolato.", + "assignee_title": "Cambio di assegnatario", + "assignee_subtitle": "Quando un elemento di lavoro viene assegnato o riassegnato a qualcuno.", + "due_date_title": "Cambio di data", + "due_date_subtitle": "Quando la data di inizio o di scadenza di un elemento di lavoro viene aggiornata.", + "module_title": "Aggiornamento del modulo", + "cycle_title": "Aggiornamento del ciclo", + "mentioned_comments_title": "Menzioni", + "mentioned_comments_subtitle": "Quando qualcuno ti menziona in un commento.", + "new_comments_title": "Nuovi commenti", + "new_comments_subtitle": "Quando viene aggiunto un nuovo commento a un'attività che stai seguendo.", + "reaction_comments_title": "Reazioni", + "reaction_comments_subtitle": "Ricevi una notifica quando qualcuno reagisce ai tuoi commenti o attività con un'emoji.", + "setting_updated_successfully": "Impostazione aggiornata con successo", + "failed_to_update_setting": "Aggiornamento dell'impostazione non riuscito" } } diff --git a/packages/i18n/src/locales/ja/translations.json b/packages/i18n/src/locales/ja/translations.json index 44c9488baed..f811c165d36 100644 --- a/packages/i18n/src/locales/ja/translations.json +++ b/packages/i18n/src/locales/ja/translations.json @@ -2437,5 +2437,39 @@ "last_edited_by": "最終編集者", "previously_edited_by": "以前の編集者", "edited_by": "編集者" + }, + + "notification_settings": { + "page_label": "{workspace} - 受信トレイ設定", + "inbox_settings": "受信トレイ設定", + "inbox_settings_description": "ワークスペース内のアクティビティに関する通知の受け取り方法をカスタマイズします。変更は自動的に保存されます。", + "advanced_settings": "詳細設定", + "in_plane": "Plane内", + "email": "メール", + "slack": "Slack", + "task_updates": "作業項目の更新", + "task_updates_subtitle": "ワークスペース内の作業項目が更新されたときに通知を受け取ります。", + "comments": "コメント", + "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/ko/translations.json b/packages/i18n/src/locales/ko/translations.json index f0f74991f6d..84805af2fd8 100644 --- a/packages/i18n/src/locales/ko/translations.json +++ b/packages/i18n/src/locales/ko/translations.json @@ -2439,5 +2439,39 @@ "last_edited_by": "마지막 편집자", "previously_edited_by": "이전 편집자", "edited_by": "편집자" + }, + + "notification_settings": { + "page_label": "{workspace} - 받은 편지함 설정", + "inbox_settings": "받은 편지함 설정", + "inbox_settings_description": "작업 공간의 활동에 대한 알림을 받는 방법을 사용자 지정합니다. 변경 사항은 자동으로 저장됩니다.", + "advanced_settings": "고급 설정", + "in_plane": "Plane 내", + "email": "이메일", + "slack": "Slack", + "task_updates": "작업 항목 업데이트", + "task_updates_subtitle": "작업 공간의 작업 항목이 업데이트될 때 알림을 받습니다.", + "comments": "댓글", + "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/pl/translations.json b/packages/i18n/src/locales/pl/translations.json index 048df2a2ffd..bba7339eb4e 100644 --- a/packages/i18n/src/locales/pl/translations.json +++ b/packages/i18n/src/locales/pl/translations.json @@ -2396,5 +2396,39 @@ "last_edited_by": "Ostatnio edytowane przez", "previously_edited_by": "Wcześniej edytowane przez", "edited_by": "Edytowane przez" + }, + + "notification_settings": { + "page_label": "{workspace} - Ustawienia skrzynki odbiorczej", + "inbox_settings": "Ustawienia skrzynki odbiorczej", + "inbox_settings_description": "Dostosuj sposób otrzymywania powiadomień o aktywnościach w swoim obszarze roboczym. Twoje zmiany są zapisywane automatycznie.", + "advanced_settings": "Zaawansowane ustawienia", + "in_plane": "W Plane", + "email": "Email", + "slack": "Slack", + "task_updates": "Aktualizacje elementów pracy", + "task_updates_subtitle": "Otrzymuj powiadomienia, gdy elementy pracy w twoim obszarze roboczym są aktualizowane.", + "comments": "Komentarze", + "comments_subtitle": "Bądź na bieżąco z dyskusjami w swoim obszarze roboczym.", + "work_item_property_title": "Aktualizacja dowolnej właściwości elementu pracy", + "work_item_property_subtitle": "Otrzymuj powiadomienia, gdy elementy pracy w twoim obszarze roboczym są aktualizowane.", + "status_title": "Zmiana stanu", + "status_subtitle": "Gdy stan elementu pracy jest aktualizowany.", + "priority_title": "Zmiana priorytetu", + "priority_subtitle": "Gdy poziom priorytetu elementu pracy jest dostosowywany.", + "assignee_title": "Zmiana przypisanego", + "assignee_subtitle": "Gdy element pracy jest przypisywany lub przypisywany ponownie komuś.", + "due_date_title": "Zmiana daty", + "due_date_subtitle": "Gdy data rozpoczęcia lub termin elementu pracy jest aktualizowany.", + "module_title": "Aktualizacja modułu", + "cycle_title": "Aktualizacja cyklu", + "mentioned_comments_title": "Wzmianki", + "mentioned_comments_subtitle": "Gdy ktoś wspomina cię w komentarzu.", + "new_comments_title": "Nowe komentarze", + "new_comments_subtitle": "Gdy nowy komentarz zostanie dodany do zadania, które śledzisz.", + "reaction_comments_title": "Reakcje", + "reaction_comments_subtitle": "Otrzymuj powiadomienia, gdy ktoś reaguje na twoje komentarze lub zadania za pomocą emotikony.", + "setting_updated_successfully": "Ustawienie zaktualizowane pomyślnie", + "failed_to_update_setting": "Nie udało się zaktualizować ustawienia" } } diff --git a/packages/i18n/src/locales/pt-BR/translations.json b/packages/i18n/src/locales/pt-BR/translations.json index 97b08022129..4ca813f4ed7 100644 --- a/packages/i18n/src/locales/pt-BR/translations.json +++ b/packages/i18n/src/locales/pt-BR/translations.json @@ -2432,5 +2432,39 @@ "last_edited_by": "Última edição por", "previously_edited_by": "Anteriormente editado por", "edited_by": "Editado por" + }, + + "notification_settings": { + "page_label": "{workspace} - Configurações da caixa de entrada", + "inbox_settings": "Configurações da caixa de entrada", + "inbox_settings_description": "Personalize como você recebe notificações para atividades no seu espaço de trabalho. Suas alterações são salvas automaticamente.", + "advanced_settings": "Configurações avançadas", + "in_plane": "No Plane", + "email": "Email", + "slack": "Slack", + "task_updates": "Atualizações de itens de trabalho", + "task_updates_subtitle": "Receba notificações quando os itens de trabalho no seu espaço de trabalho forem atualizados.", + "comments": "Comentários", + "comments_subtitle": "Fique atualizado sobre as discussões no seu espaço de trabalho.", + "work_item_property_title": "Atualização de qualquer propriedade do item de trabalho", + "work_item_property_subtitle": "Receba notificações quando os itens de trabalho no seu espaço de trabalho forem atualizados.", + "status_title": "Mudança de estado", + "status_subtitle": "Quando o estado de um item de trabalho é atualizado.", + "priority_title": "Mudança de prioridade", + "priority_subtitle": "Quando o nível de prioridade de um item de trabalho é ajustado.", + "assignee_title": "Mudança de responsável", + "assignee_subtitle": "Quando um item de trabalho é atribuído ou reatribuído a alguém.", + "due_date_title": "Mudança de data", + "due_date_subtitle": "Quando a data de início ou de vencimento de um item de trabalho é atualizada.", + "module_title": "Atualização de módulo", + "cycle_title": "Atualização de ciclo", + "mentioned_comments_title": "Menções", + "mentioned_comments_subtitle": "Quando alguém menciona você em um comentário.", + "new_comments_title": "Novos comentários", + "new_comments_subtitle": "Quando um novo comentário é adicionado a uma tarefa que você está seguindo.", + "reaction_comments_title": "Reações", + "reaction_comments_subtitle": "Receba notificações quando alguém reagir aos seus comentários ou tarefas com um emoji.", + "setting_updated_successfully": "Configuração atualizada com sucesso", + "failed_to_update_setting": "Falha ao atualizar a configuração" } } diff --git a/packages/i18n/src/locales/ro/translations.json b/packages/i18n/src/locales/ro/translations.json index 71d5183e7b2..7f81d5078da 100644 --- a/packages/i18n/src/locales/ro/translations.json +++ b/packages/i18n/src/locales/ro/translations.json @@ -2431,5 +2431,39 @@ "last_edited_by": "Ultima editare de către", "previously_edited_by": "Editat anterior de către", "edited_by": "Editat de" + }, + + "notification_settings": { + "page_label": "{workspace} - Setări inbox", + "inbox_settings": "Setări inbox", + "inbox_settings_description": "Personalizează modul în care primești notificări pentru activitățile din spațiul tău de lucru. Modificările tale sunt salvate automat.", + "advanced_settings": "Setări avansate", + "in_plane": "În Plane", + "email": "Email", + "slack": "Slack", + "task_updates": "Actualizări ale elementelor de lucru", + "task_updates_subtitle": "Primește notificări când elementele de lucru din spațiul tău de lucru sunt actualizate.", + "comments": "Comentarii", + "comments_subtitle": "Rămâi la curent cu discuțiile din spațiul tău de lucru.", + "work_item_property_title": "Actualizare a oricărei proprietăți a elementului de lucru", + "work_item_property_subtitle": "Primește notificări când elementele de lucru din spațiul tău de lucru sunt actualizate.", + "status_title": "Schimbare de stare", + "status_subtitle": "Când starea unui element de lucru este actualizată.", + "priority_title": "Schimbare de prioritate", + "priority_subtitle": "Când nivelul de prioritate al unui element de lucru este ajustat.", + "assignee_title": "Schimbare de responsabil", + "assignee_subtitle": "Când un element de lucru este atribuit sau reatribuit cuiva.", + "due_date_title": "Schimbare de dată", + "due_date_subtitle": "Când data de început sau de finalizare a unui element de lucru este actualizată.", + "module_title": "Actualizare modul", + "cycle_title": "Actualizare ciclu", + "mentioned_comments_title": "Mențiuni", + "mentioned_comments_subtitle": "Când cineva te menționează într-un comentariu.", + "new_comments_title": "Comentarii noi", + "new_comments_subtitle": "Când un comentariu nou este adăugat la o sarcină pe care o urmărești.", + "reaction_comments_title": "Reacții", + "reaction_comments_subtitle": "Primește notificări când cineva reacționează la comentariile sau sarcinile tale cu un emoji.", + "setting_updated_successfully": "Setarea a fost actualizată cu succes", + "failed_to_update_setting": "Actualizarea setării a eșuat" } } diff --git a/packages/i18n/src/locales/ru/translations.json b/packages/i18n/src/locales/ru/translations.json index 0b43356f998..534dc3f425a 100644 --- a/packages/i18n/src/locales/ru/translations.json +++ b/packages/i18n/src/locales/ru/translations.json @@ -2437,5 +2437,39 @@ "last_edited_by": "Последнее редактирование", "previously_edited_by": "Ранее отредактировано", "edited_by": "Отредактировано" + }, + + "notification_settings": { + "page_label": "{workspace} - Настройки входящих", + "inbox_settings": "Настройки входящих", + "inbox_settings_description": "Настройте, как вы получаете уведомления о действиях в вашем рабочем пространстве. Ваши изменения сохраняются автоматически.", + "advanced_settings": "Расширенные настройки", + "in_plane": "В Plane", + "email": "Электронная почта", + "slack": "Slack", + "task_updates": "Обновления рабочих элементов", + "task_updates_subtitle": "Получайте уведомления, когда рабочие элементы в вашем рабочем пространстве обновляются.", + "comments": "Комментарии", + "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/sk/translations.json b/packages/i18n/src/locales/sk/translations.json index 63acc072358..d4523442c94 100644 --- a/packages/i18n/src/locales/sk/translations.json +++ b/packages/i18n/src/locales/sk/translations.json @@ -2436,5 +2436,39 @@ "last_edited_by": "Naposledy upravené používateľom", "previously_edited_by": "Predtým upravené používateľom", "edited_by": "Upravené používateľom" + }, + + "notification_settings": { + "page_label": "{workspace} - Nastavenia doručenej pošty", + "inbox_settings": "Nastavenia doručenej pošty", + "inbox_settings_description": "Prispôsobte si, ako dostávate upozornenia na aktivity vo vašom pracovnom priestore. Vaše zmeny sa ukladajú automaticky.", + "advanced_settings": "Pokročilé nastavenia", + "in_plane": "V Plane", + "email": "Email", + "slack": "Slack", + "task_updates": "Aktualizácie pracovných položiek", + "task_updates_subtitle": "Dostávajte upozornenia, keď sa aktualizujú pracovné položky vo vašom pracovnom priestore.", + "comments": "Komentáre", + "comments_subtitle": "Buďte informovaní o diskusiách vo vašom pracovnom priestore.", + "work_item_property_title": "Aktualizácia akejkoľvek vlastnosti pracovnej položky", + "work_item_property_subtitle": "Dostávajte upozornenia, keď sa aktualizujú pracovné položky vo vašom pracovnom priestore.", + "status_title": "Zmena stavu", + "status_subtitle": "Keď sa aktualizuje stav pracovnej položky.", + "priority_title": "Zmena priority", + "priority_subtitle": "Keď sa upraví úroveň priority pracovnej položky.", + "assignee_title": "Zmena priradeného", + "assignee_subtitle": "Keď sa pracovná položka priradí alebo priradí niekomu inému.", + "due_date_title": "Zmena dátumu", + "due_date_subtitle": "Keď sa aktualizuje dátum začiatku alebo termín pracovnej položky.", + "module_title": "Aktualizácia modulu", + "cycle_title": "Aktualizácia cyklu", + "mentioned_comments_title": "Zmienenia", + "mentioned_comments_subtitle": "Keď vás niekto zmieni v komentári.", + "new_comments_title": "Nové komentáre", + "new_comments_subtitle": "Keď sa pridá nový komentár k úlohe, ktorú sledujete.", + "reaction_comments_title": "Reakcie", + "reaction_comments_subtitle": "Dostávajte upozornenia, keď niekto reaguje na vaše komentáre alebo úlohy pomocou emotikonu.", + "setting_updated_successfully": "Nastavenie bolo úspešne aktualizované", + "failed_to_update_setting": "Nepodarilo sa aktualizovať nastavenie" } } diff --git a/packages/i18n/src/locales/ua/translations.json b/packages/i18n/src/locales/ua/translations.json index 69236d9f1df..82041d46b5e 100644 --- a/packages/i18n/src/locales/ua/translations.json +++ b/packages/i18n/src/locales/ua/translations.json @@ -2395,5 +2395,39 @@ "last_edited_by": "Останнє редагування", "previously_edited_by": "Раніше відредаговано", "edited_by": "Відредаговано" + }, + + "notification_settings": { + "page_label": "{workspace} - Налаштування вхідних", + "inbox_settings": "Налаштування вхідних", + "inbox_settings_description": "Налаштуйте, як ви отримуєте сповіщення про активності у вашому робочому просторі. Ваші зміни зберігаються автоматично.", + "advanced_settings": "Розширені налаштування", + "in_plane": "У Plane", + "email": "Електронна пошта", + "slack": "Slack", + "task_updates": "Оновлення робочих елементів", + "task_updates_subtitle": "Отримуйте сповіщення, коли робочі елементи у вашому робочому просторі оновлюються.", + "comments": "Коментарі", + "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/vi-VN/translations.json b/packages/i18n/src/locales/vi-VN/translations.json index 7c2c0a40b40..a5291e5caaf 100644 --- a/packages/i18n/src/locales/vi-VN/translations.json +++ b/packages/i18n/src/locales/vi-VN/translations.json @@ -2393,5 +2393,39 @@ "last_edited_by": "Chỉnh sửa lần cuối bởi", "previously_edited_by": "Trước đây được chỉnh sửa bởi", "edited_by": "Được chỉnh sửa bởi" + }, + + "notification_settings": { + "page_label": "{workspace} - Cài đặt hộp thư đến", + "inbox_settings": "Cài đặt hộp thư đến", + "inbox_settings_description": "Tùy chỉnh cách bạn nhận thông báo cho các hoạt động trong không gian làm việc của bạn. Các thay đổi của bạn được lưu tự động.", + "advanced_settings": "Cài đặt nâng cao", + "in_plane": "Trong Plane", + "email": "Email", + "slack": "Slack", + "task_updates": "Cập nhật mục công việc", + "task_updates_subtitle": "Nhận thông báo khi các mục công việc trong không gian làm việc của bạn được cập nhật.", + "comments": "Bình luận", + "comments_subtitle": "Cập nhật thông tin về các cuộc thảo luận trong không gian làm việc của bạn.", + "work_item_property_title": "Cập nhật bất kỳ thuộc tính nào của mục công việc", + "work_item_property_subtitle": "Nhận thông báo khi các mục công việc trong không gian làm việc của bạn được cập nhật.", + "status_title": "Thay đổi trạng thái", + "status_subtitle": "Khi trạng thái của một mục công việc được cập nhật.", + "priority_title": "Thay đổi ưu tiên", + "priority_subtitle": "Khi mức độ ưu tiên của một mục công việc được điều chỉnh.", + "assignee_title": "Thay đổi người được giao", + "assignee_subtitle": "Khi một mục công việc được giao hoặc giao lại cho ai đó.", + "due_date_title": "Thay đổi ngày", + "due_date_subtitle": "Khi ngày bắt đầu hoặc ngày đến hạn của một mục công việc được cập nhật.", + "module_title": "Cập nhật mô-đun", + "cycle_title": "Cập nhật chu kỳ", + "mentioned_comments_title": "Đề cập", + "mentioned_comments_subtitle": "Khi ai đó đề cập đến bạn trong một bình luận.", + "new_comments_title": "Bình luận mới", + "new_comments_subtitle": "Khi một bình luận mới được thêm vào một nhiệm vụ bạn đang theo dõi.", + "reaction_comments_title": "Phản ứng", + "reaction_comments_subtitle": "Nhận thông báo khi ai đó phản ứng với bình luận hoặc nhiệm vụ của bạn bằng một biểu tượng cảm xúc.", + "setting_updated_successfully": "Cài đặt đã được cập nhật thành công", + "failed_to_update_setting": "Không thể cập nhật cài đặt" } } diff --git a/packages/i18n/src/locales/zh-CN/translations.json b/packages/i18n/src/locales/zh-CN/translations.json index a382c8c2e20..e431c7146c4 100644 --- a/packages/i18n/src/locales/zh-CN/translations.json +++ b/packages/i18n/src/locales/zh-CN/translations.json @@ -2427,5 +2427,39 @@ "last_edited_by": "最后编辑者", "previously_edited_by": "之前编辑者", "edited_by": "编辑者" + }, + + "notification_settings": { + "page_label": "{workspace} - 收件箱设置", + "inbox_settings": "收件箱设置", + "inbox_settings_description": "自定义您如何接收工作区活动的通知。您的更改会自动保存。", + "advanced_settings": "高级设置", + "in_plane": "在 Plane", + "email": "电子邮件", + "slack": "Slack", + "task_updates": "任务更新", + "task_updates_subtitle": "当您工作区中的任务更新时收到通知。", + "comments": "评论", + "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-TW/translations.json b/packages/i18n/src/locales/zh-TW/translations.json index e86642f3e00..e2b8f2cb16c 100644 --- a/packages/i18n/src/locales/zh-TW/translations.json +++ b/packages/i18n/src/locales/zh-TW/translations.json @@ -2439,5 +2439,39 @@ "last_edited_by": "最後編輯者", "previously_edited_by": "先前編輯者", "edited_by": "編輯者" + }, + + "notification_settings": { + "page_label": "{workspace} - 收件匣設定", + "inbox_settings": "收件匣設定", + "inbox_settings_description": "自訂您如何接收工作區活動的通知。您的更改會自動保存。", + "advanced_settings": "進階設定", + "in_plane": "在 Plane", + "email": "電子郵件", + "slack": "Slack", + "task_updates": "工作項更新", + "task_updates_subtitle": "當工作區中的工作項更新時收到通知。", + "comments": "評論", + "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/types/src/index.d.ts b/packages/types/src/index.d.ts index cb916a2f230..446d578fb40 100644 --- a/packages/types/src/index.d.ts +++ b/packages/types/src/index.d.ts @@ -41,4 +41,5 @@ export * from "./epics"; export * from "./charts"; export * from "./home"; export * from "./stickies"; +export * from "./notification"; export * from "./utils"; diff --git a/packages/types/src/notification.d.ts b/packages/types/src/notification.d.ts new file mode 100644 index 00000000000..6e6d9177b7d --- /dev/null +++ b/packages/types/src/notification.d.ts @@ -0,0 +1,21 @@ +import { ENotificationSettingsKey, EWorkspaceNotificationTransport } from "@plane/constants"; + +export type TNotificationSettings = { + i18n_title: string, + i18n_subtitle?: string, + key: ENotificationSettingsKey +} + +export type TWorkspaceUserNotification = { + workspace: string, + user: string, + transport: EWorkspaceNotificationTransport, + 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/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..681f46a8ba4 --- /dev/null +++ b/web/app/[workspaceSlug]/(projects)/notifications/settings/page.tsx @@ -0,0 +1,45 @@ +"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, InboxSettingContentWrapper } from "@/components/inbox/settings"; +import { EmailSettingsLoader } from "@/components/ui"; +import { NOTIFICATION_SETTINGS } from "@/constants/fetch-keys"; +import { useWorkspaceNotificationSettings } from "@/hooks/store"; +import { InboxSettingsRoot } from "@/plane-web/components/inbox/settings/root"; + +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/ce/components/inbox/index.ts b/web/ce/components/inbox/index.ts new file mode 100644 index 00000000000..78c01319f3a --- /dev/null +++ b/web/ce/components/inbox/index.ts @@ -0,0 +1 @@ +export * from "./settings" \ No newline at end of file diff --git a/web/ce/components/inbox/settings/index.ts b/web/ce/components/inbox/settings/index.ts new file mode 100644 index 00000000000..657c6b1838b --- /dev/null +++ b/web/ce/components/inbox/settings/index.ts @@ -0,0 +1,2 @@ +export * from "./root"; +export * from "./update-setting-row"; \ No newline at end of file diff --git a/web/ce/components/inbox/settings/root.tsx b/web/ce/components/inbox/settings/root.tsx new file mode 100644 index 00000000000..569bf0164cd --- /dev/null +++ b/web/ce/components/inbox/settings/root.tsx @@ -0,0 +1,56 @@ +"use client"; + +import { FC } from "react"; +import { COMMENT_NOTIFICATION_SETTINGS, TASK_UPDATES_NOTIFICATION_SETTINGS } from "@plane/constants"; +import { useTranslation } from "@plane/i18n"; +import { InboxSettingUpdateRow } from "./update-setting-row"; + + +export const InboxSettingsRoot: FC = () => { + const { t } = useTranslation(); + + return ( + <> +
+
+
+ {t("notification_settings.task_updates")} +
+
+ {t("notification_settings.task_updates_subtitle")} +
+
+
+
{t("notification_settings.advanced_settings")}
+
{t("notification_settings.in_plane")}
+
{t("notification_settings.email")}
+
+ { + TASK_UPDATES_NOTIFICATION_SETTINGS?.map((item) => ( + + )) + } +
+
+
+
+ {t("notification_settings.comments")} +
+
+ {t("notification_settings.comments_subtitle")} +
+
+
+
{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/ce/components/inbox/settings/update-setting-row.tsx b/web/ce/components/inbox/settings/update-setting-row.tsx new file mode 100644 index 00000000000..ca78aeaeb14 --- /dev/null +++ b/web/ce/components/inbox/settings/update-setting-row.tsx @@ -0,0 +1,40 @@ +"use client" + +import { FC } from "react"; +import { observer } from "mobx-react"; +import { ENotificationSettingsKey, EWorkspaceNotificationTransport } from "@plane/constants"; +import { useTranslation } from "@plane/i18n"; +import { InboxSettingUpdate } from "@/components/inbox"; + +type InboxSettingUpdateRowProps = { + settings_key: ENotificationSettingsKey; + title: string; + subtitle?: string +} + +export const InboxSettingUpdateRow: FC = observer((props: InboxSettingUpdateRowProps) => { + const { title, subtitle, settings_key } = props; + + const { t } = useTranslation() + + return ( +
+
+
+ {t(title)} +
+ { + subtitle &&
+ {t(subtitle)} +
+ } +
+
+ +
+
+ +
+
+ ); +}) \ No newline at end of file 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/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..c529327220b --- /dev/null +++ b/web/core/components/inbox/settings/index.ts @@ -0,0 +1,3 @@ +export * from "./content-header" +export * from "./content-wrapper" +export * from "./update-setting" 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..83ce277cb3d --- /dev/null +++ b/web/core/components/inbox/settings/update-setting.tsx @@ -0,0 +1,54 @@ +"use client" + +import { FC } from "react"; +import { observer } from "mobx-react"; +import { ENotificationSettingsKey, EWorkspaceNotificationTransport } from "@plane/constants"; +import { useTranslation } from "@plane/i18n"; +import { setToast, TOAST_TYPE, ToggleSwitch } from "@plane/ui"; +import { useWorkspaceNotificationSettings } from "@/hooks/store"; + +type InboxSettingUpdateProps = { + settings_key: ENotificationSettingsKey; + transport: EWorkspaceNotificationTransport; +} + +export const InboxSettingUpdate: FC = observer((props: InboxSettingUpdateProps) => { + const { transport, settings_key } = props; + const { t } = useTranslation() + + + 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"), + }) + } + } + + + return ( + { + handleChange(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..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()}`; @@ -273,3 +275,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..9ce037415c3 --- /dev/null +++ b/web/core/services/workspace-notification-settings.service.ts @@ -0,0 +1,46 @@ +/* eslint-disable no-useless-catch */ + +import { EWorkspaceNotificationTransport } from "@plane/constants"; +import type { + TWorkspaceUserNotification, +} 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: EWorkspaceNotificationTransport, + 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..7580a3f5e87 --- /dev/null +++ b/web/core/store/notifications/workspace-notification-settings.store.ts @@ -0,0 +1,164 @@ +import set from "lodash/set"; +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"; +// 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: EWorkspaceNotificationTransport + ) => TWorkspaceUserNotification | undefined; + // helper actions + fetchWorkspaceUserNotificationSettings: () => Promise; + updateWorkspaceUserNotificationSettings: ( + transport: EWorkspaceNotificationTransport, + 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, + // 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 } + */ + notificationSettingsForWorkspace = computedFn(() => { + 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 { EWorkspaceNotificationTransport } transport + * @returns { TWorkspaceUserNotification } + */ + + getNotificationSettingsForTransport = computedFn((transport: EWorkspaceNotificationTransport) => { + const workspaceSlug = this.store.workspaceRoot?.currentWorkspace?.slug; + if (!workspaceSlug || !transport) { + return; + } + const notificationSettingsForTransport = this.settings[workspaceSlug][transport] || undefined; + return notificationSettingsForTransport; + }); + + // helper actions + /** + * @description handle states + * @returns { TWorkspaceUserNotification[] | undefined } + */ + fetchWorkspaceUserNotificationSettings = async (): Promise => { + + 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 { EWorkspaceNotificationTransport } + */ + updateWorkspaceUserNotificationSettings = async ( + transport: EWorkspaceNotificationTransport, + 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 d06ed2418d0..ca3526c83dc 100644 --- a/web/core/store/root.store.ts +++ b/web/core/store/root.store.ts @@ -22,6 +22,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"; @@ -60,6 +61,7 @@ export class CoreRootStore { projectEstimate: IProjectEstimateStore; multipleSelect: IMultipleSelectStore; workspaceNotification: IWorkspaceNotificationStore; + workspaceNotificationSettings: IWorkspaceNotificationSettingsStore; favorite: IFavoriteStore; transient: ITransientStore; stickyStore: IStickyStore; @@ -89,6 +91,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(); @@ -122,6 +125,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();