diff --git a/frontend/src/components/common/notice-item.js b/frontend/src/components/common/notice-item.js
index 57cad6515b..ff243d8b8c 100644
--- a/frontend/src/components/common/notice-item.js
+++ b/frontend/src/components/common/notice-item.js
@@ -4,6 +4,8 @@ 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';
+import '../../css/notice-item.css';
const propTypes = {
noticeItem: PropTypes.object.isRequired,
@@ -24,6 +26,8 @@ const MSG_TYPE_SAML_SSO_FAILED = 'saml_sso_failed';
const MSG_TYPE_REPO_SHARE_PERM_CHANGE = 'repo_share_perm_change';
const MSG_TYPE_REPO_SHARE_PERM_DELETE = 'repo_share_perm_delete';
const MSG_TYPE_FACE_CLUSTER = 'face_cluster';
+const MSG_TYPE_SEADOC_REPLY = 'reply';
+const MSG_TYPE_SEADOC_COMMENT = 'comment';
dayjs.extend(relativeTime);
@@ -35,35 +39,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
@@ -72,21 +67,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;
@@ -100,22 +91,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;
@@ -127,26 +114,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
@@ -155,60 +136,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}', '');
@@ -217,7 +188,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);
@@ -341,17 +311,11 @@ 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 };
}
@@ -371,15 +335,56 @@ class NoticeItem extends React.Component {
if (noticeType === MSG_TYPE_SAML_SSO_FAILED) {
const { error_msg } = detail;
let notice = gettext(error_msg);
-
return { avatar_url: null, notice };
}
+ if (noticeType === MSG_TYPE_SEADOC_COMMENT) {
+ 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 };
+ }
+
+ if (noticeType === MSG_TYPE_SEADOC_REPLY) {
+ 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 };
+ }
+
// if (noticeType === MSG_TYPE_GUEST_INVITATION_ACCEPTED) {
// }
- return { avatar_url: null, notice: null };
+ return { avatar_url: null, notice: null, username: null };
}
onNoticeItemClick = () => {
@@ -392,16 +397,19 @@ 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 '';
}
return this.props.tr ? (
-
+
+ {!noticeItem.seen && }
+ |
+
+ {username || gettext('System')}
|
@@ -411,13 +419,21 @@ class NoticeItem extends React.Component {
|
) : (
-
-
-
-

-
+
+
+ {!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 38dd383ed7..405bc41abd 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 {
- color: #b4b4b4;
+.notification-container .mark-all-read {
+ color: #666;
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,33 @@
.notification-body .notification-footer:hover {
text-decoration: underline;
}
+
+.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 {
+ text-decoration: underline;
+}
+
+.notification-container .notification-body .nav .nav-item .nav-link {
+ height: 46px;
+ margin-right: 15px;
+ margin-left: 15px;
+ font-size: 14px;
+ color: #212529;
+}
+
+.notification-container .notification-body .nav .nav-item .nav-link.active {
+ color: #ED7109;
+}
+
+@media (max-width: 768px) {
+ .notification-container {
+ right: -60px;
+ width: 360px;
+ }
+}
diff --git a/frontend/src/components/common/notification-popover/index.js b/frontend/src/components/common/notification-popover/index.js
index 7c7ff52f5d..1c24289a2b 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,11 @@ 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,
+ generalNoticeListUnseen: PropTypes.number,
+ discussionNoticeListUnseen: PropTypes.number,
};
static defaultProps = {
@@ -47,8 +53,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, generalNoticeListUnseen, discussionNoticeListUnseen } = this.props;
return (
-
{bodyText}
+
+
+ - this.tabItemClick('general')}>
+
+ {gettext('General')}
+ {generalNoticeListUnseen > 0 && ({generalNoticeListUnseen})}
+
+
+ - this.tabItemClick('discussion')}>
+
+ {gettext('Discussion')}
+ {discussionNoticeListUnseen > 0 && ({discussionNoticeListUnseen})}
+
+
+
+
+ {bodyText}
+
+
+ {currentTab === 'general' &&
this.notificationListRef = ref}>
this.notificationsWrapperRef = ref}>
{this.props.children}
+ }
+ {currentTab === 'discussion' &&
+
this.notificationListRef = ref}>
+
this.notificationsWrapperRef = ref}>
+ {this.props.children}
+
+
+ }
{footerText}
diff --git a/frontend/src/components/common/notification.js b/frontend/src/components/common/notification.js
index fca7dd4520..b2dd80b1ec 100644
--- a/frontend/src/components/common/notification.js
+++ b/frontend/src/components/common/notification.js
@@ -13,14 +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.unseen_count + res.data.discussion.unseen_count;
+ this.setState({ unseenCount: unseen_count });
});
}
@@ -38,28 +41,57 @@ class Notification extends React.Component {
}
};
+ tabItemClick = (tab) => {
+ const { currentTab } = this.state;
+ if (currentTab === tab) return;
+ this.setState({
+ showNotice: true,
+ currentTab: tab
+ });
+ };
+
loadNotices = () => {
let page = 1;
let perPage = 5;
- 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_list;
+ let discussionNoticeList = res.data.discussion.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;
- });
- seafileAPI.markNoticeAsRead(noticeItem.id);
- let unseenCount = this.state.unseenCount === 0 ? 0 : this.state.unseenCount - 1;
- this.setState({
- noticeList: noticeList,
- unseenCount: unseenCount,
- });
+ 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);
+ }
};
@@ -79,7 +111,7 @@ class Notification extends React.Component {
};
onMarkAllNotifications = () => {
- seafileAPI.updateNotifications().then(() => {
+ seafileAPI.updateAllNotifications().then(() => {
this.setState({
unseenCount: 0,
});
@@ -91,7 +123,9 @@ class Notification extends React.Component {
};
render() {
- const { unseenCount } = 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 0000000000..38b8c5ac2a
--- /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 6dd6024ec5..6dea937935 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 35a9261b47..3fc84b8090 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);
}
@@ -35,13 +35,55 @@
.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-side .nav-item {
+ position: relative;
+}
+
+.notification-list-content .notification-modal-body .notice-dialog-side .nav-item .nav-link {
+ border: none;
+}
+
+.notification-list-content .notification-modal-body .notice-dialog-side .nav-item .nav-link.active {
+ background-color: #F5F5F5;
+ color: #212529;
+}
+
+.notification-list-content .notification-modal-body .notice-dialog-side .nav-item .nav-link.active::before {
+ content: '';
+ position: absolute;
+ display: block;
+ width: 3px;
+ height: 26px;
+ left: -5px;
+ top: 2px;
+ background-color: #ff9800;
+ border-radius: 2px;
+ z-index: 2;
+}
+
+.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;
+ padding: 1rem;
height: 100%;
}
@@ -75,27 +117,64 @@
.notification-dialog-body table td {
padding: 0.5rem 0.1875rem;
- border-bottom: 1px solid #eee;
+ border-bottom: none;
color: #212529;
font-size: 14px;
word-break: break-all;
}
-.wechat-dialog-body {
- display: flex;
- justify-content: center;
- padding: 3rem;
- flex-direction: column;
- align-items: center;
+.notification-dialog-body table td ul {
+ list-style: none;
}
-.wechat-dialog-message {
- width: 100%;
- display: flex;
- justify-content: center;
- flex-direction: column;
- align-items: center;
- margin-top: 1rem;
- color: #666;
- font-size: 14px;
+.notification-dialog-body .notification-user-name {
+ font-weight: normal;
+}
+
+.notification-dialog-body .notification-item .avatar {
+ width: 24px;
+ height: 24px;
+}
+
+.notification-dialog-body .notification-item .notification-point {
+ top: calc(50% - 4px);
+}
+
+@media (max-width: 768px) {
+
+ .notification-list-dialog {
+ width: 100%;
+ max-width: 100%;
+ height: 100%;
+ margin: 0;
+ }
+
+ .notification-list-content .notification-modal-body {
+ display: block;
+ }
+
+ .notification-list-content .notification-modal-body .notice-dialog-side {
+ display: block;
+ padding: 0;
+ }
+
+ .notification-list-content .notification-item .notification-user-name {
+ display: none;
+ }
+
+ .notification-list-content .notice-dialog-main {
+ height: 100%;
+ }
+
+ .notification-list-content .notice-dialog-side>ul {
+ flex-direction: row !important;
+ }
+
+ .notification-list-content .notice-dialog-side>ul>li {
+ width: 50% !important;
+ }
+
+ .notification-list-content .notice-dialog-side>ul>li .nav-link:before {
+ display: none;
+ }
}
diff --git a/frontend/src/user-notifications.js b/frontend/src/user-notifications.js
index 6d264400be..90279dc97e 100644
--- a/frontend/src/user-notifications.js
+++ b/frontend/src/user-notifications.js
@@ -1,6 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
-import { Modal, ModalHeader, ModalBody, Dropdown, DropdownToggle, DropdownMenu, DropdownItem } from 'reactstrap';
+import classname from 'classnames';
+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 +25,7 @@ class UserNotificationsDialog extends React.Component {
hasNextPage: false,
items: [],
isItemMenuShow: false,
+ activeTab: 'general',
};
}
@@ -37,56 +39,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 +174,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 +208,50 @@ class UserNotificationsDialog extends React.Component {
);
};
+
+ renderNoticeContent = (content) => {
+ const { generalNoticeListUnseen, discussionNoticeListUnseen } = this.props;
+ 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;
@@ -139,13 +261,15 @@ class UserNotificationsDialog extends React.Component {
else {
const isDesktop = Utils.isDesktop();
const theadData = isDesktop ? [
- { width: '7%', text: '' },
- { width: '73%', text: gettext('Message') },
- { width: '20%', text: gettext('Time') }
+ { width: '2%', text: '' },
+ { width: '15%', text: gettext('User') },
+ { width: '63%', text: gettext('Message') },
+ { width: '20%', text: gettext('Update time') }
] : [
- { width: '15%', text: '' },
+ { width: '2%', text: '' },
+ { width: '13%', text: gettext('User') },
{ width: '52%', text: gettext('Message') },
- { width: '33%', text: gettext('Time') }
+ { width: '33%', text: gettext('Update time') }
];
content = (
this.tableRef = ref}>
@@ -158,7 +282,9 @@ class UserNotificationsDialog extends React.Component {
{items.map((item, index) => {
- return ();
+ return (
+
+ );
})}
@@ -169,13 +295,17 @@ class UserNotificationsDialog extends React.Component {
}
return (
-
+
{gettext('Notifications')}
- this.notificationTableRef = ref} onScroll={this.onHandleScroll}>
- {content}
-
+ {this.renderNoticeContent(content)}
);
@@ -183,7 +313,7 @@ class UserNotificationsDialog extends React.Component {
}
UserNotificationsDialog.propTypes = {
- onNotificationDialogToggle: PropTypes.func.isRequired,
+ onNotificationDialogToggle: PropTypes.func.isRequired
};
export default UserNotificationsDialog;
diff --git a/frontend/src/utils/seafile-api.js b/frontend/src/utils/seafile-api.js
index 1b165bb672..bc3f338a9a 100644
--- a/frontend/src/utils/seafile-api.js
+++ b/frontend/src/utils/seafile-api.js
@@ -1451,6 +1451,20 @@ 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 });
+ }
+
+ 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 = {
@@ -1460,16 +1474,36 @@ 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);
}
+ 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);
}
+ 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);
@@ -1482,6 +1516,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 2d25d845d5..51f9229871 100644
--- a/seahub/api2/endpoints/notifications.py
+++ b/seahub/api2/endpoints/notifications.py
@@ -14,13 +14,16 @@ from seahub.api2.throttling import UserRateThrottle
from seahub.notifications.models import UserNotification
from seahub.notifications.models import get_cache_key_of_unseen_notifications
-from seahub.notifications.utils import update_notice_detail
+from seahub.notifications.utils import update_notice_detail, update_sdoc_notice_detail
from seahub.api2.utils import api_error
+from seahub.seadoc.models import 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,237 @@ class NotificationView(APIView):
cache.delete(cache_key)
return Response({'success': True})
+
+
+class SdocNotificationsView(APIView):
+
+ authentication_classes = (TokenAuthentication, SessionAuthentication)
+ permission_classes = (IsAuthenticated,)
+ throttle_classes = (UserRateThrottle,)
+
+ def get(self, request):
+ """ used for get sdoc notifications
+
+ Permission checking:
+ 1. login user.
+ """
+ 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
+
+ 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]
+ result_notices = update_sdoc_notice_detail(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)
+
+ unseen_count = SeadocNotification.objects.filter(username=username, seen=False).count()
+ result['unseen_count'] = unseen_count
+
+ total_count = SeadocNotification.objects.filter(username=username).count()
+
+ result['notification_list'] = notification_list
+ result['count'] = total_count
+
+ return Response(result)
+
+ def put(self, request):
+ """mark all sdoc notifications seen"""
+ username = request.user.username
+ 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)
+
+ 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)
+
+ 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()
+
+ 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': {},
+ 'discussion': {}
+ }
+
+ username = request.user.username
+
+ try:
+ per_page = int(request.GET.get('per_page', ''))
+ page = int(request.GET.get('page', ''))
+ except ValueError:
+ per_page = 5
+ 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
+
+ general_notice_list = UserNotification.objects.get_user_notifications(username)[start:end]
+ 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(sdoc_notice_list)
+
+ notification_list = []
+ sdoc_notification_list = []
+ for i in general_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)
+
+ # for case of count value is `0`
+ if unseen_count_from_cache is not None:
+ result['general']['unseen_count'] = unseen_count_from_cache
+ else:
+ unseen_count = UserNotification.objects.filter(to_user=username, seen=False).count()
+ result['general']['unseen_count'] = unseen_count
+ cache.set(cache_key, unseen_count)
+
+ sdoc_unseen_count = SeadocNotification.objects.filter(username=username, seen=False).count()
+ result['discussion']['unseen_count'] = sdoc_unseen_count
+
+ total_count = UserNotification.objects.filter(to_user=username).count()
+ sdoc_total_count = SeadocNotification.objects.filter(username=username).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)
+
+
+ def put(self, request):
+ """ currently only used for mark all notifications seen
+
+ Permission checking:
+ 1. login user.
+ """
+
+ username = request.user.username
+ 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)
+ cache.delete(cache_key)
+
+ return Response({'success': True})
diff --git a/seahub/notifications/utils.py b/seahub/notifications/utils.py
index 8942488440..8863ef299d 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 seaserv import ccnet_api, seafile_api
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
@@ -402,6 +404,46 @@ def update_notice_detail(request, notices):
return notices
+def update_sdoc_notice_detail(notices):
+ doc_uuid_set = set()
+ for notice in notices:
+ doc_uuid_set.add(notice.doc_uuid)
+ uuid_doc_map = {}
+ uuids = FileUUIDMap.objects.get_fileuuidmap_in_uuids(doc_uuid_set)
+ for uuid in uuids:
+ if uuid not in uuid_doc_map:
+ origin_file_path = posixpath.join(uuid.parent_path, uuid.filename)
+ uuid_doc_map[str(uuid.uuid)] = (origin_file_path, uuid.filename, uuid.repo_id)
+
+ for notice in notices:
+ 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'] = doc[0]
+ d['sdoc_name'] = doc[1]
+ d['repo_id'] = doc[2]
+ notice.detail = d
+ except Exception as e:
+ logger.error(e)
+ elif notice.is_reply():
+ try:
+ d = json.loads(notice.detail)
+ url, _, _ = api_avatar_url(d['author'])
+ d['avatar_url'] = url
+ 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)
+ 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/apis.py b/seahub/seadoc/apis.py
index 26083ecf6c..6d1b73753d 100644
--- a/seahub/seadoc/apis.py
+++ b/seahub/seadoc/apis.py
@@ -1102,11 +1102,6 @@ class SeadocCommentsView(APIView):
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', '')
@@ -1172,11 +1167,6 @@ class SeadocCommentView(APIView):
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)
diff --git a/seahub/seadoc/models.py b/seahub/seadoc/models.py
index 48d0d67689..f674aa589e 100644
--- a/seahub/seadoc/models.py
+++ b/seahub/seadoc/models.py
@@ -8,7 +8,6 @@ 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
-
class SeadocHistoryNameManager(models.Manager):
def update_name(self, doc_uuid, obj_id, name):
if self.filter(doc_uuid=doc_uuid, obj_id=obj_id).exists():
@@ -247,6 +246,11 @@ class SeadocCommentReply(models.Model):
}
+
+### sdoc notification
+MSG_TYPE_REPLY = 'reply'
+MSG_TYPE_COMMENT = 'comment'
+
class SeadocNotificationManager(models.Manager):
def total_count(self, doc_uuid, username):
return self.filter(doc_uuid=doc_uuid, username=username).count()
@@ -259,6 +263,13 @@ class SeadocNotificationManager(models.Manager):
def delete_by_ids(self, doc_uuid, username, ids):
return self.filter(doc_uuid=doc_uuid, username=username, id__in=ids).delete()
+
+ def list_all_by_user(self, username):
+ return self.filter(username=username).order_by('-created_at')
+
+ def remove_user_notifications(self, username):
+ """"Remove all user notifications."""
+ self.filter(username=username).delete()
class SeadocNotification(models.Model):
@@ -285,3 +296,9 @@ class SeadocNotification(models.Model):
'detail': json.loads(self.detail),
'seen': self.seen,
}
+
+ def is_comment(self):
+ return self.msg_type == MSG_TYPE_COMMENT
+
+ def is_reply(self):
+ return self.msg_type == MSG_TYPE_REPLY
diff --git a/seahub/tags/models.py b/seahub/tags/models.py
index 74d1d703e5..b42ec66436 100644
--- a/seahub/tags/models.py
+++ b/seahub/tags/models.py
@@ -21,6 +21,10 @@ class FileUUIDMapManager(models.Manager):
return super(FileUUIDMapManager, self).get(uuid=uuid)
except self.model.DoesNotExist:
return None
+
+ def get_fileuuidmap_in_uuids(self, uuids):
+ 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
diff --git a/seahub/urls.py b/seahub/urls.py
index cb44117b19..ad1d7690b4 100644
--- a/seahub/urls.py
+++ b/seahub/urls.py
@@ -91,7 +91,7 @@ from seahub.api2.endpoints.invitations import InvitationsView, InvitationsBatchV
from seahub.api2.endpoints.invitation import InvitationView, InvitationRevokeView
from seahub.api2.endpoints.repo_share_invitations import RepoShareInvitationsView, RepoShareInvitationsBatchView
from seahub.api2.endpoints.repo_share_invitation import RepoShareInvitationView
-from seahub.api2.endpoints.notifications import NotificationsView, NotificationView
+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
@@ -522,6 +522,9 @@ urlpatterns = [
re_path(r'^api/v2.1/notifications/$', NotificationsView.as_view(), name='api-v2.1-notifications'),
re_path(r'^api/v2.1/notification/$', NotificationView.as_view(), name='api-v2.1-notification'),
+ re_path(r'^api/v2.1/sdoc-notifications/$', 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()),