diff --git a/components/Chatbot/Chatbot.tsx b/components/Chatbot/Chatbot.tsx index 70ecea20..aa5dca25 100644 --- a/components/Chatbot/Chatbot.tsx +++ b/components/Chatbot/Chatbot.tsx @@ -15,6 +15,7 @@ import ChatbotHeader from '../Interface-Chatbot/ChatbotHeader'; import ChatbotHeaderTab from '../Interface-Chatbot/ChatbotHeaderTab'; import ChatbotTextField from '../Interface-Chatbot/ChatbotTextField'; import MessageList from '../Interface-Chatbot/Messages/MessageList'; +import NotificationPage from '../Interface-Chatbot/NotificationPage'; import StarterQuestions from '../Interface-Chatbot/Messages/StarterQuestions'; // Utils @@ -88,16 +89,15 @@ function Chatbot({ chatSessionId, tabSessionId }: ChatbotProps) { const dispatch = useAppDispatch(); // State management - const { show_widget_form, greetingMessage, isToggledrawer, chatsLoading, messageIds, subThreadId, helloMsgIds } = useCustomSelector((state) => { + const { show_widget_form, isToggledrawer, chatsLoading, subThreadId, showNotificationView, notificationsCount } = useCustomSelector((state) => { const widgetInfo = state.Hello?.[chatSessionId]?.widgetInfo return ({ show_widget_form: typeof widgetInfo?.show_widget_form === 'boolean' ? widgetInfo?.show_widget_form : state.Hello?.[chatSessionId]?.showWidgetForm, - greetingMessage: state.Hello?.[chatSessionId]?.greeting as any, isToggledrawer: state.Chat.isToggledrawer, chatsLoading: state.Chat.chatsLoading, - messageIds: state.Chat.messageIds, subThreadId: state.Chat.subThreadId, - helloMsgIds: state.Chat.helloMsgIds + notificationsCount: (state.Chat.notifications || []).length, + showNotificationView: state.appInfo?.[tabSessionId]?.showNotificationView || false, }) }); @@ -109,8 +109,6 @@ function Chatbot({ chatSessionId, tabSessionId }: ChatbotProps) { const { isHelloUser, currentChatId, isDefaultNavigateToChatScreen } = useReduxStateManagement({ chatSessionId, tabSessionId }); - // Initialize RTLayer event listeners - // Effect to open drawer for new human users useEffect(() => { if (isHelloUser && !currentChatId && !mountedRef.current) { @@ -138,12 +136,6 @@ function Chatbot({ chatSessionId, tabSessionId }: ChatbotProps) { timeoutIdRef ]); - // Check if chat is empty - const isChatEmpty = isHelloUser - ? (!subThreadId || helloMsgIds[subThreadId]?.length === 0) && - (!greetingMessage || (!greetingMessage.text && !greetingMessage?.options?.length)) - : !subThreadId || messageIds[subThreadId]?.length === 0; - return (
@@ -170,17 +162,30 @@ function Chatbot({ chatSessionId, tabSessionId }: ChatbotProps) {
)} - {/* Form and UI components */} - {isHelloUser && show_widget_form && ( - + {/* Form / Call / Tab overlays — hide when user is browsing notification list + to prevent "Enter your details" form from covering notification content */} + {!(showNotificationView && notificationsCount > 0) && ( + <> + {isHelloUser && show_widget_form && ( + + )} + + + )} - - - {isChatEmpty ? ( - - ) : ( + {/* Main view routing: + 1. showNotificationView + notifications → NotificationPage (list of push notifications) + 2. subThreadId truthy → ActiveChatView (real channel OR notification-launched chat) + 3. else → EmptyChatView (new chat / idle state) */} + {showNotificationView && notificationsCount > 0 ? ( + + ) : subThreadId ? ( + // A thread is selected (real channel OR notification-launched chat) → active chat + ) : ( + // Fresh state, nothing selected → empty / new chat view + )} diff --git a/components/Chatbot/hooks/useHelloEffects.ts b/components/Chatbot/hooks/useHelloEffects.ts index 5a7a6c8c..06dd8a52 100644 --- a/components/Chatbot/hooks/useHelloEffects.ts +++ b/components/Chatbot/hooks/useHelloEffects.ts @@ -6,11 +6,12 @@ import useSocket from '@/hooks/socket'; import useSocketEvents from '@/hooks/socketEventHandler'; import socketManager from '@/hooks/socketManager'; import { setDataInAppInfoReducer } from '@/store/appInfo/appInfoSlice'; +import { setHelloEventMessage, setOpenHelloForm } from '@/store/chat/chatSlice'; import { setAgentTeams, setGreeting, setHelloClientInfo, setHelloKeysData, setJwtToken, setUnReadCount, setWidgetInfo } from '@/store/hello/helloSlice'; import { GetSessionStorageData, SetSessionStorage } from '@/utils/ChatbotUtility'; import { useCustomSelector } from '@/utils/deepCheckSelector'; import { emitEventToParent } from '@/utils/emitEventsToParent/emitEventsToParent'; -import { cleanObject, getLocalStorage } from '@/utils/utilities'; +import { cleanObject, generateNewId, getLocalStorage } from '@/utils/utilities'; import debounce from 'lodash.debounce'; import { useCallback, useContext, useEffect } from 'react'; import { useDispatch } from 'react-redux'; @@ -22,17 +23,6 @@ import { useScreenSize } from './useScreenSize'; import { useTabVisibility } from './useTabVisibility'; import { useReplyContext } from '@/components/Interface-Chatbot/contexts/ReplyContext'; -interface HelloMessage { - role: string; - message_id?: string; - from_name?: string; - content: string; - id?: string; - chat_id?: string; - urls?: string[]; - channel?: string; -} - interface UseHelloIntegrationProps { messageRef: React.RefObject chatSessionId: string; @@ -50,7 +40,7 @@ export const useHelloEffects = ({ chatSessionId, messageRef, tabSessionId }: Use const { currentChannelId, isHelloUser } = useReduxStateManagement({ chatSessionId, tabSessionId }); - const { companyId, botId, reduxChatSessionId, totalNoOfUnreadMsgs, isToggledrawer, isChatbotOpen, isChatbotMinimized, unReadCountInCurrentChannel, callToken, demo_widget, helloVariables } = useCustomSelector((state) => ({ + const { companyId, botId, reduxChatSessionId, totalNoOfUnreadMsgs, isToggledrawer, isChatbotOpen, isChatbotMinimized, unReadCountInCurrentChannel, callToken, demo_widget, helloVariables, unreadNotificationCount } = useCustomSelector((state) => ({ companyId: state.Hello?.[chatSessionId]?.widgetInfo?.company_id || '', botId: state.Hello?.[chatSessionId]?.widgetInfo?.bot_id || '', reduxChatSessionId: state.draftData?.chatSessionId, @@ -61,6 +51,7 @@ export const useHelloEffects = ({ chatSessionId, messageRef, tabSessionId }: Use }, 0); return unreadCount; })(), + unreadNotificationCount: (state.Chat?.notifications || []).filter((n: any) => !n.read).length, isToggledrawer: state.Chat?.isToggledrawer, isChatbotOpen: state.appInfo?.[tabSessionId]?.isChatbotOpen, isChatbotMinimized: state.draftData?.isChatbotMinimized, @@ -92,9 +83,10 @@ export const useHelloEffects = ({ chatSessionId, messageRef, tabSessionId }: Use useEffect(() => { if (!demo_widget) { - emitEventToParent('SET_BADGE_COUNT', { badgeCount: totalNoOfUnreadMsgs > 99 ? '99+' : totalNoOfUnreadMsgs, channelId: '*' }) + const combined = (totalNoOfUnreadMsgs || 0) + (unreadNotificationCount || 0); + emitEventToParent('SET_BADGE_COUNT', { badgeCount: combined > 99 ? '99+' : combined, channelId: '*' }) } - }, [totalNoOfUnreadMsgs, demo_widget]) + }, [totalNoOfUnreadMsgs, unreadNotificationCount, demo_widget]) useSocketEvents({ messageRef, fetchChannels, chatSessionId, setLoading, tabSessionId }); useNotificationSocketEventHandler({ chatSessionId }) @@ -167,6 +159,56 @@ export const useHelloEffects = ({ chatSessionId, messageRef, tabSessionId }: Use clearReply(); }, [currentChannelId]); + // Handle OPEN_WITH_NOTIFICATION messages from parent widget + // When user clicks the launcher message preview popup, the parent script posts + // 'OPEN_WITH_NOTIFICATION' with the raw notification HTML. This opens a fresh + // chat thread and inserts the notification as a bot-side message (rendered via + // ShadowDomComponent) so the user can reply to the campaign notification. + useEffect(() => { + const handleNotificationMessage = (event: MessageEvent) => { + if (event?.data?.type === 'OPEN_WITH_NOTIFICATION') { + const content = event?.data?.data?.content || ''; + // Generate a fresh sub-thread key so this new chat has its own bucket + const newSubThreadId = `notification-${generateNewId()}`; + // Reset to a fresh channel and close notification view + dispatch(setDataInAppInfoReducer({ + subThreadId: newSubThreadId, + currentTeamId: '', + currentChannelId: '', + currentChatId: '', + overrideChannelId: '', + showNotificationView: false, + })); + // Bypass "Enter your details" form popup BEFORE the message is added + dispatch(setOpenHelloForm(false)); + // Push the notification as a bot-side message on the LEFT in the new chat + // (rendered via ShadowDomComponent for pushNotification message_type) + dispatch(setHelloEventMessage({ + subThreadId: newSubThreadId, + message: { + type: 'chat', + message_type: 'pushNotification', + sender_id: 'bot', + is_auto_response: true, + content: { + text: content, + attachment: [] + }, + from_name: '', + id: generateNewId(), + } + })); + } + }; + + if (isHelloUser) { + window.addEventListener('message', handleNotificationMessage); + return () => { + window.removeEventListener('message', handleNotificationMessage); + }; + } + }, [isHelloUser]); + const initializeHelloServices = async (widgetToken: string = '') => { // Prevent duplicate initialization @@ -313,9 +355,7 @@ export const useHelloEffects = ({ chatSessionId, messageRef, tabSessionId }: Use } } - if (true) { - emitEventToParent("ENABLE_DOMAIN_TRACKING") - } + emitEventToParent("ENABLE_DOMAIN_TRACKING") } catch (error) { console.error("Error initializing Hello services:", error); diff --git a/components/Interface-Chatbot/ChatbotDrawer.tsx b/components/Interface-Chatbot/ChatbotDrawer.tsx index d2ba2073..278552b4 100644 --- a/components/Interface-Chatbot/ChatbotDrawer.tsx +++ b/components/Interface-Chatbot/ChatbotDrawer.tsx @@ -1,7 +1,7 @@ 'use client'; -import { AlignLeft, ChevronDown, ChevronRight, ChevronUp, MessageSquareText, Phone, Send, Users, X } from "lucide-react"; -import { useContext, useEffect, useMemo, useState } from "react"; +import { AlignLeft, Bell, ChevronDown, ChevronRight, ChevronUp, MessageSquareText, Phone, Send, Users, X } from "lucide-react"; +import { useContext, useCallback, useEffect, useMemo, useState } from "react"; import { useDispatch } from "react-redux"; // API and Services @@ -54,11 +54,12 @@ const ChatbotDrawer = ({ const { setNewMessage, setOptions, setImages, setLoading, setToggleDrawer } = useChatActions(); - const { images, allMessages, allMessagesData, isToggledrawer } = useCustomSelector((state) => ({ + const { images, allMessages, allMessagesData, isToggledrawer, notifications } = useCustomSelector((state) => ({ images: state.Chat.images || [], allMessages: state.Chat.messageIds || [], allMessagesData: state.Chat.msgIdAndDataMap || {}, isToggledrawer: state.Chat.isToggledrawer, + notifications: state.Chat.notifications || [], })) const { currentChatId, currentTeamId, currentChannelId } = useReduxStateManagement({ chatSessionId, tabSessionId }); @@ -239,6 +240,13 @@ const ChatbotDrawer = ({ focusTextField(); }; + const unreadNotificationCount = notifications.filter(n => !n.read).length; + + const handleOpenNotificationView = useCallback(() => { + dispatch(setDataInAppInfoReducer({ showNotificationView: true })); + if (isSmallScreen) setToggleDrawer(false); + }, [dispatch, isSmallScreen, setToggleDrawer]); + // Memoized components const DrawerList = useMemo(() => (
@@ -265,11 +273,45 @@ const ChatbotDrawer = ({ const TeamsList = useMemo(() => ( <> - {((channelList?.length > 0 && channelList.some((thread: any) => thread?.id)) || teamsList?.length > 0) && ( -
+ {((channelList?.length > 0 && channelList.some((thread: any) => thread?.id)) || teamsList?.length > 0 || notifications.length > 0) && ( +
+ {/* Notifications Row — shown above conversations when push notifications exist. + Clicking navigates to NotificationPage via showNotificationView state. */} + {notifications.length > 0 && ( +
+
+
+
+ +
+
+
+ Notifications +
+
+ {unreadNotificationCount > 0 && ( + + {unreadNotificationCount > 99 ? '99+' : unreadNotificationCount} + + )} + +
+
+
+ )} + {/* Conversations Section */} {(channelList || []).length > 0 && channelList.some((thread: any) => thread?.id) && ( -
+
0 ? '' : 'mt-3'}`}>

Continue Conversations

@@ -301,21 +343,29 @@ const ChatbotDrawer = ({ || 'Conversation'; const subtitleHtml = (() => { + const stripHtmlToText = (html: string) => { + if (!html) return ''; + const tmp = document.createElement('div'); + tmp.innerHTML = html; + return (tmp.textContent || tmp.innerText || '').replace(/\s+/g, ' ').trim(); + }; if (lastMessage) { const isUserMessage = lastMessage?.role == "user" || lastMessage?.role === "voice_call"; - const text = lastMessage?.message_type === 'pushNotification' + const rawText = lastMessage?.message_type === 'pushNotification' ? "Custom Notification" : (lastMessage.messageJson?.text || (lastMessage.messageJson?.attachment?.length > 0 ? "Attachment" : lastMessage.messageJson?.message_type || "New conversation")); - return `${isUserMessage ? "You: " : ""}${text}`; + const text = stripHtmlToText(rawText); + return `${isUserMessage ? "You: " : ""}${text || "New conversation"}`; } if (channel?.last_message) { const isYou = !channel?.last_message?.message?.sender_id && !channel?.last_message?.message.is_auto_response; - const text = channel?.last_message?.message?.content?.text + const rawText = channel?.last_message?.message?.content?.text || (channel?.last_message?.message?.content?.attachment?.length > 0 ? "Attachment" : channel?.last_message?.message?.message_type || "New conversation"); - return `${isYou ? "You: " : ""}${text}`; + const text = stripHtmlToText(rawText); + return `${isYou ? "You: " : ""}${text || "New conversation"}`; } return "New conversation"; })(); @@ -383,9 +433,10 @@ const ChatbotDrawer = ({ )}
+ className="text-xs opacity-70 line-clamp-1 break-all" + > + {subtitleHtml} +
@@ -423,7 +474,7 @@ const ChatbotDrawer = ({ {/* Teams Section */} {(teamsList || []).length > 0 && ( -
+
0 && ((channelList || []).length > 0 && channelList.some((thread: any) => thread?.id))? '' : 'mt-3'}`}>

Talk to our teams

@@ -498,7 +549,7 @@ const ChatbotDrawer = ({ )} {/* Voice Call Section */} - {(voice_call_widget || (teamsList || []).length === 0) && ( + {voice_call_widget && (
- - - Message - - - )} +
)} @@ -556,7 +605,10 @@ const ChatbotDrawer = ({ handleSendMessageWithNoTeam, handleVoiceCall, allMessages, - allMessagesData + allMessagesData, + notifications, + unreadNotificationCount, + handleOpenNotificationView, //tick ]); @@ -623,7 +675,7 @@ const ChatbotDrawer = ({
{/* Content area with overflow handling - the scrollbar will appear at the edge */} -
+
{!isHelloUser ? DrawerList : TeamsList}
diff --git a/components/Interface-Chatbot/ChatbotHeader.tsx b/components/Interface-Chatbot/ChatbotHeader.tsx index e74f3dd4..f0c7a387 100644 --- a/components/Interface-Chatbot/ChatbotHeader.tsx +++ b/components/Interface-Chatbot/ChatbotHeader.tsx @@ -317,7 +317,7 @@ const ChatbotHeader: React.FC = ({ preview = false, chatSess
); - const { isToggledrawer, bridgeName: reduxBridgeName, headerButtons, messageIds, lastMessage, unReadCount, isChatbotMinimized, isFullScreen, isOpenInParentContainer } = useCustomSelector((state) => { + const { isToggledrawer, bridgeName: reduxBridgeName, headerButtons, messageIds, lastMessage, unReadCount, isChatbotMinimized, isFullScreen, isOpenInParentContainer, unreadNotifications, unreadTotalMsgs } = useCustomSelector((state) => { const helloConfig = state.Hello?.[chatSessionId]?.helloConfig; const fullScreen = helloConfig?.fullScreen @@ -338,7 +338,11 @@ const ChatbotHeader: React.FC = ({ preview = false, chatSess )?.widget_unread_count || 0, isChatbotMinimized: state.draftData?.isChatbotMinimized || false, isFullScreen: (fullScreen === true || fullScreen === 'true') ?? false, - isOpenInParentContainer: parentId + isOpenInParentContainer: parentId, + unreadNotifications: (state.Chat?.notifications || []).filter((n: any) => !n.read).length, + unreadTotalMsgs: (state.Hello?.[chatSessionId]?.channelListData?.channels || []).reduce( + (acc: number, c: any) => acc + (c?.widget_unread_count || 0), 0 + ) } } ) @@ -375,7 +379,9 @@ const ChatbotHeader: React.FC = ({ preview = false, chatSess isHelloUser, teams, agentTeamName, - isMobileSDK + isMobileSDK, + showNotificationView, + notificationsCount } = useCustomSelector((state: $ReduxCoreType) => { const show_close_button = state.Hello?.[chatSessionId]?.helloConfig?.show_close_button return ({ @@ -391,7 +397,8 @@ const ChatbotHeader: React.FC = ({ preview = false, chatSess agentTeamName: getAgentTeamName(state, chatSessionId, currentChannelId), subThreadList: state.Interface?.[chatSessionId]?.interfaceContext?.[bridgeName]?.threadList?.[threadId] || [], isHelloUser: state.draftData?.isHelloUser || false, - voice_call_widget: state.Hello?.[chatSessionId]?.widgetInfo?.voice_call_widget || false + showNotificationView: state.appInfo?.[tabSessionId]?.showNotificationView || false, + notificationsCount: (state.Chat?.notifications || []).length }) }); // Determine if we should show the create thread button @@ -453,18 +460,33 @@ const ChatbotHeader: React.FC = ({ preview = false, chatSess }, [teams, currentTeamId]); // Memoized drawer toggle button + const hasUnreadIndicator = (unreadNotifications || 0) > 0 || (unreadTotalMsgs || 0) > 0; + const DrawerToggleButton = useMemo(() => { if (!(subThreadList?.length > 1 || isHelloUser)) return null; return ( ); - }, [subThreadList?.length, isHelloUser, isToggledrawer, setToggleDrawer]); + }, [subThreadList?.length, isHelloUser, isToggledrawer, setToggleDrawer, hasUnreadIndicator]); // Memoized create thread button const CreateThreadButton = useMemo(() => { @@ -484,7 +506,8 @@ const ChatbotHeader: React.FC = ({ preview = false, chatSess // Memoized header title section const HeaderTitleSection = useMemo(() => { - const displayTitle = isChatbotMinimized && lastMessage?.role === 'user' ? 'You' : chatTitle || chatbotTitle || (isHelloUser ? (agentTeamName || teamName || "Conversation") : "AI Assistant"); + const isNotificationActive = showNotificationView && notificationsCount > 0; + const displayTitle = isNotificationActive ? 'Notifications' : (isChatbotMinimized && lastMessage?.role === 'user' ? 'You' : chatTitle || chatbotTitle || (isHelloUser ? (agentTeamName || teamName || "Conversation") : "AI Assistant")); const displaySubtitle = chatSubTitle || chatbotSubtitle || "Do you have any questions? Ask us!"; // Minimized version of the header @@ -514,14 +537,16 @@ const ChatbotHeader: React.FC = ({ preview = false, chatSess {lastMessage && (

:

-
0 ? "Attachment" : - lastMessage.messageJson?.message_type || - "New conversation")) - }}>
+
{(() => { + if (lastMessage?.message_type === 'pushNotification') return "Custom Notification"; + const raw = lastMessage.messageJson?.text + || (lastMessage.messageJson?.attachment?.length > 0 ? "Attachment" + : lastMessage.messageJson?.message_type || "New conversation"); + if (typeof document === 'undefined') return raw; + const tmp = document.createElement('div'); + tmp.innerHTML = raw || ''; + return (tmp.textContent || tmp.innerText || '').replace(/\s+/g, ' ').trim() || 'New conversation'; + })()}
)}
@@ -570,7 +595,9 @@ const ChatbotHeader: React.FC = ({ preview = false, chatSess agentTeamName, isChatbotMinimized, lastMessage, - unReadCount + unReadCount, + showNotificationView, + notificationsCount ]); // Memoized fullscreen toggle button diff --git a/components/Interface-Chatbot/Messages/HumanOrBotMessage.tsx b/components/Interface-Chatbot/Messages/HumanOrBotMessage.tsx index 81144aa3..7751d9e8 100644 --- a/components/Interface-Chatbot/Messages/HumanOrBotMessage.tsx +++ b/components/Interface-Chatbot/Messages/HumanOrBotMessage.tsx @@ -198,7 +198,6 @@ ShadowDomComponent.displayName = 'ShadowDomComponent'; const MessageContent = React.memo(({ message, isBot }: { message: any; isBot: boolean }) => { const content = useMemo(() => { const messageType = message?.message_type; - switch (messageType) { case MESSAGE_TYPES.VIDEO_CALL: return ; @@ -213,6 +212,8 @@ const MessageContent = React.memo(({ message, isBot }: { message: any; isBot: bo case MESSAGE_TYPES.FEEDBACK: return ; + // Push notification content rendered in a Shadow DOM to safely display + // raw campaign HTML without affecting the parent page styles case MESSAGE_TYPES.PUSH_NOTIFICATION: return ( { const role = message?.role; - switch (role) { case ROLE_USER: return ( diff --git a/components/Interface-Chatbot/NotificationPage.tsx b/components/Interface-Chatbot/NotificationPage.tsx new file mode 100644 index 00000000..1fb2b674 --- /dev/null +++ b/components/Interface-Chatbot/NotificationPage.tsx @@ -0,0 +1,173 @@ +'use client'; + +import { Bell, X } from "lucide-react"; +import React, { useCallback } from "react"; +import { useDispatch } from "react-redux"; + +import { setDataInAppInfoReducer } from "@/store/appInfo/appInfoSlice"; +import { removeNotification, setHelloEventMessage } from "@/store/chat/chatSlice"; +import { useCustomSelector } from "@/utils/deepCheckSelector"; +import { generateNewId } from "@/utils/utilities"; +import { useColor } from "../Chatbot/hooks/useColor"; +import { useChatActions } from "../Chatbot/hooks/useChatActions"; + +/** + * NotificationPage — displays a list of push notifications (message_type: "Message") + * received from campaigns via the notification socket channel. + * + * Each notification is rendered in a sandboxed iframe (via buildIframeSrcDoc) to safely + * display the raw HTML content. Users can dismiss notifications (X button) or initiate + * a new chat based on the notification ("Chat with us" button). + * + * "Chat with us" opens a fresh chat thread with the notification content shown as a + * bot-side message (rendered via ShadowDomComponent with message_type: 'pushNotification'). + */ +const NotificationPage = () => { + const dispatch = useDispatch(); + const { primaryTextColor, primaryTintColor } = useColor(); + const { setImages } = useChatActions(); + + const { notifications, images } = useCustomSelector((state) => ({ + notifications: state.Chat.notifications || [], + images: state.Chat.images || [], + })); + + const handleDismiss = useCallback((e: React.MouseEvent, notificationId: string) => { + e.stopPropagation(); + dispatch(removeNotification(notificationId)); + }, [dispatch]); + + const handleChatWithUs = useCallback((notification: { id: string; content: string; timestamp: number; read: boolean }) => { + dispatch(removeNotification(notification.id)); + if (images?.length > 0) setImages([]); + // Generate a fresh sub-thread key so this new chat has its own bucket + const newSubThreadId = `notification-${generateNewId()}`; + // Reset to a fresh thread so a new chat is opened + dispatch(setDataInAppInfoReducer({ + showNotificationView: false, + subThreadId: newSubThreadId, + currentTeamId: '', + currentChannelId: '', + currentChatId: '', + overrideChannelId: '', + })); + // Push the notification as a bot-side message on the LEFT in the new chat + // (rendered via ShadowDomComponent for pushNotification message_type) + dispatch(setHelloEventMessage({ + subThreadId: newSubThreadId, + message: { + type: 'chat', + message_type: 'pushNotification', + sender_id: 'bot', + is_auto_response: true, + content: { + text: notification.content, + attachment: [] + }, + from_name: '', + id: generateNewId(), + } + })); + }, [dispatch, images, setImages]); + + const formatTime = (timestamp: number) => { + const diff = Date.now() - timestamp; + const minutes = Math.floor(diff / 60000); + if (minutes < 1) return 'Just now'; + if (minutes < 60) return `${minutes}m ago`; + const hours = Math.floor(minutes / 60); + if (hours < 24) return `${hours}h ago`; + const days = Math.floor(hours / 24); + return `${days}d ago`; + }; + + const buildIframeSrcDoc = (html: string) => { + // If content already has full HTML structure, use as-is; otherwise wrap it. + const hasHtmlTag = /]/i.test(html); + if (hasHtmlTag) return html; + return `${html}`; + }; + + return ( +
+ {/* Notification List */} +
+
+ {notifications.length === 0 ? ( +
+ +

No notifications

+
+ ) : ( +
+ {notifications.map((notification, idx) => { + const timeLabel = formatTime(notification.timestamp); + const srcDoc = buildIframeSrcDoc(notification.content); + const isLast = idx === notifications.length - 1; + + return ( +
+
+ {/* Bell icon */} +
+
+ +
+
+ + {/* Content */} +
+
+ + {timeLabel} + +
+ +
+
+
+