Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 11 additions & 77 deletions webapp/src/component/HelpMenu.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { useEffect, useState } from 'react';
import { useState } from 'react';
import {
styled,
useTheme,
Tooltip,
Fab,
Menu,
Expand All @@ -26,16 +25,10 @@ import {
} from '@untitled-ui/icons-react';
import { T, useTranslate } from '@tolgee/react';

import {
useConfig,
usePreferredOrganization,
useUser,
} from 'tg.globalContext/helpers';
import { usePreferredOrganization } from 'tg.globalContext/helpers';
import { GitHub, Slack } from './CustomIcons';
import { TranslationsShortcuts } from './shortcuts/TranslationsShortcuts';

const BASE_URL = 'https://app.chatwoot.com';
let scriptPromise: Promise<void> | null = null;
import { useChatwoot } from 'tg.hooks/useChatwoot';

const StyledHelpButton = styled('div')`
position: fixed;
Expand All @@ -45,44 +38,10 @@ const StyledHelpButton = styled('div')`
border-radius: 50%;
`;

export const loadScript = (websiteToken: string, darkMode: boolean) => {
return (function (doc, tag) {
if (!scriptPromise) {
const g = doc.createElement(tag) as HTMLScriptElement,
s = doc.getElementsByTagName(tag)[0] as HTMLScriptElement;
g.src = BASE_URL + '/packs/js/sdk.js';
g.defer = true;
g.async = true;
s!.parentNode!.insertBefore(g, s);
// @ts-ignore
window.chatwootSettings = {
darkMode: darkMode ? 'auto' : 'light',
hideMessageBubble: true,
};
scriptPromise = new Promise<void>((resolve) => {
g.onload = function () {
// @ts-ignore
window.chatwootSDK.run({
websiteToken,
baseUrl: BASE_URL,
});
resolve();
};
});
}
return scriptPromise;
})(document, 'script');
};

export const HelpMenu = () => {
const { t } = useTranslate();
const user = useUser();
const config = useConfig();
const { preferredOrganization } = usePreferredOrganization();
const token = config?.chatwootToken;
const {
palette: { mode },
} = useTheme();
const { chatwootAvailable, openChatwoot } = useChatwoot();

const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);

Expand All @@ -107,28 +66,6 @@ export const HelpMenu = () => {
handleClose();
}

const darkMode = mode === 'dark';

useEffect(() => {
if (token) {
loadScript(token!, darkMode);
}
}, [token]);

const openChatwoot = () => {
handleClose();
loadScript(token!, darkMode).then(() => {
// @ts-ignore
window.$chatwoot.setUser(user.id, {
email: user!.username,
name: user!.name,
url: window.location,
});
// @ts-ignore
window.$chatwoot.toggle();
});
};

function buttonLink(url: string) {
return { href: url, target: 'blank', rel: 'noreferrer noopener' };
}
Expand All @@ -137,14 +74,6 @@ export const HelpMenu = () => {
return null;
}

const enabledFeatures = preferredOrganization.enabledFeatures;

const hasStandardSupport =
enabledFeatures.includes('STANDARD_SUPPORT') ||
enabledFeatures.includes('PREMIUM_SUPPORT');

const displayChat = token && user && hasStandardSupport;

return (
<>
<Tooltip
Expand Down Expand Up @@ -212,8 +141,13 @@ export const HelpMenu = () => {
secondary={t('help_menu_slack_community_description')}
/>
</MenuItem>
{displayChat && (
<MenuItem onClick={openChatwoot}>
{chatwootAvailable && (
<MenuItem
onClick={() => {
handleClose();
openChatwoot();
}}
>
<ListItemIcon>
<MessageSquare01 />
</ListItemIcon>
Expand Down
129 changes: 129 additions & 0 deletions webapp/src/hooks/useChatwoot.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import { useEffect } from 'react';
import { useTheme } from '@mui/material';
import {
useConfig,
usePreferredOrganization,
useUser,
} from 'tg.globalContext/helpers';
import { components } from 'tg.service/apiSchema.generated';

type User = components['schemas']['PrivateUserAccountModel'];
type Organization = components['schemas']['PrivateOrganizationModel'];

const BASE_URL = 'https://app.chatwoot.com';
let chatwootLoadPromise: Promise<void> | null = null;

function loadScript(doc: Document, url: string) {
return new Promise<void>((resolve) => {
const element = doc.createElement('script') as HTMLScriptElement;
const existingElement = doc.getElementsByTagName(
'script'
)[0] as HTMLScriptElement;

element.src = url;
element.defer = true;
element.async = true;
element.onload = () => {
resolve();
};

existingElement?.parentNode?.insertBefore(element, existingElement);
});
}

async function loadChatwoot(websiteToken: string, darkMode: boolean) {
// @ts-ignore
window.chatwootSettings = {
darkMode: darkMode ? 'auto' : 'light',
hideMessageBubble: true,
};

await loadScript(document, BASE_URL + '/packs/js/sdk.js');

// @ts-ignore
window.chatwootSDK?.run({
websiteToken,
baseUrl: BASE_URL,
});
}
Comment on lines +34 to +48
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Avoid @ts-ignore by declaring proper Window interface augmentation.

The @ts-ignore comments on lines 35 and 43 directly contradict the previous review feedback from JanCizmar who stated "I wouldn't use @ts-ignore ignore here." Instead, declare proper TypeScript types for the Chatwoot properties.

Add a type declaration file or augment the Window interface at the top of this file:

+declare global {
+  interface Window {
+    chatwootSettings?: {
+      darkMode: 'auto' | 'light';
+      hideMessageBubble: boolean;
+    };
+    chatwootSDK?: {
+      run: (config: { websiteToken: string; baseUrl: string }) => void;
+    };
+    $chatwoot?: {
+      setUser: (id: number, data: { email: string; name: string; url: Location }) => void;
+      setCustomAttributes: (attributes: Record<string, unknown>) => void;
+      toggle: () => void;
+    };
+  }
+}
+
 type User = components['schemas']['PrivateUserAccountModel'];
 type Organization = components['schemas']['PrivateOrganizationModel'];

Then remove all @ts-ignore comments throughout the file.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In webapp/src/hooks/useChatwoot.ts around lines 34 to 48, remove the two
@ts-ignore comments and instead add proper TypeScript declarations for the
Chatwoot properties on the Window interface (either in a dedicated .d.ts file or
via a local declare global block at the top of this file). Declare
window.chatwootSettings with the expected shape (darkMode and hideMessageBubble)
and window.chatwootSDK with a run method that accepts the websiteToken/baseUrl
options, ensure the file picks up the declaration (export {} if using local
augmentation), then delete the @ts-ignore lines so the compiler recognizes the
properties.


async function loadChatwootOnce(websiteToken: string, darkMode: boolean) {
if (!chatwootLoadPromise) {
chatwootLoadPromise = loadChatwoot(websiteToken, darkMode);
}

await chatwootLoadPromise;
}

function setChatwootUser(user: User) {
// @ts-ignore
window.$chatwoot?.setUser(user.id, {
email: user!.username,
name: user!.name,
url: window.location,
});
}

function setChatwootAttributes(organization: Organization) {
const subscription = organization.activeCloudSubscription;
// @ts-ignore
window.$chatwoot?.setCustomAttributes({
plan: subscription?.plan?.name || 'free',
subscriptionStatus: subscription?.status || 'inactive',
organizationId: organization.id,
organizationName: organization.name,
enabledFeatures: organization.enabledFeatures.join(', '),
currentUserRole: organization.currentUserRole,
});
}

function toggleChatwoot() {
// @ts-ignore
window.$chatwoot?.toggle();
}

export function useChatwoot() {
const user = useUser();
const { preferredOrganization } = usePreferredOrganization();
const config = useConfig();
const token = config?.chatwootToken;

const enabledFeatures = preferredOrganization?.enabledFeatures;

const hasStandardSupport =
enabledFeatures?.includes('STANDARD_SUPPORT') ||
enabledFeatures?.includes('PREMIUM_SUPPORT');

const available = !!(token && user && hasStandardSupport);

const {
palette: { mode },
} = useTheme();

const darkMode = mode === 'dark';

useEffect(() => {
if (token) {
loadChatwootOnce(token, darkMode);
}
}, [token]);
Comment on lines +105 to +109
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add darkMode to the dependency array.

The useEffect depends on darkMode (used in loadChatwootOnce) but only includes [token] in the dependency array. If the user switches between light and dark themes, the Chatwoot widget settings won't be updated.

Apply this diff:

   useEffect(() => {
     if (token) {
       loadChatwootOnce(token, darkMode);
     }
-  }, [token]);
+  }, [token, darkMode]);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
useEffect(() => {
if (token) {
loadChatwootOnce(token, darkMode);
}
}, [token]);
useEffect(() => {
if (token) {
loadChatwootOnce(token, darkMode);
}
}, [token, darkMode]);
🤖 Prompt for AI Agents
In webapp/src/hooks/useChatwoot.ts around lines 105 to 109, the useEffect calls
loadChatwootOnce(token, darkMode) but only lists [token] as dependencies; add
darkMode to the dependency array so the effect re-runs when the theme changes
(i.e., change the dependencies to [token, darkMode]) to ensure the Chatwoot
widget updates when switching between light and dark modes.


const openChatwoot = async () => {
if (!available) {
return;
}

await loadChatwootOnce(token, darkMode);
setChatwootUser(user);
if (preferredOrganization) {
setChatwootAttributes(preferredOrganization);
}

toggleChatwoot();
};

return {
chatwootAvailable: available,
openChatwoot,
};
}