[WEB-5559] improvement: chat support functionality and remove Intercom provider (#8217)

* [WEB-5559] improve: chat support functionality and remove Intercom provider

- Added ChatSupportModal component for chat support integration.
- Replaced IntercomProvider with ChatSupportModal in AppProvider.
- Introduced useChatSupport hook to manage chat support state and actions.
- Updated help commands to utilize chat support instead of Intercom.
- Removed obsolete Intercom-related components and hooks.

* refactor: lazy load ChatSupportModal in AppProvider
This commit is contained in:
Prateek Shourya 2025-12-03 00:03:56 +05:30 committed by GitHub
parent e650b19933
commit cacd1b489e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 97 additions and 394 deletions

View file

@ -25,8 +25,8 @@ const PostHogProvider = lazy(function PostHogProvider() {
return import("@/lib/posthog-provider");
});
const IntercomProvider = lazy(function IntercomProvider() {
return import("@/lib/intercom-provider");
const ChatSupportModal = lazy(function ChatSupportModal() {
return import("@/components/global/chat-support-modal");
});
export interface IAppProvider {
@ -50,11 +50,10 @@ export function AppProvider(props: IAppProvider) {
<StoreWrapper>
<InstanceWrapper>
<Suspense>
<IntercomProvider>
<PostHogProvider>
<SWRConfig value={WEB_SWR_CONFIG}>{children}</SWRConfig>
</PostHogProvider>
</IntercomProvider>
<ChatSupportModal />
<PostHogProvider>
<SWRConfig value={WEB_SWR_CONFIG}>{children}</SWRConfig>
</PostHogProvider>
</Suspense>
</InstanceWrapper>
</StoreWrapper>

View file

@ -0,0 +1,43 @@
import { useEffect } from "react";
import { Intercom, shutdown, show } from "@intercom/messenger-js-sdk";
import { observer } from "mobx-react";
// custom events
import { CHAT_SUPPORT_EVENTS } from "@/custom-events/chat-support";
// store hooks
import { useInstance } from "@/hooks/store/use-instance";
import { useUser } from "@/hooks/store/user";
const ChatSupportModal = observer(function ChatSupportModal() {
// store hooks
const { data: user } = useUser();
const { config } = useInstance();
// derived values
const intercomAppId = config?.intercom_app_id;
const isEnabled = Boolean(user && config?.is_intercom_enabled && intercomAppId);
useEffect(() => {
if (!isEnabled || !user || !intercomAppId) return;
Intercom({
app_id: intercomAppId,
user_id: user.id,
name: `${user.first_name} ${user.last_name}`,
email: user.email,
hide_default_launcher: true,
});
const handleOpenChatSupport = () => {
show();
};
window.addEventListener(CHAT_SUPPORT_EVENTS.open, handleOpenChatSupport);
return () => {
window.removeEventListener(CHAT_SUPPORT_EVENTS.open, handleOpenChatSupport);
shutdown();
};
}, [user, intercomAppId, isEnabled]);
return null;
});
export default ChatSupportModal;

View file

@ -5,7 +5,7 @@ import { DiscordIcon } from "@plane/propel/icons";
import type { TPowerKCommandConfig } from "@/components/power-k/core/types";
// hooks
import { usePowerK } from "@/hooks/store/use-power-k";
import { useTransient } from "@/hooks/store/use-transient";
import { useChatSupport } from "@/hooks/use-chat-support";
/**
* Help commands - Help related commands
@ -13,7 +13,7 @@ import { useTransient } from "@/hooks/store/use-transient";
export const usePowerKHelpCommands = (): TPowerKCommandConfig[] => {
// store
const { toggleShortcutsListModal } = usePowerK();
const { toggleIntercom } = useTransient();
const { isEnabled: isChatSupportEnabled, openChatSupport } = useChatSupport();
return [
{
@ -73,9 +73,9 @@ export const usePowerKHelpCommands = (): TPowerKCommandConfig[] => {
group: "help",
i18n_title: "power_k.help_actions.chat_with_us",
icon: MessageSquare,
action: () => toggleIntercom(true),
isEnabled: () => true,
isVisible: () => true,
action: () => openChatSupport(),
isEnabled: () => isChatSupportEnabled,
isVisible: () => isChatSupportEnabled,
closeOnSelect: true,
},
];

View file

@ -1,125 +0,0 @@
import React, { useState } from "react";
import { observer } from "mobx-react";
import { HelpCircle, MessagesSquare, User } from "lucide-react";
import { useTranslation } from "@plane/i18n";
import { PageIcon } from "@plane/propel/icons";
// ui
import { Tooltip } from "@plane/propel/tooltip";
import { CustomMenu } from "@plane/ui";
// components
import { cn } from "@plane/utils";
import { ProductUpdatesModal } from "@/components/global";
// helpers
// hooks
import { useInstance } from "@/hooks/store/use-instance";
import { usePowerK } from "@/hooks/store/use-power-k";
import { useTransient } from "@/hooks/store/use-transient";
import { usePlatformOS } from "@/hooks/use-platform-os";
// plane web components
import { PlaneVersionNumber } from "@/plane-web/components/global";
export interface WorkspaceHelpSectionProps {
setSidebarActive?: React.Dispatch<React.SetStateAction<boolean>>;
}
export const HelpMenu = observer(function HelpMenu(_props: WorkspaceHelpSectionProps) {
// store hooks
const { t } = useTranslation();
const { toggleShortcutsListModal } = usePowerK();
const { isMobile } = usePlatformOS();
const { config } = useInstance();
const { isIntercomToggle, toggleIntercom } = useTransient();
// states
const [isNeedHelpOpen, setIsNeedHelpOpen] = useState(false);
const [isProductUpdatesModalOpen, setProductUpdatesModalOpen] = useState(false);
const handleCrispWindowShow = () => {
toggleIntercom(!isIntercomToggle);
};
return (
<>
<ProductUpdatesModal isOpen={isProductUpdatesModalOpen} handleClose={() => setProductUpdatesModalOpen(false)} />
<div className="relative flex flex-shrink-0 items-center gap-1 justify-evenly">
<CustomMenu
customButton={
<div
className={cn(
"grid place-items-center rounded-md p-1 outline-none text-custom-text-200 hover:text-custom-text-100 hover:bg-custom-background-90",
{
"bg-custom-background-90": isNeedHelpOpen,
}
)}
>
<Tooltip tooltipContent="Help" isMobile={isMobile} disabled={isNeedHelpOpen}>
<HelpCircle className="h-[18px] w-[18px] outline-none" />
</Tooltip>
</div>
}
customButtonClassName="relative grid place-items-center rounded-md p-1.5 outline-none"
menuButtonOnClick={() => !isNeedHelpOpen && setIsNeedHelpOpen(true)}
onMenuClose={() => setIsNeedHelpOpen(false)}
placement="top-end"
maxHeight="lg"
closeOnSelect
>
<CustomMenu.MenuItem
onClick={() => window.open("https://go.plane.so/p-docs", "_blank", "noopener,noreferrer")}
>
<div className="flex items-center gap-x-2 rounded text-xs hover:bg-custom-background-80">
<PageIcon className="h-3.5 w-3.5 text-custom-text-200" height={14} width={14} />
<span className="text-xs">{t("documentation")}</span>
</div>
</CustomMenu.MenuItem>
{config?.intercom_app_id && config?.is_intercom_enabled && (
<CustomMenu.MenuItem>
<button
type="button"
onClick={handleCrispWindowShow}
className="flex w-full items-center gap-x-2 rounded text-xs hover:bg-custom-background-80"
>
<MessagesSquare className="h-3.5 w-3.5 text-custom-text-200" />
<span className="text-xs">{t("message_support")}</span>
</button>
</CustomMenu.MenuItem>
)}
<CustomMenu.MenuItem onClick={() => window.open("mailto:sales@plane.so", "_blank", "noopener,noreferrer")}>
<div className="flex items-center gap-x-2 rounded text-xs hover:bg-custom-background-80">
<User className="h-3.5 w-3.5 text-custom-text-200" size={14} />
<span className="text-xs">{t("contact_sales")}</span>
</div>
</CustomMenu.MenuItem>
<div className="my-1 border-t border-custom-border-200" />
<CustomMenu.MenuItem>
<button
type="button"
onClick={() => toggleShortcutsListModal(true)}
className="flex w-full items-center justify-start text-xs hover:bg-custom-background-80"
>
<span className="text-xs">{t("keyboard_shortcuts")}</span>
</button>
</CustomMenu.MenuItem>
<CustomMenu.MenuItem>
<button
type="button"
onClick={() => setProductUpdatesModalOpen(true)}
className="flex w-full items-center justify-start text-xs hover:bg-custom-background-80"
>
<span className="text-xs">{t("whats_new")}</span>
</button>
</CustomMenu.MenuItem>
<CustomMenu.MenuItem
onClick={() => window.open("https://go.plane.so/p-discord", "_blank", "noopener,noreferrer")}
>
<div className="flex items-center gap-x-2 rounded text-xs hover:bg-custom-background-80">
<span className="text-xs">Discord</span>
</div>
</CustomMenu.MenuItem>
<div className="px-1 pt-2 mt-1 text-xs text-custom-text-200 border-t border-custom-border-200">
<PlaneVersionNumber />
</div>
</CustomMenu>
</div>
</>
);
});

View file

@ -1,147 +0,0 @@
import React, { useState } from "react";
import { observer } from "mobx-react";
import { HelpCircle, MessagesSquare, MoveLeft, User } from "lucide-react";
// plane imports
import { useTranslation } from "@plane/i18n";
import { PageIcon } from "@plane/propel/icons";
import { Tooltip } from "@plane/propel/tooltip";
import { CustomMenu } from "@plane/ui";
import { cn } from "@plane/utils";
// components
import { ProductUpdatesModal } from "@/components/global";
// hooks
import { useAppTheme } from "@/hooks/store/use-app-theme";
import { useInstance } from "@/hooks/store/use-instance";
import { usePowerK } from "@/hooks/store/use-power-k";
import { useTransient } from "@/hooks/store/use-transient";
import { usePlatformOS } from "@/hooks/use-platform-os";
// plane web components
import { PlaneVersionNumber } from "@/plane-web/components/global";
import { WorkspaceEditionBadge } from "@/plane-web/components/workspace/edition-badge";
export interface WorkspaceHelpSectionProps {
setSidebarActive?: React.Dispatch<React.SetStateAction<boolean>>;
}
export const SidebarHelpSection = observer(function SidebarHelpSection(_props: WorkspaceHelpSectionProps) {
// store hooks
const { t } = useTranslation();
const { sidebarCollapsed: isCollapsed, toggleSidebar, sidebarPeek, toggleSidebarPeek } = useAppTheme();
const { toggleShortcutsListModal } = usePowerK();
const { isMobile } = usePlatformOS();
const { config } = useInstance();
const { isIntercomToggle, toggleIntercom } = useTransient();
// states
const [isNeedHelpOpen, setIsNeedHelpOpen] = useState(false);
const [isProductUpdatesModalOpen, setProductUpdatesModalOpen] = useState(false);
const handleCrispWindowShow = () => {
toggleIntercom(!isIntercomToggle);
};
return (
<>
<ProductUpdatesModal isOpen={isProductUpdatesModalOpen} handleClose={() => setProductUpdatesModalOpen(false)} />
<div className="flex w-full items-center justify-between px-2 self-baseline border-t border-custom-border-200 bg-custom-sidebar-background-100 h-12 flex-shrink-0">
<div className="relative flex flex-shrink-0 items-center gap-1 justify-evenly">
<CustomMenu
customButton={
<div
className={cn(
"grid place-items-center rounded-md p-1 outline-none text-custom-text-200 hover:text-custom-text-100 hover:bg-custom-background-90",
{
"bg-custom-background-90": isNeedHelpOpen,
}
)}
>
<Tooltip tooltipContent="Help" isMobile={isMobile} disabled={isNeedHelpOpen}>
<HelpCircle className="h-[18px] w-[18px] outline-none" />
</Tooltip>
</div>
}
customButtonClassName="relative grid place-items-center rounded-md p-1.5 outline-none"
menuButtonOnClick={() => !isNeedHelpOpen && setIsNeedHelpOpen(true)}
onMenuClose={() => setIsNeedHelpOpen(false)}
placement="top-end"
maxHeight="lg"
closeOnSelect
>
<CustomMenu.MenuItem onClick={() => window.open("https://go.plane.so/p-docs", "_blank")}>
<div className="flex items-center gap-x-2 rounded text-xs">
<PageIcon className="h-3.5 w-3.5 text-custom-text-200" height={14} width={14} />
<span className="text-xs">{t("documentation")}</span>
</div>
</CustomMenu.MenuItem>
{config?.intercom_app_id && config?.is_intercom_enabled && (
<CustomMenu.MenuItem>
<button
type="button"
onClick={handleCrispWindowShow}
className="flex w-full items-center gap-x-2 rounded text-xs hover:bg-custom-background-80"
>
<MessagesSquare className="h-3.5 w-3.5 text-custom-text-200" />
<span className="text-xs">{t("message_support")}</span>
</button>
</CustomMenu.MenuItem>
)}
<CustomMenu.MenuItem onClick={() => window.open("mailto:sales@plane.so", "_blank")}>
<div className="flex items-center gap-x-2 rounded text-xs">
<User className="h-3.5 w-3.5 text-custom-text-200" size={14} />
<span className="text-xs">{t("contact_sales")}</span>
</div>
</CustomMenu.MenuItem>
<div className="my-1 border-t border-custom-border-200" />
<CustomMenu.MenuItem>
<button
type="button"
onClick={() => toggleShortcutsListModal(true)}
className="flex w-full items-center justify-start text-xs hover:bg-custom-background-80"
>
<span className="text-xs">{t("keyboard_shortcuts")}</span>
</button>
</CustomMenu.MenuItem>
<CustomMenu.MenuItem>
<button
type="button"
onClick={() => setProductUpdatesModalOpen(true)}
className="flex w-full items-center justify-start text-xs hover:bg-custom-background-80"
>
<span className="text-xs">{t("whats_new")}</span>
</button>
</CustomMenu.MenuItem>
<CustomMenu.MenuItem onClick={() => window.open("https://go.plane.so/p-discord", "_blank")}>
<div className="flex items-center gap-x-2 rounded text-xs">
<span className="text-xs">Discord</span>
</div>
</CustomMenu.MenuItem>
<div className="px-1 pt-2 mt-1 text-xs text-custom-text-200 border-t border-custom-border-200">
<PlaneVersionNumber />
</div>
</CustomMenu>
</div>
<div className="w-full flex-grow px-0.5">
<WorkspaceEditionBadge />
</div>
<div className="flex flex-shrink-0 items-center gap-1 justify-evenly">
<Tooltip tooltipContent={`${isCollapsed ? "Expand" : "Hide"}`} isMobile={isMobile}>
<button
type="button"
className="grid place-items-center rounded-md p-1 text-custom-text-200 outline-none hover:bg-custom-background-90 hover:text-custom-text-100"
onClick={() => {
if (sidebarPeek) toggleSidebarPeek(false);
toggleSidebar();
}}
aria-label={t(
isCollapsed
? "aria_labels.projects_sidebar.expand_sidebar"
: "aria_labels.projects_sidebar.collapse_sidebar"
)}
>
<MoveLeft className={`size-4 duration-300 ${isCollapsed ? "rotate-180" : ""}`} />
</button>
</Tooltip>
</div>
</div>
</>
);
});

View file

@ -9,9 +9,8 @@ import { CustomMenu } from "@plane/ui";
import { ProductUpdatesModal } from "@/components/global";
import { AppSidebarItem } from "@/components/sidebar/sidebar-item";
// hooks
import { useInstance } from "@/hooks/store/use-instance";
import { usePowerK } from "@/hooks/store/use-power-k";
import { useTransient } from "@/hooks/store/use-transient";
import { useChatSupport } from "@/hooks/use-chat-support";
// plane web components
import { PlaneVersionNumber } from "@/plane-web/components/global";
@ -19,16 +18,11 @@ export const HelpMenuRoot = observer(function HelpMenuRoot() {
// store hooks
const { t } = useTranslation();
const { toggleShortcutsListModal } = usePowerK();
const { config } = useInstance();
const { isIntercomToggle, toggleIntercom } = useTransient();
const { openChatSupport, isEnabled: isChatSupportEnabled } = useChatSupport();
// states
const [isNeedHelpOpen, setIsNeedHelpOpen] = useState(false);
const [isProductUpdatesModalOpen, setProductUpdatesModalOpen] = useState(false);
const handleCrispWindowShow = () => {
toggleIntercom(!isIntercomToggle);
};
return (
<>
<ProductUpdatesModal isOpen={isProductUpdatesModalOpen} handleClose={() => setProductUpdatesModalOpen(false)} />
@ -38,7 +32,7 @@ export const HelpMenuRoot = observer(function HelpMenuRoot() {
<AppSidebarItem
variant="button"
item={{
icon: <HelpCircle className="size-5" />,
icon: <HelpCircle className="size-4" />,
isActive: isNeedHelpOpen,
}}
/>
@ -56,11 +50,11 @@ export const HelpMenuRoot = observer(function HelpMenuRoot() {
<span className="text-xs">{t("documentation")}</span>
</div>
</CustomMenu.MenuItem>
{config?.intercom_app_id && config?.is_intercom_enabled && (
{isChatSupportEnabled && (
<CustomMenu.MenuItem>
<button
type="button"
onClick={handleCrispWindowShow}
onClick={openChatSupport}
className="flex w-full items-center gap-x-2 rounded text-xs hover:bg-custom-background-80"
>
<MessagesSquare className="h-3.5 w-3.5 text-custom-text-200" />

View file

@ -0,0 +1,13 @@
type ChatSupportType = "open";
type ChatSupportEventType = `chat-support:${ChatSupportType}`;
export const CHAT_SUPPORT_EVENTS = {
open: "chat-support:open",
} satisfies Record<ChatSupportType, ChatSupportEventType>;
export class ChatSupportEvent extends CustomEvent<ChatSupportType> {
constructor(type: ChatSupportType) {
super(CHAT_SUPPORT_EVENTS[type]);
}
}

View file

@ -1,11 +0,0 @@
import { useContext } from "react";
// mobx store
import { StoreContext } from "@/lib/store-context";
// types
import type { ITransientStore } from "@/store/transient.store";
export const useTransient = (): ITransientStore => {
const context = useContext(StoreContext);
if (context === undefined) throw new Error("useTransient must be used within StoreProvider");
return context.transient;
};

View file

@ -0,0 +1,25 @@
import { useCallback } from "react";
// custom events
import { ChatSupportEvent } from "@/custom-events/chat-support";
// hooks
import { useInstance } from "@/hooks/store/use-instance";
import { useUser } from "@/hooks/store/user";
export interface IUseChatSupport {
openChatSupport: () => void;
isEnabled: boolean;
}
export const useChatSupport = (): IUseChatSupport => {
const { data: user } = useUser();
const { config } = useInstance();
// derived values
const isEnabled = Boolean(user && config?.is_intercom_enabled && config?.intercom_app_id);
const openChatSupport = useCallback(() => {
if (!isEnabled) return;
window.dispatchEvent(new ChatSupportEvent("open"));
}, [isEnabled]);
return { openChatSupport, isEnabled };
};

View file

@ -1,55 +0,0 @@
import React, { useEffect, useRef, useState } from "react";
import { Intercom, show, hide, onHide } from "@intercom/messenger-js-sdk";
import { observer } from "mobx-react";
// store hooks
import { useInstance } from "@/hooks/store/use-instance";
import { useTransient } from "@/hooks/store/use-transient";
import { useUser } from "@/hooks/store/user";
export type IntercomProviderProps = {
children: React.ReactNode;
};
const IntercomProvider = observer(function IntercomProvider(props: IntercomProviderProps) {
const { children } = props;
// hooks
const { data: user } = useUser();
const { config } = useInstance();
const { isIntercomToggle, toggleIntercom } = useTransient();
// refs
const isInitializedRef = useRef(false);
// states
const [hydrated, setHydrated] = useState(false);
// derived values
const isIntercomEnabled = user && config && config.is_intercom_enabled && config.intercom_app_id;
useEffect(() => {
if (!hydrated) return;
if (isIntercomToggle) show();
else hide();
}, [hydrated, isIntercomToggle]);
useEffect(() => {
if (!hydrated) return;
onHide(() => {
toggleIntercom(false);
});
}, [hydrated, toggleIntercom]);
useEffect(() => {
if (!isIntercomEnabled || isInitializedRef.current) return; // prevent multiple initializations
Intercom({
app_id: config.intercom_app_id || "",
user_id: user.id,
name: `${user.first_name} ${user.last_name}`,
email: user.email,
hide_default_launcher: true,
});
isInitializedRef.current = true;
setHydrated(true);
}, [isIntercomEnabled, config, user]);
return <>{children}</>;
});
export default IntercomProvider;

View file

@ -58,8 +58,6 @@ import type { IStickyStore } from "./sticky/sticky.store";
import { StickyStore } from "./sticky/sticky.store";
import type { IThemeStore } from "./theme.store";
import { ThemeStore } from "./theme.store";
import type { ITransientStore } from "./transient.store";
import { TransientStore } from "./transient.store";
import type { IUserStore } from "./user";
import { UserStore } from "./user";
import type { IWorkspaceRootStore } from "./workspace";
@ -93,7 +91,6 @@ export class CoreRootStore {
multipleSelect: IMultipleSelectStore;
workspaceNotification: IWorkspaceNotificationStore;
favorite: IFavoriteStore;
transient: ITransientStore;
stickyStore: IStickyStore;
editorAssetStore: IEditorAssetStore;
workItemFilters: IWorkItemFilterStore;
@ -124,7 +121,6 @@ export class CoreRootStore {
this.projectEstimate = new ProjectEstimateStore(this);
this.workspaceNotification = new WorkspaceNotificationStore(this);
this.favorite = new FavoriteStore(this);
this.transient = new TransientStore();
this.stickyStore = new StickyStore();
this.editorAssetStore = new EditorAssetStore();
this.analytics = new AnalyticsStore();
@ -159,7 +155,6 @@ export class CoreRootStore {
this.projectEstimate = new ProjectEstimateStore(this);
this.workspaceNotification = new WorkspaceNotificationStore(this);
this.favorite = new FavoriteStore(this);
this.transient = new TransientStore();
this.stickyStore = new StickyStore();
this.editorAssetStore = new EditorAssetStore();
this.workItemFilters = new WorkItemFilterStore();

View file

@ -1,28 +0,0 @@
import { action, observable, makeObservable } from "mobx";
export interface ITransientStore {
// observables
isIntercomToggle: boolean;
// actions
toggleIntercom: (intercomToggle: boolean) => void;
}
export class TransientStore implements ITransientStore {
// observables
isIntercomToggle: boolean = false;
constructor() {
makeObservable(this, {
// observable
isIntercomToggle: observable.ref,
// action
toggleIntercom: action,
});
}
/**
* @description Toggle the intercom collapsed state
* @param { boolean } intercomToggle
*/
toggleIntercom = (intercomToggle: boolean) => (this.isIntercomToggle = intercomToggle);
}