From 27dcce694b7020270bcc10f703d74eef0c5cabfe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=AD=99=E6=B0=B8=E5=BC=BA?= <11704063+s-yongqiang@user.noreply.gitee.com> Date: Thu, 21 Nov 2024 10:59:13 +0800 Subject: [PATCH] Update notifications.py --- frontend/src/components/common/notice-item.js | 20 +++++- .../common/notification-popover/index.css | 29 ++++++--- .../common/notification-popover/index.js | 55 +++++++++++++++- .../src/components/common/notification.js | 34 ++++++++-- frontend/src/utils/seafile-api.js | 10 +++ seahub/api2/endpoints/notifications.py | 63 ++++++++++++++++++- seahub/notifications/utils.py | 18 ++++++ seahub/seadoc/models.py | 14 +++++ seahub/urls.py | 3 +- 9 files changed, 228 insertions(+), 18 deletions(-) diff --git a/frontend/src/components/common/notice-item.js b/frontend/src/components/common/notice-item.js index 57cad6515b..62a18beebb 100644 --- a/frontend/src/components/common/notice-item.js +++ b/frontend/src/components/common/notice-item.js @@ -24,6 +24,8 @@ const MSG_TYPE_SAML_SSO_FAILED = 'saml_sso_failed'; const MSG_TYPE_REPO_SHARE_PERM_CHANGE = 'repo_share_perm_change'; const MSG_TYPE_REPO_SHARE_PERM_DELETE = 'repo_share_perm_delete'; const MSG_TYPE_FACE_CLUSTER = 'face_cluster'; +const MSG_TYPE_SEADOC_REPLY = 'reply'; +const MSG_TYPE_SEADOC_COMMENT = 'comment'; dayjs.extend(relativeTime); @@ -33,7 +35,7 @@ class NoticeItem extends React.Component { let noticeItem = this.props.noticeItem; let noticeType = noticeItem.type; let detail = noticeItem.detail; - + console.log(detail, noticeItem, noticeType) if (noticeType === MSG_TYPE_ADD_USER_TO_GROUP) { let avatar_url = detail.group_staff_avatar_url; @@ -375,6 +377,22 @@ class NoticeItem extends React.Component { return { avatar_url: null, notice }; } + if (noticeType === MSG_TYPE_SEADOC_COMMENT) { + let avatar_url = detail.share_from_user_avatar_url; + let notice = ''; + console.log(111) + notice = Utils.HTMLescape(notice); + return { avatar_url, notice }; + } + + if (noticeType === MSG_TYPE_SEADOC_REPLY) { + let avatar_url = detail.share_from_user_avatar_url; + let notice = detail.reply; + notice = Utils.HTMLescape(notice); + console.log(notice) + return { avatar_url, notice }; + } + // if (noticeType === MSG_TYPE_GUEST_INVITATION_ACCEPTED) { // } diff --git a/frontend/src/components/common/notification-popover/index.css b/frontend/src/components/common/notification-popover/index.css index 38dd383ed7..50b91b7307 100644 --- a/frontend/src/components/common/notification-popover/index.css +++ b/frontend/src/components/common/notification-popover/index.css @@ -5,7 +5,7 @@ .notification-container { position: absolute; background: #fff; - width: 320px; + width: 400px; right: -16px; top: -1px; border-radius: 3px; @@ -65,21 +65,15 @@ margin-left: 20px; } -.notification-container .notification-body .mark-notifications { +.notification-container .mark-all-read { color: #b4b4b4; cursor: pointer; - border-bottom: 1px solid #ededed; - height: 36px; display: flex; align-items: center; justify-content: flex-end; padding-right: 1rem; } -.notification-container .notification-body .mark-notifications:hover { - text-decoration: underline; -} - .notification-body .notification-list-container { max-height: 260px; overflow: auto; @@ -190,3 +184,22 @@ .notification-body .notification-footer:hover { text-decoration: underline; } + +.notification-container .notification-body .mark-notifications { + display: flex; +} + +.notification-container .notification-body .mark-notifications .mark-all-read:hover { + text-decoration: underline; +} + +.notification-container .notification-body .nav .nav-item .nav-link { + height: 46px; + margin-right: 15px; + margin-left: 15px; + font-size: 14px; +} + +.notification-container .notification-body .nav .nav-item .nav-link.active { + color: #ED7109 !important; +} \ No newline at end of file diff --git a/frontend/src/components/common/notification-popover/index.js b/frontend/src/components/common/notification-popover/index.js index 7c7ff52f5d..73a50951b7 100644 --- a/frontend/src/components/common/notification-popover/index.js +++ b/frontend/src/components/common/notification-popover/index.js @@ -1,6 +1,8 @@ import React from 'react'; import PropTypes from 'prop-types'; import { Popover } from 'reactstrap'; +import { gettext } from '../../../utils/constants'; + import './index.css'; export default class NotificationPopover extends React.Component { @@ -13,7 +15,9 @@ export default class NotificationPopover extends React.Component { onNotificationDialogToggle: PropTypes.func, listNotifications: PropTypes.func, onMarkAllNotifications: PropTypes.func, + tabItemClick: PropTypes.func, children: PropTypes.any, + currentTab: PropTypes.string, }; static defaultProps = { @@ -47,8 +51,12 @@ export default class NotificationPopover extends React.Component { } }; + tabItemClick = (tab) => { + this.props.tabItemClick(tab); + }; + render() { - const { headerText, bodyText, footerText } = this.props; + const { headerText, bodyText, footerText, currentTab } = this.props; return (
-
{bodyText}
+
+
    +
  • this.tabItemClick('general')}> + + {gettext('General')} + +
  • +
  • this.tabItemClick('discussion')}> + + {gettext('Discussion')} + + +
  • +
