From 18454d479f4002afafa50bb723df12c5b56f67f0 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 01/13] 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 b9df6d2fcb8..3f28a46eb8c 100644 --- a/frontend/src/components/common/notice-item.js +++ b/frontend/src/components/common/notice-item.js @@ -23,6 +23,8 @@ const MSG_TYPE_DELETED_FILES = 'deleted_files'; 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_SEADOC_REPLY = 'reply'; +const MSG_TYPE_SEADOC_COMMENT = 'comment'; dayjs.extend(relativeTime); @@ -32,7 +34,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; @@ -361,6 +363,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 38dd383ed7a..50b91b73072 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 7c7ff52f5d9..73a50951b72 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 fca7dd45204..f6b2198c606 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 50c1867fe4a..4ef57b8fd44 100644 --- a/frontend/src/utils/seafile-api.js +++ b/frontend/src/utils/seafile-api.js @@ -1454,6 +1454,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 2d25d845d55..5e0d1451f6b 100644 --- a/seahub/api2/endpoints/notifications.py +++ b/seahub/api2/endpoints/notifications.py @@ -14,13 +14,16 @@ 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 @@ def put(self, request): 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 677e18b516e..4936980b008 100644 --- a/seahub/notifications/utils.py +++ b/seahub/notifications/utils.py @@ -396,6 +396,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 48d0d676896..34bb33b4e44 100644 --- a/seahub/seadoc/models.py +++ b/seahub/seadoc/models.py @@ -247,6 +247,11 @@ def to_dict(self): } + +### 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 @@ def list_by_unseen(self, doc_uuid, username): 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 @@ def to_dict(self): '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 12000ee4a0c..f19fc3de282 100644 --- a/seahub/urls.py +++ b/seahub/urls.py @@ -89,7 +89,7 @@ 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 @@ -514,6 +514,7 @@ 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()), From 7bddee457a8f2553a8e353c8baf86e83d5f49b44 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 17:11:02 +0800 Subject: [PATCH 02/13] update --- frontend/src/components/common/notice-item.js | 37 +++++---- .../common/notification-popover/index.js | 45 ++++++----- .../src/components/common/notification.js | 18 +++-- frontend/src/utils/seafile-api.js | 12 +++ seahub/api2/endpoints/notifications.py | 76 +++++++++++++++++-- seahub/notifications/utils.py | 17 ++++- seahub/seadoc/models.py | 7 +- seahub/urls.py | 5 +- 8 files changed, 163 insertions(+), 54 deletions(-) diff --git a/frontend/src/components/common/notice-item.js b/frontend/src/components/common/notice-item.js index 3f28a46eb8c..a9869b1df89 100644 --- a/frontend/src/components/common/notice-item.js +++ b/frontend/src/components/common/notice-item.js @@ -4,6 +4,7 @@ import dayjs from 'dayjs'; import relativeTime from 'dayjs/plugin/relativeTime'; import { gettext, siteRoot } from '../../utils/constants'; import { Utils } from '../../utils/utils'; +import { processor } from '@seafile/seafile-editor'; const propTypes = { noticeItem: PropTypes.object.isRequired, @@ -34,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; @@ -364,19 +365,23 @@ class NoticeItem extends React.Component { } 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 }; + let avatar_url = detail.avatar_url; + let notice = detail.comment; + let username = detail.user_name; + processor.process(notice, (error, vfile) => { + notice = String(vfile); + }); + return { avatar_url, username, notice }; } if (noticeType === MSG_TYPE_SEADOC_REPLY) { - let avatar_url = detail.share_from_user_avatar_url; + let avatar_url = detail.avatar_url; let notice = detail.reply; - notice = Utils.HTMLescape(notice); - console.log(notice) - return { avatar_url, notice }; + let username = detail.user_name; + processor.process(notice, (error, vfile) => { + notice = String(vfile); + }); + return { avatar_url, username, notice }; } // if (noticeType === MSG_TYPE_GUEST_INVITATION_ACCEPTED) { @@ -396,8 +401,7 @@ class NoticeItem extends React.Component { render() { let noticeItem = this.props.noticeItem; - let { avatar_url, notice } = this.generatorNoticeInfo(); - + let { avatar_url, username, notice } = this.generatorNoticeInfo(); if (!avatar_url && !notice) { return ''; } @@ -418,8 +422,13 @@ class NoticeItem extends React.Component {
  • - -

    +
    + +

    {username}

    +
    +
    +

    +

    {dayjs(noticeItem.time).fromNow()}

    diff --git a/frontend/src/components/common/notification-popover/index.js b/frontend/src/components/common/notification-popover/index.js index 73a50951b72..bf30cebb2fb 100644 --- a/frontend/src/components/common/notification-popover/index.js +++ b/frontend/src/components/common/notification-popover/index.js @@ -72,39 +72,38 @@ export default class NotificationPopover extends React.Component {
-
-
    -
  • this.tabItemClick('general')}> - - {gettext('General')} - -
  • -
  • this.tabItemClick('discussion')}> - - {gettext('Discussion')} - - -
  • -
- - {gettext('Mark all as read')} - -
- {currentTab === 'general' && +
+
    +
  • this.tabItemClick('general')}> + + {gettext('General')} + +
  • +
  • this.tabItemClick('discussion')}> + + {gettext('Discussion')} + +
  • +
