feat(integrations): add Discord/Telegram Coming soon placeholders

Show greyed non-interactive messenger cards with a Coming soon badge
(matching integration card chrome, no expandable controls), and refresh
third-party statuses immediately after Apply & Restart clears pending
plugin restarts.

Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
This commit is contained in:
Serhii Dziupin
2026-08-14 15:19:25 +00:00
committed by Cursor Agent
co-authored by Serhii Dziupin
parent 2303740b4a
commit a838f62b41
9 changed files with 203 additions and 1 deletions
@@ -0,0 +1,74 @@
import React from 'react';
import { Icon } from '@/components/icon/Icon';
import { SettingsSection } from '@/components/sections/shared/SettingsSection';
import type { IconName } from '@/components/icon/icons';
import { useI18n, type I18nKey } from '@/lib/i18n';
import { cn } from '@/lib/utils';
type ComingSoonMessenger = {
id: 'discord' | 'telegram';
icon: IconName;
brandClassName: string;
nameKey: I18nKey;
descriptionKey: I18nKey;
};
const COMING_SOON_MESSENGERS: readonly ComingSoonMessenger[] = [
{
id: 'discord',
icon: 'discord-fill',
brandClassName: 'text-[#5865F2]',
nameKey: 'settings.integrations.messengers.discord.name',
descriptionKey: 'settings.integrations.messengers.discord.description',
},
{
id: 'telegram',
icon: 'telegram-fill',
brandClassName: 'text-[#2AABEE]',
nameKey: 'settings.integrations.messengers.telegram.name',
descriptionKey: 'settings.integrations.messengers.telegram.description',
},
] as const;
/**
* Non-interactive Discord/Telegram placeholders — same card chrome as live
* integrations, greyed out, with a Coming soon badge and no expandable body.
*/
export const ComingSoonMessengersSection: React.FC = () => {
const { t } = useI18n();
return (
<SettingsSection
title={t('settings.integrations.messengers.title')}
info={t('settings.integrations.messengers.info')}
divider={false}
settingsItem="integrations.messengers"
contentClassName="space-y-3"
>
{COMING_SOON_MESSENGERS.map((messenger) => (
<div
key={messenger.id}
data-settings-item={`integrations.messengers.${messenger.id}`}
aria-disabled="true"
className={cn(
'flex min-w-0 items-center gap-3 rounded-xl border border-[var(--interactive-border)] bg-[var(--surface-elevated)] px-4 py-3',
'pointer-events-none opacity-60',
)}
>
<div className="flex size-10 shrink-0 items-center justify-center rounded-[10px] bg-[var(--surface-muted)]">
<Icon name={messenger.icon} className={cn('size-5', messenger.brandClassName)} />
</div>
<div className="min-w-0 flex-1">
<div className="truncate text-sm font-semibold text-foreground">{t(messenger.nameKey)}</div>
<p className="mt-0.5 line-clamp-1 text-xs leading-snug text-muted-foreground">
{t(messenger.descriptionKey)}
</p>
</div>
<span className="max-w-36 shrink-0 truncate rounded-full bg-[var(--surface-muted)] px-2 py-0.5 text-[10px] font-medium text-muted-foreground">
{t('settings.common.state.comingSoon')}
</span>
</div>
))}
</SettingsSection>
);
};
@@ -1,6 +1,7 @@
import React from 'react';
import { SettingsPageLayout } from '@/components/sections/shared/SettingsPageLayout';
import { useI18n } from '@/lib/i18n';
import { ComingSoonMessengersSection } from './ComingSoonMessengersSection';
import { ThirdPartyIntegrationsSection } from './ThirdPartyIntegrationsSection';
interface IntegrationsPageProps {
@@ -20,6 +21,7 @@ export const IntegrationsPage: React.FC<IntegrationsPageProps> = ({
description={t('settings.page.integrations.description')}
showSaveStatus={false}
>
<ComingSoonMessengersSection />
<ThirdPartyIntegrationsSection
onOpenProviderSetup={onOpenProviderSetup}
onOpenPluginManager={onOpenPluginManager}
@@ -20,6 +20,7 @@ import {
usePluginsStore,
type PluginMutationResult,
} from '@/stores/usePluginsStore';
import { usePendingOpenCodeRestartStore } from '@/stores/usePendingOpenCodeRestartStore';
import {
getCatalogPluginPrimaryAction,
getCatalogPluginPresentation,
@@ -112,6 +113,30 @@ export const ThirdPartyIntegrationsSection: React.FC<ThirdPartyIntegrationsSecti
void refresh();
}, [refresh]);
const pendingPluginRestartCount = usePendingOpenCodeRestartStore(
(state) => state.changes.filter((change) => change.scope === 'plugins').length,
);
const isApplyingRestart = usePendingOpenCodeRestartStore((state) => state.isApplying);
const previousPluginRestartCountRef = React.useRef(pendingPluginRestartCount);
// When deferred plugin restarts are applied (pending plugins scope clears), drop
// local restart/unavailable flags and reload so statuses update immediately.
React.useEffect(() => {
const previousCount = previousPluginRestartCountRef.current;
previousPluginRestartCountRef.current = pendingPluginRestartCount;
if (isApplyingRestart) {
return;
}
if (previousCount <= 0 || pendingPluginRestartCount > 0) {
return;
}
setRestartRequiredIds(new Set());
setProviderUnavailableIds(new Set());
void refresh();
}, [isApplyingRestart, pendingPluginRestartCount, refresh]);
const setRestartRequired = React.useCallback((pluginId: string, required: boolean) => {
setRestartRequiredIds((current) => {
const next = new Set(current);