Merge pull request #17955 from Tainan404/add-chat-header

This commit is contained in:
Tainan Felipe 2023-06-02 15:55:58 -03:00 committed by GitHub
commit 9fb13c653d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 425 additions and 51 deletions

View File

@ -11,6 +11,13 @@ export interface LockSettings {
webcamsOnlyForModerator: boolean;
}
export interface WelcomeSettings {
welcomeMsg: string;
modOnlyMessage: string;
welcomeMsgTemplate: string;
meetingId: string;
}
export interface UsersPolicies {
allowModsToEjectCameras: boolean;
allowModsToUnmuteUsers: boolean;

View File

@ -0,0 +1,164 @@
import React, { useEffect, useMemo, useRef, useState } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import BBBMenu from '/imports/ui/components/common/menu/component';
import { layoutSelect } from '/imports/ui/components/layout/context';
import { Layout } from '/imports/ui/components/layout/layoutTypes';
import { useLazyQuery } from '@apollo/client';
import { GET_CHAT_MESSAGE_HISTORY, GET_PERMISSIONS, getChatMessageHistory, getPermissions } from './queries';
import { uid } from 'radash';
import Button from '/imports/ui/components/common/button/component';
import { clearPublicChatHistory, generateExportedMessages } from './services'
import { getDateString } from '/imports/utils/string-utils';
const CHAT_CONFIG = Meteor.settings.public.chat;
const ENABLE_SAVE_AND_COPY_PUBLIC_CHAT = CHAT_CONFIG.enableSaveAndCopyPublicChat;
const intlMessages = defineMessages({
clear: {
id: 'app.chat.dropdown.clear',
description: 'Clear button label',
},
save: {
id: 'app.chat.dropdown.save',
description: 'Clear button label',
},
copy: {
id: 'app.chat.dropdown.copy',
description: 'Copy button label',
},
copySuccess: {
id: 'app.chat.copySuccess',
description: 'aria success alert',
},
copyErr: {
id: 'app.chat.copyErr',
description: 'aria error alert',
},
options: {
id: 'app.chat.dropdown.options',
description: 'Chat Options',
},
});
export const ChatActions: React.FC = () => {
const intl = useIntl();
const isRTL = layoutSelect((i: Layout) => i.isRTL);
const uniqueIdsRef = useRef<string[]>([uid(1), uid(2), uid(3)]);
const downloadOrCopyRef = useRef<'download' | 'copy' | null>(null);
const [userIsModerator, setUserIsmoderator] = useState<boolean>(false);
const [meetingIsBreakout, setMeetingIsBreakout] = useState<boolean>(false);
const [
getChatMessageHistory,
{
loading: loadingHistory,
error: errorHistory,
data: dataHistory,
}] = useLazyQuery<getChatMessageHistory>(GET_CHAT_MESSAGE_HISTORY, { fetchPolicy: 'no-cache' });
const [
getPermissions,
{
loading: loadingPermissions,
error: errorPermissions,
data: dataPermissions,
}] = useLazyQuery<getPermissions>(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 <p>Error loading chat history: {JSON.stringify(errorHistory)}</p>;
if (errorPermissions) return <p>Error loading permissions: {JSON.stringify(errorPermissions)}</p>;
return (
<BBBMenu
trigger={
<Button
label={intl.formatMessage(intlMessages.options)}
aria-label={intl.formatMessage(intlMessages.options)}
hideLabel
size="sm"
icon="more"
onClick={() => {
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}
/>
);
}

View File

@ -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<Message>
meeting: Array<Meeting>;
user_welcomeMsgs: Array<{welcomeMsg: string, welcomeMsgForModerators: string | null}>;
};
export type getPermissions = {
user_current: Array<User>,
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
}
}
`;

View File

@ -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('<br/>', '\n');
return unescapeHtml(stripTags(replacedBRs));
};
export const generateExportedMessages = (messages: Array<Message>, 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'));

View File

@ -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<ChatHeaderProps> = ({ chatId, isPublicChat, title}) => {
const HIDE_CHAT_AK = useShortcutHelp('hideprivatechat');
const CLOSE_CHAT_AK = useShortcutHelp('closeprivatechat');
const layoutContextDispatch = layoutDispatch();
const intl = useIntl();
return (
<Header
data-test="chatTitle"
leftButtonProps={{
accessKey: chatId !== 'public' ? HIDE_CHAT_AK : null,
'aria-label': intl.formatMessage(intlMessages.hideChatLabel, { 0: title }),
'data-test': isPublicChat ? 'hidePublicChat' : 'hidePrivateChat',
label: title,
onClick: () => {
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? <ChatActions /> : null}
/>
);
};
const isChatResponse = (data: unknown): data is GetChatDataResponse => {
return (data as GetChatDataResponse).chat !== undefined;
};
const ChatHeaderContainer: React.FC<ChatHeaderProps> = () => {
const intl = useIntl();
const idChatOpen = layoutSelect((i: Layout) => i.idChatOpen);
const {
data: chatData,
loading: chatDataLoading,
error: chatDataError,
} = useQuery<GetChatDataResponse>(GET_CHAT_DATA, {
variables: { chatId: idChatOpen },
});
if (chatDataLoading) return null;
if (chatDataError) return (<div>Error: {JSON.stringify(chatDataError)}</div>);
if (!isChatResponse(chatData)) return (<div>Error: {JSON.stringify(chatData)}</div>);
const isPublicChat = chatData.chat[0]?.public;
const title = isPublicChat ? intl.formatMessage(intlMessages.titlePublic)
: intl.formatMessage(intlMessages.titlePrivate, { 0: chatData?.chat[0]?.participant?.name });
return (
<ChatHeader
chatId={idChatOpen}
isPublicChat={isPublicChat}
title={title}
/>
);
};
export default ChatHeaderContainer;

View File

@ -0,0 +1,25 @@
import { gql } from '@apollo/client';
type chatContent = {
chatId: string;
public: boolean;
participant: {
name: string;
};
}
export interface GetChatDataResponse {
chat: Array<chatContent>;
}
export const GET_CHAT_DATA = gql`
query GetChatData($chatId: String!) {
chat(where: {chatId: {_eq: $chatId }}) {
chatId
public
participant {
name
}
}
}
`;

View File

@ -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);
}
};

View File

@ -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'}
>
<Header
data-test="chatTitle"
leftButtonProps={{
accessKey: chatID !== 'public' ? HIDE_CHAT_AK : null,
'aria-label': intl.formatMessage(intlMessages.hideChatLabel, { 0: title }),
'data-test': isPublicChat ? 'hidePublicChat' : 'hidePrivateChat',
label: title,
onClick: () => {
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 && (
<ChatDropdownContainer {...{
meetingIsBreakout, isMeteorConnected, amIModerator, timeWindowsValues,
}}
/>
)}
/>
<ChatHeader />
<TimeWindowList
id={ELEMENT_ID}
chatId={chatID}