+ + {bodyText} + +
+ {currentTab === 'general' &&
this.notificationListRef = ref}>
this.notificationsWrapperRef = ref}> {this.props.children}
- } - {currentTab === 'discussion' && + } + {currentTab === 'discussion' &&
this.notificationListRef = ref}>
this.notificationsWrapperRef = ref}> {this.props.children}
- } - + } + {/*
  • diff --git a/frontend/src/components/common/notification.js b/frontend/src/components/common/notification.js index f6b2198c606..26beef83f07 100644 --- a/frontend/src/components/common/notification.js +++ b/frontend/src/components/common/notification.js @@ -38,16 +38,16 @@ class Notification extends React.Component { this.setState({ showNotice: true }); } }; - + tabItemClick = (tab) => { const { currentTab } = this.state; if (currentTab === tab) return; - this.setState({ + this.setState({ showNotice: true, currentTab: tab - }, () => { + }, () => { this.loadNotices(); - }); + }); }; loadNotices = () => { @@ -62,7 +62,6 @@ class Notification extends React.Component { if (this.state.currentTab === 'discussion') { seafileAPI.listSdocNotifications(page, perPage).then(res => { let noticeList = res.data.notification_list; - console.log(noticeList) this.setState({ noticeList: noticeList }); }); } @@ -76,7 +75,14 @@ class Notification extends React.Component { } return item; }); - seafileAPI.markNoticeAsRead(noticeItem.id); + + if (this.state.currentTab === 'general') { + seafileAPI.markNoticeAsRead(noticeItem.id); + } + if (this.state.currentTab === 'discussion') { + seafileAPI.markSdocNoticeAsRead(noticeItem.id); + } + let unseenCount = this.state.unseenCount === 0 ? 0 : this.state.unseenCount - 1; this.setState({ noticeList: noticeList, diff --git a/frontend/src/utils/seafile-api.js b/frontend/src/utils/seafile-api.js index 4ef57b8fd44..ec812375395 100644 --- a/frontend/src/utils/seafile-api.js +++ b/frontend/src/utils/seafile-api.js @@ -1469,6 +1469,11 @@ class SeafileAPI { return this.req.put(url); } + updateSdocNotifications() { + const url = this.server + '/api/v2.1/sdoc-notifications/'; + return this.req.put(url); + } + deleteNotifications() { const url = this.server + '/api/v2.1/notifications/'; return this.req.delete(url); @@ -1486,6 +1491,13 @@ class SeafileAPI { return this.req.put(url, from); } + markSdocNoticeAsRead(noticeId) { + const url = this.server + '/api/v2.1/sdoc-notification/'; + let from = new FormData(); + from.append('notice_id', noticeId); + return this.req.put(url, from); + } + // ---- Linked Devices API listLinkedDevices() { const url = this.server + '/api2/devices/'; diff --git a/seahub/api2/endpoints/notifications.py b/seahub/api2/endpoints/notifications.py index 5e0d1451f6b..ec01b2be7a7 100644 --- a/seahub/api2/endpoints/notifications.py +++ b/seahub/api2/endpoints/notifications.py @@ -13,6 +13,7 @@ from seahub.api2.throttling import UserRateThrottle from seahub.notifications.models import UserNotification +from seahub.seadoc.models import get_cache_key_of_unseen_sdoc_notifications from seahub.notifications.models import get_cache_key_of_unseen_notifications from seahub.notifications.utils import update_notice_detail, update_sdoc_notice_detail from seahub.api2.utils import api_error @@ -166,18 +167,13 @@ def put(self, request): return Response({'success': True}) -class SdocNotificationView(APIView): +class SdocNotificationsView(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 @@ -205,7 +201,7 @@ def get(self, request): notice['seen'] = i.seen notification_list.append(notice) - cache_key = get_cache_key_of_unseen_notifications(username) + cache_key = get_cache_key_of_unseen_sdoc_notifications(username) unseen_count_from_cache = cache.get(cache_key, None) # for case of count value is `0` @@ -222,3 +218,69 @@ def get(self, request): result['count'] = total_count return Response(result) + + def put(self, request): + """ currently only used for mark all notifications seen + + Permission checking: + 1. login user. + """ + + username = request.user.username + unseen_notices = SeadocNotification.objects.filter(username, seen=False) + for notice in unseen_notices: + notice.seen = True + notice.save() + + cache_key = get_cache_key_of_unseen_sdoc_notifications(username) + cache.delete(cache_key) + + return Response({'success': True}) + + + +class SdocNotificationView(APIView): + + authentication_classes = (TokenAuthentication, SessionAuthentication) + permission_classes = (IsAuthenticated,) + throttle_classes = (UserRateThrottle,) + + def put(self, request): + """ currently only used for mark a sdoc notification seen + + Permission checking: + 1. login user. + """ + + notice_id = request.data.get('notice_id') + + # argument check + try: + int(notice_id) + except Exception as e: + error_msg = 'notice_id invalid.' + logger.error(e) + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + + # resource check + try: + notice = SeadocNotification.objects.get(id=notice_id) + except SeadocNotification.DoesNotExist as e: + logger.error(e) + error_msg = 'Notification %s not found.' % notice_id + return api_error(status.HTTP_404_NOT_FOUND, error_msg) + + # permission check + username = request.user.username + if notice.username != username: + error_msg = 'Permission denied.' + return api_error(status.HTTP_403_FORBIDDEN, error_msg) + + if not notice.seen: + notice.seen = True + notice.save() + + cache_key = get_cache_key_of_unseen_sdoc_notifications(username) + cache.delete(cache_key) + + return Response({'success': True}) \ No newline at end of file diff --git a/seahub/notifications/utils.py b/seahub/notifications/utils.py index 4936980b008..b41e6035558 100644 --- a/seahub/notifications/utils.py +++ b/seahub/notifications/utils.py @@ -2,6 +2,7 @@ import os import json import logging +import posixpath from django.core.cache import cache from django.utils.html import escape from django.utils.translation import gettext as _ @@ -10,6 +11,7 @@ from seahub.constants import CUSTOM_PERMISSION_PREFIX from seahub.notifications.models import Notification +from seahub.tags.models import FileUUIDMap from seahub.notifications.settings import NOTIFICATION_CACHE_TIMEOUT from seahub.avatar.templatetags.avatar_tags import api_avatar_url from seahub.base.templatetags.seahub_tags import email2nickname, email2contact_email @@ -397,17 +399,30 @@ def update_notice_detail(request, notices): def update_sdoc_notice_detail(request, notices): - repo_dict = {} for notice in notices: if notice.is_comment(): try: d = json.loads(notice.detail) + uuid = FileUUIDMap.objects.get_fileuuidmap_by_uuid(notice.doc_uuid) + origin_file_path = posixpath.join(uuid.parent_path, uuid.filename) + url, _, _ = api_avatar_url(d['author']) + d['avatar_url'] = url + d['sdoc_path'] = origin_file_path + d['sdoc_name'] = uuid.filename + d['repo_id'] = uuid.repo_id notice.detail = d except Exception as e: logger.error(e) elif notice.is_reply(): try: d = json.loads(notice.detail) + uuid = FileUUIDMap.objects.get_fileuuidmap_by_uuid(notice.doc_uuid) + origin_file_path = posixpath.join(uuid.parent_path, uuid.filename) + url, _, _ = api_avatar_url(d['author']) + d['avatar_url'] = url + d['sdoc_path'] = origin_file_path + d['sdoc_name'] = uuid.filename + d['repo_id'] = uuid.repo_id notice.detail = d except Exception as e: logger.error(e) diff --git a/seahub/seadoc/models.py b/seahub/seadoc/models.py index 34bb33b4e44..6afae79629a 100644 --- a/seahub/seadoc/models.py +++ b/seahub/seadoc/models.py @@ -7,7 +7,7 @@ from seahub.utils.timeutils import datetime_to_isoformat_timestr from seahub.base.templatetags.seahub_tags import email2nickname from seahub.seadoc.settings import SDOC_REVISIONS_DIR - +from seahub.utils import normalize_cache_key class SeadocHistoryNameManager(models.Manager): def update_name(self, doc_uuid, obj_id, name): @@ -251,6 +251,11 @@ def to_dict(self): ### sdoc notification MSG_TYPE_REPLY = 'reply' MSG_TYPE_COMMENT = 'comment' +SDOC_NOTIFICATION_COUNT_CACHE_PREFIX = 'SDOC_NOTIFICATION_COUNT_' + +def get_cache_key_of_unseen_sdoc_notifications(username): + return normalize_cache_key(username, + SDOC_NOTIFICATION_COUNT_CACHE_PREFIX) class SeadocNotificationManager(models.Manager): def total_count(self, doc_uuid, username): diff --git a/seahub/urls.py b/seahub/urls.py index f19fc3de282..191f7754d2b 100644 --- a/seahub/urls.py +++ b/seahub/urls.py @@ -89,7 +89,7 @@ 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 +from seahub.api2.endpoints.notifications import NotificationsView, NotificationView, SdocNotificationView, SdocNotificationsView 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 @@ -514,7 +514,8 @@ 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'), + 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'), ## user::invitations re_path(r'^api/v2.1/invitations/$', InvitationsView.as_view()), From a89fbafe6506df23a3081ac98bd35bf03cb167ed 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: Fri, 22 Nov 2024 11:18:42 +0800 Subject: [PATCH 03/13] complete --- .../src/components/common/notification.js | 88 ++++---- frontend/src/user-notifications.js | 203 ++++++++++++++---- frontend/src/utils/seafile-api.js | 14 ++ seahub/api2/endpoints/notifications.py | 112 +++++++++- seahub/seadoc/models.py | 4 + seahub/urls.py | 3 +- 6 files changed, 348 insertions(+), 76 deletions(-) diff --git a/frontend/src/components/common/notification.js b/frontend/src/components/common/notification.js index 26beef83f07..0551a11042a 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 }); - }); - } - if (this.state.currentTab === 'discussion') { - seafileAPI.listSdocNotifications(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 }); - } - + }); }; 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.noticeList.map(item => { - return (); - })} -
    + {this.state.currentTab === 'general' && +
      + {this.state.generalNoticeList.map(item => { + return (); + })} +
    + } + {this.state.currentTab === 'discussion' && +
      + {this.state.discussionNoticeList.map(item => { + return (); + })} +
    + } } {this.state.isShowNotificationDialog && - + }
); diff --git a/frontend/src/user-notifications.js b/frontend/src/user-notifications.js index 7736177046f..2d34db2e33f 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); } }; @@ -122,6 +209,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; @@ -165,9 +295,7 @@ class UserNotificationsDialog extends React.Component { zIndex={1046}> {gettext('Notifications')} -
this.notificationTableRef = ref} onScroll={this.onHandleScroll}> - {content} -
+ {this.renderNoticeContent(content)}
); @@ -176,6 +304,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 ec812375395..ce3f35bb0bb 100644 --- a/frontend/src/utils/seafile-api.js +++ b/frontend/src/utils/seafile-api.js @@ -1445,6 +1445,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 = { @@ -1479,6 +1488,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 ec01b2be7a7..dcabbabfeae 100644 --- a/seahub/api2/endpoints/notifications.py +++ b/seahub/api2/endpoints/notifications.py @@ -227,7 +227,7 @@ def put(self, request): """ 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 @@ def put(self, request): 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 @@ def put(self, request): 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 6afae79629a..7e0f96bf147 100644 --- a/seahub/seadoc/models.py +++ b/seahub/seadoc/models.py @@ -272,6 +272,10 @@ def delete_by_ids(self, doc_uuid, username, ids): 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 191f7754d2b..d0c1b357b1a 100644 --- a/seahub/urls.py +++ b/seahub/urls.py @@ -89,7 +89,7 @@ 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 @@ -516,6 +516,7 @@ 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()), From 04888f56f7caa6b88a2529c8b50ac89aca3115c9 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: Fri, 22 Nov 2024 14:17:30 +0800 Subject: [PATCH 04/13] optimize --- .../src/components/common/notification.js | 5 +-- frontend/src/user-notifications.js | 15 ++------- seahub/api2/endpoints/notifications.py | 31 +++++++++++-------- seahub/seadoc/models.py | 4 +-- 4 files changed, 23 insertions(+), 32 deletions(-) diff --git a/frontend/src/components/common/notification.js b/frontend/src/components/common/notification.js index 0551a11042a..378087f5ba1 100644 --- a/frontend/src/components/common/notification.js +++ b/frontend/src/components/common/notification.js @@ -158,10 +158,7 @@ class Notification extends React.Component { } {this.state.isShowNotificationDialog && - + }
); diff --git a/frontend/src/user-notifications.js b/frontend/src/user-notifications.js index 2d34db2e33f..6c64b125a17 100644 --- a/frontend/src/user-notifications.js +++ b/frontend/src/user-notifications.js @@ -1,16 +1,6 @@ import React, { Fragment } from 'react'; import PropTypes from 'prop-types'; -import { - Modal, - ModalHeader, - ModalBody, - Dropdown, - DropdownToggle, - DropdownMenu, - DropdownItem, - TabPane, - Nav, NavItem, NavLink, TabContent -} 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'; @@ -303,8 +293,7 @@ class UserNotificationsDialog extends React.Component { } UserNotificationsDialog.propTypes = { - onNotificationDialogToggle: PropTypes.func.isRequired, - tabItemClick: PropTypes.func.isRequired, + onNotificationDialogToggle: PropTypes.func.isRequired }; export default UserNotificationsDialog; diff --git a/seahub/api2/endpoints/notifications.py b/seahub/api2/endpoints/notifications.py index dcabbabfeae..2a389107f2e 100644 --- a/seahub/api2/endpoints/notifications.py +++ b/seahub/api2/endpoints/notifications.py @@ -168,6 +168,11 @@ def put(self, request): class SdocNotificationsView(APIView): + + authentication_classes = (TokenAuthentication, SessionAuthentication) + permission_classes = (IsAuthenticated,) + throttle_classes = (UserRateThrottle,) + def get(self, request): """ used for get sdoc notifications @@ -175,7 +180,6 @@ def get(self, request): 1. login user. """ result = {} - username = request.user.username try: @@ -185,10 +189,14 @@ def get(self, request): per_page = 25 page = 1 + if page < 1: + error_msg = 'page invalid' + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + start = (page - 1) * per_page end = page * per_page - notice_list = SeadocNotification.objects.list_all_by_user(username, start, end) + 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: @@ -220,12 +228,6 @@ def get(self, request): return Response(result) def put(self, request): - """ currently only used for mark all notifications seen - - Permission checking: - 1. login user. - """ - username = request.user.username unseen_notices = SeadocNotification.objects.filter(username=username, seen=False) for notice in unseen_notices: @@ -328,19 +330,22 @@ def get(self, request): per_page = 25 page = 1 + if page < 1: + error_msg = 'page invalid' + return api_error(status.HTTP_403_FORBIDDEN, error_msg) + 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) + general_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) + general_result_notices = update_notice_detail(request, general_notice_list) sdoc_result_notices = update_sdoc_notice_detail(request, sdoc_notice_list) notification_list = [] sdoc_notification_list = [] - - for i in result_notices: + for i in general_result_notices: if i.detail is not None: notice = {} notice['id'] = i.id diff --git a/seahub/seadoc/models.py b/seahub/seadoc/models.py index 7e0f96bf147..9a304b4ec8a 100644 --- a/seahub/seadoc/models.py +++ b/seahub/seadoc/models.py @@ -270,8 +270,8 @@ def list_by_unseen(self, doc_uuid, username): 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] + def list_all_by_user(self, username): + return self.filter(username=username).order_by('-created_at') def remove_user_notifications(self, username): """"Remove all user notifications.""" From 71f05574278484e2925f591219044638152f0785 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: Tue, 26 Nov 2024 10:56:09 +0800 Subject: [PATCH 05/13] optimize --- seahub/api2/endpoints/notifications.py | 17 +++++++++------- seahub/notifications/utils.py | 27 ++++++++++++++++---------- seahub/seadoc/apis.py | 18 +++++++---------- seahub/tags/models.py | 6 ++++++ 4 files changed, 40 insertions(+), 28 deletions(-) diff --git a/seahub/api2/endpoints/notifications.py b/seahub/api2/endpoints/notifications.py index 2a389107f2e..25cb01c402e 100644 --- a/seahub/api2/endpoints/notifications.py +++ b/seahub/api2/endpoints/notifications.py @@ -17,7 +17,7 @@ from seahub.notifications.models import get_cache_key_of_unseen_notifications 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.seadoc.models import SeadocNotification from seahub.utils.timeutils import datetime_to_isoformat_timestr logger = logging.getLogger(__name__) @@ -228,12 +228,15 @@ def get(self, request): return Response(result) def put(self, request): + """mark all sdoc notifications seen""" username = request.user.username - unseen_notices = SeadocNotification.objects.filter(username=username, seen=False) - for notice in unseen_notices: - notice.seen = True - notice.save() - + try: + SeadocNotification.objects.filter(username=username, seen=False).update(seen=True) + except Exception as e: + logger.error(e) + error_msg = 'Internal Server Error' + return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg) + cache_key = get_cache_key_of_unseen_sdoc_notifications(username) cache.delete(cache_key) @@ -327,7 +330,7 @@ def get(self, request): per_page = int(request.GET.get('per_page', '')) page = int(request.GET.get('page', '')) except ValueError: - per_page = 25 + per_page = 5 page = 1 if page < 1: diff --git a/seahub/notifications/utils.py b/seahub/notifications/utils.py index b41e6035558..b8f56c1ea0c 100644 --- a/seahub/notifications/utils.py +++ b/seahub/notifications/utils.py @@ -399,30 +399,37 @@ def update_notice_detail(request, notices): def update_sdoc_notice_detail(request, notices): + doc_uuid_set = set() for notice in notices: + doc_uuid_set.add(notice.doc_uuid) + doc_uuid_map = {} + uuids = FileUUIDMap.objects.get_fileuuidmap_in_uuids(doc_uuid_set) + for uuid in uuids: + if uuid not in doc_uuid_map: + origin_file_path = posixpath.join(uuid.parent_path, uuid.filename) + doc_uuid_map[str(uuid.uuid)] = (origin_file_path, uuid.filename, uuid.repo_id) + + for notice in notices: + uuid = doc_uuid_map[notice.doc_uuid] if notice.is_comment(): try: d = json.loads(notice.detail) - uuid = FileUUIDMap.objects.get_fileuuidmap_by_uuid(notice.doc_uuid) - origin_file_path = posixpath.join(uuid.parent_path, uuid.filename) url, _, _ = api_avatar_url(d['author']) d['avatar_url'] = url - d['sdoc_path'] = origin_file_path - d['sdoc_name'] = uuid.filename - d['repo_id'] = uuid.repo_id + d['sdoc_path'] = uuid[0] + d['sdoc_name'] = uuid[1] + d['repo_id'] = uuid[2] notice.detail = d except Exception as e: logger.error(e) elif notice.is_reply(): try: d = json.loads(notice.detail) - uuid = FileUUIDMap.objects.get_fileuuidmap_by_uuid(notice.doc_uuid) - origin_file_path = posixpath.join(uuid.parent_path, uuid.filename) url, _, _ = api_avatar_url(d['author']) d['avatar_url'] = url - d['sdoc_path'] = origin_file_path - d['sdoc_name'] = uuid.filename - d['repo_id'] = uuid.repo_id + d['sdoc_path'] = uuid[0] + d['sdoc_name'] = uuid[1] + d['repo_id'] = uuid[2] notice.detail = d except Exception as e: logger.error(e) diff --git a/seahub/seadoc/apis.py b/seahub/seadoc/apis.py index b37c7d736dc..5fd97d340f1 100644 --- a/seahub/seadoc/apis.py +++ b/seahub/seadoc/apis.py @@ -29,6 +29,7 @@ from django.utils.decorators import method_decorator from django.views.decorators.cache import cache_page from django.views.decorators.http import condition +from django.core.cache import cache from seaserv import seafile_api, check_quota, get_org_id_by_repo_id, ccnet_api @@ -53,7 +54,7 @@ from seahub.tags.models import FileUUIDMap from seahub.utils.error_msg import file_type_error_msg from seahub.utils.repo import parse_repo_perm, get_related_users_by_repo -from seahub.seadoc.models import SeadocHistoryName, SeadocRevision, SeadocCommentReply, SeadocNotification +from seahub.seadoc.models import SeadocHistoryName, SeadocRevision, SeadocCommentReply, SeadocNotification, get_cache_key_of_unseen_sdoc_notifications from seahub.avatar.templatetags.avatar_tags import api_avatar_url from seahub.base.templatetags.seahub_tags import email2nickname, \ email2contact_email @@ -1101,11 +1102,6 @@ def post(self, request, file_uuid): error_msg = 'Permission denied.' return api_error(status.HTTP_403_FORBIDDEN, error_msg) - try: - avatar_size = int(request.GET.get('avatar_size', AVATAR_DEFAULT_SIZE)) - except ValueError: - avatar_size = AVATAR_DEFAULT_SIZE - comment = request.data.get('comment', '') detail = request.data.get('detail', '') author = request.data.get('author', '') @@ -1148,6 +1144,9 @@ def post(self, request, file_uuid): )) try: SeadocNotification.objects.bulk_create(new_notifications) + # delete sdoc notification count cache + sdoc_cache_key = get_cache_key_of_unseen_sdoc_notifications(username) + cache.delete(sdoc_cache_key) except Exception as e: logger.error(e) error_msg = 'Internal Server Error' @@ -1171,11 +1170,6 @@ def get(self, request, file_uuid, comment_id): error_msg = 'Permission denied.' return api_error(status.HTTP_403_FORBIDDEN, error_msg) - try: - avatar_size = int(request.GET.get('avatar_size', AVATAR_DEFAULT_SIZE)) - except ValueError: - avatar_size = AVATAR_DEFAULT_SIZE - # resource check try: file_comment = FileComment.objects.get(pk=comment_id) @@ -1377,6 +1371,8 @@ def post(self, request, file_uuid, comment_id): )) try: SeadocNotification.objects.bulk_create(new_notifications) + sdoc_cache_key = get_cache_key_of_unseen_sdoc_notifications(username) + cache.delete(sdoc_cache_key) except Exception as e: logger.error(e) error_msg = 'Internal Server Error' diff --git a/seahub/tags/models.py b/seahub/tags/models.py index 74d1d703e54..262100ba725 100644 --- a/seahub/tags/models.py +++ b/seahub/tags/models.py @@ -21,6 +21,12 @@ def get_fileuuidmap_by_uuid(self, uuid): return super(FileUUIDMapManager, self).get(uuid=uuid) except self.model.DoesNotExist: return None + + def get_fileuuidmap_in_uuids(self, uuids): + try: + return super(FileUUIDMapManager, self).filter(uuid__in=uuids) + except self.model.DoesNotExist: + return None def get_or_create_fileuuidmap(self, repo_id, parent_path, filename, is_dir): """ create filemap by repo_id、 parent_path、filename、id_dir From e24bb7e71dc5aafe083d106f615f81002a815a97 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: Wed, 27 Nov 2024 15:06:30 +0800 Subject: [PATCH 06/13] add update all notification API --- .../src/components/common/notification.js | 8 +-- frontend/src/utils/seafile-api.js | 5 ++ seahub/api2/endpoints/notifications.py | 52 +++++++++++++++---- 3 files changed, 50 insertions(+), 15 deletions(-) diff --git a/frontend/src/components/common/notification.js b/frontend/src/components/common/notification.js index 378087f5ba1..1806b1221ba 100644 --- a/frontend/src/components/common/notification.js +++ b/frontend/src/components/common/notification.js @@ -22,7 +22,7 @@ class Notification extends React.Component { componentDidMount() { seafileAPI.listAllNotifications().then(res => { - let unseen_count = res.data.general_notification.unseen_count + res.data.discussion_notification.unseen_count; + let unseen_count = res.data.general.unseen_count + res.data.discussion.unseen_count; this.setState({ unseenCount: unseen_count }); }); } @@ -54,8 +54,8 @@ class Notification extends React.Component { let page = 1; let perPage = 5; seafileAPI.listAllNotifications(page, perPage).then(res => { - let generalNoticeList = res.data.general_notification.notification_list; - let discussionNoticeList = res.data.discussion_notification.notification_list; + let generalNoticeList = res.data.general.notification_list; + let discussionNoticeList = res.data.discussion.notification_list; this.setState({ generalNoticeList: generalNoticeList, discussionNoticeList: discussionNoticeList @@ -111,7 +111,7 @@ class Notification extends React.Component { }; onMarkAllNotifications = () => { - seafileAPI.updateNotifications().then(() => { + seafileAPI.updateAllNotifications().then(() => { this.setState({ unseenCount: 0, }); diff --git a/frontend/src/utils/seafile-api.js b/frontend/src/utils/seafile-api.js index ce3f35bb0bb..196d6e11144 100644 --- a/frontend/src/utils/seafile-api.js +++ b/frontend/src/utils/seafile-api.js @@ -1454,6 +1454,11 @@ class SeafileAPI { return this.req.get(url, { params: params }); } + updateAllNotifications() { + const url = this.server + '/api/v2.1/all-notifications/'; + return this.req.put(url); + } + listNotifications(page, perPage) { const url = this.server + '/api/v2.1/notifications/'; let params = { diff --git a/seahub/api2/endpoints/notifications.py b/seahub/api2/endpoints/notifications.py index 25cb01c402e..4ced481b12a 100644 --- a/seahub/api2/endpoints/notifications.py +++ b/seahub/api2/endpoints/notifications.py @@ -320,8 +320,8 @@ def get(self, request): 1. login user. """ result = { - 'general_notification': {}, - 'discussion_notification': {} + 'general': {}, + 'discussion': {} } username = request.user.username @@ -378,25 +378,55 @@ def get(self, request): # for case of count value is `0` if unseen_count_from_cache is not None: - result['general_notification']['unseen_count'] = unseen_count_from_cache + result['general']['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 + result['general']['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 + result['discussion']['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 + result['discussion']['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 + result['general']['notification_list'] = notification_list + result['discussion']['notification_list'] = sdoc_notification_list + result['general']['count'] = total_count + result['discussion']['count'] = sdoc_total_count - return Response(result) \ No newline at end of file + return Response(result) + + + def put(self, request): + """ currently only used for mark all notifications seen + + Permission checking: + 1. login user. + """ + + username = request.user.username + # unseen_notices = UserNotification.objects.get_user_notifications(username, + # seen=False) + # for notice in unseen_notices: + # notice.seen = True + # notice.save() + + try: + UserNotification.objects.get_user_notifications(username, seen=False).update(seen=True) + SeadocNotification.objects.filter(username=username, seen=False).update(seen=True) + except Exception as e: + logger.error(e) + error_msg = 'Internal Server Error' + return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg) + + cache_key = get_cache_key_of_unseen_notifications(username) + sdoc_cache_key = get_cache_key_of_unseen_sdoc_notifications(username) + cache.delete(cache_key) + cache.delete(sdoc_cache_key) + + return Response({'success': True}) From 16c64356ae810cb3461bf9c42f3a1929f02141db Mon Sep 17 00:00:00 2001 From: r350178982 <32759763+r350178982@users.noreply.github.com> Date: Wed, 27 Nov 2024 17:22:30 +0800 Subject: [PATCH 07/13] Update notifications.py --- seahub/api2/endpoints/notifications.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/seahub/api2/endpoints/notifications.py b/seahub/api2/endpoints/notifications.py index 4ced481b12a..e852dbc1b3e 100644 --- a/seahub/api2/endpoints/notifications.py +++ b/seahub/api2/endpoints/notifications.py @@ -410,12 +410,6 @@ def put(self, request): """ username = request.user.username - # unseen_notices = UserNotification.objects.get_user_notifications(username, - # seen=False) - # for notice in unseen_notices: - # notice.seen = True - # notice.save() - try: UserNotification.objects.get_user_notifications(username, seen=False).update(seen=True) SeadocNotification.objects.filter(username=username, seen=False).update(seen=True) From 597b731e1ebd13c6d7cff692cc78578134187914 Mon Sep 17 00:00:00 2001 From: r350178982 <32759763+r350178982@users.noreply.github.com> Date: Thu, 28 Nov 2024 15:05:01 +0800 Subject: [PATCH 08/13] update --- seahub/api2/endpoints/notifications.py | 2 +- seahub/notifications/utils.py | 24 +++++++++++++----------- seahub/tags/models.py | 6 ++---- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/seahub/api2/endpoints/notifications.py b/seahub/api2/endpoints/notifications.py index e852dbc1b3e..c411623b753 100644 --- a/seahub/api2/endpoints/notifications.py +++ b/seahub/api2/endpoints/notifications.py @@ -344,7 +344,7 @@ def get(self, request): sdoc_notice_list = SeadocNotification.objects.list_all_by_user(username)[start:end] general_result_notices = update_notice_detail(request, general_notice_list) - sdoc_result_notices = update_sdoc_notice_detail(request, sdoc_notice_list) + sdoc_result_notices = update_sdoc_notice_detail(sdoc_notice_list) notification_list = [] sdoc_notification_list = [] diff --git a/seahub/notifications/utils.py b/seahub/notifications/utils.py index b8f56c1ea0c..71daeb6a11d 100644 --- a/seahub/notifications/utils.py +++ b/seahub/notifications/utils.py @@ -398,27 +398,29 @@ def update_notice_detail(request, notices): return notices -def update_sdoc_notice_detail(request, notices): +def update_sdoc_notice_detail(notices): doc_uuid_set = set() for notice in notices: doc_uuid_set.add(notice.doc_uuid) - doc_uuid_map = {} + uuid_doc_map = {} uuids = FileUUIDMap.objects.get_fileuuidmap_in_uuids(doc_uuid_set) for uuid in uuids: - if uuid not in doc_uuid_map: + if uuid not in uuid_doc_map: origin_file_path = posixpath.join(uuid.parent_path, uuid.filename) - doc_uuid_map[str(uuid.uuid)] = (origin_file_path, uuid.filename, uuid.repo_id) + uuid_doc_map[str(uuid.uuid)] = (origin_file_path, uuid.filename, uuid.repo_id) for notice in notices: - uuid = doc_uuid_map[notice.doc_uuid] + doc = uuid_doc_map.get(notice.doc_uuid) or None + if not doc: + continue if notice.is_comment(): try: d = json.loads(notice.detail) url, _, _ = api_avatar_url(d['author']) d['avatar_url'] = url - d['sdoc_path'] = uuid[0] - d['sdoc_name'] = uuid[1] - d['repo_id'] = uuid[2] + d['sdoc_path'] = doc[0] + d['sdoc_name'] = doc[1] + d['repo_id'] = doc[2] notice.detail = d except Exception as e: logger.error(e) @@ -427,9 +429,9 @@ def update_sdoc_notice_detail(request, notices): d = json.loads(notice.detail) url, _, _ = api_avatar_url(d['author']) d['avatar_url'] = url - d['sdoc_path'] = uuid[0] - d['sdoc_name'] = uuid[1] - d['repo_id'] = uuid[2] + d['sdoc_path'] = doc[0] + d['sdoc_name'] = doc[1] + d['repo_id'] = doc[2] notice.detail = d except Exception as e: logger.error(e) diff --git a/seahub/tags/models.py b/seahub/tags/models.py index 262100ba725..b42ec66436c 100644 --- a/seahub/tags/models.py +++ b/seahub/tags/models.py @@ -23,10 +23,8 @@ def get_fileuuidmap_by_uuid(self, uuid): return None def get_fileuuidmap_in_uuids(self, uuids): - try: - return super(FileUUIDMapManager, self).filter(uuid__in=uuids) - except self.model.DoesNotExist: - return None + return super(FileUUIDMapManager, self).filter(uuid__in=uuids) + def get_or_create_fileuuidmap(self, repo_id, parent_path, filename, is_dir): """ create filemap by repo_id、 parent_path、filename、id_dir From 084b6333ffb637ab4814c1102226302dedd5d33c Mon Sep 17 00:00:00 2001 From: r350178982 <32759763+r350178982@users.noreply.github.com> Date: Thu, 28 Nov 2024 15:12:04 +0800 Subject: [PATCH 09/13] Update notifications.py --- seahub/api2/endpoints/notifications.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/seahub/api2/endpoints/notifications.py b/seahub/api2/endpoints/notifications.py index c411623b753..28e929a14d1 100644 --- a/seahub/api2/endpoints/notifications.py +++ b/seahub/api2/endpoints/notifications.py @@ -197,7 +197,7 @@ def get(self, request): end = page * per_page notice_list = SeadocNotification.objects.list_all_by_user(username)[start:end] - result_notices = update_sdoc_notice_detail(request, notice_list) + result_notices = update_sdoc_notice_detail(notice_list) notification_list = [] for i in result_notices: if i.detail is not None: From 051b8456dbab382137057eed38975979adafde3e 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, 28 Nov 2024 18:31:04 +0800 Subject: [PATCH 10/13] optimize ui --- frontend/src/components/common/notice-item.js | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/frontend/src/components/common/notice-item.js b/frontend/src/components/common/notice-item.js index a9869b1df89..07ef05920f7 100644 --- a/frontend/src/components/common/notice-item.js +++ b/frontend/src/components/common/notice-item.js @@ -368,9 +368,20 @@ class NoticeItem extends React.Component { let avatar_url = detail.avatar_url; let notice = detail.comment; let username = detail.user_name; + let is_resolved = detail.is_resolved; + let sdoc_name = detail.sdoc_name; + const repo_id = detail.repo_id; + const sdoc_path = detail.sdoc_path; + const sdoc_href = siteRoot + 'lib/' + repo_id + '/file' + sdoc_path; + let sdoc_link = '
' + sdoc_name + ''; processor.process(notice, (error, vfile) => { notice = String(vfile); }); + if (is_resolved) { + notice = 'Marked "' + detail.resolve_comment + '" as resolved in document ' + sdoc_link; + } else { + notice = 'Added a new comment in document ' + sdoc_link + ':' + notice; + } return { avatar_url, username, notice }; } @@ -378,9 +389,20 @@ class NoticeItem extends React.Component { let avatar_url = detail.avatar_url; let notice = detail.reply; let username = detail.user_name; + let is_resolved = detail.is_resolved; + let sdoc_name = detail.sdoc_name; + const repo_id = detail.repo_id; + const sdoc_path = detail.sdoc_path; + const sdoc_href = siteRoot + 'lib/' + repo_id + '/file' + sdoc_path; + let sdoc_link = '' + sdoc_name + ''; processor.process(notice, (error, vfile) => { notice = String(vfile); }); + if (is_resolved) { + notice = 'Marked "' + detail.resolve_comment + '" as resolved in document ' + sdoc_link; + } else { + notice = 'Added a new reply in document ' + sdoc_link + ':' + notice; + } return { avatar_url, username, notice }; } From 8ddb601bc17840b1560d2c5a883aaedf122e0297 Mon Sep 17 00:00:00 2001 From: Michael An <2331806369@qq.com> Date: Mon, 2 Dec 2024 10:55:29 +0800 Subject: [PATCH 11/13] change UI 1 --- frontend/src/components/common/notice-item.js | 83 +++++----------- .../common/notification-popover/index.css | 10 +- .../common/notification-popover/index.js | 23 +---- .../src/components/common/notification.js | 28 ++++-- frontend/src/css/notice-item.css | 94 +++++++++++++++++++ frontend/src/css/notification.css | 33 ------- frontend/src/css/user-notifications.css | 37 ++++---- frontend/src/user-notifications.js | 35 ++++--- 8 files changed, 188 insertions(+), 155 deletions(-) create mode 100644 frontend/src/css/notice-item.css diff --git a/frontend/src/components/common/notice-item.js b/frontend/src/components/common/notice-item.js index 07ef05920f7..3ec6bf2000e 100644 --- a/frontend/src/components/common/notice-item.js +++ b/frontend/src/components/common/notice-item.js @@ -5,6 +5,7 @@ import relativeTime from 'dayjs/plugin/relativeTime'; import { gettext, siteRoot } from '../../utils/constants'; import { Utils } from '../../utils/utils'; import { processor } from '@seafile/seafile-editor'; +import '../../css/notice-item.css'; const propTypes = { noticeItem: PropTypes.object.isRequired, @@ -37,35 +38,26 @@ class NoticeItem extends React.Component { let detail = noticeItem.detail; if (noticeType === MSG_TYPE_ADD_USER_TO_GROUP) { - let avatar_url = detail.group_staff_avatar_url; - let groupStaff = detail.group_staff_name; - // group name does not support special characters let userHref = siteRoot + 'profile/' + detail.group_staff_email + '/'; let groupHref = siteRoot + 'group/' + detail.group_id + '/'; let groupName = detail.group_name; - + let username = detail.group_staff_name; let notice = gettext('User {user_link} has added you to {group_link}'); let userLink = '' + groupStaff + ''; let groupLink = '' + groupName + ''; - notice = notice.replace('{user_link}', userLink); notice = notice.replace('{group_link}', groupLink); - - return { avatar_url, notice }; + return { avatar_url, notice, username }; } if (noticeType === MSG_TYPE_REPO_SHARE) { - let avatar_url = detail.share_from_user_avatar_url; - let shareFrom = detail.share_from_user_name; - let repoName = detail.repo_name; let repoUrl = siteRoot + 'library/' + detail.repo_id + '/' + repoName + '/'; - let path = detail.path; let notice = ''; // 1. handle translate @@ -74,21 +66,17 @@ class NoticeItem extends React.Component { } else { // share folder notice = gettext('{share_from} has shared a folder named {repo_link} to you.'); } - // 2. handle xss(cross-site scripting) notice = notice.replace('{share_from}', shareFrom); notice = notice.replace('{repo_link}', `{tagA}${repoName}{/tagA}`); notice = Utils.HTMLescape(notice); - // 3. add jump link notice = notice.replace('{tagA}', ``); notice = notice.replace('{/tagA}', ''); - - return { avatar_url, notice }; + return { avatar_url, notice, username: shareFrom }; } if (noticeType === MSG_TYPE_REPO_SHARE_PERM_CHANGE) { - let avatar_url = detail.share_from_user_avatar_url; let shareFrom = detail.share_from_user_name; let permission = detail.permission; @@ -102,22 +90,18 @@ class NoticeItem extends React.Component { } else { // share folder notice = gettext('{share_from} has changed the permission of folder {repo_link} to {permission}.'); } - // 2. handle xss(cross-site scripting) notice = notice.replace('{share_from}', shareFrom); notice = notice.replace('{repo_link}', `{tagA}${repoName}{/tagA}`); notice = notice.replace('{permission}', permission); notice = Utils.HTMLescape(notice); - // 3. add jump link notice = notice.replace('{tagA}', ``); notice = notice.replace('{/tagA}', ''); - - return { avatar_url, notice }; + return { avatar_url, notice, username: shareFrom }; } if (noticeType === MSG_TYPE_REPO_SHARE_PERM_DELETE) { - let avatar_url = detail.share_from_user_avatar_url; let shareFrom = detail.share_from_user_name; let repoName = detail.repo_name; @@ -129,26 +113,20 @@ class NoticeItem extends React.Component { } else { // share folder notice = gettext('{share_from} has cancelled the sharing of folder {repo_name}.'); } - // 2. handle xss(cross-site scripting) notice = notice.replace('{share_from}', shareFrom); notice = notice.replace('{repo_name}', repoName); notice = Utils.HTMLescape(notice); - return { avatar_url, notice }; + return { avatar_url, notice, username: shareFrom }; } if (noticeType === MSG_TYPE_REPO_SHARE_TO_GROUP) { - let avatar_url = detail.share_from_user_avatar_url; - let shareFrom = detail.share_from_user_name; - let repoName = detail.repo_name; let repoUrl = siteRoot + 'library/' + detail.repo_id + '/' + repoName + '/'; - let groupUrl = siteRoot + 'group/' + detail.group_id + '/'; let groupName = detail.group_name; - let path = detail.path; let notice = ''; // 1. handle translate @@ -157,60 +135,50 @@ class NoticeItem extends React.Component { } else { notice = gettext('{share_from} has shared a folder named {repo_link} to group {group_link}.'); } - // 2. handle xss(cross-site scripting) notice = notice.replace('{share_from}', shareFrom); notice = notice.replace('{repo_link}', `{tagA}${repoName}{/tagA}`); notice = notice.replace('{group_link}', `{tagB}${groupName}{/tagB}`); notice = Utils.HTMLescape(notice); - // 3. add jump link notice = notice.replace('{tagA}', ``); notice = notice.replace('{/tagA}', ''); notice = notice.replace('{tagB}', ``); notice = notice.replace('{/tagB}', ''); - return { avatar_url, notice }; + return { avatar_url, notice, username: shareFrom }; } if (noticeType === MSG_TYPE_REPO_TRANSFER) { - let avatar_url = detail.transfer_from_user_avatar_url; - let repoOwner = detail.transfer_from_user_name; - let repoName = detail.repo_name; let repoUrl = siteRoot + 'library/' + detail.repo_id + '/' + repoName + '/'; // 1. handle translate let notice = gettext('{user} has transfered a library named {repo_link} to you.'); - // 2. handle xss(cross-site scripting) notice = notice.replace('{user}', repoOwner); notice = notice.replace('{repo_link}', `{tagA}${repoName}{/tagA}`); notice = Utils.HTMLescape(notice); - // 3. add jump link notice = notice.replace('{tagA}', ``); notice = notice.replace('{/tagA}', ''); - return { avatar_url, notice }; + return { avatar_url, notice, username: repoOwner }; } if (noticeType === MSG_TYPE_FILE_UPLOADED) { let avatar_url = detail.uploaded_user_avatar_url; let fileName = detail.file_name; let fileLink = siteRoot + 'lib/' + detail.repo_id + '/' + 'file' + detail.file_path; - let folderName = detail.folder_name; let folderLink = siteRoot + 'library/' + detail.repo_id + '/' + detail.repo_name + detail.folder_path; let notice = ''; if (detail.repo_id) { // todo is repo exist ? // 1. handle translate notice = gettext('A file named {upload_file_link} is uploaded to {uploaded_link}.'); - // 2. handle xss(cross-site scripting) notice = notice.replace('{upload_file_link}', `{tagA}${fileName}{/tagA}`); notice = notice.replace('{uploaded_link}', `{tagB}${folderName}{/tagB}`); notice = Utils.HTMLescape(notice); - // 3. add jump link notice = notice.replace('{tagA}', ``); notice = notice.replace('{/tagA}', ''); @@ -219,7 +187,6 @@ class NoticeItem extends React.Component { } else { // 1. handle translate notice = gettext('A file named {upload_file_link} is uploaded.'); - // 2. handle xss(cross-site scripting) notice = notice.replace('{upload_file_link}', `${fileName}`); notice = Utils.HTMLescape(notice); @@ -343,24 +310,17 @@ class NoticeItem extends React.Component { } if (noticeType === MSG_TYPE_DELETED_FILES) { - const { - repo_id, - repo_name, - } = detail; - + const { repo_id, repo_name } = detail; const repoURL = `${siteRoot}library/${repo_id}/${encodeURIComponent(repo_name)}/`; const repoLink = `${Utils.HTMLescape(repo_name)}`; - let notice = gettext('Your library {libraryName} has recently deleted a large number of files.'); notice = notice.replace('{libraryName}', repoLink); - return { avatar_url: null, notice }; } if (noticeType === MSG_TYPE_SAML_SSO_FAILED) { const { error_msg } = detail; let notice = gettext(error_msg); - return { avatar_url: null, notice }; } @@ -410,7 +370,7 @@ class NoticeItem extends React.Component { // } - return { avatar_url: null, notice: null }; + return { avatar_url: null, notice: null, username: null }; } onNoticeItemClick = () => { @@ -441,18 +401,21 @@ class NoticeItem extends React.Component { ) : ( -
  • -
    -
    -
    - -

    {username}

    -
    -
    -

    +
  • +
    + {!noticeItem.seen && + + } +
    +
    + + {username || gettext('System')}
    + {dayjs(noticeItem.time).fromNow()}
    -

    {dayjs(noticeItem.time).fromNow()}

    +
    +
    +
  • ); diff --git a/frontend/src/components/common/notification-popover/index.css b/frontend/src/components/common/notification-popover/index.css index 50b91b73072..f41e4318095 100644 --- a/frontend/src/components/common/notification-popover/index.css +++ b/frontend/src/components/common/notification-popover/index.css @@ -66,7 +66,7 @@ } .notification-container .mark-all-read { - color: #b4b4b4; + color: #666; cursor: pointer; display: flex; align-items: center; @@ -187,6 +187,9 @@ .notification-container .notification-body .mark-notifications { display: flex; + justify-content: space-between; + border-bottom: 1px solid #ededed; + padding-left: 15px; } .notification-container .notification-body .mark-notifications .mark-all-read:hover { @@ -198,8 +201,9 @@ margin-right: 15px; margin-left: 15px; font-size: 14px; + color: #212529; } .notification-container .notification-body .nav .nav-item .nav-link.active { - color: #ED7109 !important; -} \ No newline at end of file + color: #ED7109; +} diff --git a/frontend/src/components/common/notification-popover/index.js b/frontend/src/components/common/notification-popover/index.js index bf30cebb2fb..1c24289a2b0 100644 --- a/frontend/src/components/common/notification-popover/index.js +++ b/frontend/src/components/common/notification-popover/index.js @@ -18,6 +18,8 @@ export default class NotificationPopover extends React.Component { tabItemClick: PropTypes.func, children: PropTypes.any, currentTab: PropTypes.string, + generalNoticeListUnseen: PropTypes.number, + discussionNoticeListUnseen: PropTypes.number, }; static defaultProps = { @@ -56,7 +58,7 @@ export default class NotificationPopover extends React.Component { }; render() { - const { headerText, bodyText, footerText, currentTab } = this.props; + const { headerText, bodyText, footerText, currentTab, generalNoticeListUnseen, discussionNoticeListUnseen } = this.props; return ( this.tabItemClick('general')}> {gettext('General')} + {generalNoticeListUnseen > 0 && ({generalNoticeListUnseen})}
  • this.tabItemClick('discussion')}> {gettext('Discussion')} + {discussionNoticeListUnseen > 0 && ({discussionNoticeListUnseen})}
  • @@ -103,23 +107,6 @@ export default class NotificationPopover extends React.Component { } - - {/*
    -
      -
    • - 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 1806b1221ba..b2dd80b1ec5 100644 --- a/frontend/src/components/common/notification.js +++ b/frontend/src/components/common/notification.js @@ -123,7 +123,9 @@ class Notification extends React.Component { }; render() { - const { unseenCount, currentTab } = this.state; + const { unseenCount, currentTab, generalNoticeList, discussionNoticeList } = this.state; + const generalNoticeListUnseen = generalNoticeList.filter(item => !item.seen).length; + const discussionNoticeListUnseen = discussionNoticeList.filter(item => !item.seen).length; return ( ); diff --git a/frontend/src/css/notice-item.css b/frontend/src/css/notice-item.css new file mode 100644 index 00000000000..38b8c5ac2a3 --- /dev/null +++ b/frontend/src/css/notice-item.css @@ -0,0 +1,94 @@ +.notification-item { + padding: 14px 16px 14px 10px; + border-bottom: 1px solid #ededed; + position: relative; + cursor: pointer; +} + +.notification-item:last-child { + border-bottom: none; +} + +.notification-item:hover { + background: #f5f5f5; +} + +.notification-item .notification-item-header { + display: flex; + align-items: center +} + +.notification-item .notification-point { + display: inline-block; + width: 8px; + height: 8px; + border-radius: 50%; + background: red; + margin-right: 12px; + position: absolute; +} + +.notification-item .notification-header-info { + display: flex; + justify-content: space-between; + flex: 1; + margin-left: 20px; + width: calc(100% - 20px); +} + +.notification-item .notification-user-detail { + display: flex; + width: 65%; +} + +.notification-item .notification-user-detail .notification-user-avatar { + width: 24px; + height: 24px; + margin-top: 3px; + border-radius: 50%; +} + +.notification-item .notification-user-detail .notification-user-name { + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + font-weight: 500; + margin-left: 8px; +} + +.notification-item .notification-header-info .notification-time { + color: #666; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + font-size: 13px; +} + +.notification-item .notification-content-wrapper { + font-size: 13px; + margin-left: 52px; +} + +.notification-item .notification-content-quotes { + width: 8px; +} + +.notification-item .notification-comment-content { + max-width: calc(100% - 16px); +} + +.notification-item .notification-comment-content p { + display: inline-block; + letter-spacing: 1px; + max-width: 100%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + margin-bottom: 0; +} + +.notification-item .notification-comment-content p img { + max-width: 70%; + height: auto; + max-height: 60px; +} diff --git a/frontend/src/css/notification.css b/frontend/src/css/notification.css index 6dd6024ec5f..6dea9379356 100644 --- a/frontend/src/css/notification.css +++ b/frontend/src/css/notification.css @@ -56,44 +56,11 @@ font-weight: normal; } -#notice-popover li { - padding: 9px 0 3px; - border-bottom: 1px solid #dfdfe1; -} - -#notice-popover li.unread { - padding-right: 10px; - padding-left: 10px; - border-left: 2px solid #feac74; -} - -#notice-popover li.read { - padding-right: 10px; - padding-left: 10px; - border-left: 2px solid transparent; -} - -#notice-popover li:hover { - background: #f5f5f7; -} - -#notice-popover li.read:hover { - background: #f5f5f7; - border-left: 2px solid #dfdfe1; -} - #notice-popover .avatar { border-radius: 1000px; float: left; } -#notice-popover .brief { - margin-left: 40px; - margin-bottom: 1rem; - font-size: 0.8125rem; - line-height: 1.5rem; -} - #notice-popover .time { margin: 0; color: #999; diff --git a/frontend/src/css/user-notifications.css b/frontend/src/css/user-notifications.css index addbad28336..7899702ca7e 100644 --- a/frontend/src/css/user-notifications.css +++ b/frontend/src/css/user-notifications.css @@ -1,5 +1,5 @@ .notification-list-dialog { - width: 720px; + width: calc(100% - 20rem); max-width: calc(100% - 1rem); height: calc(100% - 56px); } @@ -56,10 +56,26 @@ .notification-list-content .notification-modal-body { height: 100%; + display: flex; + flex-direction: row; + min-height: 27rem; overflow: hidden; padding: 0; } +.notification-list-content .notification-modal-body .notice-dialog-side { + border-right: 1px solid #eee; + display: flex; + flex: 0 0 20%; + padding: 12px 8px; +} + +.notification-list-content .notification-modal-body .notice-dialog-main { + display: flex; + flex: 0 0 80%; + overflow: inherit; +} + .notification-modal-body .notification-dialog-body { overflow: auto; padding: 2rem 1rem; @@ -101,22 +117,3 @@ font-size: 14px; word-break: break-all; } - -.wechat-dialog-body { - display: flex; - justify-content: center; - padding: 3rem; - flex-direction: column; - align-items: center; -} - -.wechat-dialog-message { - width: 100%; - display: flex; - justify-content: center; - flex-direction: column; - align-items: center; - margin-top: 1rem; - color: #666; - font-size: 14px; -} diff --git a/frontend/src/user-notifications.js b/frontend/src/user-notifications.js index 6c64b125a17..09551e45e70 100644 --- a/frontend/src/user-notifications.js +++ b/frontend/src/user-notifications.js @@ -1,4 +1,4 @@ -import React, { Fragment } from 'react'; +import React from 'react'; import PropTypes from 'prop-types'; import { Modal, ModalHeader, ModalBody, Dropdown, DropdownToggle, DropdownMenu, DropdownItem, TabPane, Nav, NavItem, NavLink, TabContent } from 'reactstrap'; import { Utils } from './utils/utils'; @@ -201,19 +201,22 @@ class UserNotificationsDialog extends React.Component { renderNoticeContent = (content) => { + const { generalNoticeListUnseen, discussionNoticeListUnseen } = this.props; let activeTab = this.state.activeTab; return ( - + <>
    -