From 889f9036d4f58f39506d2dd3cb92d4b655ea1508 Mon Sep 17 00:00:00 2001 From: mrval Date: Fri, 10 Mar 2023 13:29:37 +0100 Subject: [PATCH 01/16] wip notifications --- src/Main.tsx | 3 ++ src/components/notification/index.tsx | 1 + src/components/notification/item/index.tsx | 36 +++++++++++++++++++ src/components/notification/item/style.scss | 5 +++ src/components/notification/list/index.tsx | 27 +++++++++++++++ src/components/notification/list/style.scss | 0 src/components/user-notices/component.tsx | 38 +++++++++++++++++++++ src/components/user-notices/container.tsx | 34 ++++++++++++++++++ src/components/user-notices/index.tsx | 1 + 9 files changed, 145 insertions(+) create mode 100644 src/components/notification/index.tsx create mode 100644 src/components/notification/item/index.tsx create mode 100644 src/components/notification/item/style.scss create mode 100644 src/components/notification/list/index.tsx create mode 100644 src/components/notification/list/style.scss create mode 100644 src/components/user-notices/component.tsx create mode 100644 src/components/user-notices/container.tsx create mode 100644 src/components/user-notices/index.tsx diff --git a/src/Main.tsx b/src/Main.tsx index 51bb18a14..8a38adc71 100644 --- a/src/Main.tsx +++ b/src/Main.tsx @@ -13,6 +13,7 @@ import classNames from 'classnames'; import { Sidekick } from './components/sidekick/index'; import { withContext as withAuthenticationContext } from './components/authentication/context'; import { MessengerChat } from './components/messenger/chat'; +import { UserNotices } from './components/user-notices'; export interface Properties { hasContextPanel: boolean; @@ -63,6 +64,8 @@ export class Container extends React.Component {
+ +
diff --git a/src/components/notification/index.tsx b/src/components/notification/index.tsx new file mode 100644 index 000000000..10373cf31 --- /dev/null +++ b/src/components/notification/index.tsx @@ -0,0 +1 @@ +export { NotificationList } from './list'; diff --git a/src/components/notification/item/index.tsx b/src/components/notification/item/index.tsx new file mode 100644 index 000000000..32f80eecd --- /dev/null +++ b/src/components/notification/item/index.tsx @@ -0,0 +1,36 @@ +import React from 'react'; +import classNames from 'classnames'; +import { Avatar } from '@zero-tech/zui/components/Avatar'; + +import './style.scss'; + +interface Properties { + notRead?: boolean; +} + +export class NotificationItem extends React.Component { + render() { + return ( +
+
+ +
+
+

Zero

+

You were mentioned in #product

+ Just now +
+
{/* badge from zUI */}
+
+ ); + } +} diff --git a/src/components/notification/item/style.scss b/src/components/notification/item/style.scss new file mode 100644 index 000000000..f2ec94235 --- /dev/null +++ b/src/components/notification/item/style.scss @@ -0,0 +1,5 @@ +.notification-item { + &__wrapper { + display: flex; + } +} diff --git a/src/components/notification/list/index.tsx b/src/components/notification/list/index.tsx new file mode 100644 index 000000000..719a7a541 --- /dev/null +++ b/src/components/notification/list/index.tsx @@ -0,0 +1,27 @@ +import React from 'react'; +import { NotificationItem } from '../item'; + +import './style.scss'; + +interface Properties { + list?: any[]; +} + +export class NotificationList extends React.Component { + render() { + return ( +
+
+

Notifications

+
+
+ +
+ {Array.from(Array(10).keys()).map(() => ( + + ))} +
+
+ ); + } +} diff --git a/src/components/notification/list/style.scss b/src/components/notification/list/style.scss new file mode 100644 index 000000000..e69de29bb diff --git a/src/components/user-notices/component.tsx b/src/components/user-notices/component.tsx new file mode 100644 index 000000000..e63e87a0b --- /dev/null +++ b/src/components/user-notices/component.tsx @@ -0,0 +1,38 @@ +import React from 'react'; +import { IconBellRinging3 } from '@zero-tech/zui/components/Icons'; +import { IconButton } from '@zero-tech/zui/components/IconButton'; +import { NotificationList } from '../notification'; + +interface Properties {} +interface State { + notificationsStatus: boolean; +} + +export class UserNoticeComponent extends React.Component { + state = { notificationsStatus: true }; + + componentDidMount() { + // add event mouseDown to hide notifgication + } + + componentWillUnmount() { + // remove event listener + } + + openNotifications() { + this.setState({}); + } + + render() { + return ( +
+ + + {this.state.notificationsStatus && } +
+ ); + } +} diff --git a/src/components/user-notices/container.tsx b/src/components/user-notices/container.tsx new file mode 100644 index 000000000..152867f98 --- /dev/null +++ b/src/components/user-notices/container.tsx @@ -0,0 +1,34 @@ +import React from 'react'; +import { UserNoticeComponent } from './component'; +import { RootState } from '../../store'; +import { connectContainer } from '../../store/redux-container'; + +interface PublicProperties {} + +interface Properties extends PublicProperties {} + +export class Container extends React.Component { + static mapState(state: RootState): Partial { + const layout = state.layout.value; + + return { + hasContextPanel: layout.hasContextPanel, + isContextPanelOpen: layout.isContextPanelOpen, + isSidekickOpen: layout.isSidekickOpen, + }; + } + + static mapActions(_state: RootState): Partial { + return {}; + } + + render() { + return ( +
+ +
+ ); + } +} + +export const UserNotices = connectContainer(Container); diff --git a/src/components/user-notices/index.tsx b/src/components/user-notices/index.tsx new file mode 100644 index 000000000..a19b8b72c --- /dev/null +++ b/src/components/user-notices/index.tsx @@ -0,0 +1 @@ +export { UserNotices } from './container'; From 00e5391032b10453d85abc7fd6f4ee1df86e5033 Mon Sep 17 00:00:00 2001 From: mrval Date: Fri, 10 Mar 2023 13:50:18 +0100 Subject: [PATCH 02/16] user notices in 1 file --- src/components/user-notices/component.tsx | 38 ------------------ src/components/user-notices/container.tsx | 34 ---------------- src/components/user-notices/index.tsx | 49 ++++++++++++++++++++++- 3 files changed, 48 insertions(+), 73 deletions(-) delete mode 100644 src/components/user-notices/component.tsx delete mode 100644 src/components/user-notices/container.tsx diff --git a/src/components/user-notices/component.tsx b/src/components/user-notices/component.tsx deleted file mode 100644 index e63e87a0b..000000000 --- a/src/components/user-notices/component.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import React from 'react'; -import { IconBellRinging3 } from '@zero-tech/zui/components/Icons'; -import { IconButton } from '@zero-tech/zui/components/IconButton'; -import { NotificationList } from '../notification'; - -interface Properties {} -interface State { - notificationsStatus: boolean; -} - -export class UserNoticeComponent extends React.Component { - state = { notificationsStatus: true }; - - componentDidMount() { - // add event mouseDown to hide notifgication - } - - componentWillUnmount() { - // remove event listener - } - - openNotifications() { - this.setState({}); - } - - render() { - return ( -
- - - {this.state.notificationsStatus && } -
- ); - } -} diff --git a/src/components/user-notices/container.tsx b/src/components/user-notices/container.tsx deleted file mode 100644 index 152867f98..000000000 --- a/src/components/user-notices/container.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import React from 'react'; -import { UserNoticeComponent } from './component'; -import { RootState } from '../../store'; -import { connectContainer } from '../../store/redux-container'; - -interface PublicProperties {} - -interface Properties extends PublicProperties {} - -export class Container extends React.Component { - static mapState(state: RootState): Partial { - const layout = state.layout.value; - - return { - hasContextPanel: layout.hasContextPanel, - isContextPanelOpen: layout.isContextPanelOpen, - isSidekickOpen: layout.isSidekickOpen, - }; - } - - static mapActions(_state: RootState): Partial { - return {}; - } - - render() { - return ( -
- -
- ); - } -} - -export const UserNotices = connectContainer(Container); diff --git a/src/components/user-notices/index.tsx b/src/components/user-notices/index.tsx index a19b8b72c..53bfcf55a 100644 --- a/src/components/user-notices/index.tsx +++ b/src/components/user-notices/index.tsx @@ -1 +1,48 @@ -export { UserNotices } from './container'; +import React from 'react'; +import { IconBellRinging3 } from '@zero-tech/zui/components/Icons'; +import { IconButton } from '@zero-tech/zui/components/IconButton'; +import { NotificationList } from '../notification'; + +interface Properties {} +interface State { + notificationsStatus: boolean; +} + +export class UserNotices extends React.Component { + state = { notificationsStatus: true }; + + componentDidMount() { + // add event mouseDown to hide notifgication + } + + componentWillUnmount() { + // remove event listener + } + + openNotifications() { + this.setState({}); + } + + renderNotificationsPopover() { + if (this.state.notificationsStatus) { + return ( +
+ +
+ ); + } + } + + render() { + return ( +
+ + + {this.renderNotificationsPopover()} +
+ ); + } +} From 76928d5d1aa6c914ac6b8f5932626d3e62672735 Mon Sep 17 00:00:00 2001 From: Dale Fukami Date: Tue, 14 Mar 2023 08:22:24 -0600 Subject: [PATCH 03/16] Test notification item --- .../notification/item/index.test.tsx | 50 +++++++++++++++++++ src/components/notification/item/index.tsx | 27 ++++++---- src/components/notification/list/index.tsx | 9 +++- 3 files changed, 76 insertions(+), 10 deletions(-) create mode 100644 src/components/notification/item/index.test.tsx diff --git a/src/components/notification/item/index.test.tsx b/src/components/notification/item/index.test.tsx new file mode 100644 index 000000000..5e4b9ccd3 --- /dev/null +++ b/src/components/notification/item/index.test.tsx @@ -0,0 +1,50 @@ +import React from 'react'; +import { shallow } from 'enzyme'; + +import { NotificationItem, Properties } from '.'; +import moment from 'moment'; + +describe('NotificationItem', () => { + const subject = (props: Partial = {}) => { + const allProps = { + title: '', + body: '', + createdAt: '', + ...props, + }; + + return shallow(); + }; + + it('renders basic info', () => { + const wrapper = subject({ + title: 'You were Notified', + body: 'Here is the description', + }); + + expect(wrapper.find('h4').text()).toEqual('You were Notified'); + expect(wrapper.find('p').text()).toEqual('Here is the description'); + }); + + it('renders basic info', () => { + const wrapper = subject({ + createdAt: '2023-03-13T22:33:34.945Z', + }); + + const expectedTimeDescription = moment('2023-03-13T22:33:34.945Z').fromNow(); + + expect(wrapper.find('.notification-item__timestamp').text()).toEqual(expectedTimeDescription); + }); + + it('renders Avatar', () => { + const wrapper = subject({ + originatingName: 'Originating Name', + originatingImageUrl: 'image-url', + }); + + const avatar = wrapper.find('Avatar'); + + expect(avatar.prop('userFriendlyName')).toEqual('Originating Name'); + expect(avatar.prop('imageURL')).toEqual('image-url'); + }); +}); diff --git a/src/components/notification/item/index.tsx b/src/components/notification/item/index.tsx index 32f80eecd..0c5ca5059 100644 --- a/src/components/notification/item/index.tsx +++ b/src/components/notification/item/index.tsx @@ -3,12 +3,22 @@ import classNames from 'classnames'; import { Avatar } from '@zero-tech/zui/components/Avatar'; import './style.scss'; +import moment from 'moment'; -interface Properties { +export interface Properties { + title?: string; + body: string; + createdAt: string; + originatingName?: string; + originatingImageUrl?: string; notRead?: boolean; } export class NotificationItem extends React.Component { + get time() { + return moment('2023-03-13T22:33:34.945Z').fromNow(); + } + render() { return (
{ 'notification-item__wrapper--not-read': this.props.notRead, })} > -
+
-
-

Zero

-

You were mentioned in #product

- Just now +
+

{this.props.title}

+

{this.props.body}

+ {this.time}
-
{/* badge from zUI */}
); } diff --git a/src/components/notification/list/index.tsx b/src/components/notification/list/index.tsx index 719a7a541..bb6cb69ee 100644 --- a/src/components/notification/list/index.tsx +++ b/src/components/notification/list/index.tsx @@ -7,6 +7,13 @@ interface Properties { list?: any[]; } +const stubNotificationProps = { + title: 'Zero', + body: 'You were mentioned in #Product', + createdAt: '2023-03-13T22:33:34.945Z', + originatingName: 'Zero', + originatingImageUrl: 'https://picsum.photos/200/300', +}; export class NotificationList extends React.Component { render() { return ( @@ -18,7 +25,7 @@ export class NotificationList extends React.Component {
{Array.from(Array(10).keys()).map(() => ( - + ))}
From baf7e88cc0265b5ddec61bc9f75e911a03fbe72f Mon Sep 17 00:00:00 2001 From: Dale Fukami Date: Tue, 14 Mar 2023 11:45:23 -0600 Subject: [PATCH 04/16] Stub fetching notifications from server --- src/components/notification/index.tsx | 2 +- .../notification/list/container.test.tsx | 67 ++++++++++++ .../notification/list/container.tsx | 35 ++++++ .../notification/list/index.test.tsx | 30 ++++++ src/components/notification/list/index.tsx | 19 ++-- src/store/index.ts | 2 + src/store/notifications/api.ts | 23 ++++ src/store/notifications/index.test.ts | 31 ++++++ src/store/notifications/index.ts | 26 +++++ src/store/notifications/saga.test.ts | 100 ++++++++++++++++++ src/store/notifications/saga.ts | 34 ++++++ src/store/notifications/types.ts | 18 ++++ src/store/saga.ts | 2 + 13 files changed, 378 insertions(+), 11 deletions(-) create mode 100644 src/components/notification/list/container.test.tsx create mode 100644 src/components/notification/list/container.tsx create mode 100644 src/components/notification/list/index.test.tsx create mode 100644 src/store/notifications/api.ts create mode 100644 src/store/notifications/index.test.ts create mode 100644 src/store/notifications/index.ts create mode 100644 src/store/notifications/saga.test.ts create mode 100644 src/store/notifications/saga.ts create mode 100644 src/store/notifications/types.ts diff --git a/src/components/notification/index.tsx b/src/components/notification/index.tsx index 10373cf31..1467b3a6a 100644 --- a/src/components/notification/index.tsx +++ b/src/components/notification/index.tsx @@ -1 +1 @@ -export { NotificationList } from './list'; +export { NotificationListContainer as NotificationList } from './list/container'; diff --git a/src/components/notification/list/container.test.tsx b/src/components/notification/list/container.test.tsx new file mode 100644 index 000000000..218667c10 --- /dev/null +++ b/src/components/notification/list/container.test.tsx @@ -0,0 +1,67 @@ +import React from 'react'; +import { shallow } from 'enzyme'; + +import { RootState } from '../../../store'; +import { AsyncListStatus } from '../../../store/normalized'; + +import { Container, Properties } from './container'; + +describe('NotificationsListContainer', () => { + const subject = (props: Partial = {}) => { + const allProps = { + fetchNotifications: () => undefined, + ...props, + }; + + return shallow(); + }; + + it('passes notifications to the NotificationList', () => { + const notifications = [ + { a: 'notification' }, + { b: 'notification' }, + ]; + const wrapper = subject({ notifications }); + + expect(wrapper.find('NotificationList').prop('list')).toEqual(notifications); + }); + + it('fetches the notifications when rendered', () => { + const fetchNotifications = jest.fn(); + + subject({ fetchNotifications }); + + expect(fetchNotifications).toHaveBeenCalledOnce(); + }); + + describe('mapState', () => { + const subject = (state: Partial) => { + return Container.mapState({ + ...state, + } as RootState); + }; + + test('notifications', () => { + const state = subject({ + notificationsList: { + status: AsyncListStatus.Idle, + value: [ + 'id-1', + 'id-2', + ], + }, + normalized: { + notifications: { + 'id-1': { id: 'id-1' }, + 'id-2': { id: 'id-2' }, + }, + }, + }); + + expect(state.notifications).toIncludeAllPartialMembers([ + { id: 'id-1' }, + { id: 'id-2' }, + ]); + }); + }); +}); diff --git a/src/components/notification/list/container.tsx b/src/components/notification/list/container.tsx new file mode 100644 index 000000000..0f09d6c93 --- /dev/null +++ b/src/components/notification/list/container.tsx @@ -0,0 +1,35 @@ +import React from 'react'; + +import { connectContainer } from '../../../store/redux-container'; +import { RootState } from '../../../store'; +import { fetch as fetchNotifications, denormalize } from '../../../store/notifications'; +import { NotificationList } from '.'; + +export interface Properties { + notifications: any[]; + fetchNotifications: () => void; +} + +export class Container extends React.Component { + static mapState(state: RootState): Partial { + return { + notifications: denormalize(state.notificationsList.value, state), + }; + } + + static mapActions(_props: Properties): Partial { + return { + fetchNotifications, + }; + } + + componentDidMount() { + this.props.fetchNotifications(); + } + + render() { + return ; + } +} + +export const NotificationListContainer = connectContainer<{}>(Container); diff --git a/src/components/notification/list/index.test.tsx b/src/components/notification/list/index.test.tsx new file mode 100644 index 000000000..a9694aebe --- /dev/null +++ b/src/components/notification/list/index.test.tsx @@ -0,0 +1,30 @@ +import React from 'react'; +import { shallow } from 'enzyme'; + +import { NotificationList, Properties } from '.'; + +describe('NotificationList', () => { + const subject = (props: Partial = {}) => { + const allProps = { + list: [], + ...props, + }; + + return shallow(); + }; + + it('renders the list', () => { + const wrapper = subject({ + list: [ + { id: 'id-1', notificationType: 'chat_channel_mention', createdAt: '2023-03-13T22:33:34.945Z' }, + { id: 'id-2', notificationType: 'chat_channel_mention', createdAt: '2023-01-20T22:33:34.945Z' }, + ], + }); + + // Temporary attributes match until we parse the channel name for this type of notification + expect(wrapper.find('NotificationItem').map((n) => n.props())).toEqual([ + { title: 'Network Name', body: 'You were mentioned in', createdAt: '2023-03-13T22:33:34.945Z' }, + { title: 'Network Name', body: 'You were mentioned in', createdAt: '2023-01-20T22:33:34.945Z' }, + ]); + }); +}); diff --git a/src/components/notification/list/index.tsx b/src/components/notification/list/index.tsx index bb6cb69ee..39d7bba8a 100644 --- a/src/components/notification/list/index.tsx +++ b/src/components/notification/list/index.tsx @@ -1,19 +1,13 @@ import React from 'react'; + import { NotificationItem } from '../item'; import './style.scss'; -interface Properties { +export interface Properties { list?: any[]; } -const stubNotificationProps = { - title: 'Zero', - body: 'You were mentioned in #Product', - createdAt: '2023-03-13T22:33:34.945Z', - originatingName: 'Zero', - originatingImageUrl: 'https://picsum.photos/200/300', -}; export class NotificationList extends React.Component { render() { return ( @@ -24,8 +18,13 @@ export class NotificationList extends React.Component {
- {Array.from(Array(10).keys()).map(() => ( - + {this.props.list.map((n) => ( + ))}
diff --git a/src/store/index.ts b/src/store/index.ts index f04307c17..f42e0e8a0 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -13,6 +13,7 @@ import { reducer as apps } from './apps'; import { reducer as normalized } from './normalized'; import { reducer as authentication } from './authentication'; import { reducer as chat } from './chat'; +import { reducer as notificationsList } from './notifications'; const sagaMiddleware = createSagaMiddleware({ onError: (e) => { @@ -30,6 +31,7 @@ export const rootReducer = combineReducers({ normalized, authentication, chat, + notificationsList, }); export type RootState = ReturnType; diff --git a/src/store/notifications/api.ts b/src/store/notifications/api.ts new file mode 100644 index 000000000..b80e984fb --- /dev/null +++ b/src/store/notifications/api.ts @@ -0,0 +1,23 @@ +const stubNotification = { + id: '', + title: 'Zero', + body: 'You were mentioned in #Product', + createdAt: '2023-03-13T22:33:34.945Z', + originUser: { + id: 'b7b11fbb-0734-4987-b5fc-d4aec19a5eb6', + profileSummary: { + id: '338785c1-310a-4a0e-9e93-59a04b20b400', + firstName: 'Dale', + lastName: 'Stub', + profileImage: 'https://res.cloudinary.com/fact0ry-dev/image/upload/v1623021591/zero-assets/avatars/pfp-18.jpg', + }, + }, +}; + +const stubNotifications = Array.from(Array(10).keys()).map((v) => { + return { ...stubNotification, id: v }; +}); + +export async function fetchNotifications() { + return stubNotifications; +} diff --git a/src/store/notifications/index.test.ts b/src/store/notifications/index.test.ts new file mode 100644 index 000000000..829d72d33 --- /dev/null +++ b/src/store/notifications/index.test.ts @@ -0,0 +1,31 @@ +import { reducer, receiveNormalized, setStatus } from '.'; +import { AsyncListStatus, AsyncNormalizedListState } from '../normalized'; + +describe('notificationsList reducer', () => { + const initialExistingState: AsyncNormalizedListState = { + status: AsyncListStatus.Idle, + value: [], + }; + + it('should handle initial state', () => { + expect(reducer(undefined, { type: 'unknown' })).toEqual({ + status: AsyncListStatus.Idle, + value: [], + }); + }); + + it('should replace existing state with new state', () => { + const actual = reducer(initialExistingState, receiveNormalized(['the-id'])); + + expect(actual).toStrictEqual({ + value: ['the-id'], + status: AsyncListStatus.Idle, + }); + }); + + it('should replace existing status with new status', () => { + const actual = reducer(initialExistingState, setStatus(AsyncListStatus.Fetching)); + + expect(actual.status).toEqual(AsyncListStatus.Fetching); + }); +}); diff --git a/src/store/notifications/index.ts b/src/store/notifications/index.ts new file mode 100644 index 000000000..cea4a1a41 --- /dev/null +++ b/src/store/notifications/index.ts @@ -0,0 +1,26 @@ +import { createAction } from '@reduxjs/toolkit'; + +import { createNormalizedListSlice, createNormalizedSlice } from '../normalized'; + +import { Payload } from './saga'; + +export enum SagaActionTypes { + Fetch = 'notifications/saga/fetch', +} + +const fetch = createAction(SagaActionTypes.Fetch); + +const slice = createNormalizedSlice({ + name: 'notifications', +}); +const { schema } = slice; + +const listSlice = createNormalizedListSlice({ + name: 'notificationsList', + schema, +}); + +export const { receiveNormalized, setStatus, receive } = listSlice.actions; +export const { reducer, normalize, denormalize } = listSlice; + +export { fetch }; diff --git a/src/store/notifications/saga.test.ts b/src/store/notifications/saga.test.ts new file mode 100644 index 000000000..885178094 --- /dev/null +++ b/src/store/notifications/saga.test.ts @@ -0,0 +1,100 @@ +import { expectSaga } from 'redux-saga-test-plan'; +import * as matchers from 'redux-saga-test-plan/matchers'; + +import { AsyncListStatus } from '../normalized'; +import { rootReducer } from '..'; + +import { fetch } from './saga'; +import { setStatus } from '.'; +import { fetchNotifications } from './api'; + +describe('notifications list saga', () => { + it('sets status to fetching', async () => { + await expectSaga(fetch, { payload: {} }) + .put(setStatus(AsyncListStatus.Fetching)) + .provide([ + [ + matchers.call.fn(fetchNotifications), + [], + ], + ]) + .run(); + }); + + it('fetches notifications', async () => { + await expectSaga(fetch, { payload: {} }) + .provide([ + [ + matchers.call.fn(fetchNotifications), + [], + ], + ]) + .call(fetchNotifications) + .run(); + }); + + it('sets status to Idle', async () => { + const { + storeState: { notificationsList }, + } = await expectSaga(fetch, { payload: {} }) + .withReducer(rootReducer) + .provide([ + [ + matchers.call.fn(fetchNotifications), + [], + ], + ]) + .run(); + + expect(notificationsList.status).toBe(AsyncListStatus.Idle); + }); + + it('adds notification ids to notificationsList state', async () => { + const ids = [ + 'id-1', + 'id-2', + 'id-3', + ]; + const { + storeState: { notificationsList }, + } = await expectSaga(fetch, { payload: {} }) + .withReducer(rootReducer) + .provide([ + [ + matchers.call.fn(fetchNotifications), + [ + { id: 'id-1', notificationType: 'type-1' }, + { id: 'id-2', notificationType: 'type-2' }, + { id: 'id-3', notificationType: 'type-3' }, + ], + ], + ]) + .run(); + + expect(notificationsList.value).toStrictEqual(ids); + }); + + it('adds notifications to normalized state', async () => { + const { + storeState: { normalized }, + } = await expectSaga(fetch, { payload: {} }) + .provide([ + [ + matchers.call.fn(fetchNotifications), + [ + { id: 'id-1', notificationType: 'type-1' }, + { id: 'id-2', notificationType: 'type-2' }, + { id: 'id-3', notificationType: 'type-3' }, + ], + ], + ]) + .withReducer(rootReducer) + .run(); + + expect(normalized.notifications).toStrictEqual({ + 'id-1': { id: 'id-1', notificationType: 'type-1' }, + 'id-2': { id: 'id-2', notificationType: 'type-2' }, + 'id-3': { id: 'id-3', notificationType: 'type-3' }, + }); + }); +}); diff --git a/src/store/notifications/saga.ts b/src/store/notifications/saga.ts new file mode 100644 index 000000000..d510aadba --- /dev/null +++ b/src/store/notifications/saga.ts @@ -0,0 +1,34 @@ +import { takeLatest, put, call } from 'redux-saga/effects'; + +import { AsyncListStatus } from '../normalized'; + +import { SagaActionTypes, receive, setStatus } from '.'; +import { fetchNotifications } from './api'; + +export interface Payload { + notificationType: string; + data: object; + originUser: { + id: string; + profileSummary: { + id: string; + firstName: string; + lastName: string; + profileImage: string; + }; + }; +} + +export function* fetch(_action) { + yield put(setStatus(AsyncListStatus.Fetching)); + + const notifications = yield call(fetchNotifications); + + yield put(receive(notifications)); + + yield put(setStatus(AsyncListStatus.Idle)); +} + +export function* saga() { + yield takeLatest(SagaActionTypes.Fetch, fetch); +} diff --git a/src/store/notifications/types.ts b/src/store/notifications/types.ts new file mode 100644 index 000000000..a8d773bd6 --- /dev/null +++ b/src/store/notifications/types.ts @@ -0,0 +1,18 @@ +import { Channel, User } from '../channels'; + +export interface Payload { + channelId: string; +} + +export enum ChannelType { + Channel, + DirectMessage, +} + +export interface DirectMessage extends Channel { + otherMembers: User[]; +} + +export interface CreateMessengerConversation { + userIds: string[]; +} diff --git a/src/store/saga.ts b/src/store/saga.ts index 6ec49fe9f..eeb603561 100644 --- a/src/store/saga.ts +++ b/src/store/saga.ts @@ -10,6 +10,7 @@ import { saga as authentication } from './authentication/saga'; import { saga as chat } from './chat/saga'; import { saga as theme } from './theme/saga'; import { saga as layout } from './layout/saga'; +import { saga as notificationsList } from './notifications/saga'; export function* rootSaga() { const allSagas = { @@ -23,6 +24,7 @@ export function* rootSaga() { chat, theme, layout, + notificationsList, }; yield all( From 877b7359d40272d2dfe01fc93f159d2db4d8ea6e Mon Sep 17 00:00:00 2001 From: Dale Fukami Date: Tue, 14 Mar 2023 12:42:23 -0600 Subject: [PATCH 05/16] Map chat_channel_mention notifications --- .../notification/list/container.test.tsx | 86 +++++++++++++++++++ .../notification/list/container.tsx | 26 +++++- .../notification/list/index.test.tsx | 9 +- src/components/notification/list/index.tsx | 12 ++- src/store/notifications/api.ts | 4 +- 5 files changed, 124 insertions(+), 13 deletions(-) diff --git a/src/components/notification/list/container.test.tsx b/src/components/notification/list/container.test.tsx index 218667c10..affecb5e1 100644 --- a/src/components/notification/list/container.test.tsx +++ b/src/components/notification/list/container.test.tsx @@ -64,4 +64,90 @@ describe('NotificationsListContainer', () => { ]); }); }); + + describe('mapNotification', () => { + const subject = (notification = {}, state: Partial) => { + return Container.mapNotification(notification, state); + }; + + describe('unknown type', () => { + it('maps body with a known channel', () => { + const mappedNotification = subject({ notificationType: 'unknown_type' }, {}); + + expect(mappedNotification).toBeNull(); + }); + }); + + describe('chat_channel_mention', () => { + it('maps body with a known channel', () => { + const mappedNotification = subject( + { + notificationType: 'chat_channel_mention', + data: { chatId: 'chat-id' }, + }, + { + normalized: { + channels: { + 'chat-id': { id: 'chat-id', name: 'TestingChannel' }, + }, + }, + } + ); + + expect(mappedNotification.body).toEqual('You were mentioned in #TestingChannel'); + }); + + it('maps body with an unknown channel', () => { + const mappedNotification = subject( + { + notificationType: 'chat_channel_mention', + data: { chatId: 'chat-id' }, + }, + { + normalized: { + channels: {}, + }, + } + ); + + expect(mappedNotification.body).toEqual('You were mentioned in a channel'); + }); + + it('maps title to default value', () => { + // We don't have network information in the data store yet + const mappedNotification = subject( + { + notificationType: 'chat_channel_mention', + data: { chatId: 'chat-id' }, + }, + { + normalized: { + channels: { + 'chat-id': { id: 'chat-id', name: 'TestingChannel', networkId: 'network-id' }, + }, + }, + } + ); + + expect(mappedNotification.title).toEqual('Network Message'); + }); + + it('maps default properties', () => { + const mappedNotification = subject( + { + id: 'notification-id', + notificationType: 'chat_channel_mention', + data: { chatId: 'chat-id' }, + createdAt: '2023-01-20T22:33:34.945Z', + }, + { + normalized: { channels: {} }, + } + ); + + expect(mappedNotification.id).toEqual('notification-id'); + expect(mappedNotification.createdAt).toEqual('2023-01-20T22:33:34.945Z'); + }); + }); + }); }); diff --git a/src/components/notification/list/container.tsx b/src/components/notification/list/container.tsx index 0f09d6c93..c08726c71 100644 --- a/src/components/notification/list/container.tsx +++ b/src/components/notification/list/container.tsx @@ -3,6 +3,8 @@ import React from 'react'; import { connectContainer } from '../../../store/redux-container'; import { RootState } from '../../../store'; import { fetch as fetchNotifications, denormalize } from '../../../store/notifications'; +import { denormalize as denormalizeChannel } from '../../../store/channels'; + import { NotificationList } from '.'; export interface Properties { @@ -12,9 +14,11 @@ export interface Properties { export class Container extends React.Component { static mapState(state: RootState): Partial { - return { - notifications: denormalize(state.notificationsList.value, state), - }; + const notifications = denormalize(state.notificationsList.value, state) + .map((n) => Container.mapNotification(n, state)) + .filter((n) => !!n); + + return { notifications }; } static mapActions(_props: Properties): Partial { @@ -23,6 +27,22 @@ export class Container extends React.Component { }; } + static mapNotification(notification, state: RootState) { + if (notification.notificationType === 'chat_channel_mention') { + const channelId = notification.data.chatId; + const { name: channelName } = denormalizeChannel(channelId, state) || {}; + const channelText = channelName ? `#${channelName}` : 'a channel'; + + return { + id: notification.id, + createdAt: notification.createdAt, + title: 'Network Message', + body: `You were mentioned in ${channelText}`, + }; + } + return null; + } + componentDidMount() { this.props.fetchNotifications(); } diff --git a/src/components/notification/list/index.test.tsx b/src/components/notification/list/index.test.tsx index a9694aebe..ed7e97b90 100644 --- a/src/components/notification/list/index.test.tsx +++ b/src/components/notification/list/index.test.tsx @@ -16,15 +16,14 @@ describe('NotificationList', () => { it('renders the list', () => { const wrapper = subject({ list: [ - { id: 'id-1', notificationType: 'chat_channel_mention', createdAt: '2023-03-13T22:33:34.945Z' }, - { id: 'id-2', notificationType: 'chat_channel_mention', createdAt: '2023-01-20T22:33:34.945Z' }, + { id: 'id-1', title: 'title-1', body: 'body-1', createdAt: '2023-03-13T22:33:34.945Z' }, + { id: 'id-2', title: 'title-2', body: 'body-2', createdAt: '2023-01-20T22:33:34.945Z' }, ], }); - // Temporary attributes match until we parse the channel name for this type of notification expect(wrapper.find('NotificationItem').map((n) => n.props())).toEqual([ - { title: 'Network Name', body: 'You were mentioned in', createdAt: '2023-03-13T22:33:34.945Z' }, - { title: 'Network Name', body: 'You were mentioned in', createdAt: '2023-01-20T22:33:34.945Z' }, + { title: 'title-1', body: 'body-1', createdAt: '2023-03-13T22:33:34.945Z' }, + { title: 'title-2', body: 'body-2', createdAt: '2023-01-20T22:33:34.945Z' }, ]); }); }); diff --git a/src/components/notification/list/index.tsx b/src/components/notification/list/index.tsx index 39d7bba8a..ed85f60e0 100644 --- a/src/components/notification/list/index.tsx +++ b/src/components/notification/list/index.tsx @@ -4,8 +4,14 @@ import { NotificationItem } from '../item'; import './style.scss'; +interface Notification { + id: string; + title: string; + body: string; + createdAt: string; +} export interface Properties { - list?: any[]; + list?: Notification[]; } export class NotificationList extends React.Component { @@ -21,8 +27,8 @@ export class NotificationList extends React.Component { {this.props.list.map((n) => ( ))} diff --git a/src/store/notifications/api.ts b/src/store/notifications/api.ts index b80e984fb..c1e08c649 100644 --- a/src/store/notifications/api.ts +++ b/src/store/notifications/api.ts @@ -1,8 +1,8 @@ const stubNotification = { id: '', - title: 'Zero', - body: 'You were mentioned in #Product', + notificationType: 'chat_channel_mention', createdAt: '2023-03-13T22:33:34.945Z', + data: { chatId: '248576469_2a47f2218374efce03c060d4390300fcd1e213af' }, originUser: { id: 'b7b11fbb-0734-4987-b5fc-d4aec19a5eb6', profileSummary: { From 890711aaf0a65299d96967152fdd38b07f3d6ddc Mon Sep 17 00:00:00 2001 From: Dale Fukami Date: Tue, 14 Mar 2023 12:52:21 -0600 Subject: [PATCH 06/16] Map the originator information for rendering the Avatar --- .../notification/list/container.test.tsx | 26 +++++++++++++++++-- .../notification/list/container.tsx | 13 +++++++++- src/components/notification/list/index.tsx | 5 ++++ 3 files changed, 41 insertions(+), 3 deletions(-) diff --git a/src/components/notification/list/container.test.tsx b/src/components/notification/list/container.test.tsx index affecb5e1..20cf47038 100644 --- a/src/components/notification/list/container.test.tsx +++ b/src/components/notification/list/container.test.tsx @@ -52,8 +52,8 @@ describe('NotificationsListContainer', () => { }, normalized: { notifications: { - 'id-1': { id: 'id-1' }, - 'id-2': { id: 'id-2' }, + 'id-1': { id: 'id-1', notificationType: 'chat_channel_mention' }, + 'id-2': { id: 'id-2', notificationType: 'chat_channel_mention' }, }, }, }); @@ -148,6 +148,28 @@ describe('NotificationsListContainer', () => { expect(mappedNotification.id).toEqual('notification-id'); expect(mappedNotification.createdAt).toEqual('2023-01-20T22:33:34.945Z'); }); + + it('maps sender', () => { + const mappedNotification = subject( + { + notificationType: 'chat_channel_mention', + data: {}, + originUser: { + profileSummary: { + firstName: 'first', + lastName: 'Last', + profileImage: 'image-url', + }, + }, + }, + { + normalized: { channels: {} }, + } + ); + + expect(mappedNotification.originatingName).toEqual('first Last'); + expect(mappedNotification.originatingImageUrl).toEqual('image-url'); + }); }); }); }); diff --git a/src/components/notification/list/container.tsx b/src/components/notification/list/container.tsx index c08726c71..e9cea01a3 100644 --- a/src/components/notification/list/container.tsx +++ b/src/components/notification/list/container.tsx @@ -29,15 +29,26 @@ export class Container extends React.Component { static mapNotification(notification, state: RootState) { if (notification.notificationType === 'chat_channel_mention') { - const channelId = notification.data.chatId; + const channelId = notification.data?.chatId; const { name: channelName } = denormalizeChannel(channelId, state) || {}; const channelText = channelName ? `#${channelName}` : 'a channel'; + // This should probably be extracted to a display utility or added + // to the domain model + const displayName = [ + notification.originUser?.profileSummary?.firstName, + notification.originUser?.profileSummary?.lastName, + ] + .filter((e) => e) + .join(' '); + return { id: notification.id, createdAt: notification.createdAt, title: 'Network Message', body: `You were mentioned in ${channelText}`, + originatingName: displayName, + originatingImageUrl: notification.originUser?.profileSummary?.profileImage, }; } return null; diff --git a/src/components/notification/list/index.tsx b/src/components/notification/list/index.tsx index ed85f60e0..17e64b20e 100644 --- a/src/components/notification/list/index.tsx +++ b/src/components/notification/list/index.tsx @@ -9,7 +9,10 @@ interface Notification { title: string; body: string; createdAt: string; + originatingName?: string; + originatingImageUrl?: string; } + export interface Properties { list?: Notification[]; } @@ -30,6 +33,8 @@ export class NotificationList extends React.Component { title={n.title} body={n.body} createdAt={n.createdAt} + originatingName={n.originatingName} + originatingImageUrl={n.originatingImageUrl} /> ))} From 0ea7fad363c1445068ec89be8393a79af6fe722a Mon Sep 17 00:00:00 2001 From: Dale Fukami Date: Tue, 14 Mar 2023 13:04:15 -0600 Subject: [PATCH 07/16] Fetch notifications from the api --- .../notification/list/container.tsx | 13 +++++++-- src/store/notifications/api.ts | 29 +++++-------------- src/store/notifications/saga.test.ts | 4 +-- src/store/notifications/saga.ts | 18 ++++-------- 4 files changed, 25 insertions(+), 39 deletions(-) diff --git a/src/components/notification/list/container.tsx b/src/components/notification/list/container.tsx index e9cea01a3..05bae67b5 100644 --- a/src/components/notification/list/container.tsx +++ b/src/components/notification/list/container.tsx @@ -9,16 +9,23 @@ import { NotificationList } from '.'; export interface Properties { notifications: any[]; - fetchNotifications: () => void; + userId: string; + fetchNotifications: (payload: { userId: string }) => void; } export class Container extends React.Component { static mapState(state: RootState): Partial { + const { + authentication: { user }, + } = state; const notifications = denormalize(state.notificationsList.value, state) .map((n) => Container.mapNotification(n, state)) .filter((n) => !!n); - return { notifications }; + return { + notifications, + userId: user?.data?.id, + }; } static mapActions(_props: Properties): Partial { @@ -55,7 +62,7 @@ export class Container extends React.Component { } componentDidMount() { - this.props.fetchNotifications(); + this.props.fetchNotifications({ userId: this.props.userId }); } render() { diff --git a/src/store/notifications/api.ts b/src/store/notifications/api.ts index c1e08c649..e3adfbfb3 100644 --- a/src/store/notifications/api.ts +++ b/src/store/notifications/api.ts @@ -1,23 +1,10 @@ -const stubNotification = { - id: '', - notificationType: 'chat_channel_mention', - createdAt: '2023-03-13T22:33:34.945Z', - data: { chatId: '248576469_2a47f2218374efce03c060d4390300fcd1e213af' }, - originUser: { - id: 'b7b11fbb-0734-4987-b5fc-d4aec19a5eb6', - profileSummary: { - id: '338785c1-310a-4a0e-9e93-59a04b20b400', - firstName: 'Dale', - lastName: 'Stub', - profileImage: 'https://res.cloudinary.com/fact0ry-dev/image/upload/v1623021591/zero-assets/avatars/pfp-18.jpg', - }, - }, -}; +import { get } from '../../lib/api/rest'; -const stubNotifications = Array.from(Array(10).keys()).map((v) => { - return { ...stubNotification, id: v }; -}); - -export async function fetchNotifications() { - return stubNotifications; +export async function fetchNotifications(userId: string) { + console.log('heh', userId); + const response = await get('/api/notifications/filter', { + limit: 15, + userId, + }); + return response.body; } diff --git a/src/store/notifications/saga.test.ts b/src/store/notifications/saga.test.ts index 885178094..f46ea461d 100644 --- a/src/store/notifications/saga.test.ts +++ b/src/store/notifications/saga.test.ts @@ -22,14 +22,14 @@ describe('notifications list saga', () => { }); it('fetches notifications', async () => { - await expectSaga(fetch, { payload: {} }) + await expectSaga(fetch, { payload: { userId: 'user-id' } }) .provide([ [ matchers.call.fn(fetchNotifications), [], ], ]) - .call(fetchNotifications) + .call(fetchNotifications, 'user-id') .run(); }); diff --git a/src/store/notifications/saga.ts b/src/store/notifications/saga.ts index d510aadba..dcac3dfe3 100644 --- a/src/store/notifications/saga.ts +++ b/src/store/notifications/saga.ts @@ -6,23 +6,15 @@ import { SagaActionTypes, receive, setStatus } from '.'; import { fetchNotifications } from './api'; export interface Payload { - notificationType: string; - data: object; - originUser: { - id: string; - profileSummary: { - id: string; - firstName: string; - lastName: string; - profileImage: string; - }; - }; + userId: string; } -export function* fetch(_action) { +export function* fetch(action) { + const { userId } = action.payload; + yield put(setStatus(AsyncListStatus.Fetching)); - const notifications = yield call(fetchNotifications); + const notifications = yield call(fetchNotifications, userId); yield put(receive(notifications)); From 3522f3706e36ddcd77a026e774aa80ea0f8cb00c Mon Sep 17 00:00:00 2001 From: Dale Fukami Date: Tue, 14 Mar 2023 13:44:00 -0600 Subject: [PATCH 08/16] Render the Notification list in a popup when sidekick is clicked --- src/Main.tsx | 3 -- src/components/notification/list/style.scss | 9 ++++ src/components/sidekick/index.tsx | 8 +++- src/components/user-notices/index.tsx | 48 --------------------- 4 files changed, 16 insertions(+), 52 deletions(-) delete mode 100644 src/components/user-notices/index.tsx diff --git a/src/Main.tsx b/src/Main.tsx index 8a38adc71..51bb18a14 100644 --- a/src/Main.tsx +++ b/src/Main.tsx @@ -13,7 +13,6 @@ import classNames from 'classnames'; import { Sidekick } from './components/sidekick/index'; import { withContext as withAuthenticationContext } from './components/authentication/context'; import { MessengerChat } from './components/messenger/chat'; -import { UserNotices } from './components/user-notices'; export interface Properties { hasContextPanel: boolean; @@ -64,8 +63,6 @@ export class Container extends React.Component {
- -
diff --git a/src/components/notification/list/style.scss b/src/components/notification/list/style.scss index e69de29bb..285eb7f91 100644 --- a/src/components/notification/list/style.scss +++ b/src/components/notification/list/style.scss @@ -0,0 +1,9 @@ +@use '~@zero-tech/zui/styles/theme' as theme; + +.notification-list__wrapper { + position: absolute; + top: 75px; + right: 20px; + z-index: 500; + background-color: theme.$color-primary-2; +} diff --git a/src/components/sidekick/index.tsx b/src/components/sidekick/index.tsx index 5c512824b..37fadaf06 100644 --- a/src/components/sidekick/index.tsx +++ b/src/components/sidekick/index.tsx @@ -17,6 +17,8 @@ import { denormalize } from '../../store/channels'; import { SidekickTabs as Tabs } from './types'; import './styles.scss'; +import { createPortal } from 'react-dom'; +import { NotificationList } from '../notification'; interface PublicProperties { className?: string; @@ -159,12 +161,16 @@ export class Container extends React.Component { ); case Tabs.NOTIFICATIONS: - return
NOTIFICATIONS
; + return createPortal(this.renderNotifications(), document.body); default: return null; } } + renderNotifications() { + return ; + } + render() { return ( diff --git a/src/components/user-notices/index.tsx b/src/components/user-notices/index.tsx deleted file mode 100644 index 53bfcf55a..000000000 --- a/src/components/user-notices/index.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import React from 'react'; -import { IconBellRinging3 } from '@zero-tech/zui/components/Icons'; -import { IconButton } from '@zero-tech/zui/components/IconButton'; -import { NotificationList } from '../notification'; - -interface Properties {} -interface State { - notificationsStatus: boolean; -} - -export class UserNotices extends React.Component { - state = { notificationsStatus: true }; - - componentDidMount() { - // add event mouseDown to hide notifgication - } - - componentWillUnmount() { - // remove event listener - } - - openNotifications() { - this.setState({}); - } - - renderNotificationsPopover() { - if (this.state.notificationsStatus) { - return ( -
- -
- ); - } - } - - render() { - return ( -
- - - {this.renderNotificationsPopover()} -
- ); - } -} From a4f229d5b2882fa8a10deb41490b4f223d0e1462 Mon Sep 17 00:00:00 2001 From: Dale Fukami Date: Tue, 14 Mar 2023 14:28:10 -0600 Subject: [PATCH 09/16] Style the notifications popup --- src/components/notification/index.tsx | 2 +- src/components/notification/item/style.scss | 38 ++++++++++++++++++++ src/components/notification/list/index.tsx | 5 --- src/components/notification/list/style.scss | 9 ----- src/components/notification/popup/index.tsx | 25 +++++++++++++ src/components/notification/popup/style.scss | 26 ++++++++++++++ src/components/sidekick/index.tsx | 7 +--- 7 files changed, 91 insertions(+), 21 deletions(-) create mode 100644 src/components/notification/popup/index.tsx create mode 100644 src/components/notification/popup/style.scss diff --git a/src/components/notification/index.tsx b/src/components/notification/index.tsx index 1467b3a6a..5a9bb212a 100644 --- a/src/components/notification/index.tsx +++ b/src/components/notification/index.tsx @@ -1 +1 @@ -export { NotificationListContainer as NotificationList } from './list/container'; +export { NotificationPopup as NotificationList } from './popup'; diff --git a/src/components/notification/item/style.scss b/src/components/notification/item/style.scss index f2ec94235..44b434c24 100644 --- a/src/components/notification/item/style.scss +++ b/src/components/notification/item/style.scss @@ -1,5 +1,43 @@ +@use '~@zero-tech/zui/styles/theme' as theme; + .notification-item { &__wrapper { display: flex; + padding: 16px 16px; + } + + &__avatar { + margin-right: 16px; + } + + &__content { + h4 { + color: theme.$color-greyscale-12; + + margin: 0px; + font-style: normal; + font-weight: 700; + font-size: 14px; + line-height: 17px; + } + + p { + color: theme.$color-greyscale-12; + + margin: 4px 0px; + font-style: normal; + font-weight: 400; + font-size: 14px; + line-height: 17px; + } + } + + &__timestamp { + color: theme.$color-greyscale-11; + + font-style: normal; + font-weight: 400; + font-size: 12px; + line-height: 15px; } } diff --git a/src/components/notification/list/index.tsx b/src/components/notification/list/index.tsx index 17e64b20e..596f63745 100644 --- a/src/components/notification/list/index.tsx +++ b/src/components/notification/list/index.tsx @@ -21,11 +21,6 @@ export class NotificationList extends React.Component { render() { return (
-
-

Notifications

-
-
-
{this.props.list.map((n) => ( { + render() { + return <>{createPortal(this.renderPopup(), document.body)}; + } + + renderPopup() { + return ( +
+
+

Notifications

+
+ +
+ ); + } +} diff --git a/src/components/notification/popup/style.scss b/src/components/notification/popup/style.scss new file mode 100644 index 000000000..9370dc0b6 --- /dev/null +++ b/src/components/notification/popup/style.scss @@ -0,0 +1,26 @@ +@use '~@zero-tech/zui/styles/theme' as theme; + +.notification-popup { + position: absolute; + top: 75px; + right: 20px; + z-index: 500; + background-color: theme.$color-primary-2; + padding: 0px 16px; + + // Elevation 3 + box-shadow: 12px 11px 54px #040304; + border-radius: 16px 0px 16px 16px; + + font-family: 'Inter'; + + h3 { + color: theme.$color-white; + margin: 20px 0px; + + font-style: normal; + font-weight: 700; + font-size: 16px; + line-height: 24px; + } +} diff --git a/src/components/sidekick/index.tsx b/src/components/sidekick/index.tsx index 37fadaf06..0f2514c95 100644 --- a/src/components/sidekick/index.tsx +++ b/src/components/sidekick/index.tsx @@ -17,7 +17,6 @@ import { denormalize } from '../../store/channels'; import { SidekickTabs as Tabs } from './types'; import './styles.scss'; -import { createPortal } from 'react-dom'; import { NotificationList } from '../notification'; interface PublicProperties { @@ -161,16 +160,12 @@ export class Container extends React.Component {
); case Tabs.NOTIFICATIONS: - return createPortal(this.renderNotifications(), document.body); + return ; default: return null; } } - renderNotifications() { - return ; - } - render() { return ( From db0f75785994b0717b8715d15fca2466d8b6098a Mon Sep 17 00:00:00 2001 From: Dale Fukami Date: Wed, 15 Mar 2023 12:01:40 -0600 Subject: [PATCH 10/16] Fix notifications request --- src/lib/api/rest.ts | 6 +++++- src/store/notifications/api.ts | 6 +----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/lib/api/rest.ts b/src/lib/api/rest.ts index 365c4f2c7..cf7c94b26 100644 --- a/src/lib/api/rest.ts +++ b/src/lib/api/rest.ts @@ -24,7 +24,7 @@ function apiUrl(path: string): string { */ const xPlatFormHeader = { 'X-APP-PLATFORM': 'zos' }; -export function get(path: string, filter?: RequestFilter | string) { +export function get(path: string, filter?: RequestFilter | string, query?: any) { let queryData; if (filter) { if (typeof filter === 'string') { @@ -36,6 +36,10 @@ export function get(path: string, filter?: RequestFilter | string) { } } + if (query) { + queryData = { ...queryData, ...query }; + } + return Request.get(apiUrl(path)).set(xPlatFormHeader).withCredentials().query(queryData); } diff --git a/src/store/notifications/api.ts b/src/store/notifications/api.ts index e3adfbfb3..19d97b88b 100644 --- a/src/store/notifications/api.ts +++ b/src/store/notifications/api.ts @@ -1,10 +1,6 @@ import { get } from '../../lib/api/rest'; export async function fetchNotifications(userId: string) { - console.log('heh', userId); - const response = await get('/api/notifications/filter', { - limit: 15, - userId, - }); + const response = await get('/api/notifications/filter', { limit: 15 }, { userId }); return response.body; } From e925559dc664c299ec1db2c3bcc68666ce611f1d Mon Sep 17 00:00:00 2001 From: Dale Fukami Date: Wed, 15 Mar 2023 12:12:12 -0600 Subject: [PATCH 11/16] Fix createdAt rendering --- src/components/notification/item/index.test.tsx | 4 ++-- src/components/notification/item/index.tsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/notification/item/index.test.tsx b/src/components/notification/item/index.test.tsx index 5e4b9ccd3..2dfd7b4ea 100644 --- a/src/components/notification/item/index.test.tsx +++ b/src/components/notification/item/index.test.tsx @@ -28,10 +28,10 @@ describe('NotificationItem', () => { it('renders basic info', () => { const wrapper = subject({ - createdAt: '2023-03-13T22:33:34.945Z', + createdAt: '2023-03-10T22:33:34.945Z', }); - const expectedTimeDescription = moment('2023-03-13T22:33:34.945Z').fromNow(); + const expectedTimeDescription = moment('2023-03-10T22:33:34.945Z').fromNow(); expect(wrapper.find('.notification-item__timestamp').text()).toEqual(expectedTimeDescription); }); diff --git a/src/components/notification/item/index.tsx b/src/components/notification/item/index.tsx index 0c5ca5059..0b9e0eebf 100644 --- a/src/components/notification/item/index.tsx +++ b/src/components/notification/item/index.tsx @@ -16,7 +16,7 @@ export interface Properties { export class NotificationItem extends React.Component { get time() { - return moment('2023-03-13T22:33:34.945Z').fromNow(); + return moment(this.props.createdAt).fromNow(); } render() { From 37745dde5b802ab13254635403cc578a32fbf1e8 Mon Sep 17 00:00:00 2001 From: Dale Fukami Date: Wed, 15 Mar 2023 12:45:00 -0600 Subject: [PATCH 12/16] Fix the width of the popup --- src/components/notification/popup/style.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/notification/popup/style.scss b/src/components/notification/popup/style.scss index 9370dc0b6..1fa909289 100644 --- a/src/components/notification/popup/style.scss +++ b/src/components/notification/popup/style.scss @@ -6,6 +6,7 @@ right: 20px; z-index: 500; background-color: theme.$color-primary-2; + width: 360px; padding: 0px 16px; // Elevation 3 From c6c8d95d5f4de7a5113a254ade6ad3123ca87705 Mon Sep 17 00:00:00 2001 From: Dale Fukami Date: Wed, 15 Mar 2023 12:50:04 -0600 Subject: [PATCH 13/16] Fix sidekick test --- src/components/sidekick/index.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/sidekick/index.test.tsx b/src/components/sidekick/index.test.tsx index b036502a3..712ea3244 100644 --- a/src/components/sidekick/index.test.tsx +++ b/src/components/sidekick/index.test.tsx @@ -97,7 +97,7 @@ describe('Sidekick', () => { const wrapper = subject({ activeTab: SidekickTabs.NOTIFICATIONS }); wrapper.find('.sidekick__tabs-notifications').simulate('click'); - expect(wrapper.find('.sidekick__tab-content--notifications').exists()).toBe(true); + expect(wrapper.find('NotificationPopup').exists()).toBe(true); }); it('render messages tab content', () => { From d6bfe8a8844f510bc92c69bc20f20eadabe4d96d Mon Sep 17 00:00:00 2001 From: Dale Fukami Date: Wed, 15 Mar 2023 12:59:44 -0600 Subject: [PATCH 14/16] Remove title --- .../notification/item/index.test.tsx | 7 ++--- src/components/notification/item/index.tsx | 2 -- src/components/notification/item/style.scss | 12 +------ .../notification/list/container.test.tsx | 31 +++++++------------ .../notification/list/container.tsx | 1 - .../notification/list/index.test.tsx | 8 ++--- src/components/notification/list/index.tsx | 2 -- 7 files changed, 19 insertions(+), 44 deletions(-) diff --git a/src/components/notification/item/index.test.tsx b/src/components/notification/item/index.test.tsx index 2dfd7b4ea..a16ac5d89 100644 --- a/src/components/notification/item/index.test.tsx +++ b/src/components/notification/item/index.test.tsx @@ -7,7 +7,6 @@ import moment from 'moment'; describe('NotificationItem', () => { const subject = (props: Partial = {}) => { const allProps = { - title: '', body: '', createdAt: '', ...props, @@ -16,17 +15,15 @@ describe('NotificationItem', () => { return shallow(); }; - it('renders basic info', () => { + it('renders the message', () => { const wrapper = subject({ - title: 'You were Notified', body: 'Here is the description', }); - expect(wrapper.find('h4').text()).toEqual('You were Notified'); expect(wrapper.find('p').text()).toEqual('Here is the description'); }); - it('renders basic info', () => { + it('renders created timestamp', () => { const wrapper = subject({ createdAt: '2023-03-10T22:33:34.945Z', }); diff --git a/src/components/notification/item/index.tsx b/src/components/notification/item/index.tsx index 0b9e0eebf..226b7c0e3 100644 --- a/src/components/notification/item/index.tsx +++ b/src/components/notification/item/index.tsx @@ -6,7 +6,6 @@ import './style.scss'; import moment from 'moment'; export interface Properties { - title?: string; body: string; createdAt: string; originatingName?: string; @@ -35,7 +34,6 @@ export class NotificationItem extends React.Component { />
-

{this.props.title}

{this.props.body}

{this.time}
diff --git a/src/components/notification/item/style.scss b/src/components/notification/item/style.scss index 44b434c24..6d3b09a27 100644 --- a/src/components/notification/item/style.scss +++ b/src/components/notification/item/style.scss @@ -11,20 +11,10 @@ } &__content { - h4 { - color: theme.$color-greyscale-12; - - margin: 0px; - font-style: normal; - font-weight: 700; - font-size: 14px; - line-height: 17px; - } - p { color: theme.$color-greyscale-12; - margin: 4px 0px; + margin: 0px; font-style: normal; font-weight: 400; font-size: 14px; diff --git a/src/components/notification/list/container.test.tsx b/src/components/notification/list/container.test.tsx index 20cf47038..c33cdb7df 100644 --- a/src/components/notification/list/container.test.tsx +++ b/src/components/notification/list/container.test.tsx @@ -9,6 +9,8 @@ import { Container, Properties } from './container'; describe('NotificationsListContainer', () => { const subject = (props: Partial = {}) => { const allProps = { + notifications: [], + userId: '', fetchNotifications: () => undefined, ...props, }; @@ -37,6 +39,8 @@ describe('NotificationsListContainer', () => { describe('mapState', () => { const subject = (state: Partial) => { return Container.mapState({ + authentication: { user: { data: { id: 'user-id' } as any } }, + notificationsList: { value: [] }, ...state, } as RootState); }; @@ -63,6 +67,14 @@ describe('NotificationsListContainer', () => { { id: 'id-2' }, ]); }); + + test('userId', () => { + const state = subject({ + authentication: { user: { data: { id: 'user-id' } as any } }, + }); + + expect(state.userId).toEqual('user-id'); + }); }); describe('mapNotification', () => { @@ -113,25 +125,6 @@ describe('NotificationsListContainer', () => { expect(mappedNotification.body).toEqual('You were mentioned in a channel'); }); - it('maps title to default value', () => { - // We don't have network information in the data store yet - const mappedNotification = subject( - { - notificationType: 'chat_channel_mention', - data: { chatId: 'chat-id' }, - }, - { - normalized: { - channels: { - 'chat-id': { id: 'chat-id', name: 'TestingChannel', networkId: 'network-id' }, - }, - }, - } - ); - - expect(mappedNotification.title).toEqual('Network Message'); - }); - it('maps default properties', () => { const mappedNotification = subject( { diff --git a/src/components/notification/list/container.tsx b/src/components/notification/list/container.tsx index 05bae67b5..8a1848441 100644 --- a/src/components/notification/list/container.tsx +++ b/src/components/notification/list/container.tsx @@ -52,7 +52,6 @@ export class Container extends React.Component { return { id: notification.id, createdAt: notification.createdAt, - title: 'Network Message', body: `You were mentioned in ${channelText}`, originatingName: displayName, originatingImageUrl: notification.originUser?.profileSummary?.profileImage, diff --git a/src/components/notification/list/index.test.tsx b/src/components/notification/list/index.test.tsx index ed7e97b90..8c1e26fac 100644 --- a/src/components/notification/list/index.test.tsx +++ b/src/components/notification/list/index.test.tsx @@ -16,14 +16,14 @@ describe('NotificationList', () => { it('renders the list', () => { const wrapper = subject({ list: [ - { id: 'id-1', title: 'title-1', body: 'body-1', createdAt: '2023-03-13T22:33:34.945Z' }, - { id: 'id-2', title: 'title-2', body: 'body-2', createdAt: '2023-01-20T22:33:34.945Z' }, + { id: 'id-1', body: 'body-1', createdAt: '2023-03-13T22:33:34.945Z' }, + { id: 'id-2', body: 'body-2', createdAt: '2023-01-20T22:33:34.945Z' }, ], }); expect(wrapper.find('NotificationItem').map((n) => n.props())).toEqual([ - { title: 'title-1', body: 'body-1', createdAt: '2023-03-13T22:33:34.945Z' }, - { title: 'title-2', body: 'body-2', createdAt: '2023-01-20T22:33:34.945Z' }, + { body: 'body-1', createdAt: '2023-03-13T22:33:34.945Z' }, + { body: 'body-2', createdAt: '2023-01-20T22:33:34.945Z' }, ]); }); }); diff --git a/src/components/notification/list/index.tsx b/src/components/notification/list/index.tsx index 596f63745..a3e5cf5b0 100644 --- a/src/components/notification/list/index.tsx +++ b/src/components/notification/list/index.tsx @@ -6,7 +6,6 @@ import './style.scss'; interface Notification { id: string; - title: string; body: string; createdAt: string; originatingName?: string; @@ -25,7 +24,6 @@ export class NotificationList extends React.Component { {this.props.list.map((n) => ( Date: Wed, 15 Mar 2023 13:03:57 -0600 Subject: [PATCH 15/16] Render the originators name in the notification message --- src/components/notification/list/container.test.tsx | 13 ++++++++++--- src/components/notification/list/container.tsx | 5 +++-- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/components/notification/list/container.test.tsx b/src/components/notification/list/container.test.tsx index c33cdb7df..2f30127e5 100644 --- a/src/components/notification/list/container.test.tsx +++ b/src/components/notification/list/container.test.tsx @@ -96,6 +96,13 @@ describe('NotificationsListContainer', () => { { notificationType: 'chat_channel_mention', data: { chatId: 'chat-id' }, + originUser: { + profileSummary: { + firstName: 'Johnny', + lastName: 'Chatter', + profileImage: 'image-url', + }, + }, }, { normalized: { @@ -106,10 +113,10 @@ describe('NotificationsListContainer', () => { } ); - expect(mappedNotification.body).toEqual('You were mentioned in #TestingChannel'); + expect(mappedNotification.body).toEqual('Johnny Chatter mentioned you in #TestingChannel'); }); - it('maps body with an unknown channel', () => { + it('maps body with unknown info', () => { const mappedNotification = subject( { notificationType: 'chat_channel_mention', @@ -122,7 +129,7 @@ describe('NotificationsListContainer', () => { } ); - expect(mappedNotification.body).toEqual('You were mentioned in a channel'); + expect(mappedNotification.body).toEqual('Someone mentioned you in a channel'); }); it('maps default properties', () => { diff --git a/src/components/notification/list/container.tsx b/src/components/notification/list/container.tsx index 8a1848441..7b3b2ee98 100644 --- a/src/components/notification/list/container.tsx +++ b/src/components/notification/list/container.tsx @@ -42,17 +42,18 @@ export class Container extends React.Component { // This should probably be extracted to a display utility or added // to the domain model - const displayName = [ + let displayName = [ notification.originUser?.profileSummary?.firstName, notification.originUser?.profileSummary?.lastName, ] .filter((e) => e) .join(' '); + displayName = displayName || 'Someone'; return { id: notification.id, createdAt: notification.createdAt, - body: `You were mentioned in ${channelText}`, + body: `${displayName} mentioned you in ${channelText}`, originatingName: displayName, originatingImageUrl: notification.originUser?.profileSummary?.profileImage, }; From 5f964edbe3411a15fdc84b75d08ade763fa348fc Mon Sep 17 00:00:00 2001 From: Dale Fukami Date: Wed, 15 Mar 2023 13:08:50 -0600 Subject: [PATCH 16/16] Lint --- src/components/notification/list/container.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/notification/list/container.test.tsx b/src/components/notification/list/container.test.tsx index 2f30127e5..ab98630c7 100644 --- a/src/components/notification/list/container.test.tsx +++ b/src/components/notification/list/container.test.tsx @@ -79,7 +79,7 @@ describe('NotificationsListContainer', () => { describe('mapNotification', () => { const subject = (notification = {}, state: Partial) => { - return Container.mapNotification(notification, state); + return Container.mapNotification(notification, state as RootState); }; describe('unknown type', () => {