Skip to content

Commit

Permalink
Merge pull request #197 from graasp/187/chatbox
Browse files Browse the repository at this point in the history
187/chatbox
  • Loading branch information
pyphilia authored Aug 12, 2021
2 parents 35ee3dc + 1111ebe commit bfc7357
Show file tree
Hide file tree
Showing 17 changed files with 542 additions and 173 deletions.
31 changes: 31 additions & 0 deletions cypress/fixtures/chatbox.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { DEFAULT_FOLDER_ITEM } from './items';
import { CURRENT_USER, MEMBERS } from './members';

export const ITEM_WITH_CHATBOX_MESSAGES = {
...DEFAULT_FOLDER_ITEM,
id: 'adf09f5a-5688-11eb-ae93-0242ac130004',
path: 'adf09f5a_5688_11eb_ae93_0242ac130004',
name: 'item with chatbox messages',
chat: [
{
body: 'message1',
chatId: 'adf09f5a-5688-11eb-ae93-0242ac130004',
createdAt: '2021-08-11T12:56:36.834Z',
creator: CURRENT_USER.id,
},
{
body: 'message2',
chatId: 'adf09f5a-5688-11eb-ae93-0242ac130004',
createdAt: '2021-09-11T12:56:36.834Z',
creator: MEMBERS.BOB.id,
},
],
};

export const ITEM_WITHOUT_CHATBOX_MESSAGES = {
...DEFAULT_FOLDER_ITEM,
id: 'bdf09f5a-5688-11eb-ae93-0242ac130001',
path: 'bdf09f5a_5688_11eb_ae93_0242ac130001',
name: 'item without chatbox messages',
chat: [],
};
98 changes: 98 additions & 0 deletions cypress/integration/item/chatbox/chatbox.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { WebSocket } from '@graasp/websockets/test/mock-client';
import { buildItemPath } from '../../../../src/config/paths';
import {
CHATBOX_ID,
CHATBOX_INPUT_BOX_ID,
ITEM_CHATBOX_BUTTON_ID,
} from '../../../../src/config/selectors';
import {
ITEM_WITHOUT_CHATBOX_MESSAGES,
ITEM_WITH_CHATBOX_MESSAGES,
} from '../../../fixtures/chatbox';
import { CURRENT_USER, MEMBERS } from '../../../fixtures/members';
import { WEBSOCKETS_DELAY_TIME } from '../../../support/constants';

const openChatbox = () => {
cy.get(`#${ITEM_CHATBOX_BUTTON_ID}`).click();
cy.wait('@getItemChat');
};

describe('Chatbox Scenarios', () => {
let client;

beforeEach(() => {
client = new WebSocket();
});

it('Send messages in chatbox', () => {
const item = ITEM_WITH_CHATBOX_MESSAGES;
cy.visitAndMockWs(buildItemPath(item.id), { items: [item] }, client);

// open chatbox
openChatbox();
// check the chatbox displays the already saved messages
for (const msg of item.chat) {
cy.get(`#${CHATBOX_ID}`).should('contain', msg.body);
}

// send message
const message = 'a new message';
cy.get(`#${CHATBOX_ID} #${CHATBOX_INPUT_BOX_ID} input`).type(message);
cy.get(`#${CHATBOX_ID} #${CHATBOX_INPUT_BOX_ID} button`).click();
cy.wait('@postItemChatMessage').then(({ request: { body } }) => {
expect(body.body).to.equal(message);

// mock websocket response
client.receive({
realm: 'notif',
type: 'update',
topic: 'chat/item',
channel: item.id,
body: {
kind: 'item',
op: 'publish',
message: {
creator: CURRENT_USER.id,
chatId: item.id,
body: message,
},
},
});
cy.wait(WEBSOCKETS_DELAY_TIME);

// check the new message is visible
cy.get(`#${CHATBOX_ID}`).should('contain', message);
});
});

it('Receive messages in chatbox from websockets', () => {
const item = ITEM_WITHOUT_CHATBOX_MESSAGES;
cy.visitAndMockWs(buildItemPath(item.id), { items: [item] }, client);

openChatbox();

// check websocket: the chatbox displays someone else's message
const bobMessage = 'a message from bob';
cy.get(`#${CHATBOX_ID}`).then(() => {
client.receive({
realm: 'notif',
type: 'update',
topic: 'chat/item',
channel: item.id,
body: {
kind: 'item',
op: 'publish',
message: {
creator: MEMBERS.BOB.id,
chatId: item.id,
body: bobMessage,
},
},
});
cy.wait(WEBSOCKETS_DELAY_TIME);

// check the new message is visible
cy.get(`#${CHATBOX_ID}`).should('contain', bobMessage);
});
});
});
2 changes: 1 addition & 1 deletion cypress/integration/item/view/viewDocument.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { buildItemPath } from '../../../../src/config/paths';
import { GRAASP_DOCUMENT_ITEM } from '../../../fixtures/documents';
import { expectDocumentViewScreenLayout } from './utils';

