feat: Add fresh chat to list of support integration (#7448)

* chore: add fresh chat configs to env.example

* feat: add fresh chat menu item and render

* refactor: remove some event listeners

* chore: make popover closed after the click

* refactor the code and fix some bugs

* feat: auto open chat

---------

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
This commit is contained in:
Nafees Nazik 2023-03-08 17:00:24 +05:30 committed by GitHub
parent d28c914d3c
commit 29ceaee8d2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 289 additions and 147 deletions

View File

@ -78,6 +78,10 @@ NEXT_PUBLIC_ZENDESK_KEY=
# Help Scout Config
NEXT_PUBLIC_HELPSCOUT_KEY=
# Fresh Chat Config
NEXT_PUBLIC_FRESHCHAT_TOKEN=
NEXT_PUBLIC_FRESHCHAT_HOST=
# Inbox to send user feedback
SEND_FEEDBACK_EMAIL=

View File

@ -1,13 +1,20 @@
import FreshChatMenuItem from "../lib/freshchat/FreshChatMenuItem";
import HelpscoutMenuItem from "../lib/helpscout/HelpscoutMenuItem";
import IntercomMenuItem from "../lib/intercom/IntercomMenuItem";
import ZendeskMenuItem from "../lib/zendesk/ZendeskMenuItem";
export default function HelpMenuItem() {
interface ContactMenuItem {
onHelpItemSelect: () => void;
}
export default function ContactMenuItem(props: ContactMenuItem) {
const { onHelpItemSelect } = props;
return (
<>
<IntercomMenuItem />
<ZendeskMenuItem />
<HelpscoutMenuItem />
<IntercomMenuItem onHelpItemSelect={onHelpItemSelect} />
<ZendeskMenuItem onHelpItemSelect={onHelpItemSelect} />
<HelpscoutMenuItem onHelpItemSelect={onHelpItemSelect} />
<FreshChatMenuItem onHelpItemSelect={onHelpItemSelect} />
</>
);
}

View File

@ -7,6 +7,8 @@ import { trpc } from "@calcom/trpc/react";
import { Button, showToast } from "@calcom/ui";
import { FiExternalLink, FiAlertTriangle } from "@calcom/ui/components/icon";
import { useFreshChat } from "../lib/freshchat/FreshChatProvider";
import { isFreshChatEnabled } from "../lib/freshchat/FreshChatScript";
import ContactMenuItem from "./ContactMenuItem";
interface HelpMenuItemProps {
@ -21,6 +23,8 @@ export default function HelpMenuItem({ onHelpItemSelect }: HelpMenuItemProps) {
const [, loadChat] = useChat();
const { t } = useLocale();
const { setActive: setFreshChat } = useFreshChat();
const mutation = trpc.viewer.submitFeedback.useMutation({
onSuccess: () => {
setDisableSubmit(true);
@ -43,7 +47,6 @@ export default function HelpMenuItem({ onHelpItemSelect }: HelpMenuItemProps) {
<div className="w-full py-5">
<p className="mb-1 px-5 text-gray-500">{t("resources").toUpperCase()}</p>
<a
onClick={() => onHelpItemSelect()}
href="https://docs.cal.com/"
target="_blank"
className="flex w-full px-5 py-2 pr-4 text-sm font-medium text-gray-700 hover:bg-gray-100 hover:text-gray-900"
@ -57,7 +60,6 @@ export default function HelpMenuItem({ onHelpItemSelect }: HelpMenuItemProps) {
/>
</a>
<a
onClick={() => onHelpItemSelect()}
href="https://developer.cal.com/"
target="_blank"
className="flex w-full px-5 py-2 pr-4 text-sm font-medium text-gray-700 hover:bg-gray-100 hover:text-gray-900"
@ -70,8 +72,8 @@ export default function HelpMenuItem({ onHelpItemSelect }: HelpMenuItemProps) {
)}
/>
</a>
<div onClick={() => onHelpItemSelect()}>
<ContactMenuItem />
<div>
<ContactMenuItem onHelpItemSelect={onHelpItemSelect} />
</div>
</div>
@ -202,7 +204,11 @@ export default function HelpMenuItem({ onHelpItemSelect }: HelpMenuItemProps) {
className="font-medium underline hover:text-gray-700"
onClick={() => {
setActive(true);
loadChat({ open: true });
if (isFreshChatEnabled) {
setFreshChat(true);
} else {
loadChat({ open: true });
}
onHelpItemSelect();
}}>
{t("contact_support")}

View File

@ -0,0 +1,30 @@
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { useFreshChat } from "./FreshChatProvider";
import { isFreshChatEnabled } from "./FreshChatScript";
interface FreshChatMenuItemProps {
onHelpItemSelect: () => void;
}
export default function FreshChatMenuItem(props: FreshChatMenuItemProps) {
const { onHelpItemSelect } = props;
const { t } = useLocale();
const { setActive } = useFreshChat();
if (!isFreshChatEnabled) return null;
return (
<>
<button
onClick={() => {
setActive(true);
onHelpItemSelect();
}}
className="flex w-full px-5 py-2 pr-4 text-sm font-medium text-gray-700 hover:bg-gray-100 hover:text-gray-900">
{t("contact_support")}
</button>
</>
);
}

View File

@ -0,0 +1,25 @@
import type { ReactNode, Dispatch, SetStateAction } from "react";
import { createContext, useState, useContext } from "react";
import FreshChatScript from "./FreshChatScript";
type FreshChatContextType = { active: boolean; setActive: Dispatch<SetStateAction<boolean>> };
const FreshChatContext = createContext<FreshChatContextType>({ active: false, setActive: () => undefined });
interface FreshChatProviderProps {
children: ReactNode;
}
export const useFreshChat = () => useContext(FreshChatContext);
export default function FreshChatProvider(props: FreshChatProviderProps) {
const [active, setActive] = useState(false);
return (
<FreshChatContext.Provider value={{ active, setActive }}>
{props.children}
{active && <FreshChatScript />}
</FreshChatContext.Provider>
);
}

View File

@ -0,0 +1,40 @@
import Script from "next/script";
import { trpc } from "@calcom/trpc/react";
declare global {
interface Window {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
fcWidget: any;
}
}
// eslint-disable-next-line turbo/no-undeclared-env-vars
const host = process.env.NEXT_PUBLIC_FRESHCHAT_HOST;
// eslint-disable-next-line turbo/no-undeclared-env-vars
const token = process.env.NEXT_PUBLIC_FRESHCHAT_TOKEN;
export const isFreshChatEnabled = host !== "undefined" && token !== "undefined";
export default function FreshChatScript() {
const { data } = trpc.viewer.me.useQuery();
return (
<Script
id="fresh-chat-sdk"
src="https://wchat.freshchat.com/js/widget.js"
onLoad={() => {
window.fcWidget.init({
token,
host,
externalId: data?.id,
lastName: data?.name,
email: data?.email,
meta: {
username: data?.username,
},
open: true,
});
}}
/>
);
}

View File

@ -3,7 +3,12 @@ import { HelpScout, useChat } from "react-live-chat-loader";
import { useLocale } from "@calcom/lib/hooks/useLocale";
export default function HelpscoutMenuItem() {
interface HelpscoutMenuItemProps {
onHelpItemSelect: () => void;
}
export default function HelpscoutMenuItem(props: HelpscoutMenuItemProps) {
const { onHelpItemSelect } = props;
const { t } = useLocale();
const [active, setActive] = useState(false);
@ -12,19 +17,21 @@ export default function HelpscoutMenuItem() {
function handleClick() {
setActive(true);
loadChat({ open: true });
onHelpItemSelect();
}
// eslint-disable-next-line turbo/no-undeclared-env-vars
if (!process.env.NEXT_PUBLIC_HELPSCOUT_KEY) return null;
else
return (
<>
<button
onClick={handleClick}
className="flex w-full px-5 py-2 pr-4 text-sm font-medium text-gray-700 hover:bg-gray-100 hover:text-gray-900">
{t("contact_support")}
</button>
{active && <HelpScout color="#292929" icon="message" horizontalPosition="right" zIndex="1" />}
</>
);
return (
<>
<button
onClick={handleClick}
className="flex w-full px-5 py-2 pr-4 text-sm font-medium text-gray-700 hover:bg-gray-100 hover:text-gray-900">
{t("contact_support")}
</button>
{active && <HelpScout color="#292929" icon="message" horizontalPosition="right" zIndex="1" />}
</>
);
}

View File

@ -2,19 +2,26 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
import { useIntercom } from "./useIntercom";
export default function IntercomMenuItem() {
interface IntercomMenuItemProps {
onHelpItemSelect: () => void;
}
export default function IntercomMenuItem(props: IntercomMenuItemProps) {
const { onHelpItemSelect } = props;
const { t } = useLocale();
const { boot, show } = useIntercom();
// eslint-disable-next-line turbo/no-undeclared-env-vars
if (!process.env.NEXT_PUBLIC_INTERCOM_APP_ID) return null;
else
return (
<button
onClick={() => {
boot();
show();
}}
className="flex w-full px-5 py-2 pr-4 text-sm font-medium text-gray-700 hover:bg-gray-100 hover:text-gray-900">
{t("contact_support")}
</button>
);
return (
<button
onClick={() => {
boot();
show();
onHelpItemSelect();
}}
className="flex w-full px-5 py-2 pr-4 text-sm font-medium text-gray-700 hover:bg-gray-100 hover:text-gray-900">
{t("contact_support")}
</button>
);
}

View File

@ -3,24 +3,33 @@ import { useState } from "react";
import { useLocale } from "@calcom/lib/hooks/useLocale";
// eslint-disable-next-line turbo/no-undeclared-env-vars
const ZENDESK_KEY = process.env.NEXT_PUBLIC_ZENDESK_KEY;
export default function ZendeskMenuItem() {
interface ZendeskMenuItemProps {
onHelpItemSelect: () => void;
}
export default function ZendeskMenuItem(props: ZendeskMenuItemProps) {
const { onHelpItemSelect } = props;
const [active, setActive] = useState(false);
const { t } = useLocale();
if (!process.env.NEXT_PUBLIC_ZENDESK_KEY) return null;
else
return (
<>
<button
onClick={() => setActive(true)}
className="flex w-full px-5 py-2 pr-4 text-sm font-medium text-gray-700 hover:bg-gray-100 hover:text-gray-900">
{t("contact_support")}
</button>
{active && (
<Script id="ze-snippet" src={"https://static.zdassets.com/ekr/snippet.js?key=" + ZENDESK_KEY} />
)}
</>
);
if (!ZENDESK_KEY) return null;
return (
<>
<button
onClick={() => {
setActive(true);
onHelpItemSelect();
}}
className="flex w-full px-5 py-2 pr-4 text-sm font-medium text-gray-700 hover:bg-gray-100 hover:text-gray-900">
{t("contact_support")}
</button>
{active && (
<Script id="ze-snippet" src={"https://static.zdassets.com/ekr/snippet.js?key=" + ZENDESK_KEY} />
)}
</>
);
}

View File

@ -63,6 +63,7 @@ import {
FiArrowLeft,
} from "@calcom/ui/components/icon";
import FreshChatProvider from "../ee/support/lib/freshchat/FreshChatProvider";
import { TeamInviteBadge } from "./TeamInviteBadge";
/* TODO: Migate this */
@ -316,106 +317,112 @@ function UserDropdown({ small }: { small?: boolean }) {
</div>
<DropdownMenuPortal>
<DropdownMenuContent
onInteractOutside={() => {
setMenuOpen(false);
setHelpOpen(false);
}}
className="overflow-hidden rounded-md">
{helpOpen ? (
<HelpMenuItem onHelpItemSelect={() => onHelpItemSelect()} />
) : (
<>
<DropdownMenuItem>
<DropdownItem
type="button"
StartIcon={(props) => (
<FiMoon
className={classNames(
user.away
? "text-purple-500 group-hover:text-purple-700"
: "text-gray-500 group-hover:text-gray-700",
props.className
)}
aria-hidden="true"
/>
)}
onClick={() => {
mutation.mutate({ away: !user?.away });
utils.viewer.me.invalidate();
}}>
{user.away ? t("set_as_free") : t("set_as_away")}
</DropdownItem>
</DropdownMenuItem>
<DropdownMenuSeparator />
{user.username && (
<>
<DropdownMenuItem>
<DropdownItem
target="_blank"
rel="noopener noreferrer"
href={`${process.env.NEXT_PUBLIC_WEBSITE_URL}/${user.username}`}
StartIcon={FiExternalLink}>
{t("view_public_page")}
</DropdownItem>
</DropdownMenuItem>
<DropdownMenuItem>
<DropdownItem
type="button"
StartIcon={FiLink}
onClick={(e) => {
e.preventDefault();
navigator.clipboard.writeText(
`${process.env.NEXT_PUBLIC_WEBSITE_URL}/${user.username}`
);
showToast(t("link_copied"), "success");
}}>
{t("copy_public_page_link")}
</DropdownItem>
</DropdownMenuItem>
</>
)}
<DropdownMenuSeparator />
<DropdownMenuItem>
<DropdownItem
StartIcon={(props) => <FiSlack strokeWidth={1.5} {...props} />}
target="_blank"
rel="noreferrer"
href={JOIN_SLACK}>
{t("join_our_slack")}
</DropdownItem>
</DropdownMenuItem>
<DropdownMenuItem>
<DropdownItem StartIcon={FiMap} target="_blank" href={ROADMAP}>
{t("visit_roadmap")}
</DropdownItem>
</DropdownMenuItem>
<DropdownMenuItem>
<DropdownItem
type="button"
StartIcon={(props) => <FiHelpCircle aria-hidden="true" {...props} />}
onClick={() => setHelpOpen(true)}>
{t("help")}
</DropdownItem>
</DropdownMenuItem>
<DropdownMenuItem className="desktop-hidden hidden lg:flex">
<DropdownItem StartIcon={FiDownload} target="_blank" rel="noreferrer" href={DESKTOP_APP_LINK}>
{t("download_desktop_app")}
</DropdownItem>
</DropdownMenuItem>
<FreshChatProvider>
<DropdownMenuContent
onInteractOutside={() => {
setMenuOpen(false);
setHelpOpen(false);
}}
className="overflow-hidden rounded-md">
{helpOpen ? (
<HelpMenuItem onHelpItemSelect={() => onHelpItemSelect()} />
) : (
<>
<DropdownMenuItem>
<DropdownItem
type="button"
StartIcon={(props) => (
<FiMoon
className={classNames(
user.away
? "text-purple-500 group-hover:text-purple-700"
: "text-gray-500 group-hover:text-gray-700",
props.className
)}
aria-hidden="true"
/>
)}
onClick={() => {
mutation.mutate({ away: !user?.away });
utils.viewer.me.invalidate();
}}>
{user.away ? t("set_as_free") : t("set_as_away")}
</DropdownItem>
</DropdownMenuItem>
<DropdownMenuSeparator />
{user.username && (
<>
<DropdownMenuItem>
<DropdownItem
target="_blank"
rel="noopener noreferrer"
href={`${process.env.NEXT_PUBLIC_WEBSITE_URL}/${user.username}`}
StartIcon={FiExternalLink}>
{t("view_public_page")}
</DropdownItem>
</DropdownMenuItem>
<DropdownMenuItem>
<DropdownItem
type="button"
StartIcon={FiLink}
onClick={(e) => {
e.preventDefault();
navigator.clipboard.writeText(
`${process.env.NEXT_PUBLIC_WEBSITE_URL}/${user.username}`
);
showToast(t("link_copied"), "success");
}}>
{t("copy_public_page_link")}
</DropdownItem>
</DropdownMenuItem>
</>
)}
<DropdownMenuSeparator />
<DropdownMenuItem>
<DropdownItem
StartIcon={(props) => <FiSlack strokeWidth={1.5} {...props} />}
target="_blank"
rel="noreferrer"
href={JOIN_SLACK}>
{t("join_our_slack")}
</DropdownItem>
</DropdownMenuItem>
<DropdownMenuItem>
<DropdownItem StartIcon={FiMap} target="_blank" href={ROADMAP}>
{t("visit_roadmap")}
</DropdownItem>
</DropdownMenuItem>
<DropdownMenuItem>
<DropdownItem
type="button"
StartIcon={(props) => <FiHelpCircle aria-hidden="true" {...props} />}
onClick={() => setHelpOpen(true)}>
{t("help")}
</DropdownItem>
</DropdownMenuItem>
<DropdownMenuItem className="desktop-hidden hidden lg:flex">
<DropdownItem
StartIcon={FiDownload}
target="_blank"
rel="noreferrer"
href={DESKTOP_APP_LINK}>
{t("download_desktop_app")}
</DropdownItem>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem>
<DropdownItem
type="button"
StartIcon={(props) => <FiLogOut aria-hidden="true" {...props} />}
onClick={() => signOut({ callbackUrl: "/auth/logout" })}>
{t("sign_out")}
</DropdownItem>
</DropdownMenuItem>
</>
)}
</DropdownMenuContent>
<DropdownMenuSeparator />
<DropdownMenuItem>
<DropdownItem
type="button"
StartIcon={(props) => <FiLogOut aria-hidden="true" {...props} />}
onClick={() => signOut({ callbackUrl: "/auth/logout" })}>
{t("sign_out")}
</DropdownItem>
</DropdownMenuItem>
</>
)}
</DropdownMenuContent>
</FreshChatProvider>
</DropdownMenuPortal>
</Dropdown>
);