+ + {gettext('Mark all as read')} + +
+ {currentTab === 'general' &&
this.notificationListRef = ref}>
this.notificationsWrapperRef = ref}> {this.props.children}
+ } + {currentTab === 'discussion' && +
this.notificationListRef = ref}> +
this.notificationsWrapperRef = ref}> + {this.props.children} +
+
+ } + + {/*
+
    +
  • + General +
  • +
  • + Discussion +
  • +
+ {bodyText} +
+
this.notificationListRef = ref}> +
this.notificationsWrapperRef = ref}> + {this.props.children} +
+
*/}
{footerText}
diff --git a/frontend/src/components/common/notification.js b/frontend/src/components/common/notification.js index fca7dd4520..f6b2198c60 100644 --- a/frontend/src/components/common/notification.js +++ b/frontend/src/components/common/notification.js @@ -14,6 +14,7 @@ class Notification extends React.Component { showNotice: false, unseenCount: 0, noticeList: [], + currentTab: 'general', isShowNotificationDialog: this.getInitDialogState(), }; } @@ -37,14 +38,35 @@ class Notification extends React.Component { this.setState({ showNotice: true }); } }; + + tabItemClick = (tab) => { + const { currentTab } = this.state; + if (currentTab === tab) return; + this.setState({ + showNotice: true, + currentTab: tab + }, () => { + this.loadNotices(); + }); + }; loadNotices = () => { let page = 1; let perPage = 5; - seafileAPI.listNotifications(page, perPage).then(res => { - let noticeList = res.data.notification_list; - this.setState({ noticeList: noticeList }); - }); + if (this.state.currentTab === 'general') { + seafileAPI.listNotifications(page, perPage).then(res => { + let noticeList = res.data.notification_list; + this.setState({ noticeList: noticeList }); + }); + } + if (this.state.currentTab === 'discussion') { + seafileAPI.listSdocNotifications(page, perPage).then(res => { + let noticeList = res.data.notification_list; + console.log(noticeList) + this.setState({ noticeList: noticeList }); + }); + } + }; onNoticeItemClick = (noticeItem) => { @@ -91,7 +113,7 @@ class Notification extends React.Component { }; render() { - const { unseenCount } = this.state; + const { unseenCount, currentTab } = this.state; return (
@@ -103,9 +125,11 @@ class Notification extends React.Component { headerText={gettext('Notification')} bodyText={gettext('Mark all as read')} footerText={gettext('View all notifications')} + currentTab={currentTab} onNotificationListToggle={this.onNotificationListToggle} onNotificationDialogToggle={this.onNotificationDialogToggle} onMarkAllNotifications={this.onMarkAllNotifications} + tabItemClick={this.tabItemClick} >
    {this.state.noticeList.map(item => { diff --git a/frontend/src/utils/seafile-api.js b/frontend/src/utils/seafile-api.js index 1b165bb672..5ade98447b 100644 --- a/frontend/src/utils/seafile-api.js +++ b/frontend/src/utils/seafile-api.js @@ -1460,6 +1460,16 @@ class SeafileAPI { return this.req.get(url, { params: params }); } + + listSdocNotifications(page, perPage) { + const url = this.server + '/api/v2.1/sdoc-notifications/'; + let params = { + page: page, + per_page: perPage + }; + return this.req.get(url, { params: params }); + } + updateNotifications() { const url = this.server + '/api/v2.1/notifications/'; return this.req.put(url); diff --git a/seahub/api2/endpoints/notifications.py b/seahub/api2/endpoints/notifications.py index 2d25d845d5..5e0d1451f6 100644 --- a/seahub/api2/endpoints/notifications.py +++ b/seahub/api2/endpoints/notifications.py @@ -14,13 +14,16 @@ from seahub.api2.throttling import UserRateThrottle from seahub.notifications.models import UserNotification from seahub.notifications.models import get_cache_key_of_unseen_notifications -from seahub.notifications.utils import update_notice_detail +from seahub.notifications.utils import update_notice_detail, update_sdoc_notice_detail from seahub.api2.utils import api_error +from seahub.seadoc.models import SeadocCommentReply, SeadocNotification from seahub.utils.timeutils import datetime_to_isoformat_timestr logger = logging.getLogger(__name__) json_content_type = 'application/json; charset=utf-8' +NOTIF_TYPE = ['general', 'discussion'] + class NotificationsView(APIView): @@ -161,3 +164,61 @@ class NotificationView(APIView): cache.delete(cache_key) return Response({'success': True}) + + +class SdocNotificationView(APIView): + def get(self, request): + """ used for get sdoc notifications + + Permission checking: + 1. login user. + """ + notice_type = request.GET.get('type', 'general') + if notice_type not in NOTIF_TYPE: + error_msg = 'notice_type invalid.' + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + + result = {} + + 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 = SeadocNotification.objects.list_all_by_user(username, start, end) + result_notices = update_sdoc_notice_detail(request, notice_list) + 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.created_at) + notice['seen'] = i.seen + + notification_list.append(notice) + cache_key = get_cache_key_of_unseen_notifications(username) + unseen_count_from_cache = cache.get(cache_key, None) + + # for case of count value is `0` + if unseen_count_from_cache is not None: + result['unseen_count'] = unseen_count_from_cache + else: + unseen_count = SeadocNotification.objects.filter(username=username, seen=False).count() + result['unseen_count'] = unseen_count + cache.set(cache_key, unseen_count) + + total_count = SeadocNotification.objects.filter(username=username).count() + + result['notification_list'] = notification_list + result['count'] = total_count + + return Response(result) diff --git a/seahub/notifications/utils.py b/seahub/notifications/utils.py index 8942488440..b858bc8714 100644 --- a/seahub/notifications/utils.py +++ b/seahub/notifications/utils.py @@ -402,6 +402,24 @@ def update_notice_detail(request, notices): return notices +def update_sdoc_notice_detail(request, notices): + repo_dict = {} + for notice in notices: + if notice.is_comment(): + try: + d = json.loads(notice.detail) + notice.detail = d + except Exception as e: + logger.error(e) + elif notice.is_reply(): + try: + d = json.loads(notice.detail) + notice.detail = d + except Exception as e: + logger.error(e) + return notices + + def gen_sdoc_smart_link(doc_uuid, with_service_url=True): service_url = get_service_url() service_url = service_url.rstrip('/') diff --git a/seahub/seadoc/models.py b/seahub/seadoc/models.py index 48d0d67689..34bb33b4e4 100644 --- a/seahub/seadoc/models.py +++ b/seahub/seadoc/models.py @@ -247,6 +247,11 @@ class SeadocCommentReply(models.Model): } + +### sdoc notification +MSG_TYPE_REPLY = 'reply' +MSG_TYPE_COMMENT = 'comment' + class SeadocNotificationManager(models.Manager): def total_count(self, doc_uuid, username): return self.filter(doc_uuid=doc_uuid, username=username).count() @@ -259,6 +264,9 @@ class SeadocNotificationManager(models.Manager): def delete_by_ids(self, doc_uuid, username, ids): return self.filter(doc_uuid=doc_uuid, username=username, id__in=ids).delete() + + def list_all_by_user(self, username, start, end): + return self.filter(username=username).order_by('-created_at')[start: end] class SeadocNotification(models.Model): @@ -285,3 +293,9 @@ class SeadocNotification(models.Model): 'detail': json.loads(self.detail), 'seen': self.seen, } + + def is_comment(self): + return self.msg_type == MSG_TYPE_COMMENT + + def is_reply(self): + return self.msg_type == MSG_TYPE_REPLY diff --git a/seahub/urls.py b/seahub/urls.py index cb44117b19..2293d9c365 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 +from seahub.api2.endpoints.notifications import NotificationsView, NotificationView, SdocNotificationView 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 @@ -522,6 +522,7 @@ urlpatterns = [ re_path(r'^api/v2.1/notifications/$', NotificationsView.as_view(), name='api-v2.1-notifications'), re_path(r'^api/v2.1/notification/$', NotificationView.as_view(), name='api-v2.1-notification'), + re_path(r'^api/v2.1/sdoc-notifications/$', SdocNotificationView.as_view(), name='api-v2.1-sdoc-notifications'), ## user::invitations re_path(r'^api/v2.1/invitations/$', InvitationsView.as_view()),