import React, { useCallback, useRef } from 'react'; import { useSubscription } from '@apollo/client'; import { Meteor } from 'meteor/meteor'; import { isEqual } from 'radash'; import { defineMessages, useIntl } from 'react-intl'; import { layoutSelect, layoutSelectInput, layoutDispatch } from '/imports/ui/components/layout/context'; import { Input, Layout } from '/imports/ui/components/layout/layoutTypes'; import { PANELS } from '/imports/ui/components/layout/enums'; import { usePreviousValue } from '/imports/ui/components/utils/hooks'; import { stripTags, unescapeHtml } from '/imports/utils/string-utils'; import { ChatMessageType } from '/imports/ui/core/enums/chat'; import { CHAT_MESSAGE_PRIVATE_STREAM, CHAT_MESSAGE_PUBLIC_STREAM, PublicMessageStreamResponse, PrivateMessageStreamResponse, Message, } from './queries'; import ChatPushAlert from './push-alert/component'; import Styled from './styles'; import Service from './service'; const intlMessages = defineMessages({ appToastChatPublic: { id: 'app.toast.chat.public', description: 'when entry various message', }, appToastChatPrivate: { id: 'app.toast.chat.private', description: 'when entry various message', }, appToastChatSystem: { id: 'app.toast.chat.system', description: 'system for use', }, publicChatClear: { id: 'app.chat.clearPublicChatMessage', description: 'message of when clear the public chat', }, publicChatMsg: { id: 'app.toast.chat.public', description: 'public chat toast message title', }, privateChatMsg: { id: 'app.toast.chat.private', description: 'private chat toast message title', }, pollResults: { id: 'app.toast.chat.poll', description: 'chat toast message for polls', }, pollResultsClick: { id: 'app.toast.chat.pollClick', description: 'chat toast click message for polls', }, }); const CHAT_CONFIG = Meteor.settings.public.chat; const PUBLIC_CHAT_ID = CHAT_CONFIG.public_id; const PUBLIC_GROUP_CHAT_ID = CHAT_CONFIG.public_group_id; const ALERT_DURATION = 4000; // 4 seconds interface ChatAlertContainerGraphqlProps { audioAlertEnabled: boolean; pushAlertEnabled: boolean; } interface ChatAlertGraphqlProps extends ChatAlertContainerGraphqlProps { idChatOpen: string; layoutContextDispatch: () => void; publicUnreadMessages: Array | null; privateUnreadMessages: Array | null; } const ChatAlertGraphql: React.FC = (props) => { const { audioAlertEnabled, idChatOpen, layoutContextDispatch, pushAlertEnabled, publicUnreadMessages, privateUnreadMessages, } = props; const intl = useIntl(); const history = useRef(new Set()); const prevPublicUnreadMessages = usePreviousValue(publicUnreadMessages); const prevPrivateUnreadMessages = usePreviousValue(privateUnreadMessages); const publicMessagesDidChange = !isEqual(prevPublicUnreadMessages, publicUnreadMessages); const privateMessagesDidChange = !isEqual(prevPrivateUnreadMessages, privateUnreadMessages); const shouldRenderPublicChatAlerts = publicMessagesDidChange && publicUnreadMessages; const shouldRenderPrivateChatAlerts = privateMessagesDidChange && privateUnreadMessages; const shouldPlayAudioAlert = useCallback( (m: Message) => m.chatId !== idChatOpen && !history.current.has(m.messageId), [idChatOpen, history.current], ); let playAudioAlert = false; if (shouldRenderPublicChatAlerts) { playAudioAlert = publicUnreadMessages.some(shouldPlayAudioAlert); } if (shouldRenderPrivateChatAlerts && !playAudioAlert) { playAudioAlert = privateUnreadMessages.some(shouldPlayAudioAlert); } playAudioAlert ||= document.hidden; if (audioAlertEnabled && playAudioAlert) { Service.playAlertSound(); } const mapTextContent = (msg: Message) => { if (msg.messageType === ChatMessageType.CHAT_CLEAR) { return intl.formatMessage(intlMessages.publicChatClear); } return unescapeHtml(stripTags(msg.message)); }; const createMessage = (msg: Message) => ( {msg.senderName} {mapTextContent(msg)} ); const createPollMessage = () => ( {intl.formatMessage(intlMessages.pollResults)} {intl.formatMessage(intlMessages.pollResultsClick)} ); const renderToast = (message: Message) => { if (history.current.has(message.messageId)) return null; history.current.add(message.messageId); if (message.chatId === idChatOpen) return null; const messageChatId = message.chatId === PUBLIC_GROUP_CHAT_ID ? PUBLIC_CHAT_ID : message.chatId; const isPollResult = message.messageType === ChatMessageType.POLL; let content; if (isPollResult) { content = createPollMessage(); } else { content = createMessage(message); } return ( {intl.formatMessage(intlMessages.appToastChatPublic)} : {intl.formatMessage(intlMessages.appToastChatPrivate)} } alertDuration={ALERT_DURATION} layoutContextDispatch={layoutContextDispatch} /> ); }; return pushAlertEnabled ? [ shouldRenderPublicChatAlerts && publicUnreadMessages.map(renderToast), shouldRenderPrivateChatAlerts && privateUnreadMessages.map(renderToast), ] : null; }; const ChatAlertContainerGraphql: React.FC = (props) => { const cursor = useRef(new Date()); const { data: publicMessages } = useSubscription( CHAT_MESSAGE_PUBLIC_STREAM, { variables: { createdAt: cursor.current.toISOString() } }, ); const { data: privateMessages } = useSubscription( CHAT_MESSAGE_PRIVATE_STREAM, { variables: { createdAt: cursor.current.toISOString() } }, ); const idChatOpen = layoutSelect((i: Layout) => i.idChatOpen); const sidebarContent = layoutSelectInput((i: Input) => i.sidebarContent); const { sidebarContentPanel } = sidebarContent; const layoutContextDispatch = layoutDispatch(); const { audioAlertEnabled, pushAlertEnabled } = props; const idChat = sidebarContentPanel === PANELS.CHAT ? idChatOpen : ''; if (!publicMessages && !privateMessages) return null; return ( ); }; export default ChatAlertContainerGraphql;