Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions apps/docs/content/docs/inbox.ja.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -62,11 +62,12 @@ import { Callout } from "fumadocs-ui/components/callout";

- 担当の割り当て
- ステータス変更
- コメントとメンション
- コメント
- メンション
- 優先度と期限
- エージェントのアクティビティ

グループをオフにしても、関連する変更はイシューに記録され続けます。インボックス通知が作られなくなるだけです。リアクション、エージェント経由の作成結果、オートパイロットの一時停止など一部のタイプはこの 5 グループの対象外で、常に届きます。システム通知とブラウザ通知はデスクトップのバナーを制御するもので、インボックス自体は変わりません。
グループをオフにしても、関連する変更はイシューに記録され続けます。インボックス通知が作られなくなるだけです。リアクション、エージェント経由の作成結果、オートパイロットの一時停止など一部のタイプはこの 6 グループの対象外で、常に届きます。システム通知とブラウザ通知はデスクトップのバナーを制御するもので、インボックス自体は変わりません。

## 次のステップ

Expand Down
5 changes: 3 additions & 2 deletions apps/docs/content/docs/inbox.ko.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -62,11 +62,12 @@ import { Callout } from "fumadocs-ui/components/callout";

- 할당
- 상태 변경
- 댓글과 멘션
- 댓글
- 멘션
- 우선순위와 날짜
- 에이전트 활동

특정 유형을 꺼도 관련 변경 사항은 이슈에 계속 저장되며 인박스 알림만 생성되지 않습니다. 이모지 반응, 빠른 생성 결과, 자동화 일시 중지 같은 일부 유형은 이 다섯 그룹의 설정과 관계없이 항상 전달됩니다. 시스템 알림과 브라우저 알림 설정은 데스크톱 배너를 제어할 뿐 인박스 자체를 바꾸지 않습니다.
특정 유형을 꺼도 관련 변경 사항은 이슈에 계속 저장되며 인박스 알림만 생성되지 않습니다. 이모지 반응, 빠른 생성 결과, 자동화 일시 중지 같은 일부 유형은 이 여섯 그룹의 설정과 관계없이 항상 전달됩니다. 시스템 알림과 브라우저 알림 설정은 데스크톱 배너를 제어할 뿐 인박스 자체를 바꾸지 않습니다.

## 다음 단계

Expand Down
5 changes: 3 additions & 2 deletions apps/docs/content/docs/inbox.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -62,11 +62,12 @@ Open **Settings → Notifications** to toggle each group separately:

- assignments;
- status changes;
- comments and mentions;
- comments;
- mentions;
- priority and dates;
- agent activity.

Turning a group off doesn't stop the changes from being recorded on the issue — it only stops the inbox notifications. A few types, such as reactions, quick-create results, and autopilot pauses, sit outside these five groups and are always delivered. System and browser notifications control desktop banners and don't change the inbox itself.
Turning a group off doesn't stop the changes from being recorded on the issue — it only stops the inbox notifications. A few types, such as reactions, quick-create results, and autopilot pauses, sit outside these six groups and are always delivered. System and browser notifications control desktop banners and don't change the inbox itself.

## Next steps

Expand Down
5 changes: 3 additions & 2 deletions apps/docs/content/docs/inbox.zh.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -62,11 +62,12 @@ import { Callout } from "fumadocs-ui/components/callout";

- 分配;
- 状态变更;
- 评论与提及;
- 评论;
- 提及;
- 优先级与日期;
- 智能体活动。

关闭某一类后,相关变化仍然保存在 issue 中,只是不再创建收件箱通知。表情回应、快速创建结果和自动化暂停等少数类型不受这五组控制,始终投递。系统通知和浏览器通知控制的是桌面横幅,不会改变收件箱本身。
关闭某一类后,相关变化仍然保存在 issue 中,只是不再创建收件箱通知。表情回应、快速创建结果和自动化暂停等少数类型不受这六组控制,始终投递。系统通知和浏览器通知控制的是桌面横幅,不会改变收件箱本身。

## 接下来

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* Notification preferences subscreen. 5 inbox groups + system_notifications
* Notification preferences subscreen. 6 inbox groups + system_notifications
* toggle, each backed by an optimistic PATCH /api/notification-preferences.
*
* Copy mirrors packages/views/settings/components/notifications-tab.tsx but
Expand All @@ -20,7 +20,7 @@
import { notificationPreferenceOptions } from "@/data/queries/notification-preferences";
import { useUpdateNotificationPreferences } from "@/data/mutations/notification-preferences";

const INBOX_GROUPS: Array<{

Check warning on line 23 in apps/mobile/app/(app)/[workspace]/more/settings/notifications.tsx

View workflow job for this annotation

GitHub Actions / mobile

Array type using 'Array<T>' is forbidden. Use 'T[]' instead
key: Exclude<NotificationGroupKey, "system_notifications">;
label: string;
description: string;
Expand All @@ -40,6 +40,11 @@
label: "Comments",
description: "New comments on issues you're subscribed to.",
},
{
key: "mentions",
label: "Mentions",
description: "When someone @mentions you, including @all and @squad.",
},
{
key: "updates",
label: "Issue updates",
Expand Down
23 changes: 23 additions & 0 deletions packages/core/notification-preferences/patch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,29 @@ describe("notification preference patches", () => {
).toEqual({ status_changes: "muted", comments: "muted" });
});

