Skip to content
Closed
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
42 changes: 42 additions & 0 deletions packages/api-client/src/posthog-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ import type {
ChannelFeedMessage,
ChannelFeedMessageEvent,
CodeReferenceArtefact,
CodeUserNotificationSettings,
CommitArtefact,
CommitDiffResponse,
DismissalArtefact,
Expand Down Expand Up @@ -4112,6 +4113,47 @@ export class PostHogAPIClient {
return (await response.json()) as SignalUserAutonomyConfig;
}

async getCodeUserNotificationSettings(): Promise<CodeUserNotificationSettings> {
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<CodeUserNotificationSettings> {
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,
Expand Down
6 changes: 6 additions & 0 deletions packages/shared/src/domain-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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 (
<Flex
direction="column"
gap="2"
className="border-(--gray-5) border-t border-dashed pt-4"
>
<Flex align="center" justify="between" gap="3">
<Flex direction="column" gap="1">
<Text className="font-medium text-(--gray-12) text-sm">
Mention notifications
</Text>
<Text className="text-(--gray-11) text-[13px]">
Get a Slack DM when someone @-mentions you in a channel thread.
</Text>
</Flex>
<Switch
checked={enabled}
disabled={isLoading}
onCheckedChange={onCheckedChange}
aria-label="Slack DM on @-mention"
/>
</Flex>
</Flex>
);
}
3 changes: 3 additions & 0 deletions packages/ui/src/features/settings/sections/SlackSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -52,6 +53,8 @@ export function SlackSettings() {
isLoading={isLoading}
showHeader={false}
/>

<SlackMentionNotificationsSettings />
</Flex>
);
}
Original file line number Diff line number Diff line change
@@ -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<CodeUserNotificationSettings>(
CODE_USER_NOTIFICATION_SETTINGS_QUERY_KEY,
);
// Optimistic: the switch reflects the new state immediately, reverted on failure.
queryClient.setQueryData<CodeUserNotificationSettings>(
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 };
}
Loading