[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:
parent
e650b19933
commit
cacd1b489e
12 changed files with 97 additions and 394 deletions
|
|
@ -25,8 +25,8 @@ const PostHogProvider = lazy(function PostHogProvider() {
|
||||||
return import("@/lib/posthog-provider");
|
return import("@/lib/posthog-provider");
|
||||||
});
|
});
|
||||||
|
|
||||||
const IntercomProvider = lazy(function IntercomProvider() {
|
const ChatSupportModal = lazy(function ChatSupportModal() {
|
||||||
return import("@/lib/intercom-provider");
|
return import("@/components/global/chat-support-modal");
|
||||||
});
|
});
|
||||||
|
|
||||||
export interface IAppProvider {
|
export interface IAppProvider {
|
||||||
|
|
@ -50,11 +50,10 @@ export function AppProvider(props: IAppProvider) {
|
||||||
<StoreWrapper>
|
<StoreWrapper>
|
||||||
<InstanceWrapper>
|
<InstanceWrapper>
|
||||||
<Suspense>
|
<Suspense>
|
||||||
<IntercomProvider>
|
<ChatSupportModal />
|
||||||
<PostHogProvider>
|
<PostHogProvider>
|
||||||
<SWRConfig value={WEB_SWR_CONFIG}>{children}</SWRConfig>
|
<SWRConfig value={WEB_SWR_CONFIG}>{children}</SWRConfig>
|
||||||
</PostHogProvider>
|
</PostHogProvider>
|
||||||
</IntercomProvider>
|
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</InstanceWrapper>
|
</InstanceWrapper>
|
||||||
</StoreWrapper>
|
</StoreWrapper>
|
||||||
|
|
|
||||||
43
apps/web/core/components/global/chat-support-modal.tsx
Normal file
43
apps/web/core/components/global/chat-support-modal.tsx
Normal 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;
|
||||||
|
|
@ -5,7 +5,7 @@ import { DiscordIcon } from "@plane/propel/icons";
|
||||||
import type { TPowerKCommandConfig } from "@/components/power-k/core/types";
|
import type { TPowerKCommandConfig } from "@/components/power-k/core/types";
|
||||||
// hooks
|
// hooks
|
||||||
import { usePowerK } from "@/hooks/store/use-power-k";
|
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
|
* Help commands - Help related commands
|
||||||
|
|
@ -13,7 +13,7 @@ import { useTransient } from "@/hooks/store/use-transient";
|
||||||
export const usePowerKHelpCommands = (): TPowerKCommandConfig[] => {
|
export const usePowerKHelpCommands = (): TPowerKCommandConfig[] => {
|
||||||
// store
|
// store
|
||||||
const { toggleShortcutsListModal } = usePowerK();
|
const { toggleShortcutsListModal } = usePowerK();
|
||||||
const { toggleIntercom } = useTransient();
|
const { isEnabled: isChatSupportEnabled, openChatSupport } = useChatSupport();
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
|
|
@ -73,9 +73,9 @@ export const usePowerKHelpCommands = (): TPowerKCommandConfig[] => {
|
||||||
group: "help",
|
group: "help",
|
||||||
i18n_title: "power_k.help_actions.chat_with_us",
|
i18n_title: "power_k.help_actions.chat_with_us",
|
||||||
icon: MessageSquare,
|
icon: MessageSquare,
|
||||||
action: () => toggleIntercom(true),
|
action: () => openChatSupport(),
|
||||||
isEnabled: () => true,
|
isEnabled: () => isChatSupportEnabled,
|
||||||
isVisible: () => true,
|
isVisible: () => isChatSupportEnabled,
|
||||||
closeOnSelect: true,
|
closeOnSelect: true,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -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>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
@ -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>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
@ -9,9 +9,8 @@ import { CustomMenu } from "@plane/ui";
|
||||||
import { ProductUpdatesModal } from "@/components/global";
|
import { ProductUpdatesModal } from "@/components/global";
|
||||||
import { AppSidebarItem } from "@/components/sidebar/sidebar-item";
|
import { AppSidebarItem } from "@/components/sidebar/sidebar-item";
|
||||||
// hooks
|
// hooks
|
||||||
import { useInstance } from "@/hooks/store/use-instance";
|
|
||||||
import { usePowerK } from "@/hooks/store/use-power-k";
|
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
|
// plane web components
|
||||||
import { PlaneVersionNumber } from "@/plane-web/components/global";
|
import { PlaneVersionNumber } from "@/plane-web/components/global";
|
||||||
|
|
||||||
|
|
@ -19,16 +18,11 @@ export const HelpMenuRoot = observer(function HelpMenuRoot() {
|
||||||
// store hooks
|
// store hooks
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { toggleShortcutsListModal } = usePowerK();
|
const { toggleShortcutsListModal } = usePowerK();
|
||||||
const { config } = useInstance();
|
const { openChatSupport, isEnabled: isChatSupportEnabled } = useChatSupport();
|
||||||
const { isIntercomToggle, toggleIntercom } = useTransient();
|
|
||||||
// states
|
// states
|
||||||
const [isNeedHelpOpen, setIsNeedHelpOpen] = useState(false);
|
const [isNeedHelpOpen, setIsNeedHelpOpen] = useState(false);
|
||||||
const [isProductUpdatesModalOpen, setProductUpdatesModalOpen] = useState(false);
|
const [isProductUpdatesModalOpen, setProductUpdatesModalOpen] = useState(false);
|
||||||
|
|
||||||
const handleCrispWindowShow = () => {
|
|
||||||
toggleIntercom(!isIntercomToggle);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ProductUpdatesModal isOpen={isProductUpdatesModalOpen} handleClose={() => setProductUpdatesModalOpen(false)} />
|
<ProductUpdatesModal isOpen={isProductUpdatesModalOpen} handleClose={() => setProductUpdatesModalOpen(false)} />
|
||||||
|
|
@ -38,7 +32,7 @@ export const HelpMenuRoot = observer(function HelpMenuRoot() {
|
||||||
<AppSidebarItem
|
<AppSidebarItem
|
||||||
variant="button"
|
variant="button"
|
||||||
item={{
|
item={{
|
||||||
icon: <HelpCircle className="size-5" />,
|
icon: <HelpCircle className="size-4" />,
|
||||||
isActive: isNeedHelpOpen,
|
isActive: isNeedHelpOpen,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
@ -56,11 +50,11 @@ export const HelpMenuRoot = observer(function HelpMenuRoot() {
|
||||||
<span className="text-xs">{t("documentation")}</span>
|
<span className="text-xs">{t("documentation")}</span>
|
||||||
</div>
|
</div>
|
||||||
</CustomMenu.MenuItem>
|
</CustomMenu.MenuItem>
|
||||||
{config?.intercom_app_id && config?.is_intercom_enabled && (
|
{isChatSupportEnabled && (
|
||||||
<CustomMenu.MenuItem>
|
<CustomMenu.MenuItem>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={handleCrispWindowShow}
|
onClick={openChatSupport}
|
||||||
className="flex w-full items-center gap-x-2 rounded text-xs hover:bg-custom-background-80"
|
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" />
|
<MessagesSquare className="h-3.5 w-3.5 text-custom-text-200" />
|
||||||
|
|
|
||||||
13
apps/web/core/custom-events/chat-support.ts
Normal file
13
apps/web/core/custom-events/chat-support.ts
Normal 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]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
|
||||||
};
|
|
||||||
25
apps/web/core/hooks/use-chat-support.ts
Normal file
25
apps/web/core/hooks/use-chat-support.ts
Normal 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 };
|
||||||
|
};
|
||||||
|
|
@ -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;
|
|
||||||
|
|
@ -58,8 +58,6 @@ import type { IStickyStore } from "./sticky/sticky.store";
|
||||||
import { StickyStore } from "./sticky/sticky.store";
|
import { StickyStore } from "./sticky/sticky.store";
|
||||||
import type { IThemeStore } from "./theme.store";
|
import type { IThemeStore } from "./theme.store";
|
||||||
import { ThemeStore } 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 type { IUserStore } from "./user";
|
||||||
import { UserStore } from "./user";
|
import { UserStore } from "./user";
|
||||||
import type { IWorkspaceRootStore } from "./workspace";
|
import type { IWorkspaceRootStore } from "./workspace";
|
||||||
|
|
@ -93,7 +91,6 @@ export class CoreRootStore {
|
||||||
multipleSelect: IMultipleSelectStore;
|
multipleSelect: IMultipleSelectStore;
|
||||||
workspaceNotification: IWorkspaceNotificationStore;
|
workspaceNotification: IWorkspaceNotificationStore;
|
||||||
favorite: IFavoriteStore;
|
favorite: IFavoriteStore;
|
||||||
transient: ITransientStore;
|
|
||||||
stickyStore: IStickyStore;
|
stickyStore: IStickyStore;
|
||||||
editorAssetStore: IEditorAssetStore;
|
editorAssetStore: IEditorAssetStore;
|
||||||
workItemFilters: IWorkItemFilterStore;
|
workItemFilters: IWorkItemFilterStore;
|
||||||
|
|
@ -124,7 +121,6 @@ export class CoreRootStore {
|
||||||
this.projectEstimate = new ProjectEstimateStore(this);
|
this.projectEstimate = new ProjectEstimateStore(this);
|
||||||
this.workspaceNotification = new WorkspaceNotificationStore(this);
|
this.workspaceNotification = new WorkspaceNotificationStore(this);
|
||||||
this.favorite = new FavoriteStore(this);
|
this.favorite = new FavoriteStore(this);
|
||||||
this.transient = new TransientStore();
|
|
||||||
this.stickyStore = new StickyStore();
|
this.stickyStore = new StickyStore();
|
||||||
this.editorAssetStore = new EditorAssetStore();
|
this.editorAssetStore = new EditorAssetStore();
|
||||||
this.analytics = new AnalyticsStore();
|
this.analytics = new AnalyticsStore();
|
||||||
|
|
@ -159,7 +155,6 @@ export class CoreRootStore {
|
||||||
this.projectEstimate = new ProjectEstimateStore(this);
|
this.projectEstimate = new ProjectEstimateStore(this);
|
||||||
this.workspaceNotification = new WorkspaceNotificationStore(this);
|
this.workspaceNotification = new WorkspaceNotificationStore(this);
|
||||||
this.favorite = new FavoriteStore(this);
|
this.favorite = new FavoriteStore(this);
|
||||||
this.transient = new TransientStore();
|
|
||||||
this.stickyStore = new StickyStore();
|
this.stickyStore = new StickyStore();
|
||||||
this.editorAssetStore = new EditorAssetStore();
|
this.editorAssetStore = new EditorAssetStore();
|
||||||
this.workItemFilters = new WorkItemFilterStore();
|
this.workItemFilters = new WorkItemFilterStore();
|
||||||
|
|
|
||||||
|
|
@ -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);
|
|
||||||
}
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue