diff --git a/src/app/UserWrapper.tsx b/src/app/UserWrapper.tsx index e69ea85..fac8487 100644 --- a/src/app/UserWrapper.tsx +++ b/src/app/UserWrapper.tsx @@ -14,6 +14,7 @@ import { AppThemeProvider } from 'theme/AppThemeProvider' import HTTPProvider from 'providers/http' import { Crisp } from "crisp-sdk-web"; import { useMobile, useSnackbar } from 'hooks' +import { initCrisp } from '../chat/CrispChat' // import FirmwareLoader from './FirmwareLoader' const reducer = (state: GlobalState, action: GlobalStateAction): GlobalState => { @@ -113,20 +114,14 @@ export default function UserWrapper(props: Props) { }, [setGlobal]) useEffect(() => { - if (!crispInitialized.current && global?.user) { - Crisp.configure(import.meta.env.VITE_CRISP_WEBSITE_ID); - crispInitialized.current = true; - Crisp.session.reset(); - Crisp.user.setEmail(global?.user.settings.email); - // Optional: Set nickname or other info - Crisp.user.setNickname(global?.user.settings.name || global?.user.settings.email); - Crisp.user.setPhone(global?.user.settings.phoneNumber); - - // For extra security (marks email as verified, prevents spoofing): - // Crisp.user.setEmail(user.email, "your-hmac-signature-here"); // See Crisp docs for signing - - // Optional: For session continuity across devices/browsers - Crisp.setTokenId(global?.user.id()); // e.g., your DB user ID + if (global?.user) { + initCrisp({ + websiteId: import.meta.env.VITE_CRISP_WEBSITE_ID, + email: global.user.settings.email, + nickname: global.user.settings.name || global.user.settings.email, + phone: global.user.settings.phoneNumber, + tokenId: global.user.id(), + }); } }, [global]); diff --git a/src/assets/common/robotIconDark.png b/src/assets/common/robotIconDark.png new file mode 100644 index 0000000..85c8d51 Binary files /dev/null and b/src/assets/common/robotIconDark.png differ diff --git a/src/assets/common/robotIconLight.png b/src/assets/common/robotIconLight.png new file mode 100644 index 0000000..724fd18 Binary files /dev/null and b/src/assets/common/robotIconLight.png differ diff --git a/src/chat/ChatDrawer.tsx b/src/chat/ChatDrawer.tsx index 9b02649..2ae3081 100644 --- a/src/chat/ChatDrawer.tsx +++ b/src/chat/ChatDrawer.tsx @@ -1,5 +1,5 @@ import { ChevronRight } from "@mui/icons-material"; -import { Avatar, Box, Drawer, IconButton, Theme, Typography } from "@mui/material"; +import { Avatar, Box, Divider, Drawer, IconButton, Theme, Typography } from "@mui/material"; import { makeStyles } from "@mui/styles"; import { useMobile } from "hooks"; import { Team } from "models"; @@ -7,6 +7,8 @@ import Chat from "./Chat"; import { pond } from "protobuf-ts/pond"; import { useEffect, useState } from "react"; import { useTeamAPI } from "providers"; +import { openCrispChat } from './CrispChat'; +import RobotIcon from "products/CommonIcons/robotIcon"; const useStyles = makeStyles((theme: Theme) => ({ chatDrawer: { @@ -60,6 +62,11 @@ export function ChatDrawer(props: Props) { }) }, [teamAPI]) + const openCrisp = () => { + openCrispChat() + onClose() + } + return ( + + + + {teams.map((t, i)=> { if (t.settings?.key === team.key()) return null; return ( diff --git a/src/chat/ChatMessage.tsx b/src/chat/ChatMessage.tsx index ac9600b..50b9b61 100644 --- a/src/chat/ChatMessage.tsx +++ b/src/chat/ChatMessage.tsx @@ -74,9 +74,22 @@ export default function ChatMessage(props: Props) { const mappedPerm = pond.Permission[perm as keyof typeof pond.Permission]; return mappedPerm; // Return null for invalid keys }).filter(Boolean); // Optionally filter out any null values + if (user.id() === props.chat.profile?.id) { + perms = [ + pond.Permission.PERMISSION_READ, + pond.Permission.PERMISSION_WRITE + ] + } } else { // console.error("data.permissions is not an array or does not exist"); - perms = []; + if (user.id() === props.chat.profile?.id) { + perms = [ + pond.Permission.PERMISSION_READ, + pond.Permission.PERMISSION_WRITE + ] + } else { + perms = []; + } } return perms } diff --git a/src/chat/CrispChat.ts b/src/chat/CrispChat.ts new file mode 100644 index 0000000..a0220fb --- /dev/null +++ b/src/chat/CrispChat.ts @@ -0,0 +1,111 @@ +import { Crisp } from "crisp-sdk-web"; + +const CRISP_HIDDEN_STYLE_ID = "crisp-hide-override"; +const ANIMATION_DURATION_MS = 300; + +let initialized = false; + +/** + * Initialize Crisp and immediately hide the default chat button. + * Call this once on app load (e.g., in UserWrapper after user data is ready). + */ +export function initCrisp(opts: { + websiteId: string; + email?: string; + nickname?: string; + phone?: string; + tokenId?: string; +}) { + if (initialized) return; + + Crisp.configure(opts.websiteId); + Crisp.session.reset(); + + if (opts.email) Crisp.user.setEmail(opts.email); + if (opts.nickname) Crisp.user.setNickname(opts.nickname); + if (opts.phone) Crisp.user.setPhone(opts.phone); + if (opts.tokenId) Crisp.setTokenId(opts.tokenId); + + initialized = true; + + // Hide the chat button off-screen + injectCrispStyles(); + + // Auto-hide when the user closes the chat + Crisp.chat.onChatClosed(() => { + hideCrispChatButton(); + }); +} + +/** + * Inject base styles that keep Crisp hidden until we want it. + * Uses opacity + visibility so Crisp keeps its default bottom-right positioning. + */ +function injectCrispStyles() { + let style = document.getElementById(CRISP_HIDDEN_STYLE_ID); + if (!style) { + style = document.createElement("style"); + style.id = CRISP_HIDDEN_STYLE_ID; + document.head.appendChild(style); + } + + style.textContent = ` + #crisp-chatbox { + z-index: 99999 !important; + transition: opacity ${ANIMATION_DURATION_MS}ms ease-in-out, + visibility ${ANIMATION_DURATION_MS}ms ease-in-out !important; + opacity: 0 !important; + visibility: hidden !important; + pointer-events: none !important; + } + #crisp-chatbox.crisp-visible { + opacity: 1 !important; + visibility: visible !important; + pointer-events: auto !important; + } + `; +} + +/** + * Show the Crisp widget and open the chat window. + * Safe to call from any onClick handler. + */ +export function openCrispChat() { + const chatbox = document.getElementById("crisp-chatbox"); + if (chatbox) { + chatbox.classList.add("crisp-visible"); + setTimeout(() => { + Crisp.chat.open(); + }, ANIMATION_DURATION_MS); + } else { + Crisp.chat.open(); + } +} + +/** + * Close the chat window and hide the widget. + */ +export function closeCrispChat() { + Crisp.chat.close(); + hideCrispChatButton(); +} + +/** + * Show the default Crisp button without opening the chat. + */ +export function showCrispButton() { + const chatbox = document.getElementById("crisp-chatbox"); + if (chatbox) { + chatbox.classList.add("crisp-visible"); + } +} + +/** + * Hide the default Crisp button. + */ +export function hideCrispChatButton() { + const chatbox = document.getElementById("crisp-chatbox"); + if (chatbox) { + chatbox.classList.remove("crisp-visible"); + } +} \ No newline at end of file diff --git a/src/pages/Devices.tsx b/src/pages/Devices.tsx index 0e030c1..80a3f0d 100644 --- a/src/pages/Devices.tsx +++ b/src/pages/Devices.tsx @@ -323,21 +323,21 @@ export default function Devices() { return false } - useEffect(() => { - console.log("has no2: "+hasNo2) - }, [hasNo2]) + // useEffect(() => { + // console.log("has no2: "+hasNo2) + // }, [hasNo2]) - useEffect(() => { - console.log("has co2: "+hasCo2) - }, [hasCo2]) + // useEffect(() => { + // console.log("has co2: "+hasCo2) + // }, [hasCo2]) - useEffect(() => { - console.log("has co: "+hasNo2) - }, [hasNo2]) + // useEffect(() => { + // console.log("has co: "+hasNo2) + // }, [hasNo2]) - useEffect(() => { - console.log("has o2: "+hasO2) - }, [hasO2]) + // useEffect(() => { + // console.log("has o2: "+hasO2) + // }, [hasO2]) const openProvisionDialog = () => { setIsProvisionDialogOpen(true); diff --git a/src/products/CommonIcons/marketplaceIcon.tsx b/src/products/CommonIcons/marketplaceIcon.tsx index 20933b9..ca2f35b 100644 --- a/src/products/CommonIcons/marketplaceIcon.tsx +++ b/src/products/CommonIcons/marketplaceIcon.tsx @@ -2,7 +2,6 @@ import MarketWhite from "assets/marketplaceImages/marketplaceIconLight.png"; import MarketBlack from "assets/marketplaceImages/marketplaceIconDark.png"; import { ImgIcon } from "common/ImgIcon"; import { useThemeType } from "hooks"; -import React from "react"; interface Props { type?: "light" | "dark"; diff --git a/src/products/CommonIcons/robotIcon.tsx b/src/products/CommonIcons/robotIcon.tsx new file mode 100644 index 0000000..8c86754 --- /dev/null +++ b/src/products/CommonIcons/robotIcon.tsx @@ -0,0 +1,23 @@ +import RobotWhite from "assets/common/robotIconLight.png"; +import RobotBlack from "assets/common/robotIconDark.png"; +import { ImgIcon } from "common/ImgIcon"; +import { useThemeType } from "hooks"; + +interface Props { + type?: "light" | "dark"; +} + +export default function RobotIcon(props: Props) { + const themeType = useThemeType(); + const { type } = props; + + const src = () => { + if (type) { + return type === "light" ? RobotWhite : RobotBlack; + } + + return themeType === "light" ? RobotBlack : RobotWhite; + }; + + return ; +}