Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pause ALL my notifications #910

Merged
merged 13 commits into from
Jun 17, 2024
111 changes: 81 additions & 30 deletions assets/components/NotificationList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,17 @@ import PropTypes from 'prop-types';
import classNames from 'classnames';
import {get} from 'lodash';
import {Tooltip} from 'bootstrap';

import {gettext} from 'utils';
import {formatDate, gettext} from 'utils';
import {isTouchDevice} from '../utils';
import NotificationListItem from './NotificationListItem';
import {postNotificationSchedule} from 'user-profile/actions';

interface IState {
displayItems: boolean;
connected: boolean;
}

class NotificationList extends React.Component<any, any> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If it's quick, can you type the props as well?

class NotificationList extends React.Component<any, IState> {
tomaskikutis marked this conversation as resolved.
Show resolved Hide resolved
static propTypes: any;
tooltip: any;
elem: any;
Expand Down Expand Up @@ -60,7 +65,7 @@ class NotificationList extends React.Component<any, any> {
}

toggleDisplay() {
this.setState({displayItems:!this.state.displayItems});
this.setState({displayItems: !this.state.displayItems});
if (!this.state.displayItems) {
this.props.loadNotifications();
(document.getElementById('header-notification') as HTMLElement).classList.add('navbar-notifications--open');
Expand All @@ -70,45 +75,66 @@ class NotificationList extends React.Component<any, any> {
}

render() {
return (
<div className="navbar-notifications__inner">
<h3 className="a11y-only">Notification Bell</h3>
{this.props.count > 0 &&
<div className="navbar-notifications__badge">
{this.props.count}
</div>
}

<span
className={classNames(
'navbar-notifications__inner-circle',
{'navbar-notifications__inner-circle--disconnected': !this.state.connected}
)}
ref={(elem: any) => this.elem = elem}
title={gettext('Notifications')}>
<h3 className="a11y-only">Notification bell</h3>
<i className='icon--alert' onClick={this.toggleDisplay} />
</span>

{!this.state.displayItems ? null : this.props.count === 0 ? (
const notificationPopUp = () => {
if (this.props.fullUser.notification_schedule?.pauseFrom != '' && this.props.fullUser.notification_schedule?.pauseFrom != '') {
tomaskikutis marked this conversation as resolved.
Show resolved Hide resolved
return (
<div className="notif__list dropdown-menu dropdown-menu-right show">
<div className='notif__list__header d-flex'>
<span className='notif__list__header-headline ms-3'>{gettext('Notifications')}</span>
</div>
<div className='notif__list__message'>
{gettext('No new notifications!')}

<div className='d-flex flex-column gap-2 p-3'>
<div className='nh-container nh-container__text--alert p-2'>
{gettext('All notifications are paused until {{pauseTo}}', {pauseTo: formatDate(this.props.fullUser.notification_schedule?.pauseTo)})}
tomaskikutis marked this conversation as resolved.
Show resolved Hide resolved
</div>

<button
type="button"
className="nh-button nh-button--small nh-button--tertiary"
onClick={() => {
tomaskikutis marked this conversation as resolved.
Show resolved Hide resolved
postNotificationSchedule(this.props.user, {pauseFrom: '', pauseTo: ''}).then(() =>
this.props.updateUserNotificationPause()
);
}}
>
{gettext('Resume all notifications')}
</button>
</div>
</div>
) : (
);
} else {
if (this.props.count === 0) {
return (
<div className="notif__list dropdown-menu dropdown-menu-right show">
<div className='notif__list__header d-flex'>
<span className='notif__list__header-headline ms-3'>{gettext('Notifications')}</span>
</div>

<div className='notif__list__message'>
{gettext('No new notifications!')}
</div>
</div>
);
} else {
<div className="notif__list dropdown-menu dropdown-menu-right show">
<div className='notif__list__header d-flex'>
<span className='notif__list__header-headline ms-3'>{gettext('Notifications')}</span>

<button
type="button"
className="button-pill ms-auto me-3"
onClick={this.props.clearAll}>{gettext('Clear All')}
onClick={this.props.clearAll}
>
{gettext('Clear All')}
</button>
</div>

<div className='p-3'>
<div className='nh-container nh-container__text--info p-2'>
{gettext('All notifications are set to be paused from {{pauseFrom}} to {{pauseTo}}', {pauseFrom: formatDate(this.props.fullUser.notification_schedule?.pauseFrom), pauseTo: formatDate(this.props.fullUser.notification_schedule?.pauseTo)})}
tomaskikutis marked this conversation as resolved.
Show resolved Hide resolved
</div>
</div>

{this.props.loading ? (
<div className='notif__list__message'>
{gettext('Loading...')}
Expand All @@ -127,8 +153,33 @@ class NotificationList extends React.Component<any, any> {
/>
))
)}
</div>;
}
}
};

return (
<div className="navbar-notifications__inner">
<h3 className="a11y-only">Notification Bell</h3>
tomaskikutis marked this conversation as resolved.
Show resolved Hide resolved
{this.props.count > 0 &&
<div className="navbar-notifications__badge">
{this.props.count}
</div>
)}
}

<span
className={classNames(
'navbar-notifications__inner-circle',
{'navbar-notifications__inner-circle--disconnected': !this.state.connected}
)}
ref={(elem: any) => this.elem = elem}
title={gettext('Notifications')}
>
<h3 className="a11y-only">{gettext('Notification bell')}</h3>
<i className='icon--alert' onClick={this.toggleDisplay} />
</span>

{this.state.displayItems && notificationPopUp()}
tomaskikutis marked this conversation as resolved.
Show resolved Hide resolved
</div>
);
}
Expand Down
4 changes: 3 additions & 1 deletion assets/interfaces/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,10 @@ export interface IUser {
timezone: string;
times: Array<string>;
last_run_time?: TDatetime;
pauseFrom?: string;
pauseTo?: string;
};
}

type IUserProfileEditable = Pick<IUser, 'first_name' | 'last_name' | 'phone' | 'mobile' | 'role' | 'locale' | 'receive_email' | 'receive_app_notifications'>;
export type IUserProfileUpdates = Partial<IUserProfileEditable>;
export type IUserProfileUpdates = Partial<IUserProfileEditable>;
27 changes: 25 additions & 2 deletions assets/notifications/actions.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
import {get} from 'lodash';
import {gettext, notify, errorHandler} from 'utils';
import server from 'server';
import {IUser} from 'interfaces';

export const UPDATE_NOTIFICATION_COUNT = 'UPDATE_NOTIFICATION_COUNT';
export function updateNotificationCount(count: any) {
return {type: UPDATE_NOTIFICATION_COUNT, count};
}

export const INIT_DATA = 'INIT_DATA';
export function initData(data: any) {
return {type: INIT_DATA, data};
export function initData(notificationData: any, profileData: any) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you type props here?

return {type: INIT_DATA, data: {...notificationData, fullUser: profileData.user}};
}

export const GET_USER = 'GET_USER';
export function getUser(user: IUser): {type: string, user: IUser} {
return {type: GET_USER, user};
}


Expand Down Expand Up @@ -108,3 +114,20 @@ export function pushNotification(push: any): any {
}
};
}