it("carries the mentions group independently of comments", () => {
// The point of the split (#6468): muting comment volume must not travel
// to mentions. A missing key in NOTIFICATION_GROUP_KEYS would make the
// toggle silently un-sendable, so derive and apply are both covered.
expect(
deriveNotificationPreferencePatch(
{ comments: "muted" },
{ comments: "muted", mentions: "muted" },
),
).toEqual({ mentions: "muted" });

expect(
deriveNotificationPreferencePatch({ comments: "muted" }, {}),
).toEqual({ comments: "all" });

expect(
applyNotificationPreferencePatch(
{ comments: "muted" },
{ mentions: "muted" },
),
).toEqual({ comments: "muted", mentions: "muted" });
});

it("does not roll back a key that a later mutation already changed", () => {
expect(
rollbackNotificationPreferencePatch(
Expand Down
1 change: 1 addition & 0 deletions packages/core/notification-preferences/patch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const NOTIFICATION_GROUP_KEYS: readonly NotificationGroupKey[] = [
"assignments",
"status_changes",
"comments",
"mentions",
"updates",
"agent_activity",
"system_notifications",
Expand Down
1 change: 1 addition & 0 deletions packages/core/types/notification-preference.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export type NotificationGroupKey =
| "assignments"
| "status_changes"
| "comments"
| "mentions"
| "updates"
| "agent_activity"
| "system_notifications";
Expand Down
8 changes: 6 additions & 2 deletions packages/views/locales/en/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -329,8 +329,12 @@
"description": "When an issue you follow changes status (e.g. todo, in progress, done)"
},
"comments": {
"label": "Comments & Mentions",
"description": "New comments on issues you follow, or when someone @mentions you"
"label": "Comments",
"description": "New comments on issues you follow"
},
"mentions": {
"label": "Mentions",
"description": "When someone @mentions you, including @all and @squad"
},
"updates": {
"label": "Priority & Due date",
Expand Down
8 changes: 6 additions & 2 deletions packages/views/locales/ja/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -328,8 +328,12 @@
"description": "フォロー中のタスクのステータスが変わったとき(例: todo、in progress、done)"
},
"comments": {
"label": "コメントとメンション",
"description": "フォロー中のタスクに新しいコメントが付いたとき、または誰かがあなたを @メンションしたとき"
"label": "コメント",
"description": "フォロー中のタスクに新しいコメントが付いたとき"
},
"mentions": {
"label": "メンション",
"description": "誰かがあなたを @メンションしたとき(@all と @squad を含む)"
},
"updates": {
"label": "優先度と期限",
Expand Down
8 changes: 6 additions & 2 deletions packages/views/locales/ko/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -328,8 +328,12 @@
"description": "팔로우 중인 태스크의 상태가 바뀔 때(예: 할 일, 진행 중, 완료)"
},
"comments": {
"label": "댓글 및 멘션",
"description": "팔로우 중인 태스크에 새 댓글이 달리거나 누군가 나를 @멘션할 때"
"label": "댓글",
"description": "팔로우 중인 태스크에 새 댓글이 달릴 때"
},
"mentions": {
"label": "멘션",
"description": "누군가 나를 @멘션할 때(@all, @squad 포함)"
},
"updates": {
"label": "우선순위 및 마감일",
Expand Down
8 changes: 6 additions & 2 deletions packages/views/locales/zh-Hans/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -328,8 +328,12 @@
"description": "你订阅的任务状态变化时(例如 todo、in progress、done)"
},
"comments": {
"label": "评论与提及",
"description": "你订阅的任务有新评论,或有人 @ 你时"
"label": "评论",
"description": "你订阅的任务有新评论时"
},
"mentions": {
"label": "提及",
"description": "有人 @ 你时,包括 @all 和 @squad"
},
"updates": {
"label": "优先级与截止日期",
Expand Down
1 change: 1 addition & 0 deletions packages/views/settings/components/notifications-tab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ const INBOX_GROUP_KEYS = [
"assignments",
"status_changes",
"comments",
"mentions",
"updates",
"agent_activity",
] as const;
Expand Down
6 changes: 4 additions & 2 deletions server/cmd/server/notification_listeners.go
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ var notifTypeToGroup = map[string]string{
"assignee_changed": "assignments",
"status_changed": "status_changes",
"new_comment": "comments",
"mentioned": "comments",
"mentioned": "mentions",
"priority_changed": "updates",
"start_date_changed": "updates",
"due_date_changed": "updates",
Expand Down Expand Up @@ -578,7 +578,9 @@ func notifyMentionedMembers(
if id == e.ActorID || skip[id] {
continue
}
// Skip if mentions/comments are muted by this user
// Skip if mentions are muted by this user. This is deliberately a
// different group from `comments`: muting comment volume must not
// silence someone asking for you by name.
if p, ok := mentionPrefs[id]; ok && isNotifMuted(p, "mentioned") {
continue
}
Expand Down
59 changes: 59 additions & 0 deletions server/cmd/server/notification_mute_group_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package main

import "testing"

// TestIsNotifMutedGroupSplit pins the contract the `mentions` group exists for:
// muting comment VOLUME must never silence someone asking for you BY NAME.
// `new_comment` and `mentioned` shared the `comments` group until #6468, so a
// user who muted comment churn also lost every mention — and a muted item is
// never created, so it was unrecoverable rather than merely delayed.
func TestIsNotifMutedGroupSplit(t *testing.T) {
cases := []struct {
name string
prefs map[string]string
notifType string
want bool
}{
// The regression this split fixes.
{"muted comments still delivers mentions", map[string]string{"comments": "muted"}, "mentioned", false},
{"muted comments drops new_comment", map[string]string{"comments": "muted"}, "new_comment", true},
{"muted mentions drops mentioned", map[string]string{"mentions": "muted"}, "mentioned", true},
{"muted mentions still delivers new_comment", map[string]string{"mentions": "muted"}, "new_comment", false},
{"both muted drops both", map[string]string{"comments": "muted", "mentions": "muted"}, "mentioned", true},

// Absent key means the default "all" — preferences are stored sparse,
// which is why existing `{"comments":"muted"}` rows need no migration.
{"empty prefs delivers mentions", map[string]string{}, "mentioned", false},
{"empty prefs delivers comments", map[string]string{}, "new_comment", false},
{"explicit all delivers", map[string]string{"comments": "all"}, "new_comment", false},

// Other groups are unaffected by the split.
{"assignments group intact", map[string]string{"assignments": "muted"}, "issue_assigned", true},
{"status group intact", map[string]string{"status_changes": "muted"}, "status_changed", true},
{"muting comments leaves assignments alone", map[string]string{"comments": "muted"}, "issue_assigned", false},

// Types outside the map are always delivered.
{"unconfigurable type always delivered", map[string]string{"comments": "muted"}, "reaction_added", false},
}

for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
if got := isNotifMuted(tc.prefs, tc.notifType); got != tc.want {
t.Fatalf("isNotifMuted(%v, %q) = %v, want %v",
tc.prefs, tc.notifType, got, tc.want)
}
})
}
}

// TestNotifTypeToGroupMentionsIsOwnGroup guards the mapping itself: a future
// edit that folds `mentioned` back under `comments` should fail here rather
// than silently reintroduce the coupling.
func TestNotifTypeToGroupMentionsIsOwnGroup(t *testing.T) {
if got := notifTypeToGroup["mentioned"]; got != "mentions" {
t.Fatalf("notifTypeToGroup[mentioned] = %q, want %q", got, "mentions")
}
if got := notifTypeToGroup["new_comment"]; got != "comments" {
t.Fatalf("notifTypeToGroup[new_comment] = %q, want %q", got, "comments")
}
}
1 change: 1 addition & 0 deletions server/internal/handler/notification_preference.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ var validNotifGroups = map[string]bool{
"assignments": true,
"status_changes": true,
"comments": true,
"mentions": true,
"updates": true,
"agent_activity": true,
"system_notifications": true,
Expand Down
37 changes: 37 additions & 0 deletions server/internal/handler/notification_preference_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,43 @@ func TestPatchNotificationPreferencesMergesWithoutReplacing(t *testing.T) {
}
}

// TestPatchNotificationPreferencesAcceptsMentions covers the group added by
// #6468. The whitelist rejects unknown keys, so a client shipped ahead of the
// server would 400 on this key — the reason the server must deploy first.
func TestPatchNotificationPreferencesAcceptsMentions(t *testing.T) {
if testHandler == nil || testPool == nil {
t.Skip("database not available")
}

t.Cleanup(func() {
_, _ = testPool.Exec(context.Background(), `
DELETE FROM notification_preference
WHERE workspace_id = $1 AND user_id = $2
`, testWorkspaceID, testUserID)
})

recorder := httptest.NewRecorder()
testHandler.PatchNotificationPreferences(
recorder,
notificationPreferenceRequest(t, http.MethodPatch, map[string]string{
"mentions": "muted",
}),
)
if recorder.Code != http.StatusOK {
t.Fatalf("status=%d body=%s", recorder.Code, recorder.Body.String())
}

var response struct {
Preferences map[string]string `json:"preferences"`
}
if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil {
t.Fatalf("decode response: %v", err)
}
if response.Preferences["mentions"] != "muted" {
t.Fatalf("mentions not persisted: %#v", response.Preferences)
}
}

func TestPatchNotificationPreferencesRejectsUnknownGroups(t *testing.T) {
if testHandler == nil || testPool == nil {
t.Skip("database not available")
Expand Down
Loading