add flags for enabling notifications for product updates & new releases

This commit is contained in:
Fred KISSIE
2025-11-08 00:51:56 +01:00
parent 94e1c534ca
commit 579a4e1021
6 changed files with 112 additions and 60 deletions

View File

@@ -89,6 +89,16 @@ export class Config {
? "true" ? "true"
: "false"; : "false";
process.env.PRODUCT_UPDATES_NOTIFICATION_ENABLED = parsedConfig.app
.notifications.product_updates
? "true"
: "false";
process.env.NEW_RELEASES_NOTIFICATION_ENABLED = parsedConfig.app
.notifications.new_releases
? "true"
: "false";
if (parsedConfig.server.maxmind_db_path) { if (parsedConfig.server.maxmind_db_path) {
process.env.MAXMIND_DB_PATH = parsedConfig.server.maxmind_db_path; process.env.MAXMIND_DB_PATH = parsedConfig.server.maxmind_db_path;
} }

View File

@@ -31,6 +31,13 @@ export const configSchema = z
anonymous_usage: z.boolean().optional().default(true) anonymous_usage: z.boolean().optional().default(true)
}) })
.optional() .optional()
.default({}),
notifications: z
.object({
product_updates: z.boolean().optional().default(true),
new_releases: z.boolean().optional().default(true)
})
.optional()
.default({}) .default({})
}) })
.optional() .optional()
@@ -40,6 +47,10 @@ export const configSchema = z
log_failed_attempts: false, log_failed_attempts: false,
telemetry: { telemetry: {
anonymous_usage: true anonymous_usage: true
},
notifications: {
product_updates: true,
new_releases: true
} }
}), }),
domains: z domains: z
@@ -205,7 +216,10 @@ export const configSchema = z
.default(["newt", "wireguard", "local"]), .default(["newt", "wireguard", "local"]),
allow_raw_resources: z.boolean().optional().default(true), allow_raw_resources: z.boolean().optional().default(true),
file_mode: z.boolean().optional().default(false), file_mode: z.boolean().optional().default(false),
pp_transport_prefix: z.string().optional().default("pp-transport-v") pp_transport_prefix: z
.string()
.optional()
.default("pp-transport-v")
}) })
.optional() .optional()
.default({}), .default({}),
@@ -315,8 +329,15 @@ export const configSchema = z
nameservers: z nameservers: z
.array(z.string().optional().optional()) .array(z.string().optional().optional())
.optional() .optional()
.default(["ns1.pangolin.net", "ns2.pangolin.net", "ns3.pangolin.net"]), .default([
cname_extension: z.string().optional().default("cname.pangolin.net") "ns1.pangolin.net",
"ns2.pangolin.net",
"ns3.pangolin.net"
]),
cname_extension: z
.string()
.optional()
.default("cname.pangolin.net")
}) })
.optional() .optional()
.default({}) .default({})

View File

