diff --git a/frontend/src/components/common/notification.js b/frontend/src/components/common/notification.js index 26beef83f0..0551a11042 100644 --- a/frontend/src/components/common/notification.js +++ b/frontend/src/components/common/notification.js @@ -13,15 +13,17 @@ class Notification extends React.Component { this.state = { showNotice: false, unseenCount: 0, - noticeList: [], + generalNoticeList: [], + discussionNoticeList: [], currentTab: 'general', isShowNotificationDialog: this.getInitDialogState(), }; } componentDidMount() { - seafileAPI.getUnseenNotificationCount().then(res => { - this.setState({ unseenCount: res.data.unseen_count }); + seafileAPI.listAllNotifications().then(res => { + let unseen_count = res.data.general_notification.unseen_count + res.data.discussion_notification.unseen_count; + this.setState({ unseenCount: unseen_count }); }); } @@ -45,50 +47,52 @@ class Notification extends React.Component { this.setState({ showNotice: true, currentTab: tab - }, () => { - this.loadNotices(); }); }; loadNotices = () => { let page = 1; let perPage = 5; - if (this.state.currentTab === 'general') { - seafileAPI.listNotifications(page, perPage).then(res => { - let noticeList = res.data.notification_list; - this.setState({ noticeList: noticeList }); + seafileAPI.listAllNotifications(page, perPage).then(res => { + let generalNoticeList = res.data.general_notification.notification_list; + let discussionNoticeList = res.data.discussion_notification.notification_list; + this.setState({ + generalNoticeList: generalNoticeList, + discussionNoticeList: discussionNoticeList }); - } - if (this.state.currentTab === 'discussion') { - seafileAPI.listSdocNotifications(page, perPage).then(res => { - let noticeList = res.data.notification_list; - this.setState({ noticeList: noticeList }); - }); - } - + }); }; onNoticeItemClick = (noticeItem) => { - let noticeList = this.state.noticeList.map(item => { - if (item.id === noticeItem.id) { - item.seen = true; - } - return item; - }); - if (this.state.currentTab === 'general') { + let noticeList = this.state.generalNoticeList.map(item => { + if (item.id === noticeItem.id) { + item.seen = true; + } + return item; + }); + let unseenCount = this.state.unseenCount === 0 ? 0 : this.state.unseenCount - 1; + this.setState({ + generalNoticeList: noticeList, + unseenCount: unseenCount, + }); seafileAPI.markNoticeAsRead(noticeItem.id); } if (this.state.currentTab === 'discussion') { + let noticeList = this.state.discussionNoticeList.map(item => { + if (item.id === noticeItem.id) { + item.seen = true; + } + return item; + }); + let unseenCount = this.state.unseenCount === 0 ? 0 : this.state.unseenCount - 1; + this.setState({ + discussionNoticeList: noticeList, + unseenCount: unseenCount, + }); seafileAPI.markSdocNoticeAsRead(noticeItem.id); } - let unseenCount = this.state.unseenCount === 0 ? 0 : this.state.unseenCount - 1; - this.setState({ - noticeList: noticeList, - unseenCount: unseenCount, - }); - }; getInitDialogState = () => { @@ -137,15 +141,27 @@ class Notification extends React.Component { onMarkAllNotifications={this.onMarkAllNotifications} tabItemClick={this.tabItemClick} > - + {this.state.currentTab === 'general' && + + } + {this.state.currentTab === 'discussion' && + + } } {this.state.isShowNotificationDialog && - + } ); diff --git a/frontend/src/user-notifications.js b/frontend/src/user-notifications.js index 6d264400be..c2d669e967 100644 --- a/frontend/src/user-notifications.js +++ b/frontend/src/user-notifications.js @@ -1,6 +1,16 @@ -import React from 'react'; +import React, { Fragment } from 'react'; import PropTypes from 'prop-types'; -import { Modal, ModalHeader, ModalBody, Dropdown, DropdownToggle, DropdownMenu, DropdownItem } from 'reactstrap'; +import { + Modal, + ModalHeader, + ModalBody, + Dropdown, + DropdownToggle, + DropdownMenu, + DropdownItem, + TabPane, + Nav, NavItem, NavLink, TabContent +} from 'reactstrap'; import { Utils } from './utils/utils'; import { gettext } from './utils/constants'; import { seafileAPI } from './utils/seafile-api'; @@ -24,6 +34,7 @@ class UserNotificationsDialog extends React.Component { hasNextPage: false, items: [], isItemMenuShow: false, + activeTab: 'general', }; } @@ -37,56 +48,132 @@ class UserNotificationsDialog extends React.Component { }); } - getItems = (page) => { + getItems = (page, is_scroll = false) => { this.setState({ isLoading: true }); - seafileAPI.listNotifications(page, PER_PAGE).then((res) => { - this.setState({ - isLoading: false, - items: [...this.state.items, ...res.data.notification_list], - currentPage: page, - hasNextPage: Utils.hasNextPage(page, PER_PAGE, res.data.count) + if (this.state.activeTab === 'general') { + seafileAPI.listNotifications(page, PER_PAGE).then((res) => { + if (is_scroll) { + this.setState({ + isLoading: false, + items: [...this.state.items, ...res.data.notification_list], + currentPage: page, + hasNextPage: Utils.hasNextPage(page, PER_PAGE, res.data.count) + }); + } else { + this.setState({ + isLoading: false, + items: [...res.data.notification_list], + currentPage: page, + hasNextPage: Utils.hasNextPage(page, PER_PAGE, res.data.count) + }); + } + }).catch((error) => { + this.setState({ + isLoading: false, + errorMsg: Utils.getErrorMsg(error, true) // true: show login tip if 403 + }); }); - }).catch((error) => { - this.setState({ - isLoading: false, - errorMsg: Utils.getErrorMsg(error, true) // true: show login tip if 403 + } else if (this.state.activeTab === 'discussion') { + seafileAPI.listSdocNotifications(page, PER_PAGE).then((res) => { + if (is_scroll) { + this.setState({ + isLoading: false, + items: [...this.state.items, ...res.data.notification_list], + currentPage: page, + hasNextPage: Utils.hasNextPage(page, PER_PAGE, res.data.count) + }); + } else { + this.setState({ + isLoading: false, + items: [...res.data.notification_list], + currentPage: page, + hasNextPage: Utils.hasNextPage(page, PER_PAGE, res.data.count) + }); + } + + }).catch((error) => { + this.setState({ + isLoading: false, + errorMsg: Utils.getErrorMsg(error, true) // true: show login tip if 403 + }); }); - }); + } + }; markAllRead = () => { - seafileAPI.updateNotifications().then((res) => { - this.setState({ - items: this.state.items.map(item => { - item.seen = true; - return item; - }) + if (this.state.activeTab === 'general') { + seafileAPI.updateNotifications().then((res) => { + this.setState({ + items: this.state.items.map(item => { + item.seen = true; + return item; + }) + }); + }).catch((error) => { + this.setState({ + isLoading: false, + errorMsg: Utils.getErrorMsg(error, true) // true: show login tip if 403 + }); }); - }).catch((error) => { - this.setState({ - isLoading: false, - errorMsg: Utils.getErrorMsg(error, true) // true: show login tip if 403 + } else if (this.state.activeTab === 'discussion') { + seafileAPI.updateSdocNotifications().then((res) => { + this.setState({ + items: this.state.items.map(item => { + item.seen = true; + return item; + }) + }); + }).catch((error) => { + this.setState({ + isLoading: false, + errorMsg: Utils.getErrorMsg(error, true) // true: show login tip if 403 + }); }); - }); + } + }; clearAll = () => { - seafileAPI.deleteNotifications().then((res) => { - this.setState({ - items: [] + if (this.state.activeTab === 'general') { + seafileAPI.deleteNotifications().then((res) => { + this.setState({ + items: [] + }); + }).catch((error) => { + this.setState({ + isLoading: false, + errorMsg: Utils.getErrorMsg(error, true) // true: show login tip if 403 + }); }); - }).catch((error) => { - this.setState({ - isLoading: false, - errorMsg: Utils.getErrorMsg(error, true) // true: show login tip if 403 + } else if (this.state.activeTab === 'discussion') { + seafileAPI.deleteSdocNotifications().then((res) => { + this.setState({ + items: [] + }); + }).catch((error) => { + this.setState({ + isLoading: false, + errorMsg: Utils.getErrorMsg(error, true) // true: show login tip if 403 + }); }); - }); + } }; toggle = () => { this.props.onNotificationDialogToggle(); }; + tabItemClick = (e) => { + let tab = e.target.getAttribute('value'); + this.setState({ + activeTab: tab, + currentPage: 1 + }, () => { + this.getItems(this.state.currentPage); + }); + }; + toggleDropDownMenu = () => { this.setState({ isItemMenuShow: !this.state.isItemMenuShow }); }; @@ -96,7 +183,7 @@ class UserNotificationsDialog extends React.Component { return; } if (this.notificationTableRef.offsetHeight + this.notificationTableRef.scrollTop + 1 >= this.tableRef.offsetHeight) { - this.getItems(this.state.currentPage + 1); + this.getItems(this.state.currentPage + 1, true); } }; @@ -130,6 +217,49 @@ class UserNotificationsDialog extends React.Component { ); }; + + renderNoticeContent = (content) => { + let activeTab = this.state.activeTab; + return ( + +
+ +
+
+ + {activeTab === 'general' && + +
this.notificationTableRef = ref} + onScroll={this.onHandleScroll}> + {content} +
+
+ } + {activeTab === 'discussion' && + +
this.notificationTableRef = ref} + onScroll={this.onHandleScroll}> + {content} +
+
+ } +
+
+
+ ); + }; + render() { const { isLoading, errorMsg, items } = this.state; let content; @@ -173,9 +303,7 @@ class UserNotificationsDialog extends React.Component { zIndex={1046}> {gettext('Notifications')} -
this.notificationTableRef = ref} onScroll={this.onHandleScroll}> - {content} -
+ {this.renderNoticeContent(content)}
); @@ -184,6 +312,7 @@ class UserNotificationsDialog extends React.Component { UserNotificationsDialog.propTypes = { onNotificationDialogToggle: PropTypes.func.isRequired, + tabItemClick: PropTypes.func.isRequired, }; export default UserNotificationsDialog; diff --git a/frontend/src/utils/seafile-api.js b/frontend/src/utils/seafile-api.js index 4d0f794c98..0b819d171c 100644 --- a/frontend/src/utils/seafile-api.js +++ b/frontend/src/utils/seafile-api.js @@ -1451,6 +1451,15 @@ class SeafileAPI { } // ---- Notification API + listAllNotifications(page, perPage) { + const url = this.server + '/api/v2.1/all-notifications/'; + let params = { + page: page, + per_page: perPage + }; + return this.req.get(url, { params: params }); + } + listNotifications(page, perPage) { const url = this.server + '/api/v2.1/notifications/'; let params = { @@ -1485,6 +1494,11 @@ class SeafileAPI { return this.req.delete(url); } + deleteSdocNotifications() { + const url = this.server + '/api/v2.1/sdoc-notifications/'; + return this.req.delete(url); + } + getUnseenNotificationCount() { const url = this.server + '/api/v2.1/notifications/'; return this.req.get(url); diff --git a/seahub/api2/endpoints/notifications.py b/seahub/api2/endpoints/notifications.py index ec01b2be7a..dcabbabfea 100644 --- a/seahub/api2/endpoints/notifications.py +++ b/seahub/api2/endpoints/notifications.py @@ -227,7 +227,7 @@ class SdocNotificationsView(APIView): """ username = request.user.username - unseen_notices = SeadocNotification.objects.filter(username, seen=False) + unseen_notices = SeadocNotification.objects.filter(username=username, seen=False) for notice in unseen_notices: notice.seen = True notice.save() @@ -236,6 +236,21 @@ class SdocNotificationsView(APIView): cache.delete(cache_key) return Response({'success': True}) + + def delete(self, request): + """ delete a sdoc notification by username + + Permission checking: + 1. login user. + """ + username = request.user.username + + SeadocNotification.objects.remove_user_notifications(username) + + cache_key = get_cache_key_of_unseen_sdoc_notifications(username) + cache.delete(cache_key) + + return Response({'success': True}) @@ -283,4 +298,97 @@ class SdocNotificationView(APIView): cache_key = get_cache_key_of_unseen_sdoc_notifications(username) cache.delete(cache_key) - return Response({'success': True}) \ No newline at end of file + return Response({'success': True}) + + +class AllNotificationsView(APIView): + + authentication_classes = (TokenAuthentication, SessionAuthentication) + permission_classes = (IsAuthenticated,) + throttle_classes = (UserRateThrottle,) + + def get(self, request): + """ used for get all notifications + general and discussion + + Permission checking: + 1. login user. + """ + result = { + 'general_notification': {}, + 'discussion_notification': {} + } + + username = request.user.username + + try: + per_page = int(request.GET.get('per_page', '')) + page = int(request.GET.get('page', '')) + except ValueError: + per_page = 25 + page = 1 + + start = (page - 1) * per_page + end = page * per_page + + notice_list = UserNotification.objects.get_user_notifications(username)[start:end] + sdoc_notice_list = SeadocNotification.objects.list_all_by_user(username, start, end) + + result_notices = update_notice_detail(request, notice_list) + sdoc_result_notices = update_sdoc_notice_detail(request, sdoc_notice_list) + + notification_list = [] + sdoc_notification_list = [] + + for i in result_notices: + if i.detail is not None: + notice = {} + notice['id'] = i.id + notice['type'] = i.msg_type + notice['detail'] = i.detail + notice['time'] = datetime_to_isoformat_timestr(i.timestamp) + notice['seen'] = i.seen + + notification_list.append(notice) + + for i in sdoc_result_notices: + if i.detail is not None: + notice = {} + notice['id'] = i.id + notice['type'] = i.msg_type + notice['detail'] = i.detail + notice['time'] = datetime_to_isoformat_timestr(i.created_at) + notice['seen'] = i.seen + + sdoc_notification_list.append(notice) + + cache_key = get_cache_key_of_unseen_notifications(username) + unseen_count_from_cache = cache.get(cache_key, None) + + sdoc_cache_key = get_cache_key_of_unseen_sdoc_notifications(username) + sdoc_unseen_count_from_cache = cache.get(sdoc_cache_key, None) + + # for case of count value is `0` + if unseen_count_from_cache is not None: + result['general_notification']['unseen_count'] = unseen_count_from_cache + else: + unseen_count = UserNotification.objects.filter(to_user=username, seen=False).count() + result['general_notification']['unseen_count'] = unseen_count + cache.set(cache_key, unseen_count) + + if sdoc_unseen_count_from_cache is not None: + result['discussion_notification']['unseen_count'] = sdoc_unseen_count_from_cache + else: + sdoc_unseen_count = SeadocNotification.objects.filter(username=username, seen=False).count() + result['discussion_notification']['unseen_count'] = sdoc_unseen_count + cache.set(sdoc_cache_key, sdoc_unseen_count) + + total_count = UserNotification.objects.filter(to_user=username).count() + sdoc_total_count = SeadocNotification.objects.filter(username=username).count() + + result['general_notification']['notification_list'] = notification_list + result['discussion_notification']['notification_list'] = sdoc_notification_list + result['general_notification']['count'] = total_count + result['discussion_notification']['count'] = sdoc_total_count + + return Response(result) \ No newline at end of file diff --git a/seahub/seadoc/models.py b/seahub/seadoc/models.py index 6afae79629..7e0f96bf14 100644 --- a/seahub/seadoc/models.py +++ b/seahub/seadoc/models.py @@ -272,6 +272,10 @@ class SeadocNotificationManager(models.Manager): def list_all_by_user(self, username, start, end): return self.filter(username=username).order_by('-created_at')[start: end] + + def remove_user_notifications(self, username): + """"Remove all user notifications.""" + self.filter(username=username).delete() class SeadocNotification(models.Model): diff --git a/seahub/urls.py b/seahub/urls.py index e3a09781cd..ad1d7690b4 100644 --- a/seahub/urls.py +++ b/seahub/urls.py @@ -91,7 +91,7 @@ from seahub.api2.endpoints.invitations import InvitationsView, InvitationsBatchV from seahub.api2.endpoints.invitation import InvitationView, InvitationRevokeView from seahub.api2.endpoints.repo_share_invitations import RepoShareInvitationsView, RepoShareInvitationsBatchView from seahub.api2.endpoints.repo_share_invitation import RepoShareInvitationView -from seahub.api2.endpoints.notifications import NotificationsView, NotificationView, SdocNotificationView, SdocNotificationsView +from seahub.api2.endpoints.notifications import NotificationsView, NotificationView, SdocNotificationView, SdocNotificationsView, AllNotificationsView from seahub.api2.endpoints.repo_file_uploaded_bytes import RepoFileUploadedBytesView from seahub.api2.endpoints.user_avatar import UserAvatarView from seahub.api2.endpoints.wikis import WikisView, WikiView @@ -524,6 +524,7 @@ urlpatterns = [ re_path(r'^api/v2.1/notification/$', NotificationView.as_view(), name='api-v2.1-notification'), re_path(r'^api/v2.1/sdoc-notifications/$', SdocNotificationsView.as_view(), name='api-v2.1-sdoc-notifications'), re_path(r'^api/v2.1/sdoc-notification/$', SdocNotificationView.as_view(), name='api-v2.1-notification'), + re_path(r'^api/v2.1/all-notifications/$', AllNotificationsView.as_view(), name='api-v2.1-all-notification'), ## user::invitations re_path(r'^api/v2.1/invitations/$', InvitationsView.as_view()),