(GET_PERMISSIONS, { fetchPolicy: 'cache-and-network' });
+
+ useEffect(() => {
+ if (dataHistory) {
+ console.log('dataHistory', dataHistory);
+ const exportedString = generateExportedMessages(
+ dataHistory.chat_message_public,
+ dataHistory.user_welcomeMsgs[0],
+ );
+ if (downloadOrCopyRef.current === 'download') {
+ const link = document.createElement('a');
+ const mimeType = 'text/plain';
+ link.setAttribute('download', `bbb-${dataHistory.meeting[0].name}[public-chat]_${getDateString()}.txt`);
+ link.setAttribute(
+ 'href',
+ `data: ${mimeType};charset=utf-8,`
+ + `${encodeURIComponent(exportedString)}`,
+ );
+ link.dispatchEvent(new MouseEvent('click', { bubbles: false, cancelable: true, view: window }));
+ downloadOrCopyRef.current = null;
+ } else if (downloadOrCopyRef.current === 'copy') {
+ navigator.clipboard.writeText(exportedString);
+ downloadOrCopyRef.current = null;
+ }
+ }
+ }, [dataHistory]);
+
+ useEffect(() => {
+ if (dataPermissions) {
+ setUserIsmoderator(dataPermissions.user_current[0].isModerator);
+ setMeetingIsBreakout(dataPermissions.meeting[0].isBreakout);
+ }
+ }, [dataPermissions]);
+
+ const actions = useMemo(()=>{
+ const dropdownActions = [
+ {
+ key: uniqueIdsRef.current[0],
+ enable: ENABLE_SAVE_AND_COPY_PUBLIC_CHAT,
+ icon: 'download',
+ dataTest: 'chatSave',
+ label: intl.formatMessage(intlMessages.save),
+ onClick: () => {
+ getChatMessageHistory();
+ downloadOrCopyRef.current = 'download';
+ },
+ },
+ {
+ key: uniqueIdsRef.current[1],
+ enable: ENABLE_SAVE_AND_COPY_PUBLIC_CHAT,
+ icon: 'copy',
+ id: 'clipboardButton',
+ dataTest: 'chatCopy',
+ label: intl.formatMessage(intlMessages.copy),
+ onClick: () => {
+ getChatMessageHistory();
+ downloadOrCopyRef.current = 'copy';
+ },
+ },
+ {
+ key: uniqueIdsRef.current[2],
+ enable: userIsModerator && !meetingIsBreakout,
+ icon: 'download',
+ dataTest: 'chatClear',
+ label: intl.formatMessage(intlMessages.clear),
+ onClick: () => clearPublicChatHistory(),
+ },
+ ];
+ return dropdownActions.filter((action) => action.enable);
+ },[userIsModerator, meetingIsBreakout]);
+ if (errorHistory) return Error loading chat history: {JSON.stringify(errorHistory)}
;
+ if (errorPermissions) return Error loading permissions: {JSON.stringify(errorPermissions)}
;
+ return (
+ {
+ getPermissions();
+ }}
+ data-test="chatOptionsMenu"
+ />
+ }
+ opts={{
+ id: 'chat-options-dropdown-menu',
+ keepMounted: true,
+ transitionDuration: 0,
+ elevation: 3,
+ getContentAnchorEl: null,
+ fullwidth: 'true',
+ anchorOrigin: { vertical: 'bottom', horizontal: isRTL ? 'right' : 'left' },
+ transformOrigin: { vertical: 'top', horizontal: isRTL ? 'right' : 'left' },
+ }}
+ actions={actions}
+ />
+ );
+}
\ No newline at end of file
diff --git a/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-header/chat-actions/queries.ts b/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-header/chat-actions/queries.ts
new file mode 100644
index 0000000000..103e7315e3
--- /dev/null
+++ b/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-header/chat-actions/queries.ts
@@ -0,0 +1,49 @@
+import { gql } from "@apollo/client";
+import { User} from '/imports/ui/Types/user'
+import { Meeting } from "/imports/ui/Types/meeting";
+import { Message } from "/imports/ui/Types/message";
+
+export type getChatMessageHistory = {
+ chat_message_public: Array
+ meeting: Array;
+ user_welcomeMsgs: Array<{welcomeMsg: string, welcomeMsgForModerators: string | null}>;
+};
+
+export type getPermissions = {
+ user_current: Array,
+ meeting: Array<{isBreakout: boolean}>
+};
+
+export const GET_CHAT_MESSAGE_HISTORY = gql`
+query getChatMessageHistory {
+ chat_message_public(order_by: {createdTime: asc}) {
+ message
+ messageId
+ createdTime
+ user {
+ userId
+ name
+ role
+ }
+ }
+ meeting {
+ name
+ }
+ user_welcomeMsgs {
+ welcomeMsg
+ welcomeMsgForModerators
+ }
+}
+`;
+
+export const GET_PERMISSIONS = gql`
+query getPermissions {
+ user_current {
+ isModerator
+ }
+ meeting {
+ isBreakout
+ name
+ }
+}
+`;
diff --git a/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-header/chat-actions/services.ts b/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-header/chat-actions/services.ts
new file mode 100644
index 0000000000..e3efa2b6fc
--- /dev/null
+++ b/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-header/chat-actions/services.ts
@@ -0,0 +1,40 @@
+import { Message } from "/imports/ui/Types/message";
+import { makeCall } from "/imports/ui/services/api";
+import { stripTags, unescapeHtml } from '/imports/utils/string-utils';
+
+export const htmlDecode = (input: string) => {
+ const replacedBRs = input.replaceAll('
', '\n');
+ return unescapeHtml(stripTags(replacedBRs));
+};
+
+export const generateExportedMessages = (messages: Array, welcomeSettings: {welcomeMsg: string, welcomeMsgForModerators: string | null} ): string => {
+ const welcomeMessage = htmlDecode(welcomeSettings.welcomeMsg);
+ const modOnlyMessage = welcomeSettings.welcomeMsgForModerators && htmlDecode(welcomeSettings.welcomeMsgForModerators);
+ const systemMessages = `${welcomeMessage ? `system: ${welcomeMessage}`: ''}\n ${modOnlyMessage ? `system: ${modOnlyMessage}`: ''}\n`
+
+ const text = messages.reduce((acc, message) => {
+ const date = new Date(message.createdTime);
+ const hour = date.getHours().toString().padStart(2, 0);
+ const min = date.getMinutes().toString().padStart(2, 0);
+ const hourMin = `[${hour}:${min}]`;
+ const userName = message.user.name;
+ const messageText = htmlDecode(message.message);
+ return `${acc}${hourMin} [${userName} : ${message.user.role}]: ${messageText}\n`;
+ },welcomeMessage? systemMessages : '');
+ return text;
+};
+
+export const getDateString = (date = new Date()) => {
+ const hours = date.getHours().toString().padStart(2, 0);
+ const minutes = date.getMinutes().toString().padStart(2, 0);
+ const month = (date.getMonth() + 1).toString().padStart(2, 0);
+ const dayOfMonth = date.getDate().toString().padStart(2, 0);
+ const time = `${hours}-${minutes}`;
+ const dateString = `${date.getFullYear()}-${month}-${dayOfMonth}_${time}`;
+ return dateString;
+};
+
+
+// TODO: Make action using mutations
+export const clearPublicChatHistory = () => (makeCall('clearPublicChatHistory'));
+
diff --git a/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-header/component.tsx b/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-header/component.tsx
new file mode 100644
index 0000000000..0639157b08
--- /dev/null
+++ b/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-header/component.tsx
@@ -0,0 +1,124 @@
+import React from 'react';
+import Header from '/imports/ui/components/common/control-header/component';
+import { useQuery } from '@apollo/client';
+import { GET_CHAT_DATA, GetChatDataResponse } from './queries';
+import { defineMessages, useIntl } from 'react-intl';
+import { closePrivateChat } from './services';
+import { layoutSelect, layoutDispatch } from '../../../layout/context';
+import { useShortcutHelp } from '../../../shortcut-help/useShortcutHelp';
+import { Layout } from '../../../layout/layoutTypes';
+import { ACTIONS, PANELS } from '../../../layout/enums';
+import { ChatActions } from './chat-actions/component';
+
+interface ChatHeaderProps {
+ chatId: string;
+ isPublicChat: boolean;
+ title: string;
+}
+
+const intlMessages = defineMessages({
+ closeChatLabel: {
+ id: 'app.chat.closeChatLabel',
+ description: 'aria-label for closing chat button',
+ },
+ hideChatLabel: {
+ id: 'app.chat.hideChatLabel',
+ description: 'aria-label for hiding chat button',
+ },
+ titlePublic: {
+ id: 'app.chat.titlePublic',
+ description: 'Public chat title',
+ },
+ titlePrivate: {
+ id: 'app.chat.titlePrivate',
+ description: 'Private chat title',
+ },
+});
+
+const ChatHeader: React.FC = ({ chatId, isPublicChat, title}) => {
+
+ const HIDE_CHAT_AK = useShortcutHelp('hideprivatechat');
+ const CLOSE_CHAT_AK = useShortcutHelp('closeprivatechat');
+ const layoutContextDispatch = layoutDispatch();
+ const intl = useIntl();
+ return (
+ {
+ layoutContextDispatch({
+ type: ACTIONS.SET_SIDEBAR_CONTENT_IS_OPEN,
+ value: false,
+ });
+ layoutContextDispatch({
+ type: ACTIONS.SET_ID_CHAT_OPEN,
+ value: '',
+ });
+ layoutContextDispatch({
+ type: ACTIONS.SET_SIDEBAR_CONTENT_PANEL,
+ value: PANELS.NONE,
+ });
+ },
+ }}
+ rightButtonProps={{
+ accessKey: CLOSE_CHAT_AK,
+ 'aria-label': intl.formatMessage(intlMessages.closeChatLabel, { 0: title }),
+ 'data-test': 'closePrivateChat',
+ icon: 'close',
+ label: intl.formatMessage(intlMessages.closeChatLabel, { 0: title }),
+ onClick: () => {
+ closePrivateChat(chatId);
+ layoutContextDispatch({
+ type: ACTIONS.SET_SIDEBAR_CONTENT_IS_OPEN,
+ value: false,
+ });
+ layoutContextDispatch({
+ type: ACTIONS.SET_ID_CHAT_OPEN,
+ value: '',
+ });
+ layoutContextDispatch({
+ type: ACTIONS.SET_SIDEBAR_CONTENT_PANEL,
+ value: PANELS.NONE,
+ });
+ },
+ }}
+ customRightButton={isPublicChat? : null}
+ />
+ );
+};
+
+const isChatResponse = (data: unknown): data is GetChatDataResponse => {
+ return (data as GetChatDataResponse).chat !== undefined;
+};
+
+const ChatHeaderContainer: React.FC = () => {
+ const intl = useIntl();
+ const idChatOpen = layoutSelect((i: Layout) => i.idChatOpen);
+ const {
+ data: chatData,
+ loading: chatDataLoading,
+ error: chatDataError,
+ } = useQuery(GET_CHAT_DATA, {
+ variables: { chatId: idChatOpen },
+ });
+
+ if (chatDataLoading) return null;
+ if (chatDataError) return (Error: {JSON.stringify(chatDataError)}
);
+ if (!isChatResponse(chatData)) return (Error: {JSON.stringify(chatData)}
);
+ const isPublicChat = chatData.chat[0]?.public;
+ const title = isPublicChat ? intl.formatMessage(intlMessages.titlePublic)
+ : intl.formatMessage(intlMessages.titlePrivate, { 0: chatData?.chat[0]?.participant?.name });
+ return (
+
+ );
+};
+
+export default ChatHeaderContainer;
\ No newline at end of file
diff --git a/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-header/queries.ts b/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-header/queries.ts
new file mode 100644
index 0000000000..ae0142073d
--- /dev/null
+++ b/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-header/queries.ts
@@ -0,0 +1,25 @@
+import { gql } from '@apollo/client';
+
+type chatContent = {
+ chatId: string;
+ public: boolean;
+ participant: {
+ name: string;
+ };
+}
+
+export interface GetChatDataResponse {
+ chat: Array;
+}
+
+export const GET_CHAT_DATA = gql`
+query GetChatData($chatId: String!) {
+ chat(where: {chatId: {_eq: $chatId }}) {
+ chatId
+ public
+ participant {
+ name
+ }
+ }
+}
+`;
\ No newline at end of file
diff --git a/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-header/services.ts b/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-header/services.ts
new file mode 100644
index 0000000000..d51f326cef
--- /dev/null
+++ b/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-header/services.ts
@@ -0,0 +1,14 @@
+import Storage from '/imports/ui/services/storage/session';
+import { indexOf } from '/imports/utils/array-utils';
+
+// old code
+const CLOSED_CHAT_LIST_KEY = 'closedChatList';
+export const closePrivateChat = (chatId: string) => {
+ const currentClosedChats = (Storage.getItem(CLOSED_CHAT_LIST_KEY) || []) as string[];
+
+ if (indexOf(currentClosedChats, chatId) < 0) {
+ currentClosedChats.push(chatId);
+
+ Storage.setItem(CLOSED_CHAT_LIST_KEY, currentClosedChats);
+ }
+};
\ No newline at end of file
diff --git a/bigbluebutton-html5/imports/ui/components/chat/component.jsx b/bigbluebutton-html5/imports/ui/components/chat/component.jsx
index 7d76cca972..f946fb5e70 100755
--- a/bigbluebutton-html5/imports/ui/components/chat/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/chat/component.jsx
@@ -16,6 +16,7 @@ import browserInfo from '/imports/utils/browserInfo';
import Header from '/imports/ui/components/common/control-header/component';
import { CLOSE_PRIVATE_CHAT_MUTATION } from '../user-list/user-list-content/user-messages/chat-list/queries';
import { useMutation, gql } from '@apollo/client';
+import ChatHeader from './chat-graphql/chat-header/component';
const CHAT_CONFIG = Meteor.settings.public.chat;
const PUBLIC_CHAT_ID = CHAT_CONFIG.public_id;
@@ -81,57 +82,7 @@ const Chat = (props) => {
isChrome={isChrome}
data-test={isPublicChat ? 'publicChat' : 'privateChat'}
>
- {
- layoutContextDispatch({
- type: ACTIONS.SET_SIDEBAR_CONTENT_IS_OPEN,
- value: false,
- });
- layoutContextDispatch({
- type: ACTIONS.SET_ID_CHAT_OPEN,
- value: '',
- });
- layoutContextDispatch({
- type: ACTIONS.SET_SIDEBAR_CONTENT_PANEL,
- value: PANELS.NONE,
- });
- },
- }}
- rightButtonProps={{
- accessKey: CLOSE_CHAT_AK,
- 'aria-label': intl.formatMessage(intlMessages.closeChatLabel, { 0: title }),
- 'data-test': "closePrivateChat",
- icon: "close",
- label: intl.formatMessage(intlMessages.closeChatLabel, { 0: title }),
- onClick: () => {
- handleClosePrivateChat();
- layoutContextDispatch({
- type: ACTIONS.SET_SIDEBAR_CONTENT_IS_OPEN,
- value: false,
- });
- layoutContextDispatch({
- type: ACTIONS.SET_ID_CHAT_OPEN,
- value: '',
- });
- layoutContextDispatch({
- type: ACTIONS.SET_SIDEBAR_CONTENT_PANEL,
- value: PANELS.NONE,
- });
- },
- }}
- customRightButton={isPublicChat && (
-
- )}
- />
+