describe('View Space', () => {
describe('View Document', () => {
describe('Grid', () => {
beforeEach(() => {
cy.setUpApi({
Expand Down
2 changes: 1 addition & 1 deletion cypress/integration/item/view/viewFolder.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import { GRAASP_LINK_ITEM } from '../../../fixtures/links';
import { REQUEST_FAILURE_TIME } from '../../../support/constants';
import { expectFolderViewScreenLayout } from './utils';

describe('View Space', () => {
describe('View Folder', () => {
describe('Grid', () => {
beforeEach(() => {
cy.setUpApi({
Expand Down
19 changes: 19 additions & 0 deletions cypress/support/commands.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ import {
mockGetItems,
mockGetFlags,
mockPostItemFlag,
mockGetItemChat,
mockPostItemChatMessage,
} from './server';
import './commands/item';
import './commands/navigation';
Expand Down Expand Up @@ -76,6 +78,7 @@ Cypress.Commands.add(
putItemLoginError = false,
editMemberError = false,
postItemFlagError = false,
getItemChatError = false,
} = {}) => {
const cachedItems = JSON.parse(JSON.stringify(items));
const cachedMembers = JSON.parse(JSON.stringify(members));
Expand Down Expand Up @@ -152,6 +155,10 @@ Cypress.Commands.add(
mockGetPublicChildren(items);

mockGetItems({ items, currentMember });

mockGetItemChat({ items }, getItemChatError);

mockPostItemChatMessage();
},
);

Expand All @@ -168,3 +175,15 @@ Cypress.Commands.add('switchMode', (mode) => {
break;
}
});

Cypress.Commands.add(
'visitAndMockWs',
(visitRoute, sampleData, wsClientStub) => {
cy.setUpApi(sampleData);
cy.visit(visitRoute, {
onBeforeLoad: (win) => {
cy.stub(win, 'WebSocket', () => wsClientStub);
},
});
},
);
39 changes: 39 additions & 0 deletions cypress/support/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ const {
buildDeleteItemMembershipRoute,
buildPostItemFlagRoute,
GET_FLAGS_ROUTE,
buildGetItemChatRoute,
buildPostItemChatMessageRoute,
} = API_ROUTES;

const API_HOST = Cypress.env('API_HOST');
Expand Down Expand Up @@ -826,3 +828,40 @@ export const mockPostItemFlag = (items, shouldThrowError) => {
},
).as('postItemFlag');
};

export const mockGetItemChat = ({ items }, shouldThrowError) => {
cy.intercept(
{
method: DEFAULT_GET.method,
url: new RegExp(`${API_HOST}/${buildGetItemChatRoute(ID_FORMAT)}$`),
},
({ reply, url }) => {
if (shouldThrowError) {
return reply({ statusCode: StatusCodes.BAD_REQUEST });
}

const itemId = url.slice(API_HOST.length).split('/')[2];
const item = items.find(({ id }) => itemId === id);

return reply({ id: itemId, messages: item?.chat });
},
).as('getItemChat');
};

export const mockPostItemChatMessage = (shouldThrowError) => {
cy.intercept(
{
method: DEFAULT_POST.method,
url: new RegExp(
`${API_HOST}/${buildPostItemChatMessageRoute(ID_FORMAT)}$`,
),
},
({ reply, body }) => {
if (shouldThrowError) {
return reply({ statusCode: StatusCodes.BAD_REQUEST });
}

return reply(body);
},
).as('postItemChatMessage');
};
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"private": true,
"license": "AGPL-3.0-only",
"dependencies": {
"@graasp/chatbox": "git://github.com/graasp/graasp-chatbox.git#main",
"@graasp/query-client": "git://github.com/graasp/graasp-query-client.git#main",
"@graasp/ui": "git://github.com/graasp/graasp-ui.git#41/rollup",
"@material-ui/core": "4.11.2",
Expand Down Expand Up @@ -106,7 +107,7 @@
"npm-run-all": "4.1.5",
"nyc": "15.1.0",
"prettier": "2.2.1",
"pretty-quick": "3.1.0",
"pretty-quick": "3.1.1",
"standard-version": "9.1.0",
"typescript": "4.1.3",
"wait-on": "5.3.0"
Expand Down
53 changes: 53 additions & 0 deletions src/components/common/Chatbox.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Typography } from '@material-ui/core';
import { useTranslation } from 'react-i18next';
import GraaspChatbox from '@graasp/chatbox';
import { MUTATION_KEYS } from '@graasp/query-client';
import { Map } from 'immutable';
import { Loader } from '@graasp/ui';
import { hooks, useMutation } from '../../config/queryClient';
import { HEADER_HEIGHT } from '../../config/constants';
import { CHATBOX_INPUT_BOX_ID, CHATBOX_ID } from '../../config/selectors';

const { useItemChat, useCurrentMember } = hooks;

const Chatbox = ({ item }) => {
const { t } = useTranslation();
const { data: chat, isLoading: isChatLoading } = useItemChat(item.get('id'));
const { data: currentMember, isLoadingCurrentMember } = useCurrentMember();
const { mutate: sendMessage } = useMutation(
MUTATION_KEYS.POST_ITEM_CHAT_MESSAGE,
);

const renderChatbox = () => {
if (isChatLoading || isLoadingCurrentMember) {
return <Loader />;
}

return (
<GraaspChatbox
id={CHATBOX_ID}
sendMessageBoxId={CHATBOX_INPUT_BOX_ID}
currentMember={currentMember}
chatId={item.get('id')}
messages={chat?.get('messages')}
height={window.innerHeight - HEADER_HEIGHT * 2}
sendMessageFunction={sendMessage}
/>
);
};

return (
<>
<Typography variant="h5">{t('Comments')}</Typography>
{renderChatbox()}
</>
);
};

Chatbox.propTypes = {
item: PropTypes.instanceOf(Map).isRequired,
};

export default Chatbox;
3 changes: 3 additions & 0 deletions src/components/context/LayoutContext.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ const LayoutContextProvider = ({ children }) => {
const [isItemMetadataMenuOpen, setIsItemMetadataMenuOpen] = useState(
isItemPanelOpen,
);
const [isChatboxMenuOpen, setIsChatboxMenuOpen] = useState(false);

return (
<LayoutContext.Provider
Expand All @@ -40,6 +41,8 @@ const LayoutContextProvider = ({ children }) => {
setIsItemSettingsOpen,
isItemMetadataMenuOpen,
setIsItemMetadataMenuOpen,
isChatboxMenuOpen,
setIsChatboxMenuOpen,
}}
>
{children}
Expand Down
35 changes: 27 additions & 8 deletions src/components/item/ItemMain.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ import ItemHeader from './header/ItemHeader';
import ItemPanel from './ItemPanel';
import { ITEM_MAIN_CLASS } from '../../config/selectors';
import { LayoutContext } from '../context/LayoutContext';
import Chatbox from '../common/Chatbox';
import ItemMetadataContent from './ItemMetadataContent';

const useStyles = makeStyles((theme) => ({
root: {},
menuButton: {},
hide: {
display: 'none',
},
Expand Down Expand Up @@ -49,23 +49,42 @@ const useStyles = makeStyles((theme) => ({

const ItemMain = ({ id, children, item }) => {
const classes = useStyles();
const { isItemMetadataMenuOpen, setIsItemMetadataMenuOpen } = useContext(
LayoutContext,
);
const {
isItemMetadataMenuOpen,
setIsItemMetadataMenuOpen,
isChatboxMenuOpen,
setIsChatboxMenuOpen,
} = useContext(LayoutContext);

const handleToggleMetadataMenu = () => {
setIsItemMetadataMenuOpen(!isItemMetadataMenuOpen);
setIsChatboxMenuOpen(false);
};
const handleToggleChatboxMenu = () => {
setIsChatboxMenuOpen(!isChatboxMenuOpen);
setIsItemMetadataMenuOpen(false);
};

return (
<div id={id} className={ITEM_MAIN_CLASS}>
<ItemPanel item={item} open={isItemMetadataMenuOpen} />
{isChatboxMenuOpen && (
<ItemPanel open={isChatboxMenuOpen}>
<Chatbox item={item} />
</ItemPanel>
)}
<ItemPanel open={isItemMetadataMenuOpen}>
<ItemMetadataContent item={item} />
</ItemPanel>

<div
className={clsx(classes.root, classes.content, {
[classes.contentShift]: isItemMetadataMenuOpen,
[classes.contentShift]: isItemMetadataMenuOpen || isChatboxMenuOpen,
})}
>
<ItemHeader onClick={handleToggleMetadataMenu} />
<ItemHeader
onClickMetadata={handleToggleMetadataMenu}
onClickChatbox={handleToggleChatboxMenu}
/>

{children}
</div>
Expand Down
Loading

0 comments on commit bfc7357

Please sign in to comment.