export function updateUserNotificationPause() {
tomaskikutis marked this conversation as resolved.
Show resolved Hide resolved
return (dispatch: any, getState: any) => {
const fullUser = getState().fullUser;
return dispatch(fetchUser(fullUser._id));
tomaskikutis marked this conversation as resolved.
Show resolved Hide resolved
};
}

export function fetchUser(id: any) {
return function (dispatch: any) {
return server.get(`/users/${id}`)
.then((data: IUser) => {
dispatch(getUser(data));
})
.catch((error: any) => errorHandler(error, dispatch));
};
}
9 changes: 9 additions & 0 deletions assets/notifications/components/NotificationsApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
deleteNotification,
deleteAllNotifications,
loadNotifications,
updateUserNotificationPause,
} from '../actions';

import NotificationList from 'components/NotificationList';
Expand All @@ -26,13 +27,17 @@ class NotificationsApp extends React.Component<any, any> {
clearAll={this.props.clearAll}
loadNotifications={this.props.loadNotifications}
loading={this.props.loading}
fullUser={this.props.fullUser}
user={this.props.user}
tomaskikutis marked this conversation as resolved.
Show resolved Hide resolved
updateUserNotificationPause={this.props.updateUserNotificationPause}
/>,
];
}
}

NotificationsApp.propTypes = {
user: PropTypes.string,
fullUser: PropTypes.object,
tomaskikutis marked this conversation as resolved.
Show resolved Hide resolved
items: PropTypes.object,
notifications: PropTypes.arrayOf(PropTypes.object),
count: PropTypes.number,
Expand All @@ -44,6 +49,7 @@ NotificationsApp.propTypes = {

const mapStateToProps = (state: any) => ({
user: state.user,
fullUser: state.fullUser,
items: state.items,
notifications: state.notifications,
count: state.notificationCount,
Expand All @@ -54,6 +60,9 @@ const mapDispatchToProps = (dispatch: any) => ({
clearNotification: (id: any) => dispatch(deleteNotification(id)),
clearAll: () => dispatch(deleteAllNotifications()),
loadNotifications: () => dispatch(loadNotifications()),
updateUserNotificationPause: () => (
dispatch(updateUserNotificationPause())
),
});

export default connect(mapStateToProps, mapDispatchToProps)(NotificationsApp);
2 changes: 1 addition & 1 deletion assets/notifications/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const store = createStore(notificationReducer, 'Notifications');


if (window.notificationData) {
store.dispatch(initData(window.notificationData));
store.dispatch(initData(window.notificationData, window.profileData));
}


Expand Down
10 changes: 10 additions & 0 deletions assets/notifications/reducers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@ import {
INIT_DATA,
CLEAR_NOTIFICATION,
CLEAR_ALL_NOTIFICATIONS,
GET_USER,
} from './actions';

const initialState: any = {
user: null,
fullUser: null,
items: {},
notifications: [],
notificationCount: 0,
Expand Down Expand Up @@ -63,13 +65,21 @@ export default function notificationReducer(state: any = initialState, action: a
return {
...state,
user: action.data.user || null,
fullUser: action.data.fullUser || null,
items: {},
notifications: [],
notificationCount: action.data.notificationCount,
loading: false,
};
}

case GET_USER: {
return {
...state,
fullUser: action.user,
};
}

default:
return state;
}
Expand Down
2 changes: 2 additions & 0 deletions assets/search/components/TopicEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
hideModal,
setTopicEditorFullscreen,
openEditTopicNotificationsModal,
openPauseNotificationModal,
setTopicSubscribers,
saveFolder,
} from 'user-profile/actions';
Expand Down Expand Up @@ -596,6 +597,7 @@ const mapDispatchToProps = (dispatch: any) => ({
dispatch(loadMyWireTopic(topic._id)),
setTopicEditorFullscreen: (fullscreen: boolean) => dispatch(setTopicEditorFullscreen(fullscreen)),
openEditTopicNotificationsModal: () => dispatch(openEditTopicNotificationsModal()),
openPauseNotificationModal: () => dispatch(openPauseNotificationModal()),
tomaskikutis marked this conversation as resolved.
Show resolved Hide resolved
setTopicSubscribers: (topic: ITopic, subscribers: ITopic['subscribers']) =>
dispatch(setTopicSubscribers(topic, subscribers)),
});
Expand Down
8 changes: 8 additions & 0 deletions assets/styles/index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -4148,6 +4148,14 @@ article.list {
border-radius: var(--border-radius--m);
padding: var(--space--2);
}
&.nh-container__text--alert {
color: var(--color-alert);
background-color: alpha(var(--color-alert), 0.25);
}
&.nh-container__text--info {
color: var(--color-info);
background-color: alpha(var(--color-info), 0.25);
}
}

.a11y-only {
Expand Down
28 changes: 28 additions & 0 deletions assets/user-profile/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,14 @@ export function openEditTopicNotificationsModal() {
};
}

export function openPauseNotificationModal() {
return (dispatch: any, getState: any) => {
const user = getState().user;

dispatch(renderModal('pauseNotificationModal', {user}));
};
}

/**
* Submit share followed topic form and close modal if that works
*
Expand Down Expand Up @@ -356,6 +364,26 @@ export function updateUserNotificationSchedules(schedule: Omit<IUser['notificati
};
}

export function postNotificationSchedule(userId: string, schedule: Omit<IUser['notification_schedule'], 'last_run_time'>):Promise<void> {
tomaskikutis marked this conversation as resolved.
Show resolved Hide resolved
return server.post(`/users/${userId}/notification_schedules`, schedule)
.then(() => {
notify.success(gettext('pause notification updated'));
tomaskikutis marked this conversation as resolved.
Show resolved Hide resolved
});
}

export function updateUserNotificationPause(schedule: Omit<IUser['notification_schedule'], 'last_run_time'>) {
tomaskikutis marked this conversation as resolved.
Show resolved Hide resolved
return (dispatch: any, getState: any) => {
const user = getState().user;

return postNotificationSchedule(user._id, schedule)
.then(() => {
dispatch(fetchUser(user._id));
dispatch(closeModal());
})
.catch((error) => errorHandler(error, dispatch, setError(error)));
};
}

export function setTopicSubscribers(topic: ITopic, subscribers: ITopic['subscribers']) {
return (dispatch: any, getState: any) => {
const user = getState().user;
Expand Down
Loading
Loading