From 90c7c24f2b2aa9943507a81d0b6cadad3edd88c1 Mon Sep 17 00:00:00 2001 From: Arash Koushkebaghi Date: Wed, 11 Sep 2019 16:21:58 -0700 Subject: [PATCH] feat(ActivityStream): implement empty state --- src/components/WebexActivityStream/README.md | 31 +++ .../WebexActivityStream.js | 151 ++++++++++++++ .../WebexActivityStream.scss | 18 ++ .../WebexActivityStream.stories.js | 24 +++ .../WebexActivityStream.test.js | 64 ++++++ .../WebexActivityStream.test.js.snap | 191 ++++++++++++++++++ .../hooks/__mocks__/useActivityStream.js | 6 + src/components/hooks/__mocks__/useRoom.js | 13 ++ src/components/hooks/index.js | 2 + src/components/hooks/useActivityStream.js | 28 +++ src/components/hooks/useRoom.js | 26 +++ src/styles/_mixins.scss | 5 +- 12 files changed, 557 insertions(+), 2 deletions(-) create mode 100644 src/components/WebexActivityStream/README.md create mode 100644 src/components/WebexActivityStream/WebexActivityStream.js create mode 100644 src/components/WebexActivityStream/WebexActivityStream.scss create mode 100644 src/components/WebexActivityStream/WebexActivityStream.stories.js create mode 100644 src/components/WebexActivityStream/WebexActivityStream.test.js create mode 100644 src/components/WebexActivityStream/__snapshots__/WebexActivityStream.test.js.snap create mode 100644 src/components/hooks/__mocks__/useActivityStream.js create mode 100644 src/components/hooks/__mocks__/useRoom.js create mode 100644 src/components/hooks/useActivityStream.js create mode 100644 src/components/hooks/useRoom.js diff --git a/src/components/WebexActivityStream/README.md b/src/components/WebexActivityStream/README.md new file mode 100644 index 000000000..f0c36b5b5 --- /dev/null +++ b/src/components/WebexActivityStream/README.md @@ -0,0 +1,31 @@ +# Webex Activity Stream Component + +Webex activity stream component displays a list of activities of a room. + +

+ picture coming up +

