diff --git a/packages/api-client/src/posthog-client.ts b/packages/api-client/src/posthog-client.ts index a9c5e72318..e282dc915d 100644 --- a/packages/api-client/src/posthog-client.ts +++ b/packages/api-client/src/posthog-client.ts @@ -55,6 +55,7 @@ import type { ChannelFeedMessage, ChannelFeedMessageEvent, CodeReferenceArtefact, + CodeUserNotificationSettings, CommitArtefact, CommitDiffResponse, DismissalArtefact, @@ -4112,6 +4113,47 @@ export class PostHogAPIClient { return (await response.json()) as SignalUserAutonomyConfig; } + async getCodeUserNotificationSettings(): Promise { + const url = new URL(`${this.api.baseUrl}/api/code/user_settings/`); + const path = "/api/code/user_settings/"; + + const response = await this.api.fetcher.fetch({ + method: "get", + url, + path, + }); + + if (!response.ok) { + throw new Error( + `Failed to fetch Code notification settings: ${response.statusText}`, + ); + } + return (await response.json()) as CodeUserNotificationSettings; + } + + async updateCodeUserNotificationSettings(updates: { + slack_mention_notifications: boolean; + }): Promise { + const url = new URL(`${this.api.baseUrl}/api/code/user_settings/`); + const path = "/api/code/user_settings/"; + + const response = await this.api.fetcher.fetch({ + method: "post", + url, + path, + overrides: { + body: JSON.stringify(updates), + }, + }); + + if (!response.ok) { + throw new Error( + `Failed to update Code notification settings: ${response.statusText}`, + ); + } + return (await response.json()) as CodeUserNotificationSettings; + } + async getSlackChannelsForIntegration( integrationId: number, params?: SlackChannelsQueryParams, diff --git a/packages/shared/src/domain-types.ts b/packages/shared/src/domain-types.ts index 98f9d90ff9..77d69dc07a 100644 --- a/packages/shared/src/domain-types.ts +++ b/packages/shared/src/domain-types.ts @@ -796,6 +796,12 @@ export interface SignalUserAutonomyConfig { updated_at?: string; } +/** Per-user PostHog Code notification preferences, served by `/api/code/user_settings/`. */ +export interface CodeUserNotificationSettings { + /** Send a Slack DM when someone @-mentions the user in a channel thread. Opt-in. */ + slack_mention_notifications: boolean; +} + export interface SlackChannelOption { id: string; name: string; diff --git a/packages/ui/src/features/settings/sections/SlackMentionNotificationsSettings.tsx b/packages/ui/src/features/settings/sections/SlackMentionNotificationsSettings.tsx new file mode 100644 index 0000000000..280dfa293f --- /dev/null +++ b/packages/ui/src/features/settings/sections/SlackMentionNotificationsSettings.tsx @@ -0,0 +1,65 @@ +import { ANALYTICS_EVENTS, PROJECT_BLUEBIRD_FLAG } from "@posthog/shared"; +import { useFeatureFlag } from "@posthog/ui/features/feature-flags/useFeatureFlag"; +import { useIntegrationSelectors } from "@posthog/ui/features/integrations/store"; +import { + useCodeUserNotificationSettings, + useCodeUserNotificationSettingsMutations, +} from "@posthog/ui/features/settings/sections/useCodeUserNotificationSettings"; +import { track } from "@posthog/ui/shell/analytics"; +import { Flex, Switch, Text } from "@radix-ui/themes"; + +/** + * Opt-in Slack DMs for channel-thread @-mentions. The preference lives on the + * PostHog backend (per user, across projects); delivery uses the team's Slack + * integration, so the section only shows once a workspace is connected. + */ +export function SlackMentionNotificationsSettings() { + // Channel threads (and their @-mentions) only exist behind the bluebird flag. + const channelsEnabled = useFeatureFlag( + PROJECT_BLUEBIRD_FLAG, + import.meta.env.DEV, + ); + const { hasSlackIntegration } = useIntegrationSelectors(); + const { data: settings, isLoading } = useCodeUserNotificationSettings(); + const { handleUpdateSlackMentionNotifications } = + useCodeUserNotificationSettingsMutations(); + + // The connect-workspace prompt is the parent section's job. + if (!channelsEnabled || !hasSlackIntegration) return null; + + const enabled = settings?.slack_mention_notifications ?? false; + + const onCheckedChange = (checked: boolean) => { + track(ANALYTICS_EVENTS.SETTING_CHANGED, { + setting_name: "slack_mention_notifications", + new_value: checked, + old_value: enabled, + }); + void handleUpdateSlackMentionNotifications(checked); + }; + + return ( + + + + + Mention notifications + + + Get a Slack DM when someone @-mentions you in a channel thread. + + + + + + ); +} diff --git a/packages/ui/src/features/settings/sections/SlackSettings.tsx b/packages/ui/src/features/settings/sections/SlackSettings.tsx index f93e66845d..dc17c79255 100644 --- a/packages/ui/src/features/settings/sections/SlackSettings.tsx +++ b/packages/ui/src/features/settings/sections/SlackSettings.tsx @@ -5,6 +5,7 @@ import { openUrlInBrowser } from "@posthog/ui/utils/browser"; import { getPostHogUrl } from "@posthog/ui/utils/urls"; import { Button, Flex, Text, Tooltip } from "@radix-ui/themes"; import { SlackInboxNotificationsSettings } from "./SlackInboxNotificationsSettings"; +import { SlackMentionNotificationsSettings } from "./SlackMentionNotificationsSettings"; export function SlackSettings() { const projectId = useAuthStateValue((s) => s.currentProjectId); @@ -52,6 +53,8 @@ export function SlackSettings() { isLoading={isLoading} showHeader={false} /> + + ); } diff --git a/packages/ui/src/features/settings/sections/useCodeUserNotificationSettings.ts b/packages/ui/src/features/settings/sections/useCodeUserNotificationSettings.ts new file mode 100644 index 0000000000..a15fd9e0bc --- /dev/null +++ b/packages/ui/src/features/settings/sections/useCodeUserNotificationSettings.ts @@ -0,0 +1,60 @@ +import type { CodeUserNotificationSettings } from "@posthog/shared/domain-types"; +import { useOptionalAuthenticatedClient } from "@posthog/ui/features/auth/authClient"; +import { useAuthenticatedQuery } from "@posthog/ui/hooks/useAuthenticatedQuery"; +import { toast } from "@posthog/ui/primitives/toast"; +import { useQueryClient } from "@tanstack/react-query"; +import { useCallback } from "react"; + +const CODE_USER_NOTIFICATION_SETTINGS_QUERY_KEY = [ + "code-user-notification-settings", +] as const; + +/** The user's PostHog Code notification settings (all-defaults until first saved). */ +export function useCodeUserNotificationSettings() { + return useAuthenticatedQuery( + CODE_USER_NOTIFICATION_SETTINGS_QUERY_KEY, + (client) => client.getCodeUserNotificationSettings(), + ); +} + +/** Mutations for the settings above; reads come from `useCodeUserNotificationSettings`. */ +export function useCodeUserNotificationSettingsMutations() { + const client = useOptionalAuthenticatedClient(); + const queryClient = useQueryClient(); + + const handleUpdateSlackMentionNotifications = useCallback( + async (enabled: boolean) => { + if (!client) return; + const previous = queryClient.getQueryData( + CODE_USER_NOTIFICATION_SETTINGS_QUERY_KEY, + ); + // Optimistic: the switch reflects the new state immediately, reverted on failure. + queryClient.setQueryData( + CODE_USER_NOTIFICATION_SETTINGS_QUERY_KEY, + { slack_mention_notifications: enabled }, + ); + try { + const fresh = await client.updateCodeUserNotificationSettings({ + slack_mention_notifications: enabled, + }); + queryClient.setQueryData( + CODE_USER_NOTIFICATION_SETTINGS_QUERY_KEY, + fresh, + ); + } catch (error: unknown) { + queryClient.setQueryData( + CODE_USER_NOTIFICATION_SETTINGS_QUERY_KEY, + previous, + ); + const message = + error instanceof Error + ? error.message + : "Failed to update mention notifications"; + toast.error(message); + } + }, + [client, queryClient], + ); + + return { handleUpdateSlackMentionNotifications }; +}