@@ -3,7 +3,11 @@
import { useEnvContext } from "@app/hooks/useEnvContext"; import { useEnvContext } from "@app/hooks/useEnvContext";
import { useLocalStorage } from "@app/hooks/useLocalStorage"; import { useLocalStorage } from "@app/hooks/useLocalStorage";
import { cn } from "@app/lib/cn"; import { cn } from "@app/lib/cn";
import { type ProductUpdate, productUpdatesQueries } from "@app/lib/queries"; import {
type LatestVersionResponse,
type ProductUpdate,
productUpdatesQueries
} from "@app/lib/queries";
import { useQueries } from "@tanstack/react-query"; import { useQueries } from "@tanstack/react-query";
import { import {
ArrowRight, ArrowRight,
@@ -32,10 +36,14 @@ export default function ProductUpdates({
}: { }: {
isCollapsed?: boolean; isCollapsed?: boolean;
}) { }) {
const { env } = useEnvContext();
const data = useQueries({ const data = useQueries({
queries: [ queries: [
productUpdatesQueries.list, productUpdatesQueries.list(env.app.notifications.product_updates),
productUpdatesQueries.latestVersion productUpdatesQueries.latestVersion(
env.app.notifications.new_releases
)
], ],
combine(result) { combine(result) {
if (result[0].isLoading || result[1].isLoading) return null; if (result[0].isLoading || result[1].isLoading) return null;
@@ -45,7 +53,6 @@ export default function ProductUpdates({
}; };
} }
}); });
const { env } = useEnvContext();
const t = useTranslations(); const t = useTranslations();
const [showMoreUpdatesText, setShowMoreUpdatesText] = React.useState(false); const [showMoreUpdatesText, setShowMoreUpdatesText] = React.useState(false);
@@ -302,15 +309,7 @@ function ProductUpdatesListPopup({
type NewVersionAvailableProps = { type NewVersionAvailableProps = {
onDimiss: () => void; onDimiss: () => void;
show: boolean; show: boolean;
version: version: LatestVersionResponse | null | undefined;
| Awaited<
ReturnType<
NonNullable<
typeof productUpdatesQueries.latestVersion.queryFn
>
>
>["data"]
| undefined;
}; };
function NewVersionAvailable({ function NewVersionAvailable({

View File

@@ -21,6 +21,14 @@ const envSchema = z.object({
.transform((val) => val === "true"), .transform((val) => val === "true"),
APP_VERSION: z.string(), APP_VERSION: z.string(),
DASHBOARD_URL: z.string(), DASHBOARD_URL: z.string(),
PRODUCT_UPDATES_NOTIFICATION_ENABLED: z
.string()
.default("true")
.transform((val) => val === "true"),
NEW_RELEASES_NOTIFICATION_ENABLED: z
.string()
.default("true")
.transform((val) => val === "true"),
// Email configuration // Email configuration
EMAIL_ENABLED: z EMAIL_ENABLED: z
@@ -112,7 +120,11 @@ export function pullEnv(): Env {
environment: env.ENVIRONMENT, environment: env.ENVIRONMENT,
sandbox_mode: env.SANDBOX_MODE, sandbox_mode: env.SANDBOX_MODE,
version: env.APP_VERSION, version: env.APP_VERSION,
dashboardUrl: env.DASHBOARD_URL dashboardUrl: env.DASHBOARD_URL,
notifications: {
product_updates: env.PRODUCT_UPDATES_NOTIFICATION_ENABLED,
new_releases: env.NEW_RELEASES_NOTIFICATION_ENABLED
}
}, },
email: { email: {
emailEnabled: env.EMAIL_ENABLED emailEnabled: env.EMAIL_ENABLED

View File

@@ -15,47 +15,53 @@ export type ProductUpdate = {
showUntil: Date; showUntil: Date;
}; };
export const productUpdatesQueries = { export type LatestVersionResponse = {
list: queryOptions({ pangolin: {
queryKey: ["PRODUCT_UPDATES"] as const, latestVersion: string;
queryFn: async ({ signal }) => { releaseNotes: string;
const sp = new URLSearchParams({ };
build };
});
const data = await remote.get<ResponseT<ProductUpdate[]>>( export const productUpdatesQueries = {
`/product-updates?${sp.toString()}`, list: (enabled: boolean) =>
{ signal } queryOptions({
); queryKey: ["PRODUCT_UPDATES"] as const,
return data.data; queryFn: async ({ signal }) => {
}, const sp = new URLSearchParams({
refetchInterval: (query) => { build
if (query.state.data) { });
return durationToMs(5, "minutes"); const data = await remote.get<ResponseT<ProductUpdate[]>>(
} `/product-updates?${sp.toString()}`,
return false; { signal }
} );
}), return data.data;
latestVersion: queryOptions({ },
queryKey: ["LATEST_VERSION"] as const, refetchInterval: (query) => {
queryFn: async ({ signal }) => { if (query.state.data) {
const data = await remote.get< return durationToMs(5, "minutes");
ResponseT<{ }
pangolin: { return false;
latestVersion: string; },
releaseNotes: string; enabled
}; }),
}> latestVersion: (enabled: boolean) =>
>("/versions", { signal }); queryOptions({
return data.data; queryKey: ["LATEST_VERSION"] as const,
}, queryFn: async ({ signal }) => {
placeholderData: keepPreviousData, const data = await remote.get<ResponseT<LatestVersionResponse>>(
refetchInterval: (query) => { "/versions",
if (query.state.data) { { signal }
return durationToMs(30, "minutes"); );
} return data.data;
return false; },
}, placeholderData: keepPreviousData,
enabled: build === "oss" || build === "enterprise" // disabled in cloud version refetchInterval: (query) => {
// because we don't need to listen for new versions there if (query.state.data) {
}) return durationToMs(30, "minutes");
}
return false;
},
enabled: enabled && (build === "oss" || build === "enterprise") // disabled in cloud version
// because we don't need to listen for new versions there
})
}; };

View File

@@ -4,6 +4,10 @@ export type Env = {
sandbox_mode: boolean; sandbox_mode: boolean;
version: string; version: string;
dashboardUrl: string; dashboardUrl: string;
notifications: {
product_updates: boolean;
new_releases: boolean;
};
}; };
server: { server: {
externalPort: string; externalPort: string;