+ +## Preview + +To see all the different possible states of the Webex Activity Stream component, you can run our Storybook: + +```shell + npm start +``` + +## Embed + +1. Create a component adapter from which the data will be retrieved (See [adapters](../../adapters)). For instance: + + ```js + const jsonAdapter = new RoomsJSONAdapter(rooms); + ``` + +2. Create a component instance by passing the room ID as a string and the [component data adapter](../../adapters/RoomsAdapter.js) that we created previously + + ```js + + ``` + +The component knows how to manage its data. If anything changes in the data source that the adapter manages, the component will also update on its own. diff --git a/src/components/WebexActivityStream/WebexActivityStream.js b/src/components/WebexActivityStream/WebexActivityStream.js new file mode 100644 index 000000000..022d7c046 --- /dev/null +++ b/src/components/WebexActivityStream/WebexActivityStream.js @@ -0,0 +1,151 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import {RoomType} from '../../adapters/RoomsAdapter'; +import './WebexActivityStream.scss'; +import {useRoom, useActivityStream} from '../hooks'; + +export function GreetingDirectSVG() { + return ( + + + + + + + + + + + + + ); +} + +export function GreetingSpaceSVG() { + return ( + + + + + + + + + + + + + + + + + + + ); +} + +export function Greeting(props) { + let svg = ; + let description = `This is a shared space between you and other group members. Here's where you'll see shared messages, files, and a call history with this space.`; + + if (props.personName) { + svg = ; + description = `This is your private conversation with ${props.personName}. Here's where you'll see shared messages, files, and a call history with this person.`; + } + + return ( +
+
+ {svg} +
{description}
+
+
+ ); +} + +Greeting.propTypes = { + personName: PropTypes.string.isRequired, +}; + +export default function WebexActivityStream(props) { + const {roomID, adapter} = props; + const {title, roomType} = useRoom(roomID, adapter); + const activityIDs = useActivityStream(roomID, adapter); + const personName = roomType === RoomType.DIRECT ? title : ''; + + return
{!activityIDs.length && }
; +} + +WebexActivityStream.propTypes = { + roomID: PropTypes.string.isRequired, + adapter: PropTypes.object.isRequired, +}; diff --git a/src/components/WebexActivityStream/WebexActivityStream.scss b/src/components/WebexActivityStream/WebexActivityStream.scss new file mode 100644 index 000000000..aa7e8c18b --- /dev/null +++ b/src/components/WebexActivityStream/WebexActivityStream.scss @@ -0,0 +1,18 @@ +.greeting { + display: block; + flex-direction: column; + justify-content: center; + align-items: center; + max-width: 37.5rem; + text-align: center; + color: $gray-dark-3; + + .greeting-header { + @include header-fonts; + } + + .greeting-description { + @include body-fonts; + padding: 0 3.125rem; + } +} diff --git a/src/components/WebexActivityStream/WebexActivityStream.stories.js b/src/components/WebexActivityStream/WebexActivityStream.stories.js new file mode 100644 index 000000000..3b7eabd10 --- /dev/null +++ b/src/components/WebexActivityStream/WebexActivityStream.stories.js @@ -0,0 +1,24 @@ +import React from 'react'; +import {storiesOf} from '@storybook/react'; + +import RoomsJSONAdapter from '../../adapters/RoomsJSONAdapter'; +import {RoomType} from '../../adapters/RoomsAdapter'; +import rooms from '../../data/rooms'; + +import WebexActivityStream from './WebexActivityStream'; + +// Setup for the stories +const [roomID] = Object.keys(rooms); +const stories = storiesOf('Webex Activity Stream', module); +const newRooms = {}; + +// Stories +stories.add('empty group stream', () => ); +stories.add('empty 1:1 stream', () => { + newRooms[roomID] = { + ...rooms[roomID], + roomType: RoomType.DIRECT, + }; + + return ; +}); diff --git a/src/components/WebexActivityStream/WebexActivityStream.test.js b/src/components/WebexActivityStream/WebexActivityStream.test.js new file mode 100644 index 000000000..13073e0f5 --- /dev/null +++ b/src/components/WebexActivityStream/WebexActivityStream.test.js @@ -0,0 +1,64 @@ +import React from 'react'; + +import RoomsJSONAdapter from '../../adapters/RoomsJSONAdapter'; +import {RoomType} from '../../adapters/RoomsAdapter'; +import rooms from '../../data/rooms'; + +import WebexActivityStream, {Greeting, GreetingSpaceSVG, GreetingDirectSVG} from './WebexActivityStream'; + +jest.mock('../hooks/useRoom'); +jest.mock('../hooks/useActivityStream'); + +describe('Webex Activity Stream component', () => { + let roomID, newRooms, roomsAdapter; + + beforeEach(() => { + [roomID] = Object.keys(rooms); + newRooms = rooms; + roomsAdapter = new RoomsJSONAdapter(rooms); + }); + + describe('Greeting Space SVG snapshot', () => { + test('matches with greeting space SVG', () => { + expect(shallow()).toMatchSnapshot(); + }); + }); + + describe('Greeting 1:1 SVG snapshot', () => { + test('matches with greeting 1:1 SVG', () => { + expect(shallow()).toMatchSnapshot(); + }); + }); + + describe('Greeting component snapshot', () => { + test('matches with empty space', () => { + expect(shallow()).toMatchSnapshot(); + }); + + test('matches with empty 1:1', () => { + expect(shallow()).toMatchSnapshot(); + }); + }); + + describe('Webex Activity Stream snapshots', () => { + test('matches with empty group stream', () => { + expect(shallow()).toMatchSnapshot(); + }); + + test('matches with empty direct stream', () => { + newRooms[roomID] = { + ...rooms[roomID], + roomType: RoomType.DIRECT, + }; + roomsAdapter = new RoomsJSONAdapter(newRooms); + + expect(shallow()).toMatchSnapshot(); + }); + }); + + afterEach(() => { + roomsAdapter = null; + newRooms = null; + roomID = null; + }); +}); diff --git a/src/components/WebexActivityStream/__snapshots__/WebexActivityStream.test.js.snap b/src/components/WebexActivityStream/__snapshots__/WebexActivityStream.test.js.snap new file mode 100644 index 000000000..978883b95 --- /dev/null +++ b/src/components/WebexActivityStream/__snapshots__/WebexActivityStream.test.js.snap @@ -0,0 +1,191 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Webex Activity Stream component Greeting 1:1 SVG snapshot matches with greeting 1:1 SVG 1`] = ` + + + + + + + + + + + + +`; + +exports[`Webex Activity Stream component Greeting Space SVG snapshot matches with greeting space SVG 1`] = ` + + + + + + + + + + + + + + + + + + +`; + +exports[`Webex Activity Stream component Greeting component snapshot matches with empty 1:1 1`] = ` +
+
+ +
+ This is your private conversation with personName. Here's where you'll see shared messages, files, and a call history with this person. +
+
+
+`; + +exports[`Webex Activity Stream component Greeting component snapshot matches with empty space 1`] = ` +
+
+ +
+ This is a shared space between you and other group members. Here's where you'll see shared messages, files, and a call history with this space. +
+
+
+`; + +exports[`Webex Activity Stream component Webex Activity Stream snapshots matches with empty direct stream 1`] = ` +
+ +
+`; + +exports[`Webex Activity Stream component Webex Activity Stream snapshots matches with empty group stream 1`] = ` +
+ +
+`; diff --git a/src/components/hooks/__mocks__/useActivityStream.js b/src/components/hooks/__mocks__/useActivityStream.js new file mode 100644 index 000000000..0ae4df912 --- /dev/null +++ b/src/components/hooks/__mocks__/useActivityStream.js @@ -0,0 +1,6 @@ +export default function useActivityStream(ID, adapter) { + const [roomID] = Object.keys(adapter.datasource); + const rooms = adapter.datasource; + + return ID === roomID ? rooms[`${ID}-activities`] : []; +} diff --git a/src/components/hooks/__mocks__/useRoom.js b/src/components/hooks/__mocks__/useRoom.js new file mode 100644 index 000000000..b299fc662 --- /dev/null +++ b/src/components/hooks/__mocks__/useRoom.js @@ -0,0 +1,13 @@ +export default function useRoom(ID, adapter) { + const [roomID] = Object.keys(adapter.datasource); + const rooms = adapter.datasource; + let room = null; + + if (ID === roomID) { + room = rooms[ID]; + } else { + throw new Error(`Could not find room with ID "${ID}"`); + } + + return room; +} diff --git a/src/components/hooks/index.js b/src/components/hooks/index.js index b0c6ca426..6339fc883 100644 --- a/src/components/hooks/index.js +++ b/src/components/hooks/index.js @@ -1,2 +1,4 @@ export {default as useActivity} from './useActivity'; export {default as usePerson} from './usePerson'; +export {default as useRoom} from './useRoom'; +export {default as useActivityStream} from './useActivityStream'; diff --git a/src/components/hooks/useActivityStream.js b/src/components/hooks/useActivityStream.js new file mode 100644 index 000000000..2574ac6ae --- /dev/null +++ b/src/components/hooks/useActivityStream.js @@ -0,0 +1,28 @@ +import {useState, useEffect} from 'react'; +import {merge} from 'rxjs'; + +/** + * Custom hook that returns activity data associated to the room of the given ID. + * + * @param {string} roomID ID of the room for which to return data. + * @param {obj} roomsAdapter Component data adapter from which to retrieve data. + * @returns {Room} Activity ID associated to the room + */ +export default function useActivityStream(roomID, roomsAdapter) { + const [activityIDs, setActivityIDs] = useState([]); + + useEffect(() => { + const activityStream = merge( + roomsAdapter.getPreviousRoomActivities(roomID), + roomsAdapter.getRoomActivities(roomID) + ); + const subscription = activityStream.subscribe(setActivityIDs); + + return () => { + subscription.unsubscribe(); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return activityIDs; +} diff --git a/src/components/hooks/useRoom.js b/src/components/hooks/useRoom.js new file mode 100644 index 000000000..b6dfd3423 --- /dev/null +++ b/src/components/hooks/useRoom.js @@ -0,0 +1,26 @@ +import {useState, useEffect} from 'react'; + +/** + * Custom hook that returns room data of the given ID. + * + * @param {string} roomID ID of the room for which to return data. + * @param {obj} roomsAdapter Component data adapter from which to retrieve data. + * @returns {Room} Data of the room + */ +export default function useRoom(roomID, roomsAdapter) { + const [room, setRoom] = useState({}); + + useEffect(() => { + const onError = (error) => { + throw error; + }; + const subscription = roomsAdapter.getRoom(roomID).subscribe(setRoom, onError); + + return () => { + subscription.unsubscribe(); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return room; +} diff --git a/src/styles/_mixins.scss b/src/styles/_mixins.scss index 6e3667222..abf37b03f 100644 --- a/src/styles/_mixins.scss +++ b/src/styles/_mixins.scss @@ -9,13 +9,14 @@ cursor: auto; } -@mixin font-size-26 { +@mixin header-fonts { font-family: $brand-font-extra-light; font-size: 1.625rem; line-height: 2rem; letter-spacing: 0.0125rem; } -@mixin font-size-16 { + +@mixin body-fonts { font-family: $brand-font-light; font-size: 1rem; line-height: 1.5rem;