Merge pull request #17955 from Tainan404/add-chat-header
This commit is contained in:
commit
9fb13c653d
@ -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;
|
||||
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
`;
|
@ -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'));
|
||||
|
@ -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;
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
@ -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);
|
||||
}
|
||||
};
|
@ -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}
|
||||
|
Loading…
Reference in New Issue
Block a user