1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-09-17 15:53:28 +00:00
This commit is contained in:
孙永强
2024-11-22 11:18:42 +08:00
parent 0f77313761
commit 631320c889
6 changed files with 348 additions and 76 deletions

View File

@@ -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,49 +47,51 @@ class Notification extends React.Component {
this.setState({
showNotice: true,
currentTab: tab
}, () => {
this.loadNotices();
});
};
loadNotices = () => {
let page = 1;
let perPage = 5;
if (this.state.currentTab === 'general') {
seafileAPI.listNotifications(page, perPage).then(res => {
let noticeList = res.data.notification_list;
this.setState({ noticeList: noticeList });
seafileAPI.listAllNotifications(page, perPage).then(res => {
let generalNoticeList = res.data.general_notification.notification_list;
let discussionNoticeList = res.data.discussion_notification.notification_list;
this.setState({
generalNoticeList: generalNoticeList,
discussionNoticeList: discussionNoticeList
});
}
if (this.state.currentTab === 'discussion') {
seafileAPI.listSdocNotifications(page, perPage).then(res => {
let noticeList = res.data.notification_list;
this.setState({ noticeList: noticeList });
});
}
};
onNoticeItemClick = (noticeItem) => {
let noticeList = this.state.noticeList.map(item => {
if (this.state.currentTab === 'general') {
let noticeList = this.state.generalNoticeList.map(item => {
if (item.id === noticeItem.id) {
item.seen = true;
}
return item;
});
if (this.state.currentTab === 'general') {
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') {
seafileAPI.markSdocNoticeAsRead(noticeItem.id);
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({
noticeList: noticeList,
discussionNoticeList: noticeList,
unseenCount: unseenCount,
});
seafileAPI.markSdocNoticeAsRead(noticeItem.id);
}
};
@@ -137,15 +141,27 @@ class Notification extends React.Component {
onMarkAllNotifications={this.onMarkAllNotifications}
tabItemClick={this.tabItemClick}
>
{this.state.currentTab === 'general' &&
<ul className="notice-list list-unstyled" id="notice-popover">
{this.state.noticeList.map(item => {
{this.state.generalNoticeList.map(item => {
return (<NoticeItem key={item.id} noticeItem={item} onNoticeItemClick={this.onNoticeItemClick}/>);
})}
</ul>
}
{this.state.currentTab === 'discussion' &&
<ul className="notice-list list-unstyled" id="notice-popover">
{this.state.discussionNoticeList.map(item => {
return (<NoticeItem key={item.id} noticeItem={item} onNoticeItemClick={this.onNoticeItemClick}/>);
})}
</ul>
}
</NotificationPopover>
}
{this.state.isShowNotificationDialog &&
<UserNotificationsDialog onNotificationDialogToggle={this.onNotificationDialogToggle} />
<UserNotificationsDialog
onNotificationDialogToggle={this.onNotificationDialogToggle}
tabItemClick={this.tabItemClick}
/>
}
</div>
);

View File

@@ -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,24 +48,61 @@ class UserNotificationsDialog extends React.Component {
});
}
getItems = (page) => {
getItems = (page, is_scroll = false) => {
this.setState({ isLoading: true });
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
});
});
} 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 = () => {
if (this.state.activeTab === 'general') {
seafileAPI.updateNotifications().then((res) => {
this.setState({
items: this.state.items.map(item => {
@@ -68,9 +116,26 @@ class UserNotificationsDialog extends React.Component {
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 = () => {
if (this.state.activeTab === 'general') {
seafileAPI.deleteNotifications().then((res) => {
this.setState({
items: []
@@ -81,12 +146,34 @@ class UserNotificationsDialog extends React.Component {
errorMsg: Utils.getErrorMsg(error, true) // true: show login tip if 403
});
});
} else if (this.state.activeTab === 'discussion') {
seafileAPI.deleteSdocNotifications().then((res) => {
this.setState({
items: []
});
}).catch((error) => {
this.setState({
isLoading: false,
errorMsg: Utils.getErrorMsg(error, true) // true: show login tip if 403
});
});
}
};
toggle = () => {
this.props.onNotificationDialogToggle();
};
tabItemClick = (e) => {
let tab = e.target.getAttribute('value');
this.setState({
activeTab: tab,
currentPage: 1
}, () => {
this.getItems(this.state.currentPage);
});
};
toggleDropDownMenu = () => {
this.setState({ isItemMenuShow: !this.state.isItemMenuShow });
};
@@ -96,7 +183,7 @@ class UserNotificationsDialog extends React.Component {
return;
}
if (this.notificationTableRef.offsetHeight + this.notificationTableRef.scrollTop + 1 >= this.tableRef.offsetHeight) {
this.getItems(this.state.currentPage + 1);
this.getItems(this.state.currentPage + 1, true);
}
};
@@ -130,6 +217,49 @@ class UserNotificationsDialog extends React.Component {
);
};
renderNoticeContent = (content) => {
let activeTab = this.state.activeTab;
return (
<Fragment>
<div className="notice-dialog-side">
<Nav pills>
<NavItem role="tab" aria-selected={activeTab === 'general'} aria-controls="general-notice-panel">
<NavLink className={activeTab === 'general' ? 'active' : ''} onClick={this.tabItemClick} tabIndex="0" value="general">
{gettext('General')}
</NavLink>
</NavItem>
<NavItem role="tab" aria-selected={activeTab === 'discussion'} aria-controls="discussion-notice-panel">
<NavLink className={activeTab === 'discussion' ? 'active' : ''} onClick={this.tabItemClick} tabIndex="1" value="discussion">
{gettext('Discussion')}
</NavLink>
</NavItem>
</Nav>
</div>
<div className="notice-dialog-main">
<TabContent activeTab={this.state.activeTab}>
{activeTab === 'general' &&
<TabPane tabId="general" role="tabpanel" id="general-notice-panel">
<div className="notification-dialog-body" ref={ref => this.notificationTableRef = ref}
onScroll={this.onHandleScroll}>
{content}
</div>
</TabPane>
}
{activeTab === 'discussion' &&
<TabPane tabId="discussion" role="tabpanel" id="discussion-notice-panel">
<div className="notification-dialog-body" ref={ref => this.notificationTableRef = ref}
onScroll={this.onHandleScroll}>
{content}
</div>
</TabPane>
}
</TabContent>
</div>
</Fragment>
);
};
render() {
const { isLoading, errorMsg, items } = this.state;
let content;
@@ -173,9 +303,7 @@ class UserNotificationsDialog extends React.Component {
zIndex={1046}>
<ModalHeader close={this.renderHeaderRowBtn()} toggle={this.toggle}>{gettext('Notifications')}</ModalHeader>
<ModalBody className="notification-modal-body">
<div className="notification-dialog-body" ref={ref => this.notificationTableRef = ref} onScroll={this.onHandleScroll}>
{content}
</div>
{this.renderNoticeContent(content)}
</ModalBody>
</Modal>
);
@@ -184,6 +312,7 @@ class UserNotificationsDialog extends React.Component {
UserNotificationsDialog.propTypes = {
onNotificationDialogToggle: PropTypes.func.isRequired,
tabItemClick: PropTypes.func.isRequired,
};
export default UserNotificationsDialog;

View File

@@ -1451,6 +1451,15 @@ class SeafileAPI {
}
// ---- Notification API
listAllNotifications(page, perPage) {
const url = this.server + '/api/v2.1/all-notifications/';
let params = {
page: page,
per_page: perPage
};
return this.req.get(url, { params: params });
}
listNotifications(page, perPage) {
const url = this.server + '/api/v2.1/notifications/';
let params = {
@@ -1485,6 +1494,11 @@ class SeafileAPI {
return this.req.delete(url);
}
deleteSdocNotifications() {
const url = this.server + '/api/v2.1/sdoc-notifications/';
return this.req.delete(url);
}
getUnseenNotificationCount() {
const url = this.server + '/api/v2.1/notifications/';
return this.req.get(url);

View File

@@ -227,7 +227,7 @@ class SdocNotificationsView(APIView):
"""
username = request.user.username
unseen_notices = SeadocNotification.objects.filter(username, seen=False)
unseen_notices = SeadocNotification.objects.filter(username=username, seen=False)
for notice in unseen_notices:
notice.seen = True
notice.save()
@@ -237,6 +237,21 @@ class SdocNotificationsView(APIView):
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})
class SdocNotificationView(APIView):
@@ -284,3 +299,96 @@ class SdocNotificationView(APIView):
cache.delete(cache_key)
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)

View File

@@ -273,6 +273,10 @@ class SeadocNotificationManager(models.Manager):
def list_all_by_user(self, username, start, end):
return self.filter(username=username).order_by('-created_at')[start: end]
def remove_user_notifications(self, username):
""""Remove all user notifications."""
self.filter(username=username).delete()
class SeadocNotification(models.Model):
doc_uuid = models.CharField(max_length=36)

View File

@@ -91,7 +91,7 @@ from seahub.api2.endpoints.invitations import InvitationsView, InvitationsBatchV
from seahub.api2.endpoints.invitation import InvitationView, InvitationRevokeView
from seahub.api2.endpoints.repo_share_invitations import RepoShareInvitationsView, RepoShareInvitationsBatchView
from seahub.api2.endpoints.repo_share_invitation import RepoShareInvitationView
from seahub.api2.endpoints.notifications import NotificationsView, NotificationView, SdocNotificationView, SdocNotificationsView
from seahub.api2.endpoints.notifications import NotificationsView, NotificationView, SdocNotificationView, SdocNotificationsView, AllNotificationsView
from seahub.api2.endpoints.repo_file_uploaded_bytes import RepoFileUploadedBytesView
from seahub.api2.endpoints.user_avatar import UserAvatarView
from seahub.api2.endpoints.wikis import WikisView, WikiView
@@ -524,6 +524,7 @@ urlpatterns = [
re_path(r'^api/v2.1/notification/$', NotificationView.as_view(), name='api-v2.1-notification'),
re_path(r'^api/v2.1/sdoc-notifications/$', SdocNotificationsView.as_view(), name='api-v2.1-sdoc-notifications'),
re_path(r'^api/v2.1/sdoc-notification/$', SdocNotificationView.as_view(), name='api-v2.1-notification'),
re_path(r'^api/v2.1/all-notifications/$', AllNotificationsView.as_view(), name='api-v2.1-all-notification'),
## user::invitations
re_path(r'^api/v2.1/invitations/$', InvitationsView